lib-e2e-cypress-for-dummys-ts 0.8.0 → 0.10.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  > A floating widget that records real browser interactions and generates ready-to-run **Cypress E2E test code** — no setup required, no Cypress knowledge needed.
4
4
 
5
+ > **Development and QA environments only.** This is a developer tool for generating test code — it has no place in a production build. Not because of security vulnerabilities or performance problems, but simply because end users have no reason to see a test recorder widget. Load it conditionally (e.g. `if (process.env.NODE_ENV !== 'production')`) or gate it behind a build flag.
6
+
5
7
  ---
6
8
 
7
9
  ## What it does
@@ -130,6 +132,8 @@ The recorder automatically captures:
130
132
  | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
131
133
  | Press Enter / Escape in a field | `cy.get('[data-cy="q"]').type('{enter}')` / `type('{esc}')` |
132
134
  | Select a `<select>` value | `cy.get('[data-cy="country"]').select('ES')` |
135
+ | Upload a file via `<input type="file">` | `cy.get('[data-cy="upload"]').selectFile('cypress/fixtures/invoice.pdf')` |
136
+ | Upload multiple files | `cy.get('[data-cy="upload"]').selectFile(['cypress/fixtures/f1.pdf', 'cypress/fixtures/f2.csv'])` |
133
137
  | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
134
138
  | Page load | `cy.visit('/current-path')` |
135
139
 
@@ -211,6 +215,16 @@ automatically (requires the Cypress folder configured — see the advanced edito
211
215
  setup). POST/PUT keep the spy + body-validation behaviour; non-JSON GETs fall back
212
216
  to a spy.
213
217
 
218
+ #### File uploads
219
+
220
+ When you interact with an `<input type="file">` while recording, the recorder captures
221
+ the selected file(s) and generates `.selectFile('cypress/fixtures/FILENAME')`.
222
+
223
+ - **Save-and-Edit with Cypress folder configured** — the file is automatically copied
224
+ into `cypress/fixtures/` and a confirmation toast appears.
225
+ - **Any other save (IndexedDB / no folder)** — a warning toast reminds you to add the
226
+ file to `cypress/fixtures/` manually before running the test.
227
+
214
228
  ---
215
229
 
216
230
  ### Selector strategy
@@ -261,13 +275,11 @@ When recording stops, a dialog prompts you to:
261
275
 
262
276
  - Give the test a **description** (becomes the `it('…')` label)
263
277
  - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
264
- - Optionally add a **ticket** reference (see [Issue tracker links](#issue-tracker-links))
265
278
  - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
266
279
 
267
280
  The **📋 Tests** panel lets you:
268
281
 
269
282
  - Browse all saved tests filtered by tag
270
- - **Group by ticket** — a toggle that clusters tests under their ticket (composes with the tag filter)
271
283
  - Expand a test to see its commands and interceptors
272
284
  - Copy the full `describe()` block to the clipboard with a custom suite name
273
285
  - Copy just the commands or just the interceptors separately
@@ -276,36 +288,6 @@ The **📋 Tests** panel lets you:
276
288
 
277
289
  ---
278
290
 
279
- ### Issue tracker links
280
-
281
- Tie a recorded test to a work item in your tracker — **link only, no integration**: the
282
- library never connects to, authenticates against, or syncs with any platform. It simply
283
- turns a ticket id into a deep link.
284
-
285
- Pick your tracker in the settings panel (**🎫 Issue tracker**) and fill the fields it needs:
286
-
287
- | Tracker | Fields | Link built |
288
- |---|---|---|
289
- | Jira | Base URL | `{baseUrl}/browse/{id}` |
290
- | Azure DevOps | Organization, Project | `…/{org}/{project}/_workitems/edit/{id}` |
291
- | GitHub | Owner, Repository | `github.com/{owner}/{repo}/issues/{id}` |
292
- | Trello | — | `trello.com/c/{id}` |
293
- | Custom | URL template (must contain `{id}`) | your template |
294
-
295
- Then, when saving a recording, type the ticket id (e.g. `PROJ-123`). The id is stored with
296
- the test and:
297
-
298
- - rendered as a **clickable link** on the test row and its group header;
299
- - emitted as a **comment above the `it()` block** when the test is inserted into a `.cy.ts`
300
- file — e.g. `// PROJ-123 — https://your-org.atlassian.net/browse/PROJ-123`.
301
-
302
- The `it()` **title is never modified**, so Cypress `--spec`/grep and snapshot names stay
303
- stable. Only `http(s)` links are ever produced; anything else falls back to plain text.
304
- Provider selection and field values are stored per provider, so switching trackers keeps
305
- your previous entries. See `docs/specs/014-ticket-reference-and-grouping.md`.
306
-
307
- ---
308
-
309
291
  ### Command previewer
310
292
 
311
293
  The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
@@ -352,7 +334,6 @@ cypress/ ← select this folder
352
334
  4. Interceptors are automatically wrapped in a `beforeEach()`
353
335
  5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
354
336
  - Inside the manual editor, click **🪄 Insertar bloques** to auto-merge the `it()` and `beforeEach()` blocks into the editor content (same placement as the automatic insert), then review the diff before saving — no copy/paste needed. The 📋 copy buttons remain available if you prefer to paste manually.
355
- - The editor is a **CodeMirror** editor with **TS/JS syntax highlighting** and **Cypress autocomplete** (`cy.get`, `.click()`, `.should`, …). CodeMirror is a normal dependency — it installs automatically with the package and is loaded lazily (its own chunk) only when the editor opens; if it can't load, the editor falls back to a plain textarea.
356
337
 
357
338
  **Sidebar toolbar:**
358
339
 
@@ -387,6 +368,21 @@ Options:
387
368
  | `--command` | `npx cypress run --spec {spec}` | Command to run. `{spec}` is replaced by the spec (passed as a single argv, never through a shell). |
388
369
  | `--cwd` | current dir | Working directory the command runs in. |
389
370
  | `--host` | `127.0.0.1` | Bind address (local only). |
371
+ | `--allow-origin` | `http://localhost` | Allowed CORS origin. Any `http://localhost:*` / `http://127.0.0.1:*` origin is always allowed regardless of this flag. Use this only when your app runs on a custom domain or a non-localhost IP. |
372
+
373
+ The allowed origin can also be set via the `RUNNER_ALLOW_ORIGIN` environment variable (CLI flag takes precedence):
374
+
375
+ ```bash
376
+ # Default — app on localhost (any port)
377
+ npx lib-e2e-cypress-runner
378
+
379
+ # App on a custom domain or shared dev IP
380
+ npx lib-e2e-cypress-runner --allow-origin=http://myapp.test
381
+ npx lib-e2e-cypress-runner --allow-origin=http://192.168.1.10:3000
382
+
383
+ # Via env-var (useful for Docker / CI scripts)
384
+ RUNNER_ALLOW_ORIGIN=http://myapp.test npx lib-e2e-cypress-runner
385
+ ```
390
386
 
391
387
  The widget POSTs `{ specPath }` to `http://localhost:8123/run-test`; configure a
392
388
  different endpoint via the `runnerUrl` property of `<file-preview>` if needed.
@@ -689,7 +685,7 @@ class RecordingService {
689
685
 
690
686
  ```typescript
691
687
  class PersistenceService {
692
- insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[], notes?: string, ticketId?: string): Promise<number>;
688
+ insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[]): Promise<number>;
693
689
  getAllTests(): Promise<TestWithDetails[]>;
694
690
  getTestById(testId: number): Promise<TestDetail | null>;
695
691
  deleteTest(id: number): Promise<void>;
@@ -711,6 +707,14 @@ class PersistenceService {
711
707
 
712
708
  ---
713
709
 
710
+ ## Breaking changes
711
+
712
+ ### v0.9.0
713
+
714
+ - **`Subject` removed from public exports.** `Subject<T>` (`src/utils/subject.ts`) is an internal reactive primitive used by the library's services. It was accidentally re-exported from the package root. If your code imports it as `import { Subject } from 'lib-e2e-cypress-for-dummys-ts'`, copy the class into your own codebase or use a reactive library (RxJS, signals, etc.) instead.
715
+
716
+ ---
717
+
714
718
  ## Technical notes
715
719
 
716
720
  - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
@@ -1,3 +1,33 @@
1
+ // src/utils/code-format.utils.ts
2
+ function gcd(a, b) {
3
+ while (b) {
4
+ const t = b;
5
+ b = a % b;
6
+ a = t;
7
+ }
8
+ return a;
9
+ }
10
+ function escapeSingleQuotes(value) {
11
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12
+ }
13
+ function escapeCssAttrValue(value) {
14
+ return value.replace(/"/g, '\\"');
15
+ }
16
+ function normalizeBlock(code, baseIndent) {
17
+ const lines = code.split("\n").map((l) => l.trimEnd());
18
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
19
+ if (!nonEmpty.length) return "";
20
+ const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
21
+ const minIndent = Math.min(...indents);
22
+ const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
23
+ const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
24
+ return lines.map((l) => {
25
+ if (!l.trim()) return "";
26
+ const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
27
+ return baseIndent + " ".repeat(level) + l.trimStart();
28
+ }).join("\n");
29
+ }
30
+
1
31
  // src/utils/selector-quality.utils.ts
2
32
  var FORBIDDEN_ID_PREFIXES = [
3
33
  "cdk-",
@@ -26,11 +56,11 @@ function getSelectorQuality(el) {
26
56
  }
27
57
  function buildPickerSelector(el) {
28
58
  const dataCy = el.getAttribute("data-cy");
29
- if (dataCy) return `[data-cy="${dataCy}"]`;
59
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
30
60
  const dataTestid = el.getAttribute("data-testid");
31
- if (dataTestid) return `[data-testid="${dataTestid}"]`;
61
+ if (dataTestid) return `[data-testid="${escapeCssAttrValue(dataTestid)}"]`;
32
62
  const ariaLabel = el.getAttribute("aria-label");
33
- if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
63
+ if (ariaLabel) return `[aria-label="${escapeCssAttrValue(ariaLabel)}"]`;
34
64
  const rawId = el.id;
35
65
  if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
36
66
  return `#${rawId}`;
@@ -366,7 +396,7 @@ var SelectorPickerElement = class extends HTMLElement {
366
396
  confirm(index) {
367
397
  const row = this.rows[index];
368
398
  if (!row) return;
369
- const command = `cy.get('${row.selector}').click()`;
399
+ const command = `cy.get('${escapeSingleQuotes(row.selector)}').click()`;
370
400
  this.recording.appendCommand(command);
371
401
  this.dispatchEvent(new CustomEvent("selectorchosen", { detail: command, bubbles: true, composed: true }));
372
402
  this.closeSilently();
@@ -384,14 +414,17 @@ var SelectorPickerElement = class extends HTMLElement {
384
414
  this.remove();
385
415
  }
386
416
  };
387
- if (!customElements.get("selector-picker")) {
388
- customElements.define("selector-picker", SelectorPickerElement);
417
+ if (!customElements.get("lib-e2e-selector-picker")) {
418
+ customElements.define("lib-e2e-selector-picker", SelectorPickerElement);
389
419
  }
390
420
 
391
421
  export {
422
+ escapeSingleQuotes,
423
+ escapeCssAttrValue,
424
+ normalizeBlock,
392
425
  FORBIDDEN_ID_PREFIXES,
393
426
  escHtml,
394
427
  escAttr,
395
428
  SelectorPickerElement
396
429
  };
397
- //# sourceMappingURL=chunk-FDMUIHDZ.js.map
430
+ //# sourceMappingURL=chunk-VT2GLZ62.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/code-format.utils.ts","../src/utils/selector-quality.utils.ts","../src/components/selector-picker/selector-picker.styles.ts","../src/utils/html.utils.ts","../src/components/selector-picker/selector-picker.template.ts","../src/components/selector-picker/selector-picker.ts"],"sourcesContent":["function gcd(a: number, b: number): number {\r\n while (b) { const t = b; b = a % b; a = t; }\r\n return a;\r\n}\r\n\r\n/**\r\n * Escapes backslashes and single quotes so a value can be safely embedded inside\r\n * a single-quoted JavaScript string literal — e.g. an `it('…')` / `describe('…')`\r\n * name. Without this, a test named `User's login` produces broken generated JS.\r\n */\r\nexport function escapeSingleQuotes(value: string): string {\r\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\r\n}\r\n\r\n/**\r\n * Escapes double quotes so a value can be safely embedded inside a CSS\r\n * double-quoted attribute selector, e.g. [data-cy=\"VALUE\"].\r\n * The result must subsequently pass through escapeSingleQuotes when it is\r\n * placed inside a single-quoted JS string literal (the JS layer doubles the\r\n * backslash introduced here).\r\n */\r\nexport function escapeCssAttrValue(value: string): string {\r\n return value.replace(/\"/g, '\\\\\"');\r\n}\r\n\r\n/**\r\n * Normalizes indentation in a code block and prefixes every line with baseIndent.\r\n *\r\n * Algorithm:\r\n * 1. Strip trailing whitespace from each line.\r\n * 2. Find the minimum leading-whitespace count of non-empty lines (dedent).\r\n * 3. Compute the indent step as the GCD of all non-zero relative indent values,\r\n * so the original indent unit (4-space, 8-space, etc.) maps to 2-space levels.\r\n * 4. Re-emit each line as: baseIndent + ' '.repeat(level) + trimmedContent.\r\n *\r\n * This handles multi-line interceptor/command blocks that were stored with\r\n * inconsistent or file-origin indentation.\r\n */\r\nexport function normalizeBlock(code: string, baseIndent: string): string {\r\n const lines = code.split('\\n').map(l => l.trimEnd());\r\n const nonEmpty = lines.filter(l => l.trim().length > 0);\r\n if (!nonEmpty.length) return '';\r\n\r\n const indents = nonEmpty.map(l => l.length - l.trimStart().length);\r\n const minIndent = Math.min(...indents);\r\n const relIndents = [...new Set(indents.map(n => n - minIndent).filter(n => n > 0))];\r\n const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;\r\n\r\n return lines\r\n .map(l => {\r\n if (!l.trim()) return '';\r\n const level = Math.round((l.length - l.trimStart().length - minIndent) / step);\r\n return baseIndent + ' '.repeat(level) + l.trimStart();\r\n })\r\n .join('\\n');\r\n}\r\n","import { escapeCssAttrValue } from './code-format.utils';\r\n\r\nexport type SelectorQuality = 'excellent' | 'good' | 'acceptable' | 'poor';\r\n\r\nexport const FORBIDDEN_ID_PREFIXES = [\r\n 'cdk-', 'mat-', 'p-', 'ng-', 'mdc-',\r\n 'primeng-', 'auto-', 'field-', 'input-', 'select-',\r\n];\r\n\r\nexport function getSelectorQuality(el: HTMLElement): SelectorQuality {\r\n if (\r\n el.hasAttribute('data-cy') ||\r\n el.hasAttribute('data-testid') ||\r\n el.hasAttribute('aria-label')\r\n ) {\r\n return 'excellent';\r\n }\r\n\r\n const rawId = el.id;\r\n if (\r\n rawId &&\r\n rawId.length < 25 &&\r\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\r\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\r\n !/^\\d+$/.test(rawId)\r\n ) {\r\n return 'good';\r\n }\r\n\r\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\r\n return 'acceptable';\r\n }\r\n\r\n return 'poor';\r\n}\r\n\r\n/** Returns the best cy.get() selector string for a given element. */\r\nexport function buildPickerSelector(el: HTMLElement): string {\r\n const dataCy = el.getAttribute('data-cy');\r\n if (dataCy) return `[data-cy=\"${escapeCssAttrValue(dataCy)}\"]`;\r\n\r\n const dataTestid = el.getAttribute('data-testid');\r\n if (dataTestid) return `[data-testid=\"${escapeCssAttrValue(dataTestid)}\"]`;\r\n\r\n const ariaLabel = el.getAttribute('aria-label');\r\n if (ariaLabel) return `[aria-label=\"${escapeCssAttrValue(ariaLabel)}\"]`;\r\n\r\n const rawId = el.id;\r\n if (\r\n rawId &&\r\n rawId.length < 25 &&\r\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\r\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\r\n !/^\\d+$/.test(rawId)\r\n ) {\r\n return `#${rawId}`;\r\n }\r\n\r\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\r\n const classes = el.className.trim().split(/\\s+/);\r\n return classes.map((c) => `.${c}`).join('');\r\n }\r\n\r\n return el.tagName.toLowerCase();\r\n}\r\n\r\n/** Human-readable key attribute/selector preview for a picker row (raw, unescaped). */\r\nexport function 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}=\"${v}\"`;\r\n }\r\n if (el.id) return `id=\"${el.id}\"`;\r\n if (el.className && el.className.trim()) {\r\n return el.className.trim().split(/\\s+/).map((c) => `.${c}`).join('');\r\n }\r\n return '';\r\n}\r\n\r\n/** Plain, serializable view-model for one picker row — no DOM held. */\r\nexport interface PickerRow {\r\n quality: SelectorQuality;\r\n selector: string;\r\n tag: string;\r\n attr: string;\r\n}\r\n\r\n/** Reads an element once and returns the plain data a picker row needs to render. */\r\nexport function describePickerRow(el: HTMLElement): PickerRow {\r\n return {\r\n quality: getSelectorQuality(el),\r\n selector: buildPickerSelector(el),\r\n tag: el.tagName.toLowerCase(),\r\n attr: keyAttrDisplay(el),\r\n };\r\n}\r\n","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","export function escHtml(s: string): string {\r\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\r\n}\r\n\r\nexport function escAttr(s: string): string {\r\n return s.replace(/\"/g, '&quot;').replace(/</g, '&lt;');\r\n}\r\n","import type { PickerRow, 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\nexport function renderPickerRows(\r\n rows: PickerRow[],\r\n selectedIndex: number,\r\n t: (key: string) => string,\r\n): string {\r\n return rows.map((row, i) => {\r\n const isSelected = i === selectedIndex;\r\n const isPoor = row.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[row.quality]}\" title=\"${escHtml(t(`SELECTOR_PICKER.QUALITY_${row.quality.toUpperCase()}`))}\">\r\n ${QUALITY_EMOJI[row.quality]}\r\n </span>\r\n <span class=\"tag-name\">&lt;${escHtml(row.tag)}&gt;</span>\r\n ${row.attr ? `<span class=\"attr-value\">${escHtml(row.attr)}</span>` : '<span class=\"attr-value\"></span>'}\r\n <span class=\"selector-preview\">${escHtml(row.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 rows: PickerRow[],\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(rows, 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 { describePickerRow, type PickerRow } from '../../utils/selector-quality.utils';\r\nimport { escapeSingleQuotes } from '../../utils/code-format.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 rows: PickerRow[] = [];\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.rows = this.ancestors.map(describePickerRow);\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.rows.length; i++) {\r\n const q = this.rows[i].quality;\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.rows,\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 row = this.rows[index];\r\n if (!row) return;\r\n const command = `cy.get('${escapeSingleQuotes(row.selector)}').click()`;\r\n this.recording.appendCommand(command);\r\n this.dispatchEvent(new CustomEvent('selectorchosen', { detail: command, bubbles: true, composed: true }));\r\n this.closeSilently();\r\n }\r\n\r\n private cancel(): void {\r\n this.dispatchEvent(new CustomEvent('pickercancelled', { bubbles: true, composed: 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('lib-e2e-selector-picker')) {\r\n customElements.define('lib-e2e-selector-picker', SelectorPickerElement);\r\n}\r\n"],"mappings":"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","names":[]}