demo done

This commit is contained in:
2026-02-21 00:45:20 +01:00
parent 00a306582e
commit f4fd3fd6c5
6 changed files with 585 additions and 312 deletions

320
server.ts
View File

@@ -9,99 +9,259 @@ const PORT = 3000;
const OUTPUT_FILE = path.join(__dirname, "recorded_clicks.ts");
const SUGGESTIONS_FILE = path.join(__dirname, "suggestions.txt");
// Dynamically find and load the first .po.ts file in the same folder
function loadPageObject(): Record<string, string> {
const files = fs.readdirSync(__dirname).filter(f => f.endsWith(".po.ts"));
// ─── Parse .po.ts file into structured data ───────────────────────────────────
if (files.length === 0) {
console.warn("No .po.ts file found in server directory!");
return {};
}
const filePath = path.join(__dirname, files[0]);
console.log(`Loading page object from: ${filePath}`);
const mod = require(filePath);
const exportedClass = Object.values(mod)[0] as any;
const selectors: Record<string, string> = {};
for (const [key, value] of Object.entries(exportedClass)) {
if (typeof value === "string") {
selectors[key] = value as string;
}
}
console.log(`Loaded ${Object.keys(selectors).length} selectors.`);
return selectors;
interface ParsedPageObject {
filePath: string;
statics: Record<string, string>; // name -> selector value
getFunctions: ParsedGetFn[];
}
function findMatches(data: any, selectors: Record<string, string>): string[] {
const results: string[] = [];
interface ParsedGetFn {
name: string;
body: string; // full body text
referencedStatics: string[]; // static variable names used in body
nccontainsText: string | null; // text in nccontains() if present
}
for (const [name, selector] of Object.entries(selectors)) {
if (selector.startsWith(".")) {
const className = selector.replace(".", "");
if (data.classes.includes(className)) {
results.push(name);
function parsePageObject(): ParsedPageObject {
const poFiles = fs.readdirSync(__dirname).filter(f => f.endsWith(".po.ts"));
if (poFiles.length === 0) throw new Error("No .po.ts file found!");
const filePath = path.join(__dirname, poFiles[0]);
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");
const statics: Record<string, string> = {};
const getFunctions: ParsedGetFn[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i].trimStart();
// Parse static declarations: static foo = ".some-class"
const staticMatch = line.match(/^static\s+(\w+)\s*=\s*["'](.+?)["']/);
if (staticMatch) {
statics[staticMatch[1]] = staticMatch[2];
i++;
continue;
}
// Parse get functions: get FooBar() {
const getMatch = line.match(/^get\s+(\w+)\s*\(/);
if (getMatch) {
const fnName = getMatch[1];
const bodyLines: string[] = [];
let depth = 0;
while (i < lines.length) {
const l = lines[i];
bodyLines.push(l);
for (const ch of l) {
if (ch === "{") depth++;
if (ch === "}") depth--;
}
i++;
if (depth === 0 && bodyLines.length > 1) break;
}
const body = bodyLines.join("\n");
// Find which static variables are referenced
const referencedStatics = Object.keys(statics).filter(s =>
body.includes(s)
);
// Extract nccontains text if present: nccontains(Mainpage.foo, 'some text')
const ncMatch = body.match(/nccontains\s*\([^,]+,\s*['"](.+?)['"]\s*\)/);
const nccontainsText = ncMatch ? ncMatch[1] : null;
getFunctions.push({ name: fnName, body, referencedStatics, nccontainsText });
continue;
}
if (data.ariaLabel && selector === data.ariaLabel) {
results.push(name);
i++;
}
console.log(`Parsed ${Object.keys(statics).length} statics, ${getFunctions.length} get functions.`);
return { filePath, statics, getFunctions };
}
// ─── Matching logic ────────────────────────────────────────────────────────────
interface MatchResult {
type: "full_match" | "static_only" | "no_match";
fnName?: string; // for full_match
staticName?: string; // for static_only and no_match (if static exists)
selectorVar?: string; // for static_only (to build the new get fn)
}
function findMatch(data: any, po: ParsedPageObject): MatchResult {
const rawText = data.text ? data.text.split("\n")[0].trim() : null;
// Step 1: find matching static names by class or aria-label
const matchingStatics: string[] = [];
for (const [name, value] of Object.entries(po.statics)) {
const classMatch = value.startsWith(".") && data.classes.includes(value.slice(1));
const ariaMatch = data.ariaLabel && value === data.ariaLabel;
if (classMatch || ariaMatch) {
matchingStatics.push(name);
}
}
return results;
if (matchingStatics.length === 0) {
return { type: "no_match" };
}
// Step 2: find a get function that references one of those statics AND has matching text
for (const fn of po.getFunctions) {
const referencesMatch = fn.referencedStatics.some(s => matchingStatics.includes(s));
if (!referencesMatch) continue;
// If we have text, require nccontains to match it
if (rawText) {
if (fn.nccontainsText && fn.nccontainsText.toLowerCase() === rawText.toLowerCase()) {
return { type: "full_match", fnName: fn.name };
}
} else {
// No text — any get function referencing the static is good enough
return { type: "full_match", fnName: fn.name };
}
}
// Static matched but no get function with matching text found
return {
type: "static_only",
staticName: matchingStatics[0],
selectorVar: `Mainpage.${matchingStatics[0]}`,
};
}
function appendToOutput(matches: string[]) {
const lines = matches.map(name => `mainpage.${name}.click();`);
fs.appendFileSync(OUTPUT_FILE, lines.join("\n") + "\n");
// ─── File insertion ────────────────────────────────────────────────────────────
function insertGetFunction(filePath: string, getBlock: string) {
const content = fs.readFileSync(filePath, "utf-8");
const fileLines = content.split("\n");
// Find the last static line
let lastStaticIndex = -1;
for (let i = 0; i < fileLines.length; i++) {
if (fileLines[i].trimStart().startsWith("static ")) lastStaticIndex = i;
}
// Find first get after static block
let firstGetIndex = -1;
for (let i = lastStaticIndex + 1; i < fileLines.length; i++) {
if (fileLines[i].trimStart().startsWith("get ")) { firstGetIndex = i; break; }
}
if (firstGetIndex === -1) {
console.warn("Could not find first get function, aborting insert.");
return;
}
const getLines = getBlock.split("\n");
fileLines.splice(firstGetIndex, 0, "", ...getLines);
fs.writeFileSync(filePath, fileLines.join("\n"), "utf-8");
}
function appendSuggestion(data: any) {
function insertStaticAndGet(filePath: string, staticLine: string, getBlock: string) {
const content = fs.readFileSync(filePath, "utf-8");
const fileLines = content.split("\n");
// Find last static line
let lastStaticIndex = -1;
for (let i = 0; i < fileLines.length; i++) {
if (fileLines[i].trimStart().startsWith("static ")) lastStaticIndex = i;
}
if (lastStaticIndex === -1) {
console.warn("Could not find static block, aborting insert.");
return;
}
// Find first get after static block
let firstGetIndex = -1;
for (let i = lastStaticIndex + 1; i < fileLines.length; i++) {
if (fileLines[i].trimStart().startsWith("get ")) { firstGetIndex = i; break; }
}
if (firstGetIndex === -1) {
console.warn("Could not find first get function, aborting insert.");
return;
}
// Insert static
fileLines.splice(lastStaticIndex + 1, 0, ` ${staticLine}`);
// Insert get (shifted by 1)
const getLines = getBlock.split("\n");
fileLines.splice(firstGetIndex + 1, 0, "", ...getLines);
fs.writeFileSync(filePath, fileLines.join("\n"), "utf-8");
}
// ─── Main handler ──────────────────────────────────────────────────────────────
function buildGetFnName(data: any): string {
const rawText = data.text ? data.text.split("\n")[0].trim() : null;
const firstClass = data.classes.length > 0 ? data.classes[0] : null;
// Generate a name from aria-label or first class
const name = data.ariaLabel
const baseName = data.ariaLabel
? data.ariaLabel.replace(/\s+/g, "").toLowerCase()
: firstClass
? firstClass.replace(/[-_.]/g, "").toLowerCase()
: "unknown";
const lines: string[] = [];
lines.push(`// --- Unmatched click (tag: <${data.tag}>, text: "${data.text}") ---`);
lines.push(`// Check if these exist and add to your page object:`);
if (firstClass) lines.push(`static ${name}class = ".${firstClass}"`);
if (data.id) lines.push(`static ${name}id = "#${data.id}"`);
if (data.ariaLabel) lines.push(`static ${name}aria = "[aria-label='${data.ariaLabel}']"`);
lines.push(``);
lines.push(`// Suggested get function:`);
// Selector: aria if available, otherwise class
const selectorVar = data.ariaLabel ? `Mainpage.${name}aria` : `Mainpage.${name}class`;
// Always include text and tag if available
const hasText = data.text && data.text.length > 0;
const filterParts: string[] = [];
if (data.tag) filterParts.push(`data.tag: "${data.tag}"`);
if (hasText) filterParts.push(`text: "${data.text}"`);
lines.push(`get ${name}() {`);
if (hasText) {
lines.push(` return cy.nccontains(${selectorVar}, '${data.text}');`);
} else {
lines.push(` return cy.get(${selectorVar});`);
}
lines.push(`}`);
lines.push(``);
lines.push(`// ------------------------------------------------`);
lines.push(``);
fs.appendFileSync(SUGGESTIONS_FILE, lines.join("\n") + "\n");
return rawText ? baseName + rawText.replace(/\s+/g, "").toLowerCase() : baseName;
}
// Load selectors once at startup
const selectors = loadPageObject();
function handleClick(data: any) {
const po = parsePageObject();
const match = findMatch(data, po);
const rawText = data.text ? data.text.split("\n")[0].trim() : null;
const hasText = rawText && rawText.length > 0;
const firstClass = data.classes.length > 0 ? data.classes[0] : null;
if (match.type === "full_match") {
// Perfect — use existing function
console.log(` >> Full match: mainpage.${match.fnName}.click();`);
fs.appendFileSync(OUTPUT_FILE, `mainpage.${match.fnName}.click();\n`);
} else if (match.type === "static_only") {
// Static exists, just create the get function
const getFnName = buildGetFnName(data);
const getBody = hasText
? ` return cy.nccontains(${match.selectorVar}, '${rawText}');`
: ` return cy.get(${match.selectorVar});`;
const getBlock = ` get ${getFnName}() {\n${getBody}\n }`;
console.log(` >> Static match, inserting get function: ${getFnName}`);
insertGetFunction(po.filePath, getBlock);
fs.appendFileSync(OUTPUT_FILE, `mainpage.${getFnName}.click();\n`);
fs.appendFileSync(SUGGESTIONS_FILE, `// Inserted get function (static already existed)\n${getBlock}\n// ----\n\n`);
} else {
// No match at all — create both static and get function
const baseName = data.ariaLabel
? data.ariaLabel.replace(/\s+/g, "").toLowerCase()
: firstClass
? firstClass.replace(/[-_.]/g, "").toLowerCase()
: "unknown";
const getFnName = buildGetFnName(data);
const selectorVar = data.ariaLabel ? `Mainpage.${baseName}aria` : `Mainpage.${baseName}class`;
const staticValue = data.ariaLabel ? `"[aria-label='${data.ariaLabel}']"` : `".${firstClass}"`;
const staticName = data.ariaLabel ? `${baseName}aria` : `${baseName}class`;
const staticLine = `static ${staticName} = ${staticValue}`;
const getBody = hasText
? ` return cy.nccontains(${selectorVar}, '${rawText}');`
: ` return cy.get(${selectorVar});`;
const getBlock = ` get ${getFnName}() {\n${getBody}\n }`;
console.log(` >> No match, inserting static + get function: ${getFnName}`);
insertStaticAndGet(po.filePath, staticLine, getBlock);
fs.appendFileSync(OUTPUT_FILE, `mainpage.${getFnName}.click();\n`);
fs.appendFileSync(SUGGESTIONS_FILE, `// Inserted static + get\n ${staticLine}\n${getBlock}\n// ----\n\n`);
}
}
// ─── Server ────────────────────────────────────────────────────────────────────
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
@@ -116,7 +276,6 @@ const server = http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/record") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
try {
@@ -129,19 +288,9 @@ const server = http.createServer((req, res) => {
console.log("Classes: ", data.classes.join(", "));
console.log("Text: ", data.text);
const matches = findMatches(data, selectors);
if (matches.length > 0) {
console.log("Matches:");
matches.forEach(m => console.log(` >> mainpage.${m}.click();`));
appendToOutput(matches);
} else {
console.log(" >> No match — writing to suggestions.txt");
appendSuggestion(data);
}
handleClick(data);
console.log("----------------------");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
} catch (err: any) {
@@ -158,4 +307,5 @@ const server = http.createServer((req, res) => {
server.listen(PORT, () => {
console.log(`Cypress Recorder server running on http://localhost:${PORT}`);
console.log(`Watching for clicks...`);
});