lib-e2e-cypress-for-dummys-ts 0.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 +459 -0
- package/dist/chunk-DRNRKHXN.js +60 -0
- package/dist/chunk-DRNRKHXN.js.map +1 -0
- package/dist/index.cjs +4848 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +431 -0
- package/dist/index.d.ts +431 -0
- package/dist/index.js +4355 -0
- package/dist/index.js.map +1 -0
- package/dist/selector-picker-VJOLGZ5H.js +340 -0
- package/dist/selector-picker-VJOLGZ5H.js.map +1 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
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
|
+
<lib-e2e-recorder></lib-e2e-recorder>
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**4. Control it programmatically** (optional):
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
|
|
100
|
+
|
|
101
|
+
const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
|
|
102
|
+
|
|
103
|
+
// Start / stop recording
|
|
104
|
+
recorder.toggle();
|
|
105
|
+
|
|
106
|
+
// Set the language explicitly
|
|
107
|
+
recorder.setLanguage('en');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Features
|
|
113
|
+
|
|
114
|
+
### Recording interactions
|
|
115
|
+
|
|
116
|
+
The recorder automatically captures:
|
|
117
|
+
|
|
118
|
+
| Interaction | Generated Cypress command |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
|
|
121
|
+
| Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
|
|
122
|
+
| Select a `<select>` value | `cy.get('[data-cy="country"]').select('ES')` |
|
|
123
|
+
| SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
|
|
124
|
+
| Page load | `cy.visit('/current-path')` |
|
|
125
|
+
|
|
126
|
+
Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### HTTP monitoring
|
|
131
|
+
|
|
132
|
+
Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
|
|
133
|
+
|
|
134
|
+
For **GET, POST and PUT** requests the recorder generates a pair of commands:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
// Interceptor (placed at the top of the test, in beforeEach)
|
|
138
|
+
cy.intercept('GET', '**/api/users').as('get-api-users')
|
|
139
|
+
|
|
140
|
+
// Wait command (placed inline with the test actions)
|
|
141
|
+
cy.wait('@get-api-users').then((interception) => { })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**DELETE** requests are intentionally ignored (read-only guard).
|
|
145
|
+
|
|
146
|
+
#### Advanced HTTP mode
|
|
147
|
+
|
|
148
|
+
Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// GET — validates response body fields
|
|
152
|
+
cy.wait('@get-api-users').then((interception) => {
|
|
153
|
+
if (interception.response) {
|
|
154
|
+
expect(interception.response.body.name).to.equal("Alice");
|
|
155
|
+
expect(interception.response.body.role).to.equal("admin");
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// POST / PUT — validates request body fields
|
|
160
|
+
cy.wait('@post-api-users').then((interception) => {
|
|
161
|
+
expect(interception.request.body.name).to.equal("Alice");
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Selector strategy
|
|
170
|
+
|
|
171
|
+
The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
|
|
172
|
+
|
|
173
|
+
| Strategy | Generated selector |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `data-cy` (default) | `[data-cy="login-btn"]` |
|
|
176
|
+
| `data-testid` | `[data-testid="login-btn"]` |
|
|
177
|
+
| `aria-label` | `[aria-label="Login"]` |
|
|
178
|
+
| `id` | `#login-btn` |
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Smart selector picker
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
|
|
191
|
+
|
|
192
|
+
| Badge | Quality | Criteria |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
|
|
195
|
+
| 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
|
|
196
|
+
| 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
|
|
197
|
+
| 🔴 Not recommended | Avoid | Only tag name or inline `style` |
|
|
198
|
+
|
|
199
|
+
**How to use:**
|
|
200
|
+
1. The picker auto-selects the best available ancestor
|
|
201
|
+
2. Use **↑ / ↓** to navigate the ancestor list
|
|
202
|
+
3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
|
|
203
|
+
4. Press **Escape** or click outside to dismiss without recording
|
|
204
|
+
|
|
205
|
+
The picker also closes automatically when recording is stopped or paused.
|
|
206
|
+
|
|
207
|
+
**Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Saving and managing tests
|
|
212
|
+
|
|
213
|
+
When recording stops, a dialog prompts you to:
|
|
214
|
+
|
|
215
|
+
- Give the test a **description** (becomes the `it('…')` label)
|
|
216
|
+
- Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
|
|
217
|
+
- Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
|
|
218
|
+
|
|
219
|
+
The **📋 Tests** panel lets you:
|
|
220
|
+
|
|
221
|
+
- Browse all saved tests filtered by tag
|
|
222
|
+
- Expand a test to see its commands and interceptors
|
|
223
|
+
- Copy the full `describe()` block to the clipboard with a custom suite name
|
|
224
|
+
- Copy just the commands or just the interceptors separately
|
|
225
|
+
- Multi-select tests and generate a combined `describe()` block
|
|
226
|
+
- Delete individual tests
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Command previewer
|
|
231
|
+
|
|
232
|
+
The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
|
|
233
|
+
|
|
234
|
+
- Reorder commands with the up/down arrows
|
|
235
|
+
- Delete individual commands
|
|
236
|
+
- Add a custom assertion with the **assertion builder**
|
|
237
|
+
|
|
238
|
+
#### Assertion builder
|
|
239
|
+
|
|
240
|
+
The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
|
|
241
|
+
|
|
242
|
+
1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
|
|
243
|
+
2. Choose the assertion type from the dropdown:
|
|
244
|
+
- `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
|
|
245
|
+
- `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
|
|
246
|
+
3. Click **➕ Añadir**
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Advanced editor (direct file insertion)
|
|
251
|
+
|
|
252
|
+
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.
|
|
253
|
+
|
|
254
|
+
**Setup (one time):**
|
|
255
|
+
|
|
256
|
+
1. Open **⚙️ Config → Carpeta Cypress**
|
|
257
|
+
2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
|
|
258
|
+
3. The browser stores the permission — you won't be asked again
|
|
259
|
+
|
|
260
|
+
**Expected folder structure:**
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
cypress/ ← select this folder
|
|
264
|
+
└── e2e/ ← the recorder reads .cy.ts files from here
|
|
265
|
+
└── login.cy.ts
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Using the editor:**
|
|
269
|
+
|
|
270
|
+
1. Open **📁 Files** → select a `.cy.ts` file from the tree
|
|
271
|
+
2. The `it()` block for the selected test appears in the panel
|
|
272
|
+
3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
|
|
273
|
+
4. Interceptors are automatically wrapped in a `beforeEach()`
|
|
274
|
+
5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
|
|
275
|
+
|
|
276
|
+
> The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Recording history
|
|
281
|
+
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### Keyboard shortcuts
|
|
287
|
+
|
|
288
|
+
| Shortcut | Action |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `Ctrl + R` | Start / stop recording |
|
|
291
|
+
| `Ctrl + P` | Pause / resume recording |
|
|
292
|
+
| `Ctrl + 1` | Open saved tests panel |
|
|
293
|
+
| `Ctrl + 2` | Open command previewer |
|
|
294
|
+
| `Ctrl + 3` | Open settings |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Import / Export
|
|
299
|
+
|
|
300
|
+
Back up all tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
|
|
301
|
+
|
|
302
|
+
- **⬆️ Export tests** — downloads `e2e-cypress-export.json`
|
|
303
|
+
- **⬇️ Import tests** — uploads a JSON file and merges it into the current database
|
|
304
|
+
|
|
305
|
+
JSON format:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
|
|
310
|
+
"interceptors": []
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### Internationalisation
|
|
317
|
+
|
|
318
|
+
The interface auto-detects the browser language. Supported languages:
|
|
319
|
+
|
|
320
|
+
| Code | Language |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `es` | Spanish (default fallback) |
|
|
323
|
+
| `en` | English |
|
|
324
|
+
| `fr` | French |
|
|
325
|
+
| `it` | Italian |
|
|
326
|
+
| `de` | German |
|
|
327
|
+
|
|
328
|
+
Override the language programmatically:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Public API
|
|
337
|
+
|
|
338
|
+
### `<lib-e2e-recorder>` element
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
class LibE2eRecorderElement extends HTMLElement {
|
|
342
|
+
// Injected services (auto-created if not set)
|
|
343
|
+
recording: RecordingService;
|
|
344
|
+
persistence: PersistenceService;
|
|
345
|
+
translation: TranslationService;
|
|
346
|
+
|
|
347
|
+
// State (read-only in normal use)
|
|
348
|
+
isRecording: boolean;
|
|
349
|
+
isPaused: boolean;
|
|
350
|
+
cypressCommands: string[];
|
|
351
|
+
interceptors: string[];
|
|
352
|
+
|
|
353
|
+
// Dialog flags
|
|
354
|
+
isCommandsDialogOpen: boolean;
|
|
355
|
+
isSavedTestsDialogOpen: boolean;
|
|
356
|
+
isSaveTestDialogOpen: boolean;
|
|
357
|
+
isSettingsDialogOpen: boolean;
|
|
358
|
+
isAdvancedEditorDialogOpen: boolean;
|
|
359
|
+
|
|
360
|
+
// Methods
|
|
361
|
+
toggle(): void; // start / stop recording
|
|
362
|
+
togglePause(): void; // pause / resume
|
|
363
|
+
setLanguage(lang?: string): void; // set UI language
|
|
364
|
+
|
|
365
|
+
showCommandsDialog(): void;
|
|
366
|
+
showSavedTestsDialog(): void;
|
|
367
|
+
showSaveTestDialog(): void;
|
|
368
|
+
showSettingsDialog(): void;
|
|
369
|
+
showAdvancedEditorDialog(testId?: number): void;
|
|
370
|
+
|
|
371
|
+
getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
|
|
372
|
+
recoverLastRecording(): void;
|
|
373
|
+
clearRecordingHistory(): void;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `RecordingService`
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
class RecordingService {
|
|
381
|
+
selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
|
|
382
|
+
|
|
383
|
+
startRecording(): void;
|
|
384
|
+
stopRecording(): void;
|
|
385
|
+
pauseRecording(): void;
|
|
386
|
+
resumeRecording(): void;
|
|
387
|
+
toggleRecording(): void;
|
|
388
|
+
togglePause(): void;
|
|
389
|
+
|
|
390
|
+
addCommand(cmd: string): void; // respects recording/paused state
|
|
391
|
+
appendCommand(cmd: string): void; // always appends (for manual assertions)
|
|
392
|
+
removeCommand(index: number): void;
|
|
393
|
+
moveCommand(from: number, to: number): void;
|
|
394
|
+
|
|
395
|
+
registerInterceptor(method: string, url: string, alias: string): void;
|
|
396
|
+
removeInterceptor(index: number): void;
|
|
397
|
+
|
|
398
|
+
clearCommands(): void;
|
|
399
|
+
|
|
400
|
+
getCommandsSnapshot(): string[];
|
|
401
|
+
getInterceptorsSnapshot(): string[];
|
|
402
|
+
|
|
403
|
+
onCommandsChange(fn: (cmds: string[]) => void): () => void;
|
|
404
|
+
onInterceptorsChange(fn: (ints: string[]) => void): () => void;
|
|
405
|
+
onRecordingChange(fn: (isRecording: boolean) => void): () => void;
|
|
406
|
+
onPauseChange(fn: (isPaused: boolean) => void): () => void;
|
|
407
|
+
|
|
408
|
+
destroy(): void;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### `PersistenceService`
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
class PersistenceService {
|
|
416
|
+
insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[]): Promise<number>;
|
|
417
|
+
getAllTests(): Promise<TestWithDetails[]>;
|
|
418
|
+
getTestById(testId: number): Promise<TestDetail | null>;
|
|
419
|
+
deleteTest(id: number): Promise<void>;
|
|
420
|
+
|
|
421
|
+
setConfig(config: Record<string, unknown>): Promise<void>;
|
|
422
|
+
setConfigKey(key: string, value: unknown): Promise<void>;
|
|
423
|
+
getConfig(key: string): Promise<Record<string, unknown> | null>;
|
|
424
|
+
getGeneralConfig(): Promise<ConfigRecord | null>;
|
|
425
|
+
|
|
426
|
+
clearAllData(): Promise<void>;
|
|
427
|
+
requestDirectoryPermissions(): Promise<void>;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Technical notes
|
|
434
|
+
|
|
435
|
+
- **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
|
|
436
|
+
- **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag.
|
|
437
|
+
- **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
|
|
438
|
+
- **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
|
|
439
|
+
- **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Development
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
npm install
|
|
447
|
+
npm run build # compile with tsup
|
|
448
|
+
npm test # run vitest unit tests
|
|
449
|
+
npm run test:coverage # coverage report (≥80% gate)
|
|
450
|
+
npm run lint # ESLint
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Licence
|
|
458
|
+
|
|
459
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/utils/selector-quality.utils.ts
|
|
2
|
+
var FORBIDDEN_ID_PREFIXES = [
|
|
3
|
+
"cdk-",
|
|
4
|
+
"mat-",
|
|
5
|
+
"p-",
|
|
6
|
+
"ng-",
|
|
7
|
+
"mdc-",
|
|
8
|
+
"primeng-",
|
|
9
|
+
"auto-",
|
|
10
|
+
"field-",
|
|
11
|
+
"input-",
|
|
12
|
+
"select-"
|
|
13
|
+
];
|
|
14
|
+
function getSelectorQuality(el) {
|
|
15
|
+
if (el.hasAttribute("data-cy") || el.hasAttribute("data-testid") || el.hasAttribute("aria-label")) {
|
|
16
|
+
return "excellent";
|
|
17
|
+
}
|
|
18
|
+
const rawId = el.id;
|
|
19
|
+
if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
|
|
20
|
+
return "good";
|
|
21
|
+
}
|
|
22
|
+
if (typeof el.className === "string" && el.className.trim().length > 0) {
|
|
23
|
+
return "acceptable";
|
|
24
|
+
}
|
|
25
|
+
return "poor";
|
|
26
|
+
}
|
|
27
|
+
function buildPickerSelector(el) {
|
|
28
|
+
const dataCy = el.getAttribute("data-cy");
|
|
29
|
+
if (dataCy) return `[data-cy="${dataCy}"]`;
|
|
30
|
+
const dataTestid = el.getAttribute("data-testid");
|
|
31
|
+
if (dataTestid) return `[data-testid="${dataTestid}"]`;
|
|
32
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
33
|
+
if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
|
|
34
|
+
const rawId = el.id;
|
|
35
|
+
if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
|
|
36
|
+
return `#${rawId}`;
|
|
37
|
+
}
|
|
38
|
+
if (typeof el.className === "string" && el.className.trim().length > 0) {
|
|
39
|
+
const classes = el.className.trim().split(/\s+/);
|
|
40
|
+
return classes.map((c) => `.${c}`).join("");
|
|
41
|
+
}
|
|
42
|
+
return el.tagName.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/utils/html.utils.ts
|
|
46
|
+
function escHtml(s) {
|
|
47
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
|
+
}
|
|
49
|
+
function escAttr(s) {
|
|
50
|
+
return s.replace(/"/g, """).replace(/</g, "<");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
FORBIDDEN_ID_PREFIXES,
|
|
55
|
+
getSelectorQuality,
|
|
56
|
+
buildPickerSelector,
|
|
57
|
+
escHtml,
|
|
58
|
+
escAttr
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=chunk-DRNRKHXN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/selector-quality.utils.ts","../src/utils/html.utils.ts"],"sourcesContent":["export type SelectorQuality = 'excellent' | 'good' | 'acceptable' | 'poor';\n\nexport const FORBIDDEN_ID_PREFIXES = [\n 'cdk-', 'mat-', 'p-', 'ng-', 'mdc-',\n 'primeng-', 'auto-', 'field-', 'input-', 'select-',\n];\n\nexport function getSelectorQuality(el: HTMLElement): SelectorQuality {\n if (\n el.hasAttribute('data-cy') ||\n el.hasAttribute('data-testid') ||\n el.hasAttribute('aria-label')\n ) {\n return 'excellent';\n }\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return 'good';\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n return 'acceptable';\n }\n\n return 'poor';\n}\n\n/** Returns the best cy.get() selector string for a given element. */\nexport function buildPickerSelector(el: HTMLElement): string {\n const dataCy = el.getAttribute('data-cy');\n if (dataCy) return `[data-cy=\"${dataCy}\"]`;\n\n const dataTestid = el.getAttribute('data-testid');\n if (dataTestid) return `[data-testid=\"${dataTestid}\"]`;\n\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return `[aria-label=\"${ariaLabel}\"]`;\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return `#${rawId}`;\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n const classes = el.className.trim().split(/\\s+/);\n return classes.map((c) => `.${c}`).join('');\n }\n\n return el.tagName.toLowerCase();\n}\n","export function escHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nexport function escAttr(s: string): string {\n return s.replace(/\"/g, '"').replace(/</g, '<');\n}\n"],"mappings":";AAEO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAC7B;AAAA,EAAY;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAC3C;AAEO,SAAS,mBAAmB,IAAkC;AACnE,MACE,GAAG,aAAa,SAAS,KACzB,GAAG,aAAa,aAAa,KAC7B,GAAG,aAAa,YAAY,GAC5B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,IAAyB;AAC3D,QAAM,SAAS,GAAG,aAAa,SAAS;AACxC,MAAI,OAAQ,QAAO,aAAa,MAAM;AAEtC,QAAM,aAAa,GAAG,aAAa,aAAa;AAChD,MAAI,WAAY,QAAO,iBAAiB,UAAU;AAElD,QAAM,YAAY,GAAG,aAAa,YAAY;AAC9C,MAAI,UAAW,QAAO,gBAAgB,SAAS;AAE/C,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,UAAM,UAAU,GAAG,UAAU,KAAK,EAAE,MAAM,KAAK;AAC/C,WAAO,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE;AAAA,EAC5C;AAEA,SAAO,GAAG,QAAQ,YAAY;AAChC;;;AC9DO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC5E;AAEO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,MAAM;AACvD;","names":[]}
|