lib-e2e-cypress-for-dummys-ts 0.9.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
@@ -354,6 +368,21 @@ Options:
354
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). |
355
369
  | `--cwd` | current dir | Working directory the command runs in. |
356
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
+ ```
357
386
 
358
387
  The widget POSTs `{ specPath }` to `http://localhost:8123/run-test`; configure a
359
388
  different endpoint via the `runnerUrl` property of `<file-preview>` if needed.
@@ -678,6 +707,14 @@ class PersistenceService {
678
707
 
679
708
  ---
680
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
+
681
718
  ## Technical notes
682
719
 
683
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-CQEDHL7A.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":[]}