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 CHANGED
@@ -1,545 +1,654 @@
1
- # lib-e2e-cypress-for-dummys
2
-
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
-
5
- ---
6
-
7
- ## What it does
8
-
9
- Drop the widget into any web app. Press record. Click around. The widget watches every click, input, select, route change, HTTP call, and navigation event, and translates each one into the matching Cypress command in real time. Stop recording, give the test a name, and the code is saved to IndexedDB — ready to copy into your test suite.
10
-
11
- **Zero framework coupling.** It is a plain Custom Element (`<lib-e2e-recorder>`). Angular, React, Vue, plain HTML — anything works.
12
-
13
- ---
14
-
15
- ## Why use it
16
-
17
- | Without this library | With this library |
18
- |---|---|
19
- | Write `cy.get('[data-cy="..."]').click()` by hand | Click in the browser, get the command automatically |
20
- | Manually craft `cy.intercept()` for every API call | Every fetch/XHR is captured and the interceptor is generated |
21
- | Guess which selector attribute to use | Choose the strategy once in settings; the recorder handles the rest |
22
- | Copy-paste commands into `.cy.ts` files | The advanced editor inserts the `it()` block directly into the file |
23
- | Maintain tests in one language | Interface in ES / EN / FR / IT / DE, auto-detected from the browser |
24
-
25
- ---
26
-
27
- ## Installation
28
-
29
- ```bash
30
- npm install lib-e2e-cypress-for-dummys-ts idb sweetalert2
31
- ```
32
-
33
- The package ships three formats:
34
-
35
- | File | Format | Use |
36
- |---|---|---|
37
- | `dist/index.js` | ESM | Modern bundlers (Vite, Angular, etc.) |
38
- | `dist/index.cjs` | CJS | CommonJS environments |
39
- | `dist/index.d.ts` | TypeScript declarations | Type-safe imports |
40
-
41
- `idb` and `sweetalert2` are peer dependencies and must be installed alongside the library.
42
-
43
- ---
44
-
45
- ## Quick start — plain HTML
46
-
47
- ```html
48
- <!DOCTYPE html>
49
- <html>
50
- <head>
51
- <!-- SweetAlert2 CSS (required for modals) -->
52
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2/dist/sweetalert2.min.css" />
53
- </head>
54
- <body>
55
- <!-- Your app content here -->
56
-
57
- <lib-e2e-recorder></lib-e2e-recorder>
58
-
59
- <script type="module">
60
- import 'path/to/dist/index.js';
61
- </script>
62
- </body>
63
- </html>
64
- ```
65
-
66
- The widget appears as a small floating panel. Press **⏺** to start recording.
67
-
68
- ---
69
-
70
- ## Quick start — Angular
71
-
72
- **1. Register the custom element schema** (in `app.config.ts` or the module):
73
-
74
- ```typescript
75
- import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
76
-
77
- // In a standalone component or NgModule:
78
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
79
- ```
80
-
81
- **2. Import the library** in `main.ts` (or any file that is loaded before the widget is used):
82
-
83
- ```typescript
84
- import 'lib-e2e-cypress-for-dummys-ts/dist/index.js';
85
- ```
86
-
87
- **3. Add the element** to any template (typically `AppComponent`):
88
-
89
- ```html
90
- <!-- Only render in non-production environments -->
91
- @if (!isProduction) {
92
- <!-- start-hidden: widget is invisible on load; press Ctrl+Shift+E to reveal it -->
93
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
94
- }
95
- ```
96
-
97
- > The included **example project** (`ejemplo/`) uses `start-hidden` so the floating button does not interfere with the app UI during development. Remove the attribute if you prefer the widget to be visible immediately.
98
- >
99
- > The `start-hidden` attribute **overrides** the value stored in the settings panel. See [Start hidden](#start-hidden-invisible-mode) for the full precedence rules.
100
-
101
- **4. Control it programmatically** (optional):
102
-
103
- ```typescript
104
- import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
105
-
106
- const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
107
-
108
- // Start / stop recording
109
- recorder.toggle();
110
-
111
- // Set the language explicitly
112
- recorder.setLanguage('en');
113
- ```
114
-
115
- ---
116
-
117
- ## Features
118
-
119
- ### Recording interactions
120
-
121
- The recorder automatically captures:
122
-
123
- | Interaction | Generated Cypress command |
124
- |---|---|
125
- | Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
126
- | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
127
- | Select a `<select>` value | `cy.get('[data-cy="country"]').select('ES')` |
128
- | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
129
- | Page load | `cy.visit('/current-path')` |
130
-
131
- Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
132
-
133
- ---
134
-
135
- ### HTTP monitoring
136
-
137
- Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
138
-
139
- For **GET, POST and PUT** requests the recorder generates a pair of commands:
140
-
141
- ```javascript
142
- // Interceptor (placed at the top of the test, in beforeEach)
143
- cy.intercept('GET', '**/api/users').as('get-api-users')
144
-
145
- // Wait command (placed inline with the test actions)
146
- cy.wait('@get-api-users').then((interception) => { })
147
- ```
148
-
149
- **DELETE** requests are intentionally ignored (read-only guard).
150
-
151
- #### Advanced HTTP mode
152
-
153
- Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
154
-
155
- ```javascript
156
- // GET — validates response body fields
157
- cy.wait('@get-api-users').then((interception) => {
158
- if (interception.response) {
159
- expect(interception.response.body.name).to.equal("Alice");
160
- expect(interception.response.body.role).to.equal("admin");
161
- }
162
- })
163
-
164
- // POST / PUT — validates request body fields
165
- cy.wait('@post-api-users').then((interception) => {
166
- expect(interception.request.body.name).to.equal("Alice");
167
- })
168
- ```
169
-
170
- Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
171
-
172
- ---
173
-
174
- ### Selector strategy
175
-
176
- The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
177
-
178
- | Strategy | Generated selector |
179
- |---|---|
180
- | `data-cy` (default) | `[data-cy="login-btn"]` |
181
- | `data-testid` | `[data-testid="login-btn"]` |
182
- | `aria-label` | `[aria-label="Login"]` |
183
- | `id` | `#login-btn` |
184
-
185
- Change the strategy at any time in **⚙️ Config → Estrategia de selector**. The recorder always falls back through the chain (`data-cy` → `id`, etc.) if the preferred attribute is missing on the target element.
186
-
187
- Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
188
-
189
- ---
190
-
191
- ### Smart selector picker
192
-
193
- When the recorder cannot generate a reliable selector for a clicked element (no `data-cy`, `data-testid`, `aria-label`, or clean `id`), the **Smart Selector Picker** appears as an overlay.
194
-
195
- The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
196
-
197
- | Badge | Quality | Criteria |
198
- |---|---|---|
199
- | 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
200
- | 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
201
- | 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
202
- | 🔴 Not recommended | Avoid | Only tag name or inline `style` |
203
-
204
- **How to use:**
205
- 1. The picker auto-selects the best available ancestor
206
- 2. Use **↑ / ↓** to navigate the ancestor list
207
- 3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
208
- 4. Press **Escape** or click outside to dismiss without recording
209
-
210
- The picker also closes automatically when recording is stopped or paused.
211
-
212
- **Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
213
-
214
- ---
215
-
216
- ### Saving and managing tests
217
-
218
- When recording stops, a dialog prompts you to:
219
-
220
- - Give the test a **description** (becomes the `it('…')` label)
221
- - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
222
- - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
223
-
224
- The **📋 Tests** panel lets you:
225
-
226
- - Browse all saved tests filtered by tag
227
- - Expand a test to see its commands and interceptors
228
- - Copy the full `describe()` block to the clipboard with a custom suite name
229
- - Copy just the commands or just the interceptors separately
230
- - Multi-select tests and generate a combined `describe()` block
231
- - Delete individual tests
232
-
233
- ---
234
-
235
- ### Command previewer
236
-
237
- The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
238
-
239
- - Reorder commands with the up/down arrows
240
- - Delete individual commands
241
- - Add a custom assertion with the **assertion builder**
242
-
243
- #### Assertion builder
244
-
245
- The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
246
-
247
- 1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
248
- 2. Choose the assertion type from the dropdown:
249
- - `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
250
- - `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
251
- 3. Click **➕ Añadir**
252
-
253
- ---
254
-
255
- ### Advanced editor (direct file insertion)
256
-
257
- The **📁 Files** panel uses the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) to browse your local Cypress folder and insert tests directly into `.cy.ts` files — no copy-paste needed.
258
-
259
- **Setup (one time):**
260
-
261
- 1. Open **⚙️ Config → Carpeta Cypress**
262
- 2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
263
- 3. The browser stores the permission — you won't be asked again
264
-
265
- **Expected folder structure:**
266
-
267
- ```
268
- cypress/ ← select this folder
269
- └── e2e/ ← the recorder reads .cy.ts files from here
270
- └── login.cy.ts
271
- ```
272
-
273
- **Using the editor:**
274
-
275
- 1. Open **📁 Files** → select a `.cy.ts` file from the tree
276
- 2. The `it()` block for the selected test appears in the panel
277
- 3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
278
- 4. Interceptors are automatically wrapped in a `beforeEach()`
279
- 5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
280
-
281
- **Sidebar toolbar:**
282
-
283
- | Button | Action |
284
- |---|---|
285
- | **+ Nuevo archivo** | Creates a new `.cy.ts` file with a basic `describe/it` scaffold in `cypress/e2e/`. Type the name (no extension needed) and press Enter or click **Crear**. |
286
- | **↻ Actualizar** | Rescans the `cypress/e2e/` directory. Useful after a `git pull`, a drag-and-drop into the folder, or any external file change. |
287
-
288
- > The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
289
-
290
- ---
291
-
292
- ### Recording history
293
-
294
- The last **5 recordings** are automatically saved to `localStorage` so you never lose work if you accidentally close the dialog without saving. Use `recorder.recoverLastRecording()` to restore the most recent recording programmatically.
295
-
296
- ---
297
-
298
- ### Keyboard shortcuts
299
-
300
- | Shortcut | Action |
301
- |---|---|
302
- | `Ctrl + Shift + E` | **Show / hide the widget** (works even when it is invisible) |
303
- | `Ctrl + R` | Start / stop recording |
304
- | `Ctrl + P` | Pause / resume recording |
305
- | `Ctrl + 1` | Open saved tests panel |
306
- | `Ctrl + 2` | Open command previewer |
307
- | `Ctrl + 3` | Open settings |
308
-
309
- > `Ctrl + R` / `Ctrl + P` and the panel shortcuts only fire when the widget is **visible**. `Ctrl + Shift + E` always works regardless of visibility state.
310
-
311
- ---
312
-
313
- ### Invisible mode record in production, stay hidden from users
314
-
315
- Most test recorders force you to choose: use them in a safe dev environment, or ship to real users and lose the tool. **Not this one.**
316
-
317
- With `start-hidden`, you can deploy the recorder to **any environment including production** — and it stays completely invisible to your users. No floating button, no visual clutter, no accidental clicks. When *you* need to record a test against the real app, with real data, real API responses, and real edge cases that are impossible to reproduce locally, just press the secret shortcut and the widget appears. Record, save, hide. Nobody saw a thing.
318
-
319
- ```
320
- Ctrl + Shift + E → show / hide the widget (works even when invisible)
321
- ```
322
-
323
- This means you can:
324
- - **Record against production data** — capture flows that only happen with real users and real back-ends.
325
- - **Debug flaky tests in the wild** reproduce the exact sequence of events that broke your CI pipeline.
326
- - **Onboard QA teams without a local setup** testers open the live app, hit the shortcut, and record directly.
327
- - **Keep staging clean** — no visible dev tooling leaked to clients or stakeholders reviewing the environment.
328
-
329
- #### Option A — HTML attribute (deploy-time, recommended)
330
-
331
- Add `start-hidden` to the element. This takes **precedence over the settings panel** and is the right choice for environment-level control:
332
-
333
- ```html
334
- <!-- Production / staging: invisible by default -->
335
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
336
-
337
- <!-- Dev: always visible (or omit the attribute entirely) -->
338
- <lib-e2e-recorder start-hidden="false"></lib-e2e-recorder>
339
- ```
340
-
341
- Works in every framework:
342
-
343
- ```html
344
- <!-- Angular -->
345
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
346
-
347
- <!-- React (JSX) -->
348
- <lib-e2e-recorder start-hidden="true" />
349
-
350
- <!-- Vue -->
351
- <lib-e2e-recorder start-hidden />
352
- ```
353
-
354
- A typical Angular setup driven by an environment variable:
355
-
356
- ```typescript
357
- // app.component.ts
358
- get startHidden(): boolean {
359
- return environment.production; // invisible in prod, visible in dev
360
- }
361
- ```
362
-
363
- ```html
364
- <!-- app.component.html -->
365
- <lib-e2e-recorder [attr.start-hidden]="startHidden ? '' : null"></lib-e2e-recorder>
366
- ```
367
-
368
- #### Option B Settings panel (runtime, per user)
369
-
370
- Enable **⚙️ Config → 👁 Widget visibility → Start hidden**. The preference is stored in IndexedDB and persists across sessions. Useful when individual team members want to tailor the behaviour without a redeployment.
371
-
372
- ---
373
-
374
- **Either way, when the widget is hidden:**
375
- - Zero DOM rendered, zero event listeners added beyond the keyboard shortcut — it is truly invisible and inert.
376
- - Your Cypress selectors are never blocked by a floating element.
377
- - Press **`Ctrl + Shift + E`** to reveal it instantly, no page reload needed.
378
- - Hide it again with the same shortcut when you are done.
379
-
380
- > **Running under Cypress?** The widget auto-disables itself when `window.Cypress` is detected — it does not render at all, regardless of the `start-hidden` attribute or the settings panel value. No extra setup, no `beforeEach` cleanup, no risk of the widget covering a selector mid-test.
381
-
382
- ---
383
-
384
- ### Import / Export
385
-
386
- Back up all tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
387
-
388
- - **⬆️ Export tests** downloads `e2e-cypress-export.json`
389
- - **⬇️ Import tests** uploads a JSON file and merges it into the current database
390
-
391
- JSON format:
392
-
393
- ```json
394
- {
395
- "tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
396
- "interceptors": []
397
- }
398
- ```
399
-
400
- ---
401
-
402
- ### Internationalisation
403
-
404
- The interface auto-detects the browser language. Supported languages:
405
-
406
- | Code | Language |
407
- |---|---|
408
- | `es` | Spanish (default fallback) |
409
- | `en` | English |
410
- | `fr` | French |
411
- | `it` | Italian |
412
- | `de` | German |
413
-
414
- Override the language programmatically:
415
-
416
- ```typescript
417
- recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
418
- ```
419
-
420
- ---
421
-
422
- ## Public API
423
-
424
- ### `<lib-e2e-recorder>` element
425
-
426
- ```typescript
427
- class LibE2eRecorderElement extends HTMLElement {
428
- // Injected services (auto-created if not set)
429
- recording: RecordingService;
430
- persistence: PersistenceService;
431
- translation: TranslationService;
432
-
433
- // State (read-only in normal use)
434
- isRecording: boolean;
435
- isPaused: boolean;
436
- cypressCommands: string[];
437
- interceptors: string[];
438
-
439
- // Dialog flags
440
- isCommandsDialogOpen: boolean;
441
- isSavedTestsDialogOpen: boolean;
442
- isSaveTestDialogOpen: boolean;
443
- isSettingsDialogOpen: boolean;
444
- isAdvancedEditorDialogOpen: boolean;
445
-
446
- // Methods
447
- toggle(): void; // start / stop recording
448
- togglePause(): void; // pause / resume
449
- setLanguage(lang?: string): void; // set UI language
450
-
451
- showCommandsDialog(): void;
452
- showSavedTestsDialog(): void;
453
- showSaveTestDialog(): void;
454
- showSettingsDialog(): void;
455
- showAdvancedEditorDialog(testId?: number): void;
456
-
457
- getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
458
- recoverLastRecording(): void;
459
- clearRecordingHistory(): void;
460
- }
461
- ```
462
-
463
- ### `RecordingService`
464
-
465
- ```typescript
466
- class RecordingService {
467
- selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
468
-
469
- startRecording(): void;
470
- stopRecording(): void;
471
- pauseRecording(): void;
472
- resumeRecording(): void;
473
- toggleRecording(): void;
474
- togglePause(): void;
475
-
476
- addCommand(cmd: string): void; // respects recording/paused state
477
- appendCommand(cmd: string): void; // always appends (for manual assertions)
478
- removeCommand(index: number): void;
479
- moveCommand(from: number, to: number): void;
480
-
481
- registerInterceptor(method: string, url: string, alias: string): void;
482
- removeInterceptor(index: number): void;
483
-
484
- clearCommands(): void;
485
-
486
- getCommandsSnapshot(): string[];
487
- getInterceptorsSnapshot(): string[];
488
-
489
- onCommandsChange(fn: (cmds: string[]) => void): () => void;
490
- onInterceptorsChange(fn: (ints: string[]) => void): () => void;
491
- onRecordingChange(fn: (isRecording: boolean) => void): () => void;
492
- onPauseChange(fn: (isPaused: boolean) => void): () => void;
493
-
494
- destroy(): void;
495
- }
496
- ```
497
-
498
- ### `PersistenceService`
499
-
500
- ```typescript
501
- class PersistenceService {
502
- insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[]): Promise<number>;
503
- getAllTests(): Promise<TestWithDetails[]>;
504
- getTestById(testId: number): Promise<TestDetail | null>;
505
- deleteTest(id: number): Promise<void>;
506
-
507
- setConfig(config: Record<string, unknown>): Promise<void>;
508
- setConfigKey(key: string, value: unknown): Promise<void>;
509
- getConfig(key: string): Promise<Record<string, unknown> | null>;
510
- getGeneralConfig(): Promise<ConfigRecord | null>;
511
-
512
- clearAllData(): Promise<void>;
513
- requestDirectoryPermissions(): Promise<void>;
514
- }
515
- ```
516
-
517
- ---
518
-
519
- ## Technical notes
520
-
521
- - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
522
- - **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag.
523
- - **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
524
- - **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
525
- - **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
526
-
527
- ---
528
-
529
- ## Development
530
-
531
- ```bash
532
- npm install
533
- npm run build # compile with tsup
534
- npm test # run vitest unit tests
535
- npm run test:coverage # coverage report (≥80% gate)
536
- npm run lint # ESLint
537
- ```
538
-
539
- See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
540
-
541
- ---
542
-
543
- ## Licence
544
-
545
- MIT
1
+ # lib-e2e-cypress-for-dummys
2
+
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
+
5
+ ---
6
+
7
+ ## What it does
8
+
9
+ Drop the widget into any web app. Press record. Click around. The widget watches every click, input, select, route change, HTTP call, and navigation event, and translates each one into the matching Cypress command in real time. Stop recording, give the test a name, and the code is saved to IndexedDB — ready to copy into your test suite.
10
+
11
+ **Zero framework coupling.** It is a plain Custom Element (`<lib-e2e-recorder>`). Angular, React, Vue, plain HTML — anything works.
12
+
13
+ ---
14
+
15
+ ## Why use it
16
+
17
+ | Without this library | With this library |
18
+ |---|---|
19
+ | Write `cy.get('[data-cy="..."]').click()` by hand | Click in the browser, get the command automatically |
20
+ | Manually craft `cy.intercept()` for every API call | Every fetch/XHR is captured and the interceptor is generated |
21
+ | Guess which selector attribute to use | Choose the strategy once in settings; the recorder handles the rest |
22
+ | Copy-paste commands into `.cy.ts` files | The advanced editor inserts the `it()` block directly into the file |
23
+ | Maintain tests in one language | Interface in ES / EN / FR / IT / DE, auto-detected from the browser |
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install lib-e2e-cypress-for-dummys-ts idb sweetalert2
31
+ ```
32
+
33
+ The package ships three formats:
34
+
35
+ | File | Format | Use |
36
+ |---|---|---|
37
+ | `dist/index.js` | ESM | Modern bundlers (Vite, Angular, etc.) |
38
+ | `dist/index.cjs` | CJS | CommonJS environments |
39
+ | `dist/index.d.ts` | TypeScript declarations | Type-safe imports |
40
+
41
+ `idb` and `sweetalert2` are peer dependencies and must be installed alongside the library.
42
+
43
+ ---
44
+
45
+ ## Quick start — plain HTML
46
+
47
+ ```html
48
+ <!DOCTYPE html>
49
+ <html>
50
+ <head>
51
+ <!-- SweetAlert2 CSS (required for modals) -->
52
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2/dist/sweetalert2.min.css" />
53
+ </head>
54
+ <body>
55
+ <!-- Your app content here -->
56
+
57
+ <lib-e2e-recorder></lib-e2e-recorder>
58
+
59
+ <script type="module">
60
+ import 'path/to/dist/index.js';
61
+ </script>
62
+ </body>
63
+ </html>
64
+ ```
65
+
66
+ The widget appears as a small floating panel. Press **⏺** to start recording.
67
+
68
+ ---
69
+
70
+ ## Quick start — Angular
71
+
72
+ **1. Register the custom element schema** (in `app.config.ts` or the module):
73
+
74
+ ```typescript
75
+ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
76
+
77
+ // In a standalone component or NgModule:
78
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
79
+ ```
80
+
81
+ **2. Import the library** in `main.ts` (or any file that is loaded before the widget is used):
82
+
83
+ ```typescript
84
+ import 'lib-e2e-cypress-for-dummys-ts/dist/index.js';
85
+ ```
86
+
87
+ **3. Add the element** to any template (typically `AppComponent`):
88
+
89
+ ```html
90
+ <!-- Only render in non-production environments -->
91
+ @if (!isProduction) {
92
+ <!-- start-hidden: widget is invisible on load; press Ctrl+Shift+E to reveal it -->
93
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
94
+ }
95
+ ```
96
+
97
+ > The included **example project** (`ejemplo/`) uses `start-hidden` so the floating button does not interfere with the app UI during development. Remove the attribute if you prefer the widget to be visible immediately.
98
+ >
99
+ > The `start-hidden` attribute **overrides** the value stored in the settings panel. See [Start hidden](#start-hidden-invisible-mode) for the full precedence rules.
100
+
101
+ **4. Control it programmatically** (optional):
102
+
103
+ ```typescript
104
+ import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
105
+
106
+ const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
107
+
108
+ // Start / stop recording
109
+ recorder.toggle();
110
+
111
+ // Set the language explicitly
112
+ recorder.setLanguage('en');
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Features
118
+
119
+ ### Recording interactions
120
+
121
+ The recorder automatically captures:
122
+
123
+ | Interaction | Generated Cypress command |
124
+ |---|---|
125
+ | Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
126
+ | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
127
+ | Select a `<select>` value | `cy.get('[data-cy="country"]').select('ES')` |
128
+ | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
129
+ | Page load | `cy.visit('/current-path')` |
130
+
131
+ Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
132
+
133
+ ---
134
+
135
+ ### HTTP monitoring
136
+
137
+ Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
138
+
139
+ For **GET, POST and PUT** requests the recorder generates a pair of commands:
140
+
141
+ ```javascript
142
+ // Interceptor (placed at the top of the test, in beforeEach)
143
+ cy.intercept('GET', '**/api/users').as('get-api-users')
144
+
145
+ // Wait command (placed inline with the test actions)
146
+ cy.wait('@get-api-users').then((interception) => { })
147
+ ```
148
+
149
+ **DELETE** requests are intentionally ignored (read-only guard).
150
+
151
+ #### Advanced HTTP mode
152
+
153
+ Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
154
+
155
+ ```javascript
156
+ // GET — validates response body fields
157
+ cy.wait('@get-api-users').then((interception) => {
158
+ if (interception.response) {
159
+ expect(interception.response.body.name).to.equal("Alice");
160
+ expect(interception.response.body.role).to.equal("admin");
161
+ }
162
+ })
163
+
164
+ // POST / PUT — validates request body fields
165
+ cy.wait('@post-api-users').then((interception) => {
166
+ expect(interception.request.body.name).to.equal("Alice");
167
+ })
168
+ ```
169
+
170
+ Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
171
+
172
+ ---
173
+
174
+ ### Selector strategy
175
+
176
+ The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
177
+
178
+ | Strategy | Generated selector |
179
+ |---|---|
180
+ | `data-cy` (default) | `[data-cy="login-btn"]` |
181
+ | `data-testid` | `[data-testid="login-btn"]` |
182
+ | `aria-label` | `[aria-label="Login"]` |
183
+ | `id` | `#login-btn` |
184
+
185
+ Change the strategy at any time in **⚙️ Config → Estrategia de selector**. The recorder always falls back through the chain (`data-cy` → `id`, etc.) if the preferred attribute is missing on the target element.
186
+
187
+ Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
188
+
189
+ ---
190
+
191
+ ### Smart selector picker
192
+
193
+ When the recorder cannot generate a reliable selector for a clicked element (no `data-cy`, `data-testid`, `aria-label`, or clean `id`), the **Smart Selector Picker** appears as an overlay.
194
+
195
+ The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
196
+
197
+ | Badge | Quality | Criteria |
198
+ |---|---|---|
199
+ | 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
200
+ | 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
201
+ | 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
202
+ | 🔴 Not recommended | Avoid | Only tag name or inline `style` |
203
+
204
+ **How to use:**
205
+ 1. The picker auto-selects the best available ancestor
206
+ 2. Use **↑ / ↓** to navigate the ancestor list
207
+ 3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
208
+ 4. Press **Escape** or click outside to dismiss without recording
209
+
210
+ The picker also closes automatically when recording is stopped or paused.
211
+
212
+ **Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
213
+
214
+ ---
215
+
216
+ ### Saving and managing tests
217
+
218
+ When recording stops, a dialog prompts you to:
219
+
220
+ - Give the test a **description** (becomes the `it('…')` label)
221
+ - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
222
+ - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
223
+
224
+ The **📋 Tests** panel lets you:
225
+
226
+ - Browse all saved tests filtered by tag
227
+ - Expand a test to see its commands and interceptors
228
+ - Copy the full `describe()` block to the clipboard with a custom suite name
229
+ - Copy just the commands or just the interceptors separately
230
+ - Multi-select tests and generate a combined `describe()` block
231
+ - Delete individual tests
232
+
233
+ ---
234
+
235
+ ### Command previewer
236
+
237
+ The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
238
+
239
+ - Reorder commands with the up/down arrows
240
+ - Delete individual commands
241
+ - Add a custom assertion with the **assertion builder**
242
+
243
+ #### Assertion builder
244
+
245
+ The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
246
+
247
+ 1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
248
+ 2. Choose the assertion type from the dropdown:
249
+ - `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
250
+ - `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
251
+ 3. Click **➕ Añadir**
252
+
253
+ ---
254
+
255
+ ### Advanced editor (direct file insertion)
256
+
257
+ The **📁 Files** panel uses the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) to browse your local Cypress folder and insert tests directly into `.cy.ts` files — no copy-paste needed.
258
+
259
+ **Setup (one time):**
260
+
261
+ 1. Open **⚙️ Config → Carpeta Cypress**
262
+ 2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
263
+ 3. The browser stores the permission — you won't be asked again
264
+
265
+ **Expected folder structure:**
266
+
267
+ ```
268
+ cypress/ ← select this folder
269
+ └── e2e/ ← the recorder reads .cy.ts files from here
270
+ └── login.cy.ts
271
+ ```
272
+
273
+ **Using the editor:**
274
+
275
+ 1. Open **📁 Files** → select a `.cy.ts` file from the tree
276
+ 2. The `it()` block for the selected test appears in the panel
277
+ 3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
278
+ 4. Interceptors are automatically wrapped in a `beforeEach()`
279
+ 5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
280
+ - 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.
281
+
282
+ **Sidebar toolbar:**
283
+
284
+ | Button | Action |
285
+ |---|---|
286
+ | **+ Nuevo archivo** | Creates a new `.cy.ts` file with a basic `describe/it` scaffold in `cypress/e2e/`. Type the name (no extension needed) and press Enter or click **Crear**. |
287
+ | **+ Nueva carpeta** | Creates a new folder in `cypress/e2e/`. Type the name and press Enter or click **Crear**. Path separators are stripped from the name. |
288
+ | **↻ Actualizar** | Rescans the `cypress/e2e/` directory. Useful after a `git pull`, a drag-and-drop into the folder, or any external file change. |
289
+
290
+ > The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
291
+
292
+ ---
293
+
294
+ ### Running a spec from the editor (local runner)
295
+
296
+ Inside the manual editor (**✏️ Editar manualmente**) the **▶ Lanzar test** button runs
297
+ **only the spec you are editing**, headless (no Cypress GUI), and shows the result
298
+ (pass/fail + output) right in the editor — for a fast record → tweak → run loop.
299
+
300
+ The browser can't spawn Cypress, so it talks to a tiny **local runner** over HTTP.
301
+ Start it once in your Cypress project:
302
+
303
+ ```bash
304
+ npx lib-e2e-cypress-runner # listens on http://127.0.0.1:8123
305
+ ```
306
+
307
+ Options:
308
+
309
+ | Flag | Default | Description |
310
+ |---|---|---|
311
+ | `--port` | `8123` | Port to listen on. |
312
+ | `--command` | `npx cypress run --spec {spec}` | Command to run. `{spec}` is replaced by the spec (passed as a single argv, never through a shell). |
313
+ | `--cwd` | current dir | Working directory the command runs in. |
314
+ | `--host` | `127.0.0.1` | Bind address (local only). |
315
+
316
+ The widget POSTs `{ specPath }` to `http://localhost:8123/run-test`; configure a
317
+ different endpoint via the `runnerUrl` property of `<file-preview>` if needed.
318
+
319
+ Notes:
320
+ - The button is **enabled only when the app is served from localhost**. On a deployed
321
+ environment it is disabled with a *"muévelo a local para poder probar"* hint —
322
+ running a local Cypress against a remote build makes no sense.
323
+ - If no runner is reachable you get a clear "no runner detected" message (no silent
324
+ failure).
325
+ - The runner is a **dev-only** tool: it binds to `127.0.0.1` and passes the spec as an
326
+ argument (no shell interpolation). Don't expose it beyond your machine.
327
+
328
+ ---
329
+
330
+ ### Recording history
331
+
332
+ The last **5 recordings** are automatically saved to `localStorage` so you never lose work if you accidentally close the dialog without saving. Use `recorder.recoverLastRecording()` to restore the most recent recording programmatically.
333
+
334
+ ---
335
+
336
+ ### Cross-app recording (single-spa / Module Federation)
337
+
338
+ Recording a flow that spans **several micro-frontends** under one domain (login app → dashboard app → admin app) used to be impossible: navigating from one project to another tore the widget down and wiped the in-progress recording. Now the **live recording session is persisted** (in IndexedDB, same origin), so it **survives the crossing** and recording continues seamlessly — the captured commands and interceptors are all still there.
339
+
340
+ It also survives an accidental **page reload** of the same origin: the recording is recovered automatically.
341
+
342
+ > **Scope:** this covers micro-frontends served from the **same origin** (same scheme + host + port, the typical single-spa setup) with **client-side** navigation. Different origins/subdomains and cross-origin `cy.origin` test generation are out of scope (see `docs/specs/006-cross-app-recording-continuity.md`).
343
+
344
+ #### Where to mount the widget
345
+
346
+ Pick **exactly one** placement — never both, or the HTTP monitor installs twice and commands get double-recorded:
347
+
348
+ - **Option A — a single instance in the shell (recommended).** Mount one `<lib-e2e-recorder>` in the shell/root application. The shell is not unmounted on app swaps, so a recording naturally spans every micro-frontend. Simplest and most robust.
349
+ - **Option B — one instance per sub-project, never in the shell.** Each micro-frontend mounts its own widget; continuity across crossings is provided by the persisted session. Do **not** also put one in the shell. *Caveat:* during a single-spa transition two apps can be briefly mounted at once, so an API call fired in that window may produce a duplicate `cy.wait` — just delete the stray line, or prefer Option A.
350
+
351
+ #### Resuming
352
+
353
+ When the recorder loads and finds an active recording session:
354
+
355
+ - **Recent** (within the resume window) → it **continues silently**.
356
+ - **Stale** (older than the window) → it asks **continue or discard**, so a forgotten session never resumes silently (important when deployed with `start-hidden`).
357
+
358
+ The resume window defaults to **30 minutes** and is editable in **⚙️ Config → ⏱ Recording continuity**. Crossing apps is instant, so it is always "recent"; the window only gates the come-back-later case.
359
+
360
+ Stopping a recording (save **or** discard) ends the session, so a finished recording never resurrects on the next navigation.
361
+
362
+ ---
363
+
364
+ ### Moving the widget (draggable)
365
+
366
+ If the floating widget covers a control you need, **drag the record button (⏺/⏹)**
367
+ to move the whole widget anywhere on screen. Press-and-move to drag; a plain click
368
+ still starts/stops recording (a ~5 px threshold tells the two apart).
369
+
370
+ The radial menu **expands toward the centre of the screen**, so the action buttons
371
+ stay fully visible wherever you drop it, and the position is **remembered** across
372
+ reloads (and app crossings). Reset it to the default corner from **⚙️ Config →
373
+ 🧲 Widget position → Reset position**, or programmatically:
374
+
375
+ ```typescript
376
+ recorder.resetWidgetPosition();
377
+ ```
378
+
379
+ ---
380
+
381
+ ### Keyboard shortcuts
382
+
383
+ | Shortcut | Action |
384
+ |---|---|
385
+ | `Ctrl + Shift + E` | **Show / hide the widget** (works even when it is invisible) |
386
+ | `Ctrl + R` | Start / stop recording |
387
+ | `Ctrl + P` | Pause / resume recording |
388
+ | `Ctrl + 1` | Open saved tests panel |
389
+ | `Ctrl + 2` | Open command previewer |
390
+ | `Ctrl + 3` | Open settings |
391
+
392
+ > `Ctrl + R` / `Ctrl + P` and the panel shortcuts only fire when the widget is **visible**. `Ctrl + Shift + E` always works regardless of visibility state.
393
+
394
+ ---
395
+
396
+ ### Invisible mode — record in production, stay hidden from users
397
+
398
+ Most test recorders force you to choose: use them in a safe dev environment, or ship to real users and lose the tool. **Not this one.**
399
+
400
+ With `start-hidden`, you can deploy the recorder to **any environment — including production** — and it stays completely invisible to your users. No floating button, no visual clutter, no accidental clicks. When *you* need to record a test against the real app, with real data, real API responses, and real edge cases that are impossible to reproduce locally, just press the secret shortcut and the widget appears. Record, save, hide. Nobody saw a thing.
401
+
402
+ ```
403
+ Ctrl + Shift + E → show / hide the widget (works even when invisible)
404
+ ```
405
+
406
+ This means you can:
407
+ - **Record against production data** — capture flows that only happen with real users and real back-ends.
408
+ - **Debug flaky tests in the wild** — reproduce the exact sequence of events that broke your CI pipeline.
409
+ - **Onboard QA teams without a local setup** — testers open the live app, hit the shortcut, and record directly.
410
+ - **Keep staging clean** — no visible dev tooling leaked to clients or stakeholders reviewing the environment.
411
+
412
+ #### Option A HTML attribute (deploy-time, recommended)
413
+
414
+ Add `start-hidden` to the element. This takes **precedence over the settings panel** and is the right choice for environment-level control:
415
+
416
+ ```html
417
+ <!-- Production / staging: invisible by default -->
418
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
419
+
420
+ <!-- Dev: always visible (or omit the attribute entirely) -->
421
+ <lib-e2e-recorder start-hidden="false"></lib-e2e-recorder>
422
+ ```
423
+
424
+ Works in every framework:
425
+
426
+ ```html
427
+ <!-- Angular -->
428
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
429
+
430
+ <!-- React (JSX) -->
431
+ <lib-e2e-recorder start-hidden="true" />
432
+
433
+ <!-- Vue -->
434
+ <lib-e2e-recorder start-hidden />
435
+ ```
436
+
437
+ A typical Angular setup driven by an environment variable:
438
+
439
+ ```typescript
440
+ // app.component.ts
441
+ get startHidden(): boolean {
442
+ return environment.production; // invisible in prod, visible in dev
443
+ }
444
+ ```
445
+
446
+ ```html
447
+ <!-- app.component.html -->
448
+ <lib-e2e-recorder [attr.start-hidden]="startHidden ? '' : null"></lib-e2e-recorder>
449
+ ```
450
+
451
+ #### Option B — Settings panel (runtime, per user)
452
+
453
+ Enable **⚙️ Config → 👁 Widget visibility → Start hidden**. The preference is stored in IndexedDB and persists across sessions. Useful when individual team members want to tailor the behaviour without a redeployment.
454
+
455
+ ---
456
+
457
+ **Either way, when the widget is hidden:**
458
+ - Zero DOM rendered, zero event listeners added beyond the keyboard shortcut — it is truly invisible and inert.
459
+ - Your Cypress selectors are never blocked by a floating element.
460
+ - Press **`Ctrl + Shift + E`** to reveal it instantly, no page reload needed.
461
+ - Hide it again with the same shortcut when you are done.
462
+
463
+ > **Running under Cypress?** The widget auto-disables itself when `window.Cypress` is detected — it does not render at all, regardless of the `start-hidden` attribute or the settings panel value. No extra setup, no `beforeEach` cleanup, no risk of the widget covering a selector mid-test.
464
+
465
+ ---
466
+
467
+ ### Import / Export
468
+
469
+ Back up tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
470
+
471
+ - **⬆️ Export tests** — opens a dialog to choose **what** to export, then downloads `e2e-cypress-export.json`:
472
+ - **Todo** — every saved test.
473
+ - **Selección manual** — tick individual tests (multiple allowed); only the checked ones are exported.
474
+ - **Por tags** — pick one or more tags; exports every test carrying **at least one** of them (OR). The dialog lists the matching tests so you can see exactly what will be exported before confirming.
475
+
476
+ A live count shows how many tests the current selection will export, and the export button is disabled when that count is `0`.
477
+ - **⬇️ Import tests** uploads a JSON file and **merges** its tests into the current database (existing tests are kept, never wiped).
478
+
479
+ The output format and filename are identical in every mode, so any exported file (full or partial) can be re-imported.
480
+
481
+ JSON format:
482
+
483
+ ```json
484
+ {
485
+ "tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
486
+ "interceptors": []
487
+ }
488
+ ```
489
+
490
+ ---
491
+
492
+ ### Internationalisation
493
+
494
+ The interface auto-detects the browser language. Supported languages:
495
+
496
+ | Code | Language |
497
+ |---|---|
498
+ | `es` | Spanish (default fallback) |
499
+ | `en` | English |
500
+ | `fr` | French |
501
+ | `it` | Italian |
502
+ | `de` | German |
503
+
504
+ Override the language programmatically:
505
+
506
+ ```typescript
507
+ recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Public API
513
+
514
+ ### `<lib-e2e-recorder>` element
515
+
516
+ ```typescript
517
+ class LibE2eRecorderElement extends HTMLElement {
518
+ // Injected services (auto-created if not set)
519
+ recording: RecordingService;
520
+ persistence: PersistenceService;
521
+ translation: TranslationService;
522
+
523
+ // State (read-only in normal use)
524
+ isRecording: boolean;
525
+ isPaused: boolean;
526
+ cypressCommands: string[];
527
+ interceptors: string[];
528
+
529
+ // Dialog flags
530
+ isCommandsDialogOpen: boolean;
531
+ isSavedTestsDialogOpen: boolean;
532
+ isSaveTestDialogOpen: boolean;
533
+ isSettingsDialogOpen: boolean;
534
+ isAdvancedEditorDialogOpen: boolean;
535
+
536
+ // Methods
537
+ toggle(): void; // start / stop recording
538
+ togglePause(): void; // pause / resume
539
+ setLanguage(lang?: string): void; // set UI language
540
+
541
+ showCommandsDialog(): void;
542
+ showSavedTestsDialog(): void;
543
+ showSaveTestDialog(): void;
544
+ showSettingsDialog(): void;
545
+ showAdvancedEditorDialog(testId?: number): void;
546
+
547
+ getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
548
+ recoverLastRecording(): void;
549
+ clearRecordingHistory(): void;
550
+
551
+ // Cross-app session (micro-frontends)
552
+ hasActiveSession(): boolean; // is a recording session persisted/active?
553
+ resumeSession(): void; // rehydrate the persisted session
554
+ discardSession(): void; // drop the persisted session
555
+
556
+ // Draggable widget
557
+ resetWidgetPosition(): void; // move the widget back to its default corner
558
+ }
559
+ ```
560
+
561
+ ### `RecordingService`
562
+
563
+ ```typescript
564
+ class RecordingService {
565
+ selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
566
+
567
+ startRecording(): void;
568
+ stopRecording(): void;
569
+ pauseRecording(): void;
570
+ resumeRecording(): void;
571
+ toggleRecording(): void;
572
+ togglePause(): void;
573
+
574
+ addCommand(cmd: string): void; // respects recording/paused state
575
+ appendCommand(cmd: string): void; // always appends (for manual assertions)
576
+ removeCommand(index: number): void;
577
+ moveCommand(from: number, to: number): void;
578
+
579
+ registerInterceptor(method: string, url: string, alias: string): void;
580
+ removeInterceptor(index: number): void;
581
+
582
+ clearCommands(): void;
583
+
584
+ // Cross-app session (micro-frontends) — see spec 006
585
+ sessionId: string | null;
586
+ restoreSession(state: ActiveSessionState): void; // rehydrate WITHOUT re-running the bootstrap
587
+ getSessionSnapshot(): ActiveSessionState;
588
+ onSessionChange(fn: (state: ActiveSessionState) => void): () => void;
589
+
590
+ getCommandsSnapshot(): string[];
591
+ getInterceptorsSnapshot(): string[];
592
+
593
+ onCommandsChange(fn: (cmds: string[]) => void): () => void;
594
+ onInterceptorsChange(fn: (ints: string[]) => void): () => void;
595
+ onRecordingChange(fn: (isRecording: boolean) => void): () => void;
596
+ onPauseChange(fn: (isPaused: boolean) => void): () => void;
597
+
598
+ destroy(): void;
599
+ }
600
+ ```
601
+
602
+ ### `PersistenceService`
603
+
604
+ ```typescript
605
+ class PersistenceService {
606
+ insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[]): Promise<number>;
607
+ getAllTests(): Promise<TestWithDetails[]>;
608
+ getTestById(testId: number): Promise<TestDetail | null>;
609
+ deleteTest(id: number): Promise<void>;
610
+
611
+ setConfig(config: Record<string, unknown>): Promise<void>;
612
+ setConfigKey(key: string, value: unknown): Promise<void>;
613
+ getConfig(key: string): Promise<Record<string, unknown> | null>;
614
+ getGeneralConfig(): Promise<ConfigRecord | null>;
615
+
616
+ // Live recording session (cross-app continuity — see spec 006)
617
+ saveActiveSession(state: ActiveSessionState): Promise<void>;
618
+ getActiveSession(): Promise<ActiveSessionState | null>;
619
+ clearActiveSession(): Promise<void>;
620
+
621
+ clearAllData(): Promise<void>;
622
+ requestDirectoryPermissions(): Promise<void>;
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Technical notes
629
+
630
+ - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
631
+ - **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag.
632
+ - **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
633
+ - **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
634
+ - **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
635
+
636
+ ---
637
+
638
+ ## Development
639
+
640
+ ```bash
641
+ npm install
642
+ npm run build # compile with tsup
643
+ npm test # run vitest unit tests
644
+ npm run test:coverage # coverage report (≥80% gate)
645
+ npm run lint # ESLint
646
+ ```
647
+
648
+ See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
649
+
650
+ ---
651
+
652
+ ## Licence
653
+
654
+ MIT