lib-e2e-cypress-for-dummys-ts 1.0.0 → 1.1.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,767 +1,771 @@
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
- > **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
-
7
- ---
8
-
9
- ## What it does
10
-
11
- 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.
12
-
13
- **Zero framework coupling.** It is a plain Custom Element (`<lib-e2e-recorder>`). Angular, React, Vue, plain HTML — anything works.
14
-
15
- ---
16
-
17
- ## Why use it
18
-
19
- | Without this library | With this library |
20
- |---|---|
21
- | Write `cy.get('[data-cy="..."]').click()` by hand | Click in the browser, get the command automatically |
22
- | Manually craft `cy.intercept()` for every API call | Every fetch/XHR is captured and the interceptor is generated |
23
- | Guess which selector attribute to use | Choose the strategy once in settings; the recorder handles the rest |
24
- | Copy-paste commands into `.cy.ts` files | The advanced editor inserts the `it()` block directly into the file |
25
- | Maintain tests in one language | Interface in ES / EN / FR / IT / DE, auto-detected from the browser |
26
-
27
- ---
28
-
29
- ## Installation
30
-
31
- ```bash
32
- npm install lib-e2e-cypress-for-dummys-ts idb sweetalert2
33
- ```
34
-
35
- The package ships three formats:
36
-
37
- | File | Format | Use |
38
- |---|---|---|
39
- | `dist/index.js` | ESM | Modern bundlers (Vite, Angular, etc.) |
40
- | `dist/index.cjs` | CJS | CommonJS environments |
41
- | `dist/index.d.ts` | TypeScript declarations | Type-safe imports |
42
-
43
- `idb` and `sweetalert2` are peer dependencies and must be installed alongside the library.
44
-
45
- ---
46
-
47
- ## Quick start — plain HTML
48
-
49
- ```html
50
- <!DOCTYPE html>
51
- <html>
52
- <head>
53
- <!-- SweetAlert2 CSS (required for modals) -->
54
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2/dist/sweetalert2.min.css" />
55
- </head>
56
- <body>
57
- <!-- Your app content here -->
58
-
59
- <lib-e2e-recorder></lib-e2e-recorder>
60
-
61
- <script type="module">
62
- import 'path/to/dist/index.js';
63
- </script>
64
- </body>
65
- </html>
66
- ```
67
-
68
- The widget appears as a small floating panel. Press **⏺** to start recording.
69
-
70
- ---
71
-
72
- ## Quick start — Angular
73
-
74
- **1. Register the custom element schema** (in `app.config.ts` or the module):
75
-
76
- ```typescript
77
- import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
78
-
79
- // In a standalone component or NgModule:
80
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
81
- ```
82
-
83
- **2. Import the library** in `main.ts` (or any file that is loaded before the widget is used):
84
-
85
- ```typescript
86
- import 'lib-e2e-cypress-for-dummys-ts/dist/index.js';
87
- ```
88
-
89
- **3. Add the element** to any template (typically `AppComponent`):
90
-
91
- ```html
92
- <!-- Only render in non-production environments -->
93
- @if (!isProduction) {
94
- <!-- start-hidden: widget is invisible on load; press Ctrl+Shift+E to reveal it -->
95
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
96
- }
97
- ```
98
-
99
- > 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.
100
- >
101
- > The `start-hidden` attribute **overrides** the value stored in the settings panel. See [Start hidden](#start-hidden-invisible-mode) for the full precedence rules.
102
-
103
- **4. Control it programmatically** (optional):
104
-
105
- ```typescript
106
- import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
107
-
108
- const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
109
-
110
- // Start / stop recording
111
- recorder.toggle();
112
-
113
- // Set the language explicitly
114
- recorder.setLanguage('en');
115
- ```
116
-
117
- ---
118
-
119
- ## Features
120
-
121
- ### Recording interactions
122
-
123
- The recorder automatically captures:
124
-
125
- | Interaction | Generated Cypress command |
126
- |---|---|
127
- | Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
128
- | Double-click | `cy.get('[data-cy="row"]').dblclick()` |
129
- | Right-click | `cy.get('[data-cy="ctx"]').rightclick()` |
130
- | Check / uncheck a checkbox | `cy.get('[data-cy="agree"]').check()` / `.uncheck()` |
131
- | Pick a radio | `cy.get('[data-cy="plan"]').check()` |
132
- | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
133
- | Press Enter / Escape in a field | `cy.get('[data-cy="q"]').type('{enter}')` / `type('{esc}')` |
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'])` |
137
- | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
138
- | Page load | `cy.visit('/current-path')` |
139
-
140
- Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
141
-
142
- #### Capturing assertions while recording — Alt+click
143
-
144
- A test that only clicks around but never checks anything is weak. While recording,
145
- hold **Alt** and click any element to record a **Cypress assertion** for it instead
146
- of a click — and the element's real action is **suppressed** (no navigation, no form
147
- submit), so asserting never changes your flow.
148
-
149
- The assertion is chosen automatically from the element:
150
-
151
- | Element | Generated assertion |
152
- |---|---|
153
- | Checkbox / radio | `.should('be.checked')` / `.should('not.be.checked')` |
154
- | Input / textarea / select with a value | `.should('have.value', '<value>')` |
155
- | Element with visible text | `.should('contain.text', '<text>')` |
156
- | Anything else | `.should('be.visible')` |
157
-
158
- The captured assertion appears in the **⌨️ Commands** previewer like any other
159
- command, so you can reorder or delete it. (You can still build assertions manually
160
- with the assertion builder — see below.)
161
-
162
- ---
163
-
164
- ### HTTP monitoring
165
-
166
- Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
167
-
168
- For **GET, POST and PUT** requests the recorder generates a pair of commands:
169
-
170
- ```javascript
171
- // Interceptor (placed at the top of the test, in beforeEach)
172
- cy.intercept('GET', '**/api/users').as('get-api-users')
173
-
174
- // Wait command (placed inline with the test actions)
175
- cy.wait('@get-api-users').then((interception) => { })
176
- ```
177
-
178
- **DELETE** requests are intentionally ignored (read-only guard).
179
-
180
- #### Advanced HTTP mode
181
-
182
- Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
183
-
184
- ```javascript
185
- // GET — validates response body fields
186
- cy.wait('@get-api-users').then((interception) => {
187
- if (interception.response) {
188
- expect(interception.response.body.name).to.equal("Alice");
189
- expect(interception.response.body.role).to.equal("admin");
190
- }
191
- })
192
-
193
- // POST / PUT — validates request body fields
194
- cy.wait('@post-api-users').then((interception) => {
195
- expect(interception.request.body.name).to.equal("Alice");
196
- })
197
- ```
198
-
199
- Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
200
-
201
- #### Fixture mode (deterministic stubs)
202
-
203
- Enable **🧪 Fixtures HTTP** in the settings panel to turn GET responses into
204
- **Cypress fixtures + stubs** instead of spies. While recording, each GET with a
205
- JSON body is saved as `cypress/fixtures/<alias>.json` and its interceptor becomes:
206
-
207
- ```javascript
208
- cy.intercept('GET', '**/api/users', { fixture: 'get-api-users.json' }).as('get-api-users')
209
- cy.wait('@get-api-users').then((interception) => { })
210
- ```
211
-
212
- so the test replays deterministic data with **no backend**. When you save the
213
- test, the fixture files are written into your `cypress/fixtures/` folder
214
- automatically (requires the Cypress folder configured — see the advanced editor
215
- setup). POST/PUT keep the spy + body-validation behaviour; non-JSON GETs fall back
216
- to a spy.
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
-
228
- ---
229
-
230
- ### Selector strategy
231
-
232
- The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
233
-
234
- | Strategy | Generated selector |
235
- |---|---|
236
- | `data-cy` (default) | `[data-cy="login-btn"]` |
237
- | `data-testid` | `[data-testid="login-btn"]` |
238
- | `aria-label` | `[aria-label="Login"]` |
239
- | `id` | `#login-btn` |
240
-
241
- 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.
242
-
243
- Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
244
-
245
- ---
246
-
247
- ### Smart selector picker
248
-
249
- 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.
250
-
251
- The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
252
-
253
- | Badge | Quality | Criteria |
254
- |---|---|---|
255
- | 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
256
- | 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
257
- | 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
258
- | 🔴 Not recommended | Avoid | Only tag name or inline `style` |
259
-
260
- **How to use:**
261
- 1. The picker auto-selects the best available ancestor
262
- 2. Use **↑ / ↓** to navigate the ancestor list
263
- 3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
264
- 4. Press **Escape** or click outside to dismiss without recording
265
-
266
- The picker also closes automatically when recording is stopped or paused.
267
-
268
- **Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
269
-
270
- ---
271
-
272
- ### Saving and managing tests
273
-
274
- When recording stops, a dialog prompts you to:
275
-
276
- - Give the test a **description** (becomes the `it('…')` label)
277
- - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
278
- - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
279
-
280
- The **📋 Tests** panel lets you:
281
-
282
- - Browse all saved tests filtered by tag
283
- - Expand a test to see its commands and interceptors
284
- - Copy the full `describe()` block to the clipboard with a custom suite name
285
- - Copy just the commands or just the interceptors separately
286
- - Multi-select tests and generate a combined `describe()` block
287
- - Delete individual tests
288
-
289
- ---
290
-
291
- ### Command previewer
292
-
293
- The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
294
-
295
- - Reorder commands with the up/down arrows
296
- - Delete individual commands
297
- - Add a custom assertion with the **assertion builder**
298
-
299
- #### Assertion builder
300
-
301
- The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
302
-
303
- 1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
304
- 2. Choose the assertion type from the dropdown:
305
- - `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
306
- - `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
307
- 3. Click **➕ Añadir**
308
-
309
- ---
310
-
311
- ### Advanced editor (direct file insertion)
312
-
313
- 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.
314
-
315
- **Setup (one time):**
316
-
317
- 1. Open **⚙️ Config → Carpeta Cypress**
318
- 2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
319
- 3. The browser stores the permission — you won't be asked again
320
-
321
- **Expected folder structure:**
322
-
323
- ```
324
- cypress/ ← select this folder
325
- └── e2e/ ← the recorder reads .cy.ts files from here
326
- └── login.cy.ts
327
- ```
328
-
329
- **Using the editor:**
330
-
331
- 1. Open **📁 Files** → select a `.cy.ts` file from the tree
332
- 2. The `it()` block for the selected test appears in the panel
333
- 3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
334
- 4. Interceptors are automatically wrapped in a `beforeEach()`
335
- 5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
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.
337
-
338
- **Sidebar toolbar:**
339
-
340
- | Button | Action |
341
- |---|---|
342
- | **+ 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**. |
343
- | **+ 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. |
344
- | **↻ Actualizar** | Rescans the `cypress/e2e/` directory. Useful after a `git pull`, a drag-and-drop into the folder, or any external file change. |
345
-
346
- > The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
347
-
348
- ---
349
-
350
- ### Running a spec from the editor (local runner)
351
-
352
- Inside the manual editor (**✏️ Editar manualmente**) the **▶ Lanzar test** button runs
353
- **only the spec you are editing**, headless (no Cypress GUI), and shows the result
354
- (pass/fail + output) right in the editor — for a fast record → tweak → run loop.
355
-
356
- The browser can't spawn Cypress, so it talks to a tiny **local runner** over HTTP.
357
- Start it once in your Cypress project:
358
-
359
- ```bash
360
- npx lib-e2e-cypress-runner # listens on http://127.0.0.1:8123
361
- ```
362
-
363
- Options:
364
-
365
- | Flag | Default | Description |
366
- |---|---|---|
367
- | `--port` | `8123` | Port to listen on. |
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). |
369
- | `--cwd` | current dir | Working directory the command runs in. |
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
- ```
386
-
387
- The widget POSTs `{ specPath }` to `http://localhost:8123/run-test`; configure a
388
- different endpoint via the `runnerUrl` property of `<file-preview>` if needed.
389
-
390
- Notes:
391
- - The button is **enabled only when the app is served from localhost**. On a deployed
392
- environment it is disabled with a *"muévelo a local para poder probar"* hint —
393
- running a local Cypress against a remote build makes no sense.
394
- - If no runner is reachable you get a clear "no runner detected" message (no silent
395
- failure).
396
- - The runner is a **dev-only** tool: it binds to `127.0.0.1` and passes the spec as an
397
- argument (no shell interpolation). Don't expose it beyond your machine.
398
-
399
- ---
400
-
401
- ### Recording history
402
-
403
- 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.
404
-
405
- ---
406
-
407
- ### Cross-app recording (single-spa / Module Federation)
408
-
409
- 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.
410
-
411
- It also survives an accidental **page reload** of the same origin: the recording is recovered automatically.
412
-
413
- > **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`).
414
-
415
- #### Where to mount the widget
416
-
417
- Pick **exactly one** placement — never both, or the HTTP monitor installs twice and commands get double-recorded:
418
-
419
- - **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.
420
- - **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.
421
-
422
- #### Resuming
423
-
424
- When the recorder loads and finds an active recording session:
425
-
426
- - **Recent** (within the resume window) → it **continues silently**.
427
- - **Stale** (older than the window) → it asks **continue or discard**, so a forgotten session never resumes silently (important when deployed with `start-hidden`).
428
-
429
- 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.
430
-
431
- Stopping a recording (save **or** discard) ends the session, so a finished recording never resurrects on the next navigation.
432
-
433
- ---
434
-
435
- ### Moving the widget (draggable)
436
-
437
- If the floating widget covers a control you need, **drag the record button (⏺/⏹)**
438
- to move the whole widget anywhere on screen. Press-and-move to drag; a plain click
439
- still starts/stops recording (a ~5 px threshold tells the two apart).
440
-
441
- The radial menu **expands toward the centre of the screen**, so the action buttons
442
- stay fully visible wherever you drop it, and the position is **remembered** across
443
- reloads (and app crossings). Reset it to the default corner from **⚙️ Config →
444
- 🧲 Widget position → Reset position**, or programmatically:
445
-
446
- ```typescript
447
- recorder.resetWidgetPosition();
448
- ```
449
-
450
- ---
451
-
452
- ### In-app help panel
453
-
454
- Not sure what the widget can do? Click the **?** button in the widget (or press
455
- **Ctrl+Shift+H**) to open a categorised cheat-sheet — shortcuts, what gets
456
- recorded, assertions, the panels, selectors, HTTP, data, and the advanced
457
- features — in your language (es/en/fr/it/de).
458
-
459
- ---
460
-
461
- ### Keyboard shortcuts
462
-
463
- | Shortcut | Action |
464
- |---|---|
465
- | `Ctrl + Shift + E` | **Show / hide the widget** (works even when it is invisible) |
466
- | `Ctrl + R` | Start / stop recording |
467
- | `Ctrl + P` | Pause / resume recording |
468
- | `Ctrl + 1` | Open saved tests panel |
469
- | `Ctrl + 2` | Open command previewer |
470
- | `Ctrl + 3` | Open settings |
471
- | `Ctrl + Shift + H` | Open the in-app **help panel** |
472
- | `Alt + click` | Capture an assertion for the clicked element |
473
-
474
- > `Ctrl + R` / `Ctrl + P` and the panel shortcuts only fire when the widget is **visible**. `Ctrl + Shift + E` always works regardless of visibility state.
475
-
476
- ---
477
-
478
- ### Invisible mode — record in production, stay hidden from users
479
-
480
- 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.**
481
-
482
- 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.
483
-
484
- ```
485
- Ctrl + Shift + E → show / hide the widget (works even when invisible)
486
- ```
487
-
488
- This means you can:
489
- - **Record against production data** — capture flows that only happen with real users and real back-ends.
490
- - **Debug flaky tests in the wild** — reproduce the exact sequence of events that broke your CI pipeline.
491
- - **Onboard QA teams without a local setup** — testers open the live app, hit the shortcut, and record directly.
492
- - **Keep staging clean** — no visible dev tooling leaked to clients or stakeholders reviewing the environment.
493
-
494
- #### Option A — HTML attribute (deploy-time, recommended)
495
-
496
- Add `start-hidden` to the element. This takes **precedence over the settings panel** and is the right choice for environment-level control:
497
-
498
- ```html
499
- <!-- Production / staging: invisible by default -->
500
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
501
-
502
- <!-- Dev: always visible (or omit the attribute entirely) -->
503
- <lib-e2e-recorder start-hidden="false"></lib-e2e-recorder>
504
- ```
505
-
506
- Works in every framework:
507
-
508
- ```html
509
- <!-- Angular -->
510
- <lib-e2e-recorder start-hidden></lib-e2e-recorder>
511
-
512
- <!-- React (JSX) -->
513
- <lib-e2e-recorder start-hidden="true" />
514
-
515
- <!-- Vue -->
516
- <lib-e2e-recorder start-hidden />
517
- ```
518
-
519
- A typical Angular setup driven by an environment variable:
520
-
521
- ```typescript
522
- // app.component.ts
523
- get startHidden(): boolean {
524
- return environment.production; // invisible in prod, visible in dev
525
- }
526
- ```
527
-
528
- ```html
529
- <!-- app.component.html -->
530
- <lib-e2e-recorder [attr.start-hidden]="startHidden ? '' : null"></lib-e2e-recorder>
531
- ```
532
-
533
- #### Option B — Settings panel (runtime, per user)
534
-
535
- 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.
536
-
537
- ---
538
-
539
- **Either way, when the widget is hidden:**
540
- - Zero DOM rendered, zero event listeners added beyond the keyboard shortcut — it is truly invisible and inert.
541
- - Your Cypress selectors are never blocked by a floating element.
542
- - Press **`Ctrl + Shift + E`** to reveal it instantly, no page reload needed.
543
- - Hide it again with the same shortcut when you are done.
544
-
545
- > **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.
546
-
547
- ---
548
-
549
- ### Import / Export
550
-
551
- Back up tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
552
-
553
- - **⬆️ Export tests** — opens a dialog to choose **what** to export, then downloads `e2e-cypress-export.json`:
554
- - **Todo** — every saved test.
555
- - **Selección manual** — tick individual tests (multiple allowed); only the checked ones are exported.
556
- - **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.
557
-
558
- A live count shows how many tests the current selection will export, and the export button is disabled when that count is `0`.
559
- - **⬇️ Import tests** — uploads a JSON file and **merges** its tests into the current database (existing tests are kept, never wiped).
560
-
561
- The output format and filename are identical in every mode, so any exported file (full or partial) can be re-imported.
562
-
563
- JSON format:
564
-
565
- ```json
566
- {
567
- "tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
568
- "interceptors": []
569
- }
570
- ```
571
-
572
- ---
573
-
574
- ### Internationalisation
575
-
576
- The interface auto-detects the browser language. Supported languages:
577
-
578
- | Code | Language |
579
- |---|---|
580
- | `es` | Spanish (default fallback) |
581
- | `en` | English |
582
- | `fr` | French |
583
- | `it` | Italian |
584
- | `de` | German |
585
-
586
- Override the language programmatically:
587
-
588
- ```typescript
589
- recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
590
- ```
591
-
592
- ---
593
-
594
- ## Public API
595
-
596
- ### `<lib-e2e-recorder>` element
597
-
598
- ```typescript
599
- class LibE2eRecorderElement extends HTMLElement {
600
- // Injected services (auto-created if not set)
601
- recording: RecordingService;
602
- persistence: PersistenceService;
603
- translation: TranslationService;
604
-
605
- // State (read-only in normal use)
606
- isRecording: boolean;
607
- isPaused: boolean;
608
- cypressCommands: string[];
609
- interceptors: string[];
610
-
611
- // Dialog flags
612
- isCommandsDialogOpen: boolean;
613
- isSavedTestsDialogOpen: boolean;
614
- isSaveTestDialogOpen: boolean;
615
- isSettingsDialogOpen: boolean;
616
- isAdvancedEditorDialogOpen: boolean;
617
-
618
- // Methods
619
- toggle(): void; // start / stop recording
620
- togglePause(): void; // pause / resume
621
- setLanguage(lang?: string): void; // set UI language
622
-
623
- showCommandsDialog(): void;
624
- showSavedTestsDialog(): void;
625
- showSaveTestDialog(): void;
626
- showSettingsDialog(): void;
627
- showAdvancedEditorDialog(testId?: number): void;
628
-
629
- getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
630
- recoverLastRecording(): void;
631
- clearRecordingHistory(): void;
632
-
633
- // Cross-app session (micro-frontends)
634
- hasActiveSession(): boolean; // is a recording session persisted/active?
635
- resumeSession(): void; // rehydrate the persisted session
636
- discardSession(): void; // drop the persisted session
637
-
638
- // Draggable widget
639
- resetWidgetPosition(): void; // move the widget back to its default corner
640
- }
641
- ```
642
-
643
- ### `RecordingService`
644
-
645
- ```typescript
646
- class RecordingService {
647
- selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
648
-
649
- startRecording(): void;
650
- stopRecording(): void;
651
- pauseRecording(): void;
652
- resumeRecording(): void;
653
- toggleRecording(): void;
654
- togglePause(): void;
655
-
656
- addCommand(cmd: string): void; // respects recording/paused state
657
- appendCommand(cmd: string): void; // always appends (for manual assertions)
658
- removeCommand(index: number): void;
659
- moveCommand(from: number, to: number): void;
660
-
661
- registerInterceptor(method: string, url: string, alias: string): void;
662
- removeInterceptor(index: number): void;
663
-
664
- clearCommands(): void;
665
-
666
- // Cross-app session (micro-frontends) — see spec 006
667
- sessionId: string | null;
668
- restoreSession(state: ActiveSessionState): void; // rehydrate WITHOUT re-running the bootstrap
669
- getSessionSnapshot(): ActiveSessionState;
670
- onSessionChange(fn: (state: ActiveSessionState) => void): () => void;
671
-
672
- getCommandsSnapshot(): string[];
673
- getInterceptorsSnapshot(): string[];
674
-
675
- onCommandsChange(fn: (cmds: string[]) => void): () => void;
676
- onInterceptorsChange(fn: (ints: string[]) => void): () => void;
677
- onRecordingChange(fn: (isRecording: boolean) => void): () => void;
678
- onPauseChange(fn: (isPaused: boolean) => void): () => void;
679
-
680
- destroy(): void;
681
- }
682
- ```
683
-
684
- ### `PersistenceService`
685
-
686
- ```typescript
687
- class PersistenceService {
688
- insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[], notes?: string, ticketId?: string): Promise<number>;
689
- getAllTests(): Promise<TestWithDetails[]>;
690
- getTestById(testId: number): Promise<TestDetail | null>;
691
- deleteTest(id: number): Promise<void>;
692
-
693
- setConfig(config: Record<string, unknown>): Promise<void>;
694
- setConfigKey(key: string, value: unknown): Promise<void>;
695
- getConfig(key: string): Promise<Record<string, unknown> | null>;
696
- getGeneralConfig(): Promise<ConfigRecord | null>;
697
-
698
- // Live recording session (cross-app continuity — see spec 006)
699
- saveActiveSession(state: ActiveSessionState): Promise<void>;
700
- getActiveSession(): Promise<ActiveSessionState | null>;
701
- clearActiveSession(): Promise<void>;
702
-
703
- // File System Access — writing fixtures / uploaded files to cypress/ (specs 012, 019)
704
- requestDirectoryPermissions(): Promise<void>;
705
- hasDirectoryAccess(): Promise<boolean>;
706
- writeFixtures(fixtures: Array<{ name: string; content: string }>): Promise<number>;
707
- writeUploadedFile(filename: string, bytes: ArrayBuffer): Promise<void>;
708
-
709
- // Login setup config (spec 015)
710
- saveLoginSetup(config: LoginSetupConfig): Promise<void>;
711
- getLoginSetup(): Promise<LoginSetupConfig | null>;
712
- clearLoginSetup(): Promise<void>;
713
-
714
- clearAllData(): Promise<void>;
715
- }
716
- ```
717
-
718
- ---
719
-
720
- ## Breaking changes
721
-
722
- See [`CHANGELOG.md`](./CHANGELOG.md) for the full history.
723
-
724
- ### v0.10.0
725
-
726
- - **Public export surface curated.** `src/index.ts` now exports an explicit
727
- allowlist instead of re-exporting every module. Internal singletons
728
- (`translationService`, `transformationService`, …), internal constants
729
- (`ACTIVE_SESSION_BREADCRUMB_KEY`, `TOGGLE_MARGIN`, …), low-level modal/drag
730
- helpers and the test-only `_resetHttpMonitorState` are **no longer exported**
731
- from the package root. The public services, components, `VERSION`,
732
- `generateAlias`, `injectStyles`, `showToast`, `makeModalResizable`,
733
- `makeSwalDraggable` and the style constants remain available.
734
-
735
- ### v0.9.0
736
-
737
- - **`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.
738
-
739
- ---
740
-
741
- ## Technical notes
742
-
743
- - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
744
- - **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag.
745
- - **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
746
- - **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
747
- - **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
748
-
749
- ---
750
-
751
- ## Development
752
-
753
- ```bash
754
- npm install
755
- npm run build # compile with tsup
756
- npm test # run vitest unit tests
757
- npm run test:coverage # coverage report (≥80% gate)
758
- npm run lint # ESLint
759
- ```
760
-
761
- See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
762
-
763
- ---
764
-
765
- ## Licence
766
-
767
- 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
+ > **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
+
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ 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.
12
+
13
+ **Zero framework coupling.** It is a plain Custom Element (`<lib-e2e-recorder>`). Angular, React, Vue, plain HTML — anything works.
14
+
15
+ ---
16
+
17
+ ## Why use it
18
+
19
+ | Without this library | With this library |
20
+ |---|---|
21
+ | Write `cy.get('[data-cy="..."]').click()` by hand | Click in the browser, get the command automatically |
22
+ | Manually craft `cy.intercept()` for every API call | Every fetch/XHR is captured and the interceptor is generated |
23
+ | Guess which selector attribute to use | Choose the strategy once in settings; the recorder handles the rest |
24
+ | Copy-paste commands into `.cy.ts` files | The advanced editor inserts the `it()` block directly into the file |
25
+ | Maintain tests in one language | Interface in ES / EN / FR / IT / DE, auto-detected from the browser |
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install lib-e2e-cypress-for-dummys-ts idb sweetalert2
33
+ ```
34
+
35
+ The package ships three formats:
36
+
37
+ | File | Format | Use |
38
+ |---|---|---|
39
+ | `dist/index.js` | ESM | Modern bundlers (Vite, Angular, etc.) |
40
+ | `dist/index.cjs` | CJS | CommonJS environments |
41
+ | `dist/index.d.ts` | TypeScript declarations | Type-safe imports |
42
+
43
+ `idb` and `sweetalert2` are peer dependencies and must be installed alongside the library.
44
+
45
+ ---
46
+
47
+ ## Quick start — plain HTML
48
+
49
+ ```html
50
+ <!DOCTYPE html>
51
+ <html>
52
+ <head>
53
+ <!-- SweetAlert2 CSS (required for modals) -->
54
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2/dist/sweetalert2.min.css" />
55
+ </head>
56
+ <body>
57
+ <!-- Your app content here -->
58
+
59
+ <lib-e2e-recorder></lib-e2e-recorder>
60
+
61
+ <script type="module">
62
+ import 'path/to/dist/index.js';
63
+ </script>
64
+ </body>
65
+ </html>
66
+ ```
67
+
68
+ The widget appears as a small floating panel. Press **⏺** to start recording.
69
+
70
+ ---
71
+
72
+ ## Quick start — Angular
73
+
74
+ **1. Register the custom element schema** (in `app.config.ts` or the module):
75
+
76
+ ```typescript
77
+ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
78
+
79
+ // In a standalone component or NgModule:
80
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
81
+ ```
82
+
83
+ **2. Import the library** in `main.ts` (or any file that is loaded before the widget is used):
84
+
85
+ ```typescript
86
+ import 'lib-e2e-cypress-for-dummys-ts/dist/index.js';
87
+ ```
88
+
89
+ **3. Add the element** to any template (typically `AppComponent`):
90
+
91
+ ```html
92
+ <!-- Only render in non-production environments -->
93
+ @if (!isProduction) {
94
+ <!-- start-hidden: widget is invisible on load; press Ctrl+Shift+E to reveal it -->
95
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
96
+ }
97
+ ```
98
+
99
+ > 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.
100
+ >
101
+ > The `start-hidden` attribute **overrides** the value stored in the settings panel. See [Start hidden](#start-hidden-invisible-mode) for the full precedence rules.
102
+
103
+ **4. Control it programmatically** (optional):
104
+
105
+ ```typescript
106
+ import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
107
+
108
+ const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
109
+
110
+ // Start / stop recording
111
+ recorder.toggle();
112
+
113
+ // Set the language explicitly
114
+ recorder.setLanguage('en');
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Features
120
+
121
+ ### Recording interactions
122
+
123
+ The recorder automatically captures:
124
+
125
+ | Interaction | Generated Cypress command |
126
+ |---|---|
127
+ | Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
128
+ | Double-click | `cy.get('[data-cy="row"]').dblclick()` |
129
+ | Right-click | `cy.get('[data-cy="ctx"]').rightclick()` |
130
+ | Check / uncheck a checkbox | `cy.get('[data-cy="agree"]').check()` / `.uncheck()` |
131
+ | Pick a radio | `cy.get('[data-cy="plan"]').check()` |
132
+ | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
133
+ | Press Enter / Escape in a field | `cy.get('[data-cy="q"]').type('{enter}')` / `type('{esc}')` |
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'])` |
137
+ | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
138
+ | Page load | `cy.visit('/current-path')` |
139
+
140
+ Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
141
+
142
+ #### Capturing assertions while recording — Alt+click
143
+
144
+ A test that only clicks around but never checks anything is weak. While recording,
145
+ hold **Alt** and click any element to record a **Cypress assertion** for it instead
146
+ of a click — and the element's real action is **suppressed** (no navigation, no form
147
+ submit), so asserting never changes your flow.
148
+
149
+ The assertion is chosen automatically from the element:
150
+
151
+ | Element | Generated assertion |
152
+ |---|---|
153
+ | Checkbox / radio | `.should('be.checked')` / `.should('not.be.checked')` |
154
+ | Input / textarea / select with a value | `.should('have.value', '<value>')` |
155
+ | Element with visible text | `.should('contain.text', '<text>')` |
156
+ | Anything else | `.should('be.visible')` |
157
+
158
+ The captured assertion appears in the **⌨️ Commands** previewer like any other
159
+ command, so you can reorder or delete it. (You can still build assertions manually
160
+ with the assertion builder — see below.)
161
+
162
+ ---
163
+
164
+ ### HTTP monitoring
165
+
166
+ Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
167
+
168
+ For **GET, POST and PUT** requests the recorder generates a pair of commands:
169
+
170
+ ```javascript
171
+ // Interceptor (placed at the top of the test, in beforeEach)
172
+ cy.intercept('GET', '**/api/users').as('get-api-users')
173
+
174
+ // Wait command (placed inline with the test actions)
175
+ cy.wait('@get-api-users').then((interception) => { })
176
+ ```
177
+
178
+ **DELETE** requests are intentionally ignored (read-only guard).
179
+
180
+ #### Advanced HTTP mode
181
+
182
+ Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
183
+
184
+ ```javascript
185
+ // GET — validates response body fields
186
+ cy.wait('@get-api-users').then((interception) => {
187
+ if (interception.response) {
188
+ expect(interception.response.body.name).to.equal("Alice");
189
+ expect(interception.response.body.role).to.equal("admin");
190
+ }
191
+ })
192
+
193
+ // POST / PUT — validates request body fields
194
+ cy.wait('@post-api-users').then((interception) => {
195
+ expect(interception.request.body.name).to.equal("Alice");
196
+ })
197
+ ```
198
+
199
+ Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
200
+
201
+ #### Fixture mode (deterministic stubs)
202
+
203
+ Enable **🧪 Fixtures HTTP** in the settings panel to turn GET responses into
204
+ **Cypress fixtures + stubs** instead of spies. While recording, each GET with a
205
+ JSON body is saved as `cypress/fixtures/<alias>.json` and its interceptor becomes:
206
+
207
+ ```javascript
208
+ cy.intercept('GET', '**/api/users', { fixture: 'get-api-users.json' }).as('get-api-users')
209
+ cy.wait('@get-api-users').then((interception) => { })
210
+ ```
211
+
212
+ so the test replays deterministic data with **no backend**. When you save the
213
+ test, the fixture files are written into your `cypress/fixtures/` folder
214
+ automatically (requires the Cypress folder configured — see the advanced editor
215
+ setup). POST/PUT keep the spy + body-validation behaviour; non-JSON GETs fall back
216
+ to a spy.
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
+
228
+ ---
229
+
230
+ ### Selector strategy
231
+
232
+ The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
233
+
234
+ | Strategy | Generated selector |
235
+ |---|---|
236
+ | `data-cy` (default) | `[data-cy="login-btn"]` |
237
+ | `data-testid` | `[data-testid="login-btn"]` |
238
+ | `aria-label` | `[aria-label="Login"]` |
239
+ | `id` | `#login-btn` |
240
+
241
+ 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.
242
+
243
+ Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
244
+
245
+ ---
246
+
247
+ ### Smart selector picker
248
+
249
+ 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.
250
+
251
+ The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
252
+
253
+ | Badge | Quality | Criteria |
254
+ |---|---|---|
255
+ | 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
256
+ | 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
257
+ | 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
258
+ | 🔴 Not recommended | Avoid | Only tag name or inline `style` |
259
+
260
+ **How to use:**
261
+ 1. The picker auto-selects the best available ancestor
262
+ 2. Use **↑ / ↓** to navigate the ancestor list
263
+ 3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
264
+ 4. Press **Escape** or click outside to dismiss without recording
265
+
266
+ The picker also closes automatically when recording is stopped or paused.
267
+
268
+ **Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
269
+
270
+ ---
271
+
272
+ ### Saving and managing tests
273
+
274
+ When recording stops, a dialog prompts you to:
275
+
276
+ - Give the test a **description** (becomes the `it('…')` label)
277
+ - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
278
+ - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
279
+
280
+ The **📋 Tests** panel lets you:
281
+
282
+ - Browse all saved tests filtered by tag
283
+ - Expand a test to see its commands and interceptors
284
+ - Copy the full `describe()` block to the clipboard with a custom suite name
285
+ - Copy just the commands or just the interceptors separately
286
+ - Multi-select tests and generate a combined `describe()` block
287
+ - Delete individual tests
288
+
289
+ ---
290
+
291
+ ### Command previewer
292
+
293
+ The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
294
+
295
+ - Reorder commands with the up/down arrows
296
+ - Delete individual commands
297
+ - Add a custom assertion with the **assertion builder**
298
+
299
+ #### Assertion builder
300
+
301
+ The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
302
+
303
+ 1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
304
+ 2. Choose the assertion type from the dropdown:
305
+ - `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
306
+ - `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
307
+ 3. Click **➕ Añadir**
308
+
309
+ ---
310
+
311
+ ### Advanced editor (direct file insertion)
312
+
313
+ 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.
314
+
315
+ **Setup (one time):**
316
+
317
+ 1. Open **⚙️ Config → Carpeta Cypress**
318
+ 2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
319
+ 3. The browser stores the permission — you won't be asked again
320
+
321
+ **Expected folder structure:**
322
+
323
+ ```
324
+ cypress/ ← select this folder
325
+ └── e2e/ ← the recorder reads .cy.ts files from here
326
+ └── login.cy.ts
327
+ ```
328
+
329
+ **Using the editor:**
330
+
331
+ 1. Open **📁 Files** → select a `.cy.ts` file from the tree
332
+ 2. The `it()` block for the selected test appears in the panel
333
+ 3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
334
+ 4. Interceptors are automatically wrapped in a `beforeEach()`
335
+ 5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
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.
337
+
338
+ **Sidebar toolbar:**
339
+
340
+ | Button | Action |
341
+ |---|---|
342
+ | **+ 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**. |
343
+ | **+ 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. |
344
+ | **↻ Actualizar** | Rescans the `cypress/e2e/` directory. Useful after a `git pull`, a drag-and-drop into the folder, or any external file change. |
345
+
346
+ > The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
347
+
348
+ ---
349
+
350
+ ### Running a spec from the editor (local runner)
351
+
352
+ Inside the manual editor (**✏️ Editar manualmente**) the **▶ Lanzar test** button runs
353
+ **only the spec you are editing**, headless (no Cypress GUI), and shows the result
354
+ (pass/fail + output) right in the editor — for a fast record → tweak → run loop.
355
+
356
+ The browser can't spawn Cypress, so it talks to a tiny **local runner** over HTTP.
357
+ Start it once in your Cypress project:
358
+
359
+ ```bash
360
+ npx lib-e2e-cypress-runner # listens on http://127.0.0.1:8123
361
+ ```
362
+
363
+ Options:
364
+
365
+ | Flag | Default | Description |
366
+ |---|---|---|
367
+ | `--port` | `8123` | Port to listen on. |
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). |
369
+ | `--cwd` | current dir | Working directory the command runs in. |
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
+ ```
386
+
387
+ The widget POSTs `{ specPath }` to `http://localhost:8123/run-test`; configure a
388
+ different endpoint via the `runnerUrl` property of `<file-preview>` if needed.
389
+
390
+ Notes:
391
+ - The button is **enabled only when the app is served from localhost**. On a deployed
392
+ environment it is disabled with a *"muévelo a local para poder probar"* hint —
393
+ running a local Cypress against a remote build makes no sense.
394
+ - If no runner is reachable you get a clear "no runner detected" message (no silent
395
+ failure).
396
+ - The runner is a **dev-only** tool: it binds to `127.0.0.1` and passes the spec as an
397
+ argument (no shell interpolation). Don't expose it beyond your machine.
398
+
399
+ ---
400
+
401
+ ### Recording history & undoing an accidental Stop
402
+
403
+ Pressing **Stop** does not wipe anything by itself — the recorded commands stay in memory until you actually save (or start a brand-new recording). Two controls build on that:
404
+
405
+ - **Continue recording** — pressing Stop opens the save dialog ("Save this recording?"). Right there, next to *Yes*/*No*, there is a **Continue recording** button: if you hit Stop by mistake, click it and recording resumes exactly where you left it, same session, no commands lost. This is different from **Pause/Resume** (`Ctrl+P`): Pause only freezes capture while the recording stays "in progress" — Continue recording is specifically the way back from a Stop you didn't mean to press.
406
+ - **Recover last recording** — the last **5 stopped recordings** are automatically archived to `localStorage`, so you don't lose work even if the dialog is long gone (reloaded the page, or already discarded). While idle (not recording), if an archived recording exists the widget's action menu shows a **↺ Recover** entry — hover it to see how many commands and how long ago. Clicking it restores commands **and** HTTP interceptors from the most recent entry and puts the widget straight back into recording mode. Programmatically: `recorder.recoverLastRecording()`.
407
+
408
+ ---
409
+
410
+ ### Cross-app recording (single-spa / Module Federation)
411
+
412
+ 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.
413
+
414
+ It also survives an accidental **page reload** of the same origin: the recording is recovered automatically.
415
+
416
+ > **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`).
417
+
418
+ #### Where to mount the widget
419
+
420
+ Pick **exactly one** placement — never both, or the HTTP monitor installs twice and commands get double-recorded:
421
+
422
+ - **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.
423
+ - **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.
424
+
425
+ #### Resuming
426
+
427
+ When the recorder loads and finds an active recording session:
428
+
429
+ - **Recent** (within the resume window) → it **continues silently**.
430
+ - **Stale** (older than the window) → it asks **continue or discard**, so a forgotten session never resumes silently (important when deployed with `start-hidden`).
431
+
432
+ 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.
433
+
434
+ Stopping a recording (save **or** discard) ends the session, so a finished recording never resurrects on the next navigation.
435
+
436
+ ---
437
+
438
+ ### Moving the widget (draggable)
439
+
440
+ If the floating widget covers a control you need, **drag the record button (⏺/⏹)**
441
+ to move the whole widget anywhere on screen. Press-and-move to drag; a plain click
442
+ still starts/stops recording (a ~5 px threshold tells the two apart).
443
+
444
+ The radial menu **expands toward the centre of the screen**, so the action buttons
445
+ stay fully visible wherever you drop it, and the position is **remembered** across
446
+ reloads (and app crossings). Reset it to the default corner from **⚙️ Config →
447
+ 🧲 Widget position → Reset position**, or programmatically:
448
+
449
+ ```typescript
450
+ recorder.resetWidgetPosition();
451
+ ```
452
+
453
+ ---
454
+
455
+ ### In-app help panel
456
+
457
+ Not sure what the widget can do? Click the **?** button in the widget (or press
458
+ **Ctrl+Shift+H**) to open a categorised cheat-sheet — shortcuts, what gets
459
+ recorded, assertions, the panels, selectors, HTTP, data, and the advanced
460
+ features — in your language (es/en/fr/it/de).
461
+
462
+ ---
463
+
464
+ ### Keyboard shortcuts
465
+
466
+ | Shortcut | Action |
467
+ |---|---|
468
+ | `Ctrl + Shift + E` | **Show / hide the widget** (works even when it is invisible) |
469
+ | `Ctrl + R` | Start / stop recording |
470
+ | `Ctrl + P` | Pause / resume recording |
471
+ | `Ctrl + 1` | Open saved tests panel |
472
+ | `Ctrl + 2` | Open command previewer |
473
+ | `Ctrl + 3` | Open settings |
474
+ | `Ctrl + Shift + H` | Open the in-app **help panel** |
475
+ | `Alt + click` | Capture an assertion for the clicked element |
476
+
477
+ > `Ctrl + R` / `Ctrl + P` and the panel shortcuts only fire when the widget is **visible**. `Ctrl + Shift + E` always works regardless of visibility state.
478
+
479
+ ---
480
+
481
+ ### Invisible mode — record in production, stay hidden from users
482
+
483
+ 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.**
484
+
485
+ 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.
486
+
487
+ ```
488
+ Ctrl + Shift + E → show / hide the widget (works even when invisible)
489
+ ```
490
+
491
+ This means you can:
492
+ - **Record against production data** — capture flows that only happen with real users and real back-ends.
493
+ - **Debug flaky tests in the wild** — reproduce the exact sequence of events that broke your CI pipeline.
494
+ - **Onboard QA teams without a local setup** — testers open the live app, hit the shortcut, and record directly.
495
+ - **Keep staging clean** — no visible dev tooling leaked to clients or stakeholders reviewing the environment.
496
+
497
+ #### Option A — HTML attribute (deploy-time, recommended)
498
+
499
+ Add `start-hidden` to the element. This takes **precedence over the settings panel** and is the right choice for environment-level control:
500
+
501
+ ```html
502
+ <!-- Production / staging: invisible by default -->
503
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
504
+
505
+ <!-- Dev: always visible (or omit the attribute entirely) -->
506
+ <lib-e2e-recorder start-hidden="false"></lib-e2e-recorder>
507
+ ```
508
+
509
+ Works in every framework:
510
+
511
+ ```html
512
+ <!-- Angular -->
513
+ <lib-e2e-recorder start-hidden></lib-e2e-recorder>
514
+
515
+ <!-- React (JSX) -->
516
+ <lib-e2e-recorder start-hidden="true" />
517
+
518
+ <!-- Vue -->
519
+ <lib-e2e-recorder start-hidden />
520
+ ```
521
+
522
+ A typical Angular setup driven by an environment variable:
523
+
524
+ ```typescript
525
+ // app.component.ts
526
+ get startHidden(): boolean {
527
+ return environment.production; // invisible in prod, visible in dev
528
+ }
529
+ ```
530
+
531
+ ```html
532
+ <!-- app.component.html -->
533
+ <lib-e2e-recorder [attr.start-hidden]="startHidden ? '' : null"></lib-e2e-recorder>
534
+ ```
535
+
536
+ #### Option B — Settings panel (runtime, per user)
537
+
538
+ 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.
539
+
540
+ ---
541
+
542
+ **Either way, when the widget is hidden:**
543
+ - Zero DOM rendered, zero event listeners added beyond the keyboard shortcut — it is truly invisible and inert.
544
+ - Your Cypress selectors are never blocked by a floating element.
545
+ - Press **`Ctrl + Shift + E`** to reveal it instantly, no page reload needed.
546
+ - Hide it again with the same shortcut when you are done.
547
+
548
+ > **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.
549
+
550
+ ---
551
+
552
+ ### Import / Export
553
+
554
+ Back up tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
555
+
556
+ - **⬆️ Export tests** — opens a dialog to choose **what** to export, then downloads `e2e-cypress-export.json`:
557
+ - **Todo** — every saved test.
558
+ - **Selección manual** — tick individual tests (multiple allowed); only the checked ones are exported.
559
+ - **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.
560
+
561
+ A live count shows how many tests the current selection will export, and the export button is disabled when that count is `0`.
562
+ - **⬇️ Import tests** — uploads a JSON file and **merges** its tests into the current database (existing tests are kept, never wiped).
563
+
564
+ The output format and filename are identical in every mode, so any exported file (full or partial) can be re-imported.
565
+
566
+ JSON format:
567
+
568
+ ```json
569
+ {
570
+ "tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
571
+ "interceptors": []
572
+ }
573
+ ```
574
+
575
+ ---
576
+
577
+ ### Internationalisation
578
+
579
+ The interface auto-detects the browser language. Supported languages:
580
+
581
+ | Code | Language |
582
+ |---|---|
583
+ | `es` | Spanish (default fallback) |
584
+ | `en` | English |
585
+ | `fr` | French |
586
+ | `it` | Italian |
587
+ | `de` | German |
588
+
589
+ Override the language programmatically:
590
+
591
+ ```typescript
592
+ recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Public API
598
+
599
+ ### `<lib-e2e-recorder>` element
600
+
601
+ ```typescript
602
+ class LibE2eRecorderElement extends HTMLElement {
603
+ // Injected services (auto-created if not set)
604
+ recording: RecordingService;
605
+ persistence: PersistenceService;
606
+ translation: TranslationService;
607
+
608
+ // State (read-only in normal use)
609
+ isRecording: boolean;
610
+ isPaused: boolean;
611
+ cypressCommands: string[];
612
+ interceptors: string[];
613
+
614
+ // Dialog flags
615
+ isCommandsDialogOpen: boolean;
616
+ isSavedTestsDialogOpen: boolean;
617
+ isSaveTestDialogOpen: boolean;
618
+ isSettingsDialogOpen: boolean;
619
+ isAdvancedEditorDialogOpen: boolean;
620
+
621
+ // Methods
622
+ toggle(): void; // start / stop recording
623
+ togglePause(): void; // pause / resume
624
+ setLanguage(lang?: string): void; // set UI language
625
+
626
+ showCommandsDialog(): void;
627
+ showSavedTestsDialog(): void;
628
+ showSaveTestDialog(): void;
629
+ showSettingsDialog(): void;
630
+ showAdvancedEditorDialog(testId?: number): void;
631
+
632
+ getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
633
+ recoverLastRecording(): void; // restores commands + interceptors, resumes recording
634
+ clearRecordingHistory(): void;
635
+
636
+ // Cross-app session (micro-frontends)
637
+ hasActiveSession(): boolean; // is a recording session persisted/active?
638
+ resumeSession(): void; // rehydrate the persisted session
639
+ discardSession(): void; // drop the persisted session
640
+
641
+ // Draggable widget
642
+ resetWidgetPosition(): void; // move the widget back to its default corner
643
+ }
644
+ ```
645
+
646
+ ### `RecordingService`
647
+
648
+ ```typescript
649
+ class RecordingService {
650
+ selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
651
+
652
+ startRecording(): void;
653
+ stopRecording(): void;
654
+ continueRecording(): void; // resume right after a Stop, in place, same session
655
+ pauseRecording(): void;
656
+ resumeRecording(): void;
657
+ toggleRecording(): void;
658
+ togglePause(): void;
659
+
660
+ addCommand(cmd: string): void; // respects recording/paused state
661
+ appendCommand(cmd: string): void; // always appends (for manual assertions)
662
+ removeCommand(index: number): void;
663
+ moveCommand(from: number, to: number): void;
664
+
665
+ registerInterceptor(method: string, url: string, alias: string): void;
666
+ removeInterceptor(index: number): void;
667
+
668
+ clearCommands(): void;
669
+
670
+ // Cross-app session (micro-frontends) — see spec 006
671
+ sessionId: string | null;
672
+ restoreSession(state: ActiveSessionState): void; // rehydrate WITHOUT re-running the bootstrap
673
+ getSessionSnapshot(): ActiveSessionState;
674
+ onSessionChange(fn: (state: ActiveSessionState) => void): () => void;
675
+
676
+ getCommandsSnapshot(): string[];
677
+ getInterceptorsSnapshot(): string[];
678
+
679
+ onCommandsChange(fn: (cmds: string[]) => void): () => void;
680
+ onInterceptorsChange(fn: (ints: string[]) => void): () => void;
681
+ onRecordingChange(fn: (isRecording: boolean) => void): () => void;
682
+ onPauseChange(fn: (isPaused: boolean) => void): () => void;
683
+
684
+ destroy(): void;
685
+ }
686
+ ```
687
+
688
+ ### `PersistenceService`
689
+
690
+ ```typescript
691
+ class PersistenceService {
692
+ insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[], notes?: string, ticketId?: string): Promise<number>;
693
+ getAllTests(): Promise<TestWithDetails[]>;
694
+ getTestById(testId: number): Promise<TestDetail | null>;
695
+ deleteTest(id: number): Promise<void>;
696
+
697
+ setConfig(config: Record<string, unknown>): Promise<void>;
698
+ setConfigKey(key: string, value: unknown): Promise<void>;
699
+ getConfig(key: string): Promise<Record<string, unknown> | null>;
700
+ getGeneralConfig(): Promise<ConfigRecord | null>;
701
+
702
+ // Live recording session (cross-app continuity — see spec 006)
703
+ saveActiveSession(state: ActiveSessionState): Promise<void>;
704
+ getActiveSession(): Promise<ActiveSessionState | null>;
705
+ clearActiveSession(): Promise<void>;
706
+
707
+ // File System Access — writing fixtures / uploaded files to cypress/ (specs 012, 019)
708
+ requestDirectoryPermissions(): Promise<void>;
709
+ hasDirectoryAccess(): Promise<boolean>;
710
+ writeFixtures(fixtures: Array<{ name: string; content: string }>): Promise<number>;
711
+ writeUploadedFile(filename: string, bytes: ArrayBuffer): Promise<void>;
712
+
713
+ // Login setup config (spec 015)
714
+ saveLoginSetup(config: LoginSetupConfig): Promise<void>;
715
+ getLoginSetup(): Promise<LoginSetupConfig | null>;
716
+ clearLoginSetup(): Promise<void>;
717
+
718
+ clearAllData(): Promise<void>;
719
+ }
720
+ ```
721
+
722
+ ---
723
+
724
+ ## Breaking changes
725
+
726
+ See [`CHANGELOG.md`](./CHANGELOG.md) for the full history.
727
+
728
+ ### v0.10.0
729
+
730
+ - **Public export surface curated.** `src/index.ts` now exports an explicit
731
+ allowlist instead of re-exporting every module. Internal singletons
732
+ (`translationService`, `transformationService`, …), internal constants
733
+ (`ACTIVE_SESSION_BREADCRUMB_KEY`, `TOGGLE_MARGIN`, …), low-level modal/drag
734
+ helpers and the test-only `_resetHttpMonitorState` are **no longer exported**
735
+ from the package root. The public services, components, `VERSION`,
736
+ `generateAlias`, `injectStyles`, `showToast`, `makeModalResizable`,
737
+ `makeSwalDraggable` and the style constants remain available.
738
+
739
+ ### v0.9.0
740
+
741
+ - **`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.
742
+
743
+ ---
744
+
745
+ ## Technical notes
746
+
747
+ - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
748
+ - **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag. Every dialog is opened with `heightAuto: false` so it never overwrites `document.body.style.height` — required for Ionic/Capacitor apps, which rely on an unbroken `html`/`body`/`ion-app` `height: 100%` chain to fill the screen; without this, opening any dialog (and especially the save-recording dialog) could collapse the host app's layout to zero height.
749
+ - **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
750
+ - **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
751
+ - **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
752
+
753
+ ---
754
+
755
+ ## Development
756
+
757
+ ```bash
758
+ npm install
759
+ npm run build # compile with tsup
760
+ npm test # run vitest unit tests
761
+ npm run test:coverage # coverage report (≥80% gate)
762
+ npm run lint # ESLint
763
+ ```
764
+
765
+ See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
766
+
767
+ ---
768
+
769
+ ## Licence
770
+
771
+ MIT