lib-e2e-cypress-for-dummys-ts 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -545
- package/dist/{selector-picker-VJOLGZ5H.js → chunk-FDMUIHDZ.js} +95 -38
- package/dist/chunk-FDMUIHDZ.js.map +1 -0
- package/dist/index.cjs +1133 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +275 -47
- package/dist/index.d.ts +275 -47
- package/dist/index.js +1065 -131
- package/dist/index.js.map +1 -1
- package/dist/runner.js +107 -0
- package/dist/selector-picker-NKYFFFGK.js +7 -0
- package/dist/selector-picker-NKYFFFGK.js.map +1 -0
- package/package.json +4 -1
- package/dist/chunk-DRNRKHXN.js +0 -60
- package/dist/chunk-DRNRKHXN.js.map +0 -1
- package/dist/selector-picker-VJOLGZ5H.js.map +0 -1
package/dist/runner.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/runner/runner.ts
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
var DEFAULT_COMMAND = "npx cypress run --spec {spec}";
|
|
7
|
+
var DEFAULT_PORT = 8123;
|
|
8
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
9
|
+
function toSpecGlob(specPath) {
|
|
10
|
+
const clean = (specPath ?? "").trim();
|
|
11
|
+
if (!clean) return "";
|
|
12
|
+
return clean.includes("/") ? clean : `**/${clean}`;
|
|
13
|
+
}
|
|
14
|
+
function resolveExecutable(cmd, platform = process.platform) {
|
|
15
|
+
return platform === "win32" && /^(npx|npm|yarn|pnpm)$/.test(cmd) ? `${cmd}.cmd` : cmd;
|
|
16
|
+
}
|
|
17
|
+
function buildCommand(template, specGlob) {
|
|
18
|
+
const parts = template.trim().split(/\s+/).filter(Boolean);
|
|
19
|
+
const substituted = parts.map((p) => p === "{spec}" ? specGlob : p);
|
|
20
|
+
const [cmd, ...args] = substituted;
|
|
21
|
+
return { cmd: cmd ?? "", args };
|
|
22
|
+
}
|
|
23
|
+
function runSpec(specPath, opts = {}, spawnFn = spawn) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const glob = toSpecGlob(specPath);
|
|
26
|
+
if (!glob) {
|
|
27
|
+
resolve({ success: false, exitCode: 1, output: "No spec provided" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const built = buildCommand(opts.command ?? DEFAULT_COMMAND, glob);
|
|
31
|
+
const exe = resolveExecutable(built.cmd);
|
|
32
|
+
const child = spawnFn(exe, built.args, { cwd: opts.cwd ?? process.cwd(), shell: false });
|
|
33
|
+
let output = "";
|
|
34
|
+
child.stdout?.on("data", (d) => {
|
|
35
|
+
output += String(d);
|
|
36
|
+
});
|
|
37
|
+
child.stderr?.on("data", (d) => {
|
|
38
|
+
output += String(d);
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (e) => {
|
|
41
|
+
resolve({ success: false, exitCode: -1, output: `${output}
|
|
42
|
+
${String(e)}` });
|
|
43
|
+
});
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
resolve({ success: code === 0, exitCode: code ?? -1, output });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function createRunnerServer(opts = {}, spawnFn = spawn) {
|
|
50
|
+
return createServer((req, res) => {
|
|
51
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
52
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
53
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
54
|
+
if (req.method === "OPTIONS") {
|
|
55
|
+
res.writeHead(204);
|
|
56
|
+
res.end();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (req.method !== "POST" || !req.url || !req.url.endsWith("/run-test")) {
|
|
60
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
61
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let body = "";
|
|
65
|
+
req.on("data", (chunk) => {
|
|
66
|
+
body += String(chunk);
|
|
67
|
+
});
|
|
68
|
+
req.on("end", () => {
|
|
69
|
+
let specPath = "";
|
|
70
|
+
try {
|
|
71
|
+
specPath = JSON.parse(body).specPath ?? "";
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
runSpec(specPath, opts, spawnFn).then((result) => {
|
|
75
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
|
+
res.end(JSON.stringify(result));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function parseRunnerArgs(argv) {
|
|
82
|
+
const opts = {};
|
|
83
|
+
for (let i = 0; i < argv.length; i++) {
|
|
84
|
+
const a = argv[i];
|
|
85
|
+
if (a === "--port") opts.port = Number(argv[++i]);
|
|
86
|
+
else if (a === "--command") opts.command = argv[++i];
|
|
87
|
+
else if (a === "--cwd") opts.cwd = argv[++i];
|
|
88
|
+
else if (a === "--host") opts.host = argv[++i];
|
|
89
|
+
else if (a.startsWith("--port=")) opts.port = Number(a.slice("--port=".length));
|
|
90
|
+
else if (a.startsWith("--command=")) opts.command = a.slice("--command=".length);
|
|
91
|
+
else if (a.startsWith("--cwd=")) opts.cwd = a.slice("--cwd=".length);
|
|
92
|
+
else if (a.startsWith("--host=")) opts.host = a.slice("--host=".length);
|
|
93
|
+
}
|
|
94
|
+
return opts;
|
|
95
|
+
}
|
|
96
|
+
function startRunner(opts = {}) {
|
|
97
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
98
|
+
const host = opts.host ?? DEFAULT_HOST;
|
|
99
|
+
const server = createRunnerServer(opts);
|
|
100
|
+
server.listen(port, host, () => {
|
|
101
|
+
console.log(`[lib-e2e-cypress] runner listening on http://${host}:${port} (command: ${opts.command ?? DEFAULT_COMMAND})`);
|
|
102
|
+
});
|
|
103
|
+
return server;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/runner/index.ts
|
|
107
|
+
startRunner(parseRunnerArgs(process.argv.slice(2)));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lib-e2e-cypress-for-dummys-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Cypress E2E test recorder — pure TypeScript, zero Angular dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"lib-e2e-cypress-runner": "./dist/runner.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./dist/index.d.ts",
|
package/dist/chunk-DRNRKHXN.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// src/utils/selector-quality.utils.ts
|
|
2
|
-
var FORBIDDEN_ID_PREFIXES = [
|
|
3
|
-
"cdk-",
|
|
4
|
-
"mat-",
|
|
5
|
-
"p-",
|
|
6
|
-
"ng-",
|
|
7
|
-
"mdc-",
|
|
8
|
-
"primeng-",
|
|
9
|
-
"auto-",
|
|
10
|
-
"field-",
|
|
11
|
-
"input-",
|
|
12
|
-
"select-"
|
|
13
|
-
];
|
|
14
|
-
function getSelectorQuality(el) {
|
|
15
|
-
if (el.hasAttribute("data-cy") || el.hasAttribute("data-testid") || el.hasAttribute("aria-label")) {
|
|
16
|
-
return "excellent";
|
|
17
|
-
}
|
|
18
|
-
const rawId = el.id;
|
|
19
|
-
if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
|
|
20
|
-
return "good";
|
|
21
|
-
}
|
|
22
|
-
if (typeof el.className === "string" && el.className.trim().length > 0) {
|
|
23
|
-
return "acceptable";
|
|
24
|
-
}
|
|
25
|
-
return "poor";
|
|
26
|
-
}
|
|
27
|
-
function buildPickerSelector(el) {
|
|
28
|
-
const dataCy = el.getAttribute("data-cy");
|
|
29
|
-
if (dataCy) return `[data-cy="${dataCy}"]`;
|
|
30
|
-
const dataTestid = el.getAttribute("data-testid");
|
|
31
|
-
if (dataTestid) return `[data-testid="${dataTestid}"]`;
|
|
32
|
-
const ariaLabel = el.getAttribute("aria-label");
|
|
33
|
-
if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
|
|
34
|
-
const rawId = el.id;
|
|
35
|
-
if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
|
|
36
|
-
return `#${rawId}`;
|
|
37
|
-
}
|
|
38
|
-
if (typeof el.className === "string" && el.className.trim().length > 0) {
|
|
39
|
-
const classes = el.className.trim().split(/\s+/);
|
|
40
|
-
return classes.map((c) => `.${c}`).join("");
|
|
41
|
-
}
|
|
42
|
-
return el.tagName.toLowerCase();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/utils/html.utils.ts
|
|
46
|
-
function escHtml(s) {
|
|
47
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
|
-
}
|
|
49
|
-
function escAttr(s) {
|
|
50
|
-
return s.replace(/"/g, """).replace(/</g, "<");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export {
|
|
54
|
-
FORBIDDEN_ID_PREFIXES,
|
|
55
|
-
getSelectorQuality,
|
|
56
|
-
buildPickerSelector,
|
|
57
|
-
escHtml,
|
|
58
|
-
escAttr
|
|
59
|
-
};
|
|
60
|
-
//# sourceMappingURL=chunk-DRNRKHXN.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/selector-quality.utils.ts","../src/utils/html.utils.ts"],"sourcesContent":["export type SelectorQuality = 'excellent' | 'good' | 'acceptable' | 'poor';\n\nexport const FORBIDDEN_ID_PREFIXES = [\n 'cdk-', 'mat-', 'p-', 'ng-', 'mdc-',\n 'primeng-', 'auto-', 'field-', 'input-', 'select-',\n];\n\nexport function getSelectorQuality(el: HTMLElement): SelectorQuality {\n if (\n el.hasAttribute('data-cy') ||\n el.hasAttribute('data-testid') ||\n el.hasAttribute('aria-label')\n ) {\n return 'excellent';\n }\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return 'good';\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n return 'acceptable';\n }\n\n return 'poor';\n}\n\n/** Returns the best cy.get() selector string for a given element. */\nexport function buildPickerSelector(el: HTMLElement): string {\n const dataCy = el.getAttribute('data-cy');\n if (dataCy) return `[data-cy=\"${dataCy}\"]`;\n\n const dataTestid = el.getAttribute('data-testid');\n if (dataTestid) return `[data-testid=\"${dataTestid}\"]`;\n\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return `[aria-label=\"${ariaLabel}\"]`;\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return `#${rawId}`;\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n const classes = el.className.trim().split(/\\s+/);\n return classes.map((c) => `.${c}`).join('');\n }\n\n return el.tagName.toLowerCase();\n}\n","export function escHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nexport function escAttr(s: string): string {\n return s.replace(/\"/g, '"').replace(/</g, '<');\n}\n"],"mappings":";AAEO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAC7B;AAAA,EAAY;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAC3C;AAEO,SAAS,mBAAmB,IAAkC;AACnE,MACE,GAAG,aAAa,SAAS,KACzB,GAAG,aAAa,aAAa,KAC7B,GAAG,aAAa,YAAY,GAC5B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,IAAyB;AAC3D,QAAM,SAAS,GAAG,aAAa,SAAS;AACxC,MAAI,OAAQ,QAAO,aAAa,MAAM;AAEtC,QAAM,aAAa,GAAG,aAAa,aAAa;AAChD,MAAI,WAAY,QAAO,iBAAiB,UAAU;AAElD,QAAM,YAAY,GAAG,aAAa,YAAY;AAC9C,MAAI,UAAW,QAAO,gBAAgB,SAAS;AAE/C,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,UAAM,UAAU,GAAG,UAAU,KAAK,EAAE,MAAM,KAAK;AAC/C,WAAO,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE;AAAA,EAC5C;AAEA,SAAO,GAAG,QAAQ,YAAY;AAChC;;;AC9DO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC5E;AAEO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,MAAM;AACvD;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/selector-picker/selector-picker.styles.ts","../src/components/selector-picker/selector-picker.template.ts","../src/components/selector-picker/selector-picker.ts"],"sourcesContent":["export const SELECTOR_PICKER_STYLES = `\r\n :host {\r\n position: fixed;\r\n z-index: 2147483647;\r\n top: 0;\r\n left: 0;\r\n width: 100vw;\r\n height: 100vh;\r\n pointer-events: none;\r\n }\r\n\r\n .overlay {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n pointer-events: all;\r\n background: #161b22;\r\n border: 1px solid #30363d;\r\n border-radius: 10px;\r\n box-shadow: 0 8px 32px rgba(0,0,0,0.6);\r\n min-width: 420px;\r\n max-width: 600px;\r\n font-family: 'Segoe UI', system-ui, sans-serif;\r\n color: #e6edf3;\r\n font-size: 13px;\r\n }\r\n\r\n .header {\r\n padding: 12px 16px 8px;\r\n border-bottom: 1px solid #21262d;\r\n font-weight: 600;\r\n font-size: 13px;\r\n color: #8b949e;\r\n letter-spacing: 0.04em;\r\n text-transform: uppercase;\r\n }\r\n\r\n .list {\r\n max-height: 320px;\r\n overflow-y: auto;\r\n padding: 6px 0;\r\n }\r\n\r\n .row {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 7px 14px;\r\n cursor: pointer;\r\n border-radius: 6px;\r\n margin: 1px 6px;\r\n transition: background 0.1s;\r\n }\r\n\r\n .row:hover,\r\n .row.selected {\r\n background: #21262d;\r\n }\r\n\r\n .row.selected {\r\n outline: 2px solid #388bfd;\r\n outline-offset: -2px;\r\n }\r\n\r\n .quality-badge {\r\n width: 18px;\r\n height: 18px;\r\n border-radius: 50%;\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 10px;\r\n }\r\n\r\n .badge-excellent { background: #238636; }\r\n .badge-good { background: #1f6feb; }\r\n .badge-acceptable{ background: #9e6a03; }\r\n .badge-poor { background: #da3633; }\r\n\r\n .tag-name {\r\n font-family: monospace;\r\n font-size: 12px;\r\n color: #8b949e;\r\n flex-shrink: 0;\r\n }\r\n\r\n .attr-value {\r\n font-family: monospace;\r\n font-size: 12px;\r\n color: #c9d1d9;\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n .selector-preview {\r\n font-family: monospace;\r\n font-size: 11px;\r\n color: #58a6ff;\r\n flex-shrink: 0;\r\n max-width: 180px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n\r\n .poor-warning {\r\n font-size: 10px;\r\n color: #f85149;\r\n margin-left: auto;\r\n }\r\n\r\n .footer {\r\n padding: 8px 14px 10px;\r\n border-top: 1px solid #21262d;\r\n display: flex;\r\n gap: 16px;\r\n font-size: 11px;\r\n color: #484f58;\r\n }\r\n\r\n .hint kbd {\r\n background: #21262d;\r\n border: 1px solid #30363d;\r\n border-radius: 3px;\r\n padding: 1px 5px;\r\n font-size: 10px;\r\n color: #8b949e;\r\n }\r\n`;\r\n","import { getSelectorQuality, buildPickerSelector, type SelectorQuality } from '../../utils/selector-quality.utils';\r\nimport { escHtml } from '../../utils/html.utils';\r\n\r\nconst QUALITY_EMOJI: Record<SelectorQuality, string> = {\r\n excellent: '🟢',\r\n good: '🔵',\r\n acceptable: '🟡',\r\n poor: '🔴',\r\n};\r\n\r\nconst QUALITY_CLASS: Record<SelectorQuality, string> = {\r\n excellent: 'badge-excellent',\r\n good: 'badge-good',\r\n acceptable: 'badge-acceptable',\r\n poor: 'badge-poor',\r\n};\r\n\r\nfunction keyAttrDisplay(el: HTMLElement): string {\r\n for (const attr of ['data-cy', 'data-testid', 'aria-label']) {\r\n const v = el.getAttribute(attr);\r\n if (v) return `${attr}=\"${escHtml(v)}\"`;\r\n }\r\n if (el.id) return `id=\"${escHtml(el.id)}\"`;\r\n if (el.className && el.className.trim()) return escHtml(el.className.trim().split(/\\s+/).map((c) => `.${c}`).join(''));\r\n return '';\r\n}\r\n\r\nexport function renderPickerRows(\r\n ancestors: HTMLElement[],\r\n selectedIndex: number,\r\n t: (key: string) => string,\r\n): string {\r\n return ancestors.map((el, i) => {\r\n const quality = getSelectorQuality(el);\r\n const selector = buildPickerSelector(el);\r\n const tag = el.tagName.toLowerCase();\r\n const attr = keyAttrDisplay(el);\r\n const isSelected = i === selectedIndex;\r\n const isPoor = quality === 'poor';\r\n\r\n return `\r\n <div class=\"row${isSelected ? ' selected' : ''}\" data-idx=\"${i}\" role=\"option\" aria-selected=\"${isSelected}\">\r\n <span class=\"quality-badge ${QUALITY_CLASS[quality]}\" title=\"${escHtml(t(`SELECTOR_PICKER.QUALITY_${quality.toUpperCase()}`))}\">\r\n ${QUALITY_EMOJI[quality]}\r\n </span>\r\n <span class=\"tag-name\"><${escHtml(tag)}></span>\r\n ${attr ? `<span class=\"attr-value\">${attr}</span>` : '<span class=\"attr-value\"></span>'}\r\n <span class=\"selector-preview\">${escHtml(selector)}</span>\r\n ${isPoor ? `<span class=\"poor-warning\" title=\"${escHtml(t('SELECTOR_PICKER.QUALITY_POOR'))}\">⚠</span>` : ''}\r\n </div>`;\r\n }).join('');\r\n}\r\n\r\nexport function renderPicker(\r\n ancestors: HTMLElement[],\r\n selectedIndex: number,\r\n t: (key: string) => string,\r\n): string {\r\n return `\r\n <div class=\"overlay\" role=\"listbox\">\r\n <div class=\"header\">${escHtml(t('SELECTOR_PICKER.TITLE'))}</div>\r\n <div class=\"list\">\r\n ${renderPickerRows(ancestors, selectedIndex, t)}\r\n </div>\r\n <div class=\"footer\">\r\n <span class=\"hint\"><kbd>↑↓</kbd> ${escHtml(t('SELECTOR_PICKER.PREVIEW_LABEL'))}</span>\r\n <span class=\"hint\"><kbd>Enter</kbd> ${escHtml(t('SELECTOR_PICKER.CONFIRM_HINT'))}</span>\r\n <span class=\"hint\"><kbd>Esc</kbd> ${escHtml(t('SELECTOR_PICKER.CANCEL_HINT'))}</span>\r\n </div>\r\n </div>`;\r\n}\r\n","import type { RecordingService } from '../../services/recording.service';\r\nimport type { TranslationService } from '../../services/translation.service';\r\nimport { getSelectorQuality, buildPickerSelector } from '../../utils/selector-quality.utils';\r\nimport { SELECTOR_PICKER_STYLES } from './selector-picker.styles';\r\nimport { renderPicker } from './selector-picker.template';\r\n\r\nconst OWN_DATA_CY = 'lib-e2e-cypress-for-dummys';\r\nconst MAX_ANCESTORS = 10;\r\n\r\nexport class SelectorPickerElement extends HTMLElement {\r\n targetElement: HTMLElement | null = null;\r\n recording!: RecordingService;\r\n translation!: TranslationService;\r\n\r\n private shadow: ShadowRoot;\r\n private ancestors: HTMLElement[] = [];\r\n private selectedIndex = 0;\r\n private keyHandler: ((e: KeyboardEvent) => void) | null = null;\r\n private unsubRecording: (() => void) | null = null;\r\n private unsubPause: (() => void) | null = null;\r\n\r\n constructor() {\r\n super();\r\n this.shadow = this.attachShadow({ mode: 'open' });\r\n }\r\n\r\n connectedCallback(): void {\r\n this.setAttribute('data-cy', 'lib-e2e-cypress-for-dummys');\r\n this.ancestors = this.buildAncestors();\r\n this.selectedIndex = this.findBestIndex();\r\n this.render();\r\n this.attachKeyListener();\r\n this.attachRowClickListeners();\r\n this.subscribeToRecordingChanges();\r\n }\r\n\r\n disconnectedCallback(): void {\r\n this.detachKeyListener();\r\n this.unsubRecording?.();\r\n this.unsubPause?.();\r\n }\r\n\r\n private t(key: string): string {\r\n return this.translation?.translate(key) ?? key;\r\n }\r\n\r\n // ── Ancestor chain ──────────────────────────────────────────────────────────\r\n\r\n private buildAncestors(): HTMLElement[] {\r\n const chain: HTMLElement[] = [];\r\n let current: HTMLElement | null = this.targetElement;\r\n\r\n while (current && current.tagName.toLowerCase() !== 'html' && chain.length < MAX_ANCESTORS) {\r\n if (current.getAttribute('data-cy') === OWN_DATA_CY) {\r\n current = current.parentElement;\r\n continue;\r\n }\r\n chain.push(current);\r\n current = current.parentElement;\r\n }\r\n\r\n return chain;\r\n }\r\n\r\n private findBestIndex(): number {\r\n for (let i = 0; i < this.ancestors.length; i++) {\r\n const q = getSelectorQuality(this.ancestors[i]);\r\n if (q === 'excellent' || q === 'good' || q === 'acceptable') return i;\r\n }\r\n return 0;\r\n }\r\n\r\n // ── Rendering ───────────────────────────────────────────────────────────────\r\n\r\n private render(): void {\r\n this.shadow.innerHTML = `<style>${SELECTOR_PICKER_STYLES}</style>${renderPicker(\r\n this.ancestors,\r\n this.selectedIndex,\r\n this.t.bind(this),\r\n )}`;\r\n }\r\n\r\n // ── Keyboard ────────────────────────────────────────────────────────────────\r\n\r\n private attachKeyListener(): void {\r\n this.keyHandler = (e: KeyboardEvent) => {\r\n if (e.key === 'ArrowDown') {\r\n e.stopPropagation();\r\n e.preventDefault();\r\n this.selectedIndex = (this.selectedIndex + 1) % this.ancestors.length;\r\n this.render();\r\n this.attachRowClickListeners();\r\n } else if (e.key === 'ArrowUp') {\r\n e.stopPropagation();\r\n e.preventDefault();\r\n this.selectedIndex = (this.selectedIndex - 1 + this.ancestors.length) % this.ancestors.length;\r\n this.render();\r\n this.attachRowClickListeners();\r\n } else if (e.key === 'Enter') {\r\n e.stopPropagation();\r\n this.confirm(this.selectedIndex);\r\n } else if (e.key === 'Escape') {\r\n e.stopPropagation();\r\n this.cancel();\r\n }\r\n };\r\n window.addEventListener('keydown', this.keyHandler, { capture: true });\r\n }\r\n\r\n private detachKeyListener(): void {\r\n if (this.keyHandler) {\r\n window.removeEventListener('keydown', this.keyHandler, { capture: true });\r\n this.keyHandler = null;\r\n }\r\n }\r\n\r\n // ── Row click listeners ─────────────────────────────────────────────────────\r\n\r\n private attachRowClickListeners(): void {\r\n this.shadow.querySelectorAll('[data-idx]').forEach((row) => {\r\n (row as HTMLElement).addEventListener('click', () => {\r\n const idx = parseInt((row as HTMLElement).dataset['idx'] ?? '0', 10);\r\n this.confirm(idx);\r\n });\r\n });\r\n }\r\n\r\n // ── Recording auto-close ────────────────────────────────────────────────────\r\n\r\n private subscribeToRecordingChanges(): void {\r\n this.unsubRecording = this.recording.onRecordingChange((isRecording) => {\r\n if (!isRecording) this.closeSilently();\r\n });\r\n this.unsubPause = this.recording.onPauseChange((isPaused) => {\r\n if (isPaused) this.closeSilently();\r\n });\r\n }\r\n\r\n // ── Actions ─────────────────────────────────────────────────────────────────\r\n\r\n private confirm(index: number): void {\r\n const el = this.ancestors[index];\r\n if (!el) return;\r\n const selector = buildPickerSelector(el);\r\n const command = `cy.get('${selector}').click()`;\r\n this.recording.appendCommand(command);\r\n this.dispatchEvent(new CustomEvent('selectorchosen', { detail: command, bubbles: true }));\r\n this.closeSilently();\r\n }\r\n\r\n private cancel(): void {\r\n this.dispatchEvent(new CustomEvent('pickercancelled', { bubbles: true }));\r\n this.closeSilently();\r\n }\r\n\r\n private closeSilently(): void {\r\n this.detachKeyListener();\r\n this.unsubRecording?.();\r\n this.unsubPause?.();\r\n this.unsubRecording = null;\r\n this.unsubPause = null;\r\n this.remove();\r\n }\r\n}\r\n\r\nif (!customElements.get('selector-picker')) {\r\n customElements.define('selector-picker', SelectorPickerElement);\r\n}\r\n"],"mappings":";;;;;;;AAAO,IAAM,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGtC,IAAM,gBAAiD;AAAA,EACrD,WAAY;AAAA,EACZ,MAAY;AAAA,EACZ,YAAY;AAAA,EACZ,MAAY;AACd;AAEA,IAAM,gBAAiD;AAAA,EACrD,WAAY;AAAA,EACZ,MAAY;AAAA,EACZ,YAAY;AAAA,EACZ,MAAY;AACd;AAEA,SAAS,eAAe,IAAyB;AAC/C,aAAW,QAAQ,CAAC,WAAW,eAAe,YAAY,GAAG;AAC3D,UAAM,IAAI,GAAG,aAAa,IAAI;AAC9B,QAAI,EAAG,QAAO,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC;AAAA,EACtC;AACA,MAAI,GAAG,GAAI,QAAO,OAAO,QAAQ,GAAG,EAAE,CAAC;AACvC,MAAI,GAAG,aAAa,GAAG,UAAU,KAAK,EAAG,QAAO,QAAQ,GAAG,UAAU,KAAK,EAAE,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;AACrH,SAAO;AACT;AAEO,SAAS,iBACd,WACA,eACA,GACQ;AACR,SAAO,UAAU,IAAI,CAAC,IAAI,MAAM;AAC9B,UAAM,UAAU,mBAAmB,EAAE;AACrC,UAAM,WAAW,oBAAoB,EAAE;AACvC,UAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,UAAM,OAAO,eAAe,EAAE;AAC9B,UAAM,aAAa,MAAM;AACzB,UAAM,SAAS,YAAY;AAE3B,WAAO;AAAA,uBACY,aAAa,cAAc,EAAE,eAAe,CAAC,kCAAkC,UAAU;AAAA,qCAC3E,cAAc,OAAO,CAAC,YAAY,QAAQ,EAAE,2BAA2B,QAAQ,YAAY,CAAC,EAAE,CAAC,CAAC;AAAA,YACzH,cAAc,OAAO,CAAC;AAAA;AAAA,qCAEG,QAAQ,GAAG,CAAC;AAAA,UACvC,OAAO,4BAA4B,IAAI,YAAY,kCAAkC;AAAA,yCACtD,QAAQ,QAAQ,CAAC;AAAA,UAChD,SAAS,qCAAqC,QAAQ,EAAE,8BAA8B,CAAC,CAAC,oBAAe,EAAE;AAAA;AAAA,EAEjH,CAAC,EAAE,KAAK,EAAE;AACZ;AAEO,SAAS,aACd,WACA,eACA,GACQ;AACR,SAAO;AAAA;AAAA,4BAEmB,QAAQ,EAAE,uBAAuB,CAAC,CAAC;AAAA;AAAA,UAErD,iBAAiB,WAAW,eAAe,CAAC,CAAC;AAAA;AAAA;AAAA,qDAGZ,QAAQ,EAAE,+BAA+B,CAAC,CAAC;AAAA,8CACxC,QAAQ,EAAE,8BAA8B,CAAC,CAAC;AAAA,4CAC5C,QAAQ,EAAE,6BAA6B,CAAC,CAAC;AAAA;AAAA;AAGrF;;;AChEA,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAEf,IAAM,wBAAN,cAAoC,YAAY;AAAA,EACrD,gBAAoC;AAAA,EACpC;AAAA,EACA;AAAA,EAEQ;AAAA,EACA,YAA2B,CAAC;AAAA,EAC5B,gBAAgB;AAAA,EAChB,aAAkD;AAAA,EAClD,iBAAsC;AAAA,EACtC,aAAkC;AAAA,EAE1C,cAAc;AACZ,UAAM;AACN,SAAK,SAAS,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAAA,EAClD;AAAA,EAEA,oBAA0B;AACxB,SAAK,aAAa,WAAW,4BAA4B;AACzD,SAAK,YAAY,KAAK,eAAe;AACrC,SAAK,gBAAgB,KAAK,cAAc;AACxC,SAAK,OAAO;AACZ,SAAK,kBAAkB;AACvB,SAAK,wBAAwB;AAC7B,SAAK,4BAA4B;AAAA,EACnC;AAAA,EAEA,uBAA6B;AAC3B,SAAK,kBAAkB;AACvB,SAAK,iBAAiB;AACtB,SAAK,aAAa;AAAA,EACpB;AAAA,EAEQ,EAAE,KAAqB;AAC7B,WAAO,KAAK,aAAa,UAAU,GAAG,KAAK;AAAA,EAC7C;AAAA;AAAA,EAIQ,iBAAgC;AACtC,UAAM,QAAuB,CAAC;AAC9B,QAAI,UAA8B,KAAK;AAEvC,WAAO,WAAW,QAAQ,QAAQ,YAAY,MAAM,UAAU,MAAM,SAAS,eAAe;AAC1F,UAAI,QAAQ,aAAa,SAAS,MAAM,aAAa;AACnD,kBAAU,QAAQ;AAClB;AAAA,MACF;AACA,YAAM,KAAK,OAAO;AAClB,gBAAU,QAAQ;AAAA,IACpB;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,gBAAwB;AAC9B,aAAS,IAAI,GAAG,IAAI,KAAK,UAAU,QAAQ,KAAK;AAC9C,YAAM,IAAI,mBAAmB,KAAK,UAAU,CAAC,CAAC;AAC9C,UAAI,MAAM,eAAe,MAAM,UAAU,MAAM,aAAc,QAAO;AAAA,IACtE;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAIQ,SAAe;AACrB,SAAK,OAAO,YAAY,UAAU,sBAAsB,WAAW;AAAA,MACjE,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,EAAE,KAAK,IAAI;AAAA,IAClB,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,oBAA0B;AAChC,SAAK,aAAa,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,aAAa;AACzB,UAAE,gBAAgB;AAClB,UAAE,eAAe;AACjB,aAAK,iBAAiB,KAAK,gBAAgB,KAAK,KAAK,UAAU;AAC/D,aAAK,OAAO;AACZ,aAAK,wBAAwB;AAAA,MAC/B,WAAW,EAAE,QAAQ,WAAW;AAC9B,UAAE,gBAAgB;AAClB,UAAE,eAAe;AACjB,aAAK,iBAAiB,KAAK,gBAAgB,IAAI,KAAK,UAAU,UAAU,KAAK,UAAU;AACvF,aAAK,OAAO;AACZ,aAAK,wBAAwB;AAAA,MAC/B,WAAW,EAAE,QAAQ,SAAS;AAC5B,UAAE,gBAAgB;AAClB,aAAK,QAAQ,KAAK,aAAa;AAAA,MACjC,WAAW,EAAE,QAAQ,UAAU;AAC7B,UAAE,gBAAgB;AAClB,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AACA,WAAO,iBAAiB,WAAW,KAAK,YAAY,EAAE,SAAS,KAAK,CAAC;AAAA,EACvE;AAAA,EAEQ,oBAA0B;AAChC,QAAI,KAAK,YAAY;AACnB,aAAO,oBAAoB,WAAW,KAAK,YAAY,EAAE,SAAS,KAAK,CAAC;AACxE,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA,EAIQ,0BAAgC;AACtC,SAAK,OAAO,iBAAiB,YAAY,EAAE,QAAQ,CAAC,QAAQ;AAC1D,MAAC,IAAoB,iBAAiB,SAAS,MAAM;AACnD,cAAM,MAAM,SAAU,IAAoB,QAAQ,KAAK,KAAK,KAAK,EAAE;AACnE,aAAK,QAAQ,GAAG;AAAA,MAClB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,8BAAoC;AAC1C,SAAK,iBAAiB,KAAK,UAAU,kBAAkB,CAAC,gBAAgB;AACtE,UAAI,CAAC,YAAa,MAAK,cAAc;AAAA,IACvC,CAAC;AACD,SAAK,aAAa,KAAK,UAAU,cAAc,CAAC,aAAa;AAC3D,UAAI,SAAU,MAAK,cAAc;AAAA,IACnC,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,QAAQ,OAAqB;AACnC,UAAM,KAAK,KAAK,UAAU,KAAK;AAC/B,QAAI,CAAC,GAAI;AACT,UAAM,WAAW,oBAAoB,EAAE;AACvC,UAAM,UAAU,WAAW,QAAQ;AACnC,SAAK,UAAU,cAAc,OAAO;AACpC,SAAK,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,SAAS,SAAS,KAAK,CAAC,CAAC;AACxF,SAAK,cAAc;AAAA,EACrB;AAAA,EAEQ,SAAe;AACrB,SAAK,cAAc,IAAI,YAAY,mBAAmB,EAAE,SAAS,KAAK,CAAC,CAAC;AACxE,SAAK,cAAc;AAAA,EACrB;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,kBAAkB;AACvB,SAAK,iBAAiB;AACtB,SAAK,aAAa;AAClB,SAAK,iBAAiB;AACtB,SAAK,aAAa;AAClB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,CAAC,eAAe,IAAI,iBAAiB,GAAG;AAC1C,iBAAe,OAAO,mBAAmB,qBAAqB;AAChE;","names":[]}
|