// server.ts - Local server that receives click data from the Chrome extension // Run with: ts-node --transpile-only server.ts import * as http from "http"; import * as fs from "fs"; import * as path from "path"; const PORT = 3000; const OUTPUT_FILE = path.join(__dirname, "recorded_clicks.ts"); const SUGGESTIONS_FILE = path.join(__dirname, "suggestions.txt"); // ─── Parse .po.ts file into structured data ─────────────────────────────────── interface ParsedPageObject { filePath: string; statics: Record; // name -> selector value getFunctions: ParsedGetFn[]; } 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 } 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 = {}; 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; } 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); } } 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]}`, }; } // ─── 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 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; const baseName = data.ariaLabel ? data.ariaLabel.replace(/\s+/g, "").toLowerCase() : firstClass ? firstClass.replace(/[-_.]/g, "").toLowerCase() : "unknown"; return rawText ? baseName + rawText.replace(/\s+/g, "").toLowerCase() : baseName; } 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", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } if (req.method === "POST" && req.url === "/record") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { try { const data = JSON.parse(body); console.log("\n--- Click Recorded ---"); console.log("Tag: ", data.tag); console.log("ID: ", data.id); console.log("Aria-label: ", data.ariaLabel); console.log("Classes: ", data.classes.join(", ")); console.log("Text: ", data.text); handleClick(data); console.log("----------------------"); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok" })); } catch (err: any) { console.error("Failed to parse body:", err.message); res.writeHead(400); res.end("Bad request"); } }); } else { res.writeHead(404); res.end("Not found"); } }); server.listen(PORT, () => { console.log(`Cypress Recorder server running on http://localhost:${PORT}`); console.log(`Watching for clicks...`); });