lib-e2e-cypress-for-dummys-ts 0.2.0 → 0.5.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/dist/index.js CHANGED
@@ -1,14 +1,25 @@
1
1
  import {
2
2
  FORBIDDEN_ID_PREFIXES,
3
+ SelectorPickerElement,
3
4
  escAttr,
4
5
  escHtml
5
- } from "./chunk-DRNRKHXN.js";
6
+ } from "./chunk-FDMUIHDZ.js";
6
7
 
7
8
  // src/models/lang.model.ts
8
9
  var SUPPORTED_LANGS = ["es", "en", "fr", "it", "de"];
9
10
  function isLang(value) {
10
11
  return SUPPORTED_LANGS.includes(value);
11
12
  }
13
+ var LOCALE_BY_LANG = {
14
+ es: "es-ES",
15
+ en: "en-GB",
16
+ fr: "fr-FR",
17
+ it: "it-IT",
18
+ de: "de-DE"
19
+ };
20
+ function localeForLang(lang) {
21
+ return isLang(lang) ? LOCALE_BY_LANG[lang] : "es-ES";
22
+ }
12
23
 
13
24
  // src/models/input-types.model.ts
14
25
  var INPUT_TYPES = [
@@ -25,7 +36,7 @@ var INPUT_TYPES = [
25
36
  // src/models/db-schema.model.ts
26
37
  var DB_SCHEMA = {
27
38
  name: "E2ECypressDB",
28
- version: 10,
39
+ version: 11,
29
40
  stores: [
30
41
  {
31
42
  name: "tests",
@@ -65,11 +76,45 @@ var DB_SCHEMA = {
65
76
  { name: "extendedHttpCommands", keyPath: "extendedHttpCommands", unique: false },
66
77
  { name: "allowReadWriteFiles", keyPath: "allowReadWriteFiles", unique: false }
67
78
  ]
79
+ },
80
+ {
81
+ // Single-record store (fixed key id=1) holding the live, in-progress
82
+ // recording session so it survives a micro-frontend crossing or reload.
83
+ // See docs/specs/006-cross-app-recording-continuity.md.
84
+ name: "activeSession",
85
+ keyPath: "id",
86
+ autoIncrement: false,
87
+ indexes: []
68
88
  }
69
89
  ]
70
90
  };
71
91
  var DB_STORE_NAMES = DB_SCHEMA.stores.map((s) => s.name);
72
92
 
93
+ // src/models/active-session.model.ts
94
+ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
95
+ var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
96
+ var DEFAULT_RESUME_TTL_MINUTES = 30;
97
+
98
+ // src/utils/subject.ts
99
+ var Subject = class {
100
+ _value;
101
+ listeners = /* @__PURE__ */ new Set();
102
+ constructor(initialValue) {
103
+ this._value = initialValue;
104
+ }
105
+ next(value) {
106
+ this._value = value;
107
+ this.listeners.forEach((l) => l(value));
108
+ }
109
+ getValue() {
110
+ return this._value;
111
+ }
112
+ subscribe(fn) {
113
+ this.listeners.add(fn);
114
+ return () => this.listeners.delete(fn);
115
+ }
116
+ };
117
+
73
118
  // src/i18n/es.ts
74
119
  var I18N_ES = {
75
120
  MAIN_FRAME: {
@@ -92,7 +137,9 @@ var I18N_ES = {
92
137
  NO_TAGS: "Sin etiquetas",
93
138
  SAVE_BTN: "\u{1F4BE} Guardar",
94
139
  SAVE_AND_EDIT: "\u{1F4DD} Guardar y editar",
95
- REMOVE_TAG_TITLE: "Quitar"
140
+ REMOVE_TAG_TITLE: "Quitar",
141
+ NOTES_LABEL: "Descripci\xF3n / Notas (opcional):",
142
+ NOTES_PLACEHOLDER: "Qu\xE9 valida este test, condiciones previas, etc."
96
143
  },
97
144
  TEST_EDITOR: {
98
145
  NO_TAGS: "Sin etiquetas",
@@ -102,8 +149,6 @@ var I18N_ES = {
102
149
  COPY_DESCRIBE: "\u{1F5C2} Copiar describe()",
103
150
  MULTI_SELECT: "\u{1F5C2} Multi-select",
104
151
  CANCEL_SELECT: "\u2715 Cancelar",
105
- SECTION_COMMANDS: "Comandos",
106
- SECTION_INTERCEPTORS: "Interceptores",
107
152
  COPY_CMDS_BTN: "\u{1F4CB} Copiar comandos",
108
153
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
109
154
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -132,6 +177,18 @@ var I18N_ES = {
132
177
  DATA_SECTION: "\u{1F4BE} Datos",
133
178
  DATA_DESC: "Exporta todos tus tests a JSON o importa una copia de seguridad.",
134
179
  EXPORT_BTN: "\u2B06\uFE0F Exportar tests",
180
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exportar tests",
181
+ EXPORT_MODE_ALL: "Todo",
182
+ EXPORT_MODE_MANUAL: "Selecci\xF3n manual",
183
+ EXPORT_MODE_TAGS: "Por tags",
184
+ EXPORT_ALL_DESC: "Se exportar\xE1n todos los tests guardados.",
185
+ EXPORT_COUNT: "A exportar:",
186
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportar",
187
+ EXPORT_CANCEL: "Cancelar",
188
+ EXPORT_EMPTY: "No hay tests para exportar.",
189
+ EXPORT_NO_TAGS: "No hay tags disponibles.",
190
+ EXPORT_TAGS_HINT: "Selecciona uno o m\xE1s tags para ver qu\xE9 pruebas se exportar\xE1n.",
191
+ EXPORT_RESULT_LABEL: "Pruebas que se exportar\xE1n:",
135
192
  IMPORT_BTN: "\u2B07\uFE0F Importar tests",
136
193
  FOLDER_UPDATED_TOAST: "\u2713 Carpeta de Cypress actualizada",
137
194
  FOLDER_ERROR_TOAST: "Error al acceder a la carpeta",
@@ -141,7 +198,13 @@ var I18N_ES = {
141
198
  JSON_BAD_FORMAT: "El archivo no tiene el formato esperado.",
142
199
  SMART_SELECTOR_SECTION: "\u{1F50D} Selector inteligente",
143
200
  SMART_SELECTOR_TITLE: "Selector inteligente",
144
- SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido."
201
+ SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido.",
202
+ START_HIDDEN_SECTION: "\u{1F441} Visibilidad del widget",
203
+ START_HIDDEN_TITLE: "Iniciar oculto",
204
+ START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo.",
205
+ RESUME_TTL_SECTION: "\u23F1 Continuidad de grabaci\xF3n",
206
+ RESUME_TTL_LABEL: "Reanudar sin preguntar (minutos)",
207
+ RESUME_TTL_HINT: "Si recargas o cambias de proyecto, una grabaci\xF3n activa se reanuda sin preguntar dentro de este margen. Pasado ese tiempo, te preguntar\xE1 si continuar o descartar."
145
208
  },
146
209
  SELECTOR_PICKER: {
147
210
  TITLE: "Selecciona un elemento del DOM",
@@ -182,8 +245,10 @@ var I18N_ES = {
182
245
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
183
246
  CLOSE_BTN: "\u2715 Cerrar",
184
247
  NEW_FILE_BTN: "+ Nuevo archivo",
248
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
185
249
  REFRESH_BTN: "\u21BB Actualizar",
186
250
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
251
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
187
252
  NEW_FILE_CONFIRM: "Crear",
188
253
  NEW_FILE_CANCEL: "Cancelar"
189
254
  },
@@ -196,10 +261,19 @@ var I18N_ES = {
196
261
  NO_CHANGES: "Sin cambios respecto al original",
197
262
  NO_CHANGES_SHORT: "Sin cambios",
198
263
  LAUNCH_BTN: "\u25B6 Lanzar test",
264
+ LAUNCH_LOCAL_ONLY: "Mu\xE9velo a local para poder probar",
265
+ LAUNCH_RUNNING: "\u23F3 Ejecutando\u2026",
266
+ LAUNCH_PASSED: "\u2713 Prueba superada",
267
+ LAUNCH_FAILED: "\u2717 Prueba fallida",
268
+ LAUNCH_NO_RUNNER: "No se detect\xF3 el runner local (\xBFlo has arrancado?)",
199
269
  DIFF_BTN: "\u{1F4CA} Diff",
200
270
  SAVE_BTN: "\u{1F4BE} Guardar",
201
271
  CLOSE_BTN: "\u2715 Cerrar",
202
- BACK_TO_EDITOR: "\u2190 Volver al editor"
272
+ BACK_TO_EDITOR: "\u2190 Volver al editor",
273
+ INSERT_BTN: "\u{1FA84} Insertar bloques",
274
+ INSERT_TITLE: "Insertar it() y beforeEach() en el contenido del editor",
275
+ INSERT_DONE: "Bloques insertados en el editor",
276
+ INSERT_ERROR: "No se encontr\xF3 un bloque describe() v\xE1lido en el archivo"
203
277
  },
204
278
  RECORDER: {
205
279
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -228,7 +302,12 @@ var I18N_ES = {
228
302
  BADGE_PAUSED: "\u23F8 PAUSA",
229
303
  BADGE_REC: "\u25CF REC",
230
304
  STOP_TITLE: "Detener (Ctrl+R)",
231
- START_TITLE: "Grabar (Ctrl+R)"
305
+ START_TITLE: "Grabar (Ctrl+R)",
306
+ SESSION_RESUME_TITLE: "\u23FA Grabaci\xF3n en curso",
307
+ SESSION_RESUME_TEXT: "Hay una grabaci\xF3n activa sin terminar.",
308
+ SESSION_RESUME_COUNT: "comandos capturados",
309
+ SESSION_CONTINUE_BTN: "Continuar grabando",
310
+ SESSION_DISCARD_BTN: "Descartar"
232
311
  }
233
312
  };
234
313
 
@@ -254,7 +333,9 @@ var I18N_EN = {
254
333
  NO_TAGS: "No tags",
255
334
  SAVE_BTN: "\u{1F4BE} Save",
256
335
  SAVE_AND_EDIT: "\u{1F4DD} Save and edit",
257
- REMOVE_TAG_TITLE: "Remove"
336
+ REMOVE_TAG_TITLE: "Remove",
337
+ NOTES_LABEL: "Description / Notes (optional):",
338
+ NOTES_PLACEHOLDER: "What this test validates, preconditions, etc."
258
339
  },
259
340
  TEST_EDITOR: {
260
341
  NO_TAGS: "No tags",
@@ -264,8 +345,6 @@ var I18N_EN = {
264
345
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
265
346
  MULTI_SELECT: "\u{1F5C2} Multi-select",
266
347
  CANCEL_SELECT: "\u2715 Cancel",
267
- SECTION_COMMANDS: "Commands",
268
- SECTION_INTERCEPTORS: "Interceptors",
269
348
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
270
349
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
271
350
  DEFAULT_DESCRIBE: "Test suite",
@@ -294,6 +373,18 @@ var I18N_EN = {
294
373
  DATA_SECTION: "\u{1F4BE} Data",
295
374
  DATA_DESC: "Export all your tests to JSON or import a backup.",
296
375
  EXPORT_BTN: "\u2B06\uFE0F Export tests",
376
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Export tests",
377
+ EXPORT_MODE_ALL: "All",
378
+ EXPORT_MODE_MANUAL: "Manual selection",
379
+ EXPORT_MODE_TAGS: "By tags",
380
+ EXPORT_ALL_DESC: "All saved tests will be exported.",
381
+ EXPORT_COUNT: "To export:",
382
+ EXPORT_CONFIRM: "\u2B06\uFE0F Export",
383
+ EXPORT_CANCEL: "Cancel",
384
+ EXPORT_EMPTY: "No tests to export.",
385
+ EXPORT_NO_TAGS: "No tags available.",
386
+ EXPORT_TAGS_HINT: "Select one or more tags to preview which tests will be exported.",
387
+ EXPORT_RESULT_LABEL: "Tests that will be exported:",
297
388
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
298
389
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
299
390
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -303,7 +394,13 @@ var I18N_EN = {
303
394
  JSON_BAD_FORMAT: "The file does not have the expected format.",
304
395
  SMART_SELECTOR_SECTION: "\u{1F50D} Smart selector",
305
396
  SMART_SELECTOR_TITLE: "Smart selector",
306
- SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector."
397
+ SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
398
+ START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
399
+ START_HIDDEN_TITLE: "Start hidden",
400
+ START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it.",
401
+ RESUME_TTL_SECTION: "\u23F1 Recording continuity",
402
+ RESUME_TTL_LABEL: "Resume without asking (minutes)",
403
+ RESUME_TTL_HINT: "If you reload or switch projects, an active recording resumes without asking within this window. After that, you will be asked to continue or discard."
307
404
  },
308
405
  SELECTOR_PICKER: {
309
406
  TITLE: "Select a DOM element",
@@ -344,8 +441,10 @@ var I18N_EN = {
344
441
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
345
442
  CLOSE_BTN: "\u2715 Close",
346
443
  NEW_FILE_BTN: "+ New file",
444
+ NEW_FOLDER_BTN: "+ New folder",
347
445
  REFRESH_BTN: "\u21BB Refresh",
348
446
  NEW_FILE_PLACEHOLDER: "test-name",
447
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
349
448
  NEW_FILE_CONFIRM: "Create",
350
449
  NEW_FILE_CANCEL: "Cancel"
351
450
  },
@@ -358,10 +457,19 @@ var I18N_EN = {
358
457
  NO_CHANGES: "No changes from original",
359
458
  NO_CHANGES_SHORT: "No changes",
360
459
  LAUNCH_BTN: "\u25B6 Run test",
460
+ LAUNCH_LOCAL_ONLY: "Move it to localhost to run tests",
461
+ LAUNCH_RUNNING: "\u23F3 Running\u2026",
462
+ LAUNCH_PASSED: "\u2713 Test passed",
463
+ LAUNCH_FAILED: "\u2717 Test failed",
464
+ LAUNCH_NO_RUNNER: "No local runner detected (did you start it?)",
361
465
  DIFF_BTN: "\u{1F4CA} Diff",
362
466
  SAVE_BTN: "\u{1F4BE} Save",
363
467
  CLOSE_BTN: "\u2715 Close",
364
- BACK_TO_EDITOR: "\u2190 Back to editor"
468
+ BACK_TO_EDITOR: "\u2190 Back to editor",
469
+ INSERT_BTN: "\u{1FA84} Insert blocks",
470
+ INSERT_TITLE: "Insert it() and beforeEach() into the editor content",
471
+ INSERT_DONE: "Blocks inserted into the editor",
472
+ INSERT_ERROR: "No valid describe() block found in the file"
365
473
  },
366
474
  RECORDER: {
367
475
  FS_TITLE: "\u{1F4C1} File access",
@@ -390,7 +498,12 @@ var I18N_EN = {
390
498
  BADGE_PAUSED: "\u23F8 PAUSED",
391
499
  BADGE_REC: "\u25CF REC",
392
500
  STOP_TITLE: "Stop (Ctrl+R)",
393
- START_TITLE: "Record (Ctrl+R)"
501
+ START_TITLE: "Record (Ctrl+R)",
502
+ SESSION_RESUME_TITLE: "\u23FA Recording in progress",
503
+ SESSION_RESUME_TEXT: "There is an unfinished active recording.",
504
+ SESSION_RESUME_COUNT: "commands captured",
505
+ SESSION_CONTINUE_BTN: "Keep recording",
506
+ SESSION_DISCARD_BTN: "Discard"
394
507
  }
395
508
  };
396
509
 
@@ -416,7 +529,9 @@ var I18N_FR = {
416
529
  NO_TAGS: "Sans \xE9tiquettes",
417
530
  SAVE_BTN: "\u{1F4BE} Enregistrer",
418
531
  SAVE_AND_EDIT: "\u{1F4DD} Enregistrer et \xE9diter",
419
- REMOVE_TAG_TITLE: "Retirer"
532
+ REMOVE_TAG_TITLE: "Retirer",
533
+ NOTES_LABEL: "Description / Notes (optionnel) :",
534
+ NOTES_PLACEHOLDER: "Ce que ce test valide, conditions pr\xE9alables, etc."
420
535
  },
421
536
  TEST_EDITOR: {
422
537
  NO_TAGS: "Sans \xE9tiquettes",
@@ -426,8 +541,6 @@ var I18N_FR = {
426
541
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
427
542
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
428
543
  CANCEL_SELECT: "\u2715 Annuler",
429
- SECTION_COMMANDS: "Commandes",
430
- SECTION_INTERCEPTORS: "Intercepteurs",
431
544
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
432
545
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
433
546
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -456,6 +569,18 @@ var I18N_FR = {
456
569
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
457
570
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
458
571
  EXPORT_BTN: "\u2B06\uFE0F Exporter les tests",
572
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exporter les tests",
573
+ EXPORT_MODE_ALL: "Tout",
574
+ EXPORT_MODE_MANUAL: "S\xE9lection manuelle",
575
+ EXPORT_MODE_TAGS: "Par tags",
576
+ EXPORT_ALL_DESC: "Tous les tests enregistr\xE9s seront export\xE9s.",
577
+ EXPORT_COUNT: "\xC0 exporter :",
578
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exporter",
579
+ EXPORT_CANCEL: "Annuler",
580
+ EXPORT_EMPTY: "Aucun test \xE0 exporter.",
581
+ EXPORT_NO_TAGS: "Aucun tag disponible.",
582
+ EXPORT_TAGS_HINT: "S\xE9lectionnez un ou plusieurs tags pour voir les tests qui seront export\xE9s.",
583
+ EXPORT_RESULT_LABEL: "Tests qui seront export\xE9s :",
459
584
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
460
585
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
461
586
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -465,7 +590,13 @@ var I18N_FR = {
465
590
  JSON_BAD_FORMAT: "Le fichier n'a pas le format attendu.",
466
591
  SMART_SELECTOR_SECTION: "\u{1F50D} S\xE9lecteur intelligent",
467
592
  SMART_SELECTOR_TITLE: "S\xE9lecteur intelligent",
468
- SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide."
593
+ SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
594
+ START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
595
+ START_HIDDEN_TITLE: "D\xE9marrer masqu\xE9",
596
+ START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer.",
597
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE9 d'enregistrement",
598
+ RESUME_TTL_LABEL: "Reprendre sans demander (minutes)",
599
+ RESUME_TTL_HINT: "Si vous rechargez ou changez de projet, un enregistrement actif reprend sans demander dans ce d\xE9lai. Au-del\xE0, il vous demandera de continuer ou d'ignorer."
469
600
  },
470
601
  SELECTOR_PICKER: {
471
602
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -506,8 +637,10 @@ var I18N_FR = {
506
637
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
507
638
  CLOSE_BTN: "\u2715 Fermer",
508
639
  NEW_FILE_BTN: "+ Nouveau fichier",
640
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
509
641
  REFRESH_BTN: "\u21BB Actualiser",
510
642
  NEW_FILE_PLACEHOLDER: "nom-du-test",
643
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
511
644
  NEW_FILE_CONFIRM: "Cr\xE9er",
512
645
  NEW_FILE_CANCEL: "Annuler"
513
646
  },
@@ -520,10 +653,19 @@ var I18N_FR = {
520
653
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
521
654
  NO_CHANGES_SHORT: "Aucun changement",
522
655
  LAUNCH_BTN: "\u25B6 Lancer le test",
656
+ LAUNCH_LOCAL_ONLY: "Passe en local pour pouvoir tester",
657
+ LAUNCH_RUNNING: "\u23F3 Ex\xE9cution\u2026",
658
+ LAUNCH_PASSED: "\u2713 Test r\xE9ussi",
659
+ LAUNCH_FAILED: "\u2717 Test \xE9chou\xE9",
660
+ LAUNCH_NO_RUNNER: "Aucun runner local d\xE9tect\xE9 (l'avez-vous d\xE9marr\xE9 ?)",
523
661
  DIFF_BTN: "\u{1F4CA} Diff",
524
662
  SAVE_BTN: "\u{1F4BE} Enregistrer",
525
663
  CLOSE_BTN: "\u2715 Fermer",
526
- BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur"
664
+ BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur",
665
+ INSERT_BTN: "\u{1FA84} Ins\xE9rer les blocs",
666
+ INSERT_TITLE: "Ins\xE9rer it() et beforeEach() dans le contenu de l'\xE9diteur",
667
+ INSERT_DONE: "Blocs ins\xE9r\xE9s dans l'\xE9diteur",
668
+ INSERT_ERROR: "Aucun bloc describe() valide trouv\xE9 dans le fichier"
527
669
  },
528
670
  RECORDER: {
529
671
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -552,7 +694,12 @@ var I18N_FR = {
552
694
  BADGE_PAUSED: "\u23F8 PAUSE",
553
695
  BADGE_REC: "\u25CF REC",
554
696
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
555
- START_TITLE: "Enregistrer (Ctrl+R)"
697
+ START_TITLE: "Enregistrer (Ctrl+R)",
698
+ SESSION_RESUME_TITLE: "\u23FA Enregistrement en cours",
699
+ SESSION_RESUME_TEXT: "Un enregistrement actif est inachev\xE9.",
700
+ SESSION_RESUME_COUNT: "commandes captur\xE9es",
701
+ SESSION_CONTINUE_BTN: "Continuer l'enregistrement",
702
+ SESSION_DISCARD_BTN: "Ignorer"
556
703
  }
557
704
  };
558
705
 
@@ -578,7 +725,9 @@ var I18N_IT = {
578
725
  NO_TAGS: "Nessuna etichetta",
579
726
  SAVE_BTN: "\u{1F4BE} Salva",
580
727
  SAVE_AND_EDIT: "\u{1F4DD} Salva e modifica",
581
- REMOVE_TAG_TITLE: "Rimuovi"
728
+ REMOVE_TAG_TITLE: "Rimuovi",
729
+ NOTES_LABEL: "Descrizione / Note (opzionale):",
730
+ NOTES_PLACEHOLDER: "Cosa valida questo test, condizioni preliminari, ecc."
582
731
  },
583
732
  TEST_EDITOR: {
584
733
  NO_TAGS: "Nessuna etichetta",
@@ -588,8 +737,6 @@ var I18N_IT = {
588
737
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
589
738
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
590
739
  CANCEL_SELECT: "\u2715 Annulla",
591
- SECTION_COMMANDS: "Comandi",
592
- SECTION_INTERCEPTORS: "Interceptor",
593
740
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
594
741
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
595
742
  DEFAULT_DESCRIBE: "Suite di test",
@@ -618,6 +765,18 @@ var I18N_IT = {
618
765
  DATA_SECTION: "\u{1F4BE} Dati",
619
766
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
620
767
  EXPORT_BTN: "\u2B06\uFE0F Esporta test",
768
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Esporta test",
769
+ EXPORT_MODE_ALL: "Tutto",
770
+ EXPORT_MODE_MANUAL: "Selezione manuale",
771
+ EXPORT_MODE_TAGS: "Per tag",
772
+ EXPORT_ALL_DESC: "Verranno esportati tutti i test salvati.",
773
+ EXPORT_COUNT: "Da esportare:",
774
+ EXPORT_CONFIRM: "\u2B06\uFE0F Esporta",
775
+ EXPORT_CANCEL: "Annulla",
776
+ EXPORT_EMPTY: "Nessun test da esportare.",
777
+ EXPORT_NO_TAGS: "Nessun tag disponibile.",
778
+ EXPORT_TAGS_HINT: "Seleziona uno o pi\xF9 tag per vedere quali test verranno esportati.",
779
+ EXPORT_RESULT_LABEL: "Test che verranno esportati:",
621
780
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
622
781
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
623
782
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -627,7 +786,13 @@ var I18N_IT = {
627
786
  JSON_BAD_FORMAT: "Il file non ha il formato previsto.",
628
787
  SMART_SELECTOR_SECTION: "\u{1F50D} Selettore intelligente",
629
788
  SMART_SELECTOR_TITLE: "Selettore intelligente",
630
- SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido."
789
+ SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
790
+ START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
791
+ START_HIDDEN_TITLE: "Avvia nascosto",
792
+ START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo.",
793
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE0 di registrazione",
794
+ RESUME_TTL_LABEL: "Riprendi senza chiedere (minuti)",
795
+ RESUME_TTL_HINT: "Se ricarichi o cambi progetto, una registrazione attiva riprende senza chiedere entro questo intervallo. Trascorso, ti chieder\xE0 se continuare o annullare."
631
796
  },
632
797
  SELECTOR_PICKER: {
633
798
  TITLE: "Seleziona un elemento DOM",
@@ -668,8 +833,10 @@ var I18N_IT = {
668
833
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
669
834
  CLOSE_BTN: "\u2715 Chiudi",
670
835
  NEW_FILE_BTN: "+ Nuovo file",
836
+ NEW_FOLDER_BTN: "+ Nuova cartella",
671
837
  REFRESH_BTN: "\u21BB Aggiorna",
672
838
  NEW_FILE_PLACEHOLDER: "nome-del-test",
839
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
673
840
  NEW_FILE_CONFIRM: "Crea",
674
841
  NEW_FILE_CANCEL: "Annulla"
675
842
  },
@@ -682,10 +849,19 @@ var I18N_IT = {
682
849
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
683
850
  NO_CHANGES_SHORT: "Nessuna modifica",
684
851
  LAUNCH_BTN: "\u25B6 Esegui test",
852
+ LAUNCH_LOCAL_ONLY: "Spostati in locale per poter testare",
853
+ LAUNCH_RUNNING: "\u23F3 Esecuzione\u2026",
854
+ LAUNCH_PASSED: "\u2713 Test superato",
855
+ LAUNCH_FAILED: "\u2717 Test fallito",
856
+ LAUNCH_NO_RUNNER: "Nessun runner locale rilevato (l'hai avviato?)",
685
857
  DIFF_BTN: "\u{1F4CA} Diff",
686
858
  SAVE_BTN: "\u{1F4BE} Salva",
687
859
  CLOSE_BTN: "\u2715 Chiudi",
688
- BACK_TO_EDITOR: "\u2190 Torna all'editor"
860
+ BACK_TO_EDITOR: "\u2190 Torna all'editor",
861
+ INSERT_BTN: "\u{1FA84} Inserisci blocchi",
862
+ INSERT_TITLE: "Inserisci it() e beforeEach() nel contenuto dell'editor",
863
+ INSERT_DONE: "Blocchi inseriti nell'editor",
864
+ INSERT_ERROR: "Nessun blocco describe() valido trovato nel file"
689
865
  },
690
866
  RECORDER: {
691
867
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -714,7 +890,12 @@ var I18N_IT = {
714
890
  BADGE_PAUSED: "\u23F8 PAUSA",
715
891
  BADGE_REC: "\u25CF REC",
716
892
  STOP_TITLE: "Ferma (Ctrl+R)",
717
- START_TITLE: "Registra (Ctrl+R)"
893
+ START_TITLE: "Registra (Ctrl+R)",
894
+ SESSION_RESUME_TITLE: "\u23FA Registrazione in corso",
895
+ SESSION_RESUME_TEXT: "C'\xE8 una registrazione attiva non terminata.",
896
+ SESSION_RESUME_COUNT: "comandi catturati",
897
+ SESSION_CONTINUE_BTN: "Continua a registrare",
898
+ SESSION_DISCARD_BTN: "Annulla"
718
899
  }
719
900
  };
720
901
 
@@ -740,7 +921,9 @@ var I18N_DE = {
740
921
  NO_TAGS: "Keine Tags",
741
922
  SAVE_BTN: "\u{1F4BE} Speichern",
742
923
  SAVE_AND_EDIT: "\u{1F4DD} Speichern und bearbeiten",
743
- REMOVE_TAG_TITLE: "Entfernen"
924
+ REMOVE_TAG_TITLE: "Entfernen",
925
+ NOTES_LABEL: "Beschreibung / Notizen (optional):",
926
+ NOTES_PLACEHOLDER: "Was dieser Test validiert, Voraussetzungen usw."
744
927
  },
745
928
  TEST_EDITOR: {
746
929
  NO_TAGS: "Keine Tags",
@@ -750,8 +933,6 @@ var I18N_DE = {
750
933
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
751
934
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
752
935
  CANCEL_SELECT: "\u2715 Abbrechen",
753
- SECTION_COMMANDS: "Befehle",
754
- SECTION_INTERCEPTORS: "Interceptors",
755
936
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
756
937
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
757
938
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -780,6 +961,18 @@ var I18N_DE = {
780
961
  DATA_SECTION: "\u{1F4BE} Daten",
781
962
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
782
963
  EXPORT_BTN: "\u2B06\uFE0F Tests exportieren",
964
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Tests exportieren",
965
+ EXPORT_MODE_ALL: "Alle",
966
+ EXPORT_MODE_MANUAL: "Manuelle Auswahl",
967
+ EXPORT_MODE_TAGS: "Nach Tags",
968
+ EXPORT_ALL_DESC: "Alle gespeicherten Tests werden exportiert.",
969
+ EXPORT_COUNT: "Zu exportieren:",
970
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportieren",
971
+ EXPORT_CANCEL: "Abbrechen",
972
+ EXPORT_EMPTY: "Keine Tests zum Exportieren.",
973
+ EXPORT_NO_TAGS: "Keine Tags verf\xFCgbar.",
974
+ EXPORT_TAGS_HINT: "W\xE4hle ein oder mehrere Tags, um zu sehen, welche Tests exportiert werden.",
975
+ EXPORT_RESULT_LABEL: "Diese Tests werden exportiert:",
783
976
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
784
977
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
785
978
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -789,7 +982,13 @@ var I18N_DE = {
789
982
  JSON_BAD_FORMAT: "Die Datei hat nicht das erwartete Format.",
790
983
  SMART_SELECTOR_SECTION: "\u{1F50D} Intelligenter Selektor",
791
984
  SMART_SELECTOR_TITLE: "Intelligenter Selektor",
792
- SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird."
985
+ SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
986
+ START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
987
+ START_HIDDEN_TITLE: "Versteckt starten",
988
+ START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden.",
989
+ RESUME_TTL_SECTION: "\u23F1 Aufzeichnungskontinuit\xE4t",
990
+ RESUME_TTL_LABEL: "Ohne Nachfrage fortsetzen (Minuten)",
991
+ RESUME_TTL_HINT: "Wenn Sie neu laden oder das Projekt wechseln, wird eine aktive Aufzeichnung innerhalb dieses Zeitfensters ohne Nachfrage fortgesetzt. Danach werden Sie gefragt, ob fortsetzen oder verwerfen."
793
992
  },
794
993
  SELECTOR_PICKER: {
795
994
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -830,8 +1029,10 @@ var I18N_DE = {
830
1029
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
831
1030
  CLOSE_BTN: "\u2715 Schlie\xDFen",
832
1031
  NEW_FILE_BTN: "+ Neue Datei",
1032
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
833
1033
  REFRESH_BTN: "\u21BB Aktualisieren",
834
1034
  NEW_FILE_PLACEHOLDER: "test-name",
1035
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
835
1036
  NEW_FILE_CONFIRM: "Erstellen",
836
1037
  NEW_FILE_CANCEL: "Abbrechen"
837
1038
  },
@@ -844,10 +1045,19 @@ var I18N_DE = {
844
1045
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
845
1046
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
846
1047
  LAUNCH_BTN: "\u25B6 Test starten",
1048
+ LAUNCH_LOCAL_ONLY: "Wechsle zu localhost, um Tests auszuf\xFChren",
1049
+ LAUNCH_RUNNING: "\u23F3 L\xE4uft\u2026",
1050
+ LAUNCH_PASSED: "\u2713 Test bestanden",
1051
+ LAUNCH_FAILED: "\u2717 Test fehlgeschlagen",
1052
+ LAUNCH_NO_RUNNER: "Kein lokaler Runner erkannt (gestartet?)",
847
1053
  DIFF_BTN: "\u{1F4CA} Diff",
848
1054
  SAVE_BTN: "\u{1F4BE} Speichern",
849
1055
  CLOSE_BTN: "\u2715 Schlie\xDFen",
850
- BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor"
1056
+ BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor",
1057
+ INSERT_BTN: "\u{1FA84} Bl\xF6cke einf\xFCgen",
1058
+ INSERT_TITLE: "it() und beforeEach() in den Editor-Inhalt einf\xFCgen",
1059
+ INSERT_DONE: "Bl\xF6cke in den Editor eingef\xFCgt",
1060
+ INSERT_ERROR: "Kein g\xFCltiger describe()-Block in der Datei gefunden"
851
1061
  },
852
1062
  RECORDER: {
853
1063
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -876,13 +1086,18 @@ var I18N_DE = {
876
1086
  BADGE_PAUSED: "\u23F8 PAUSE",
877
1087
  BADGE_REC: "\u25CF REC",
878
1088
  STOP_TITLE: "Stopp (Ctrl+R)",
879
- START_TITLE: "Aufzeichnen (Ctrl+R)"
1089
+ START_TITLE: "Aufzeichnen (Ctrl+R)",
1090
+ SESSION_RESUME_TITLE: "\u23FA Aufzeichnung l\xE4uft",
1091
+ SESSION_RESUME_TEXT: "Es gibt eine nicht abgeschlossene aktive Aufzeichnung.",
1092
+ SESSION_RESUME_COUNT: "erfasste Befehle",
1093
+ SESSION_CONTINUE_BTN: "Weiter aufzeichnen",
1094
+ SESSION_DISCARD_BTN: "Verwerfen"
880
1095
  }
881
1096
  };
882
1097
 
883
1098
  // src/services/translation.service.ts
884
1099
  var TranslationService = class {
885
- lang;
1100
+ lang$;
886
1101
  translations = {
887
1102
  es: I18N_ES,
888
1103
  en: I18N_EN,
@@ -891,17 +1106,21 @@ var TranslationService = class {
891
1106
  de: I18N_DE
892
1107
  };
893
1108
  constructor() {
894
- this.lang = this.detectLang();
1109
+ this.lang$ = new Subject(this.detectLang());
895
1110
  }
896
1111
  setLang(lang) {
897
- this.lang = lang;
1112
+ this.lang$.next(lang);
898
1113
  }
899
1114
  getLang() {
900
- return this.lang;
1115
+ return this.lang$.getValue();
1116
+ }
1117
+ /** Subscribe to language changes; returns an unsubscribe function. */
1118
+ onLangChange(fn) {
1119
+ return this.lang$.subscribe(fn);
901
1120
  }
902
1121
  translate(key) {
903
1122
  const keys = key.split(".");
904
- let value = this.translations[this.lang];
1123
+ let value = this.translations[this.lang$.getValue()];
905
1124
  for (const k of keys) {
906
1125
  value = value?.[k];
907
1126
  if (value === void 0) return key;
@@ -915,14 +1134,41 @@ var TranslationService = class {
915
1134
  };
916
1135
  var translationService = new TranslationService();
917
1136
 
1137
+ // src/utils/code-format.utils.ts
1138
+ function gcd(a, b) {
1139
+ while (b) {
1140
+ const t = b;
1141
+ b = a % b;
1142
+ a = t;
1143
+ }
1144
+ return a;
1145
+ }
1146
+ function escapeSingleQuotes(value) {
1147
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1148
+ }
1149
+ function normalizeBlock(code, baseIndent) {
1150
+ const lines = code.split("\n").map((l) => l.trimEnd());
1151
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
1152
+ if (!nonEmpty.length) return "";
1153
+ const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
1154
+ const minIndent = Math.min(...indents);
1155
+ const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
1156
+ const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
1157
+ return lines.map((l) => {
1158
+ if (!l.trim()) return "";
1159
+ const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
1160
+ return baseIndent + " ".repeat(level) + l.trimStart();
1161
+ }).join("\n");
1162
+ }
1163
+
918
1164
  // src/services/transformation.service.ts
919
1165
  var TransformationService = class {
920
1166
  toLang(lang) {
921
1167
  return isLang(lang) ? lang : "en";
922
1168
  }
923
1169
  generateItDescription(description, commands) {
924
- const commandsBlock = commands.map((cmd) => ` ${cmd}`).join("\n");
925
- return `it('${description}', () => {
1170
+ const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
1171
+ return `it('${escapeSingleQuotes(description)}', () => {
926
1172
  ${commandsBlock}
927
1173
  });`;
928
1174
  }
@@ -952,6 +1198,13 @@ ${interceptors} })
952
1198
  }
953
1199
  return content.slice(0, idx) + "\n" + itBlock + "\n" + content.slice(idx);
954
1200
  }
1201
+ buildBlockComment(notes) {
1202
+ if (!notes.trim()) return "";
1203
+ const lines = notes.split("\n").map((l) => ` * ${l}`).join("\n");
1204
+ return `/**
1205
+ ${lines}
1206
+ */`;
1207
+ }
955
1208
  isFile(file) {
956
1209
  return !!file && file.kind === "file";
957
1210
  }
@@ -969,28 +1222,14 @@ ${interceptors} })
969
1222
  };
970
1223
  var advancedTestTransformationService = new AdvancedTestTransformationService();
971
1224
 
972
- // src/utils/subject.ts
973
- var Subject = class {
974
- _value;
975
- listeners = /* @__PURE__ */ new Set();
976
- constructor(initialValue) {
977
- this._value = initialValue;
978
- }
979
- next(value) {
980
- this._value = value;
981
- this.listeners.forEach((l) => l(value));
982
- }
983
- getValue() {
984
- return this._value;
985
- }
986
- subscribe(fn) {
987
- this.listeners.add(fn);
988
- return () => this.listeners.delete(fn);
989
- }
990
- };
991
-
992
1225
  // src/services/recording.service.ts
993
1226
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
1227
+ function createSessionId() {
1228
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1229
+ return crypto.randomUUID();
1230
+ }
1231
+ return `sess-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
1232
+ }
994
1233
  var RecordingService = class {
995
1234
  commands$ = new Subject([]);
996
1235
  interceptors$ = new Subject([]);
@@ -1000,6 +1239,9 @@ var RecordingService = class {
1000
1239
  inputDebounceTimers = /* @__PURE__ */ new Map();
1001
1240
  abort = new AbortController();
1002
1241
  selectorStrategy = "data-cy";
1242
+ /** Stable id of the current live session (null when none has started). */
1243
+ sessionId = null;
1244
+ startedAt = 0;
1003
1245
  // Stored originals for history patching cleanup
1004
1246
  origPushState = history.pushState.bind(history);
1005
1247
  origReplaceState = history.replaceState.bind(history);
@@ -1011,12 +1253,42 @@ var RecordingService = class {
1011
1253
  }
1012
1254
  // ── Public API ────────────────────────────────────────────────────────────
1013
1255
  startRecording() {
1256
+ this.sessionId = createSessionId();
1257
+ this.startedAt = Date.now();
1014
1258
  this.isPaused$.next(false);
1015
1259
  this.isRecording$.next(true);
1016
1260
  this.addCommand(`cy.viewport(1900, 1200)`);
1017
1261
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1018
1262
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1019
1263
  }
1264
+ /**
1265
+ * Rehydrates a previously persisted session WITHOUT running the startRecording
1266
+ * bootstrap (no viewport/visit/hide). Used to continue a recording across a
1267
+ * micro-frontend crossing or a same-origin reload.
1268
+ * See docs/specs/006-cross-app-recording-continuity.md.
1269
+ */
1270
+ restoreSession(state) {
1271
+ this.sessionId = state.sessionId;
1272
+ this.startedAt = state.startedAt;
1273
+ this.selectorStrategy = state.selectorStrategy;
1274
+ this.commands$.next([...state.commands]);
1275
+ this.interceptors$.next([...state.interceptors]);
1276
+ this.isPaused$.next(state.isPaused);
1277
+ this.isRecording$.next(state.isRecording);
1278
+ }
1279
+ /** Full snapshot of the live session for persistence. */
1280
+ getSessionSnapshot() {
1281
+ return {
1282
+ sessionId: this.sessionId ?? createSessionId(),
1283
+ isRecording: this.isRecording$.getValue(),
1284
+ isPaused: this.isPaused$.getValue(),
1285
+ commands: this.commands$.getValue(),
1286
+ interceptors: this.interceptors$.getValue(),
1287
+ selectorStrategy: this.selectorStrategy,
1288
+ startedAt: this.startedAt,
1289
+ updatedAt: Date.now()
1290
+ };
1291
+ }
1020
1292
  stopRecording() {
1021
1293
  this.isPaused$.next(false);
1022
1294
  this.isRecording$.next(false);
@@ -1103,6 +1375,21 @@ var RecordingService = class {
1103
1375
  onPauseChange(fn) {
1104
1376
  return this.isPaused$.subscribe(fn);
1105
1377
  }
1378
+ /**
1379
+ * Fires a full session snapshot whenever any persisted field changes
1380
+ * (commands, interceptors, recording or paused state). Drives the debounced
1381
+ * persistence of the live session. Returns a combined unsubscribe.
1382
+ */
1383
+ onSessionChange(fn) {
1384
+ const emit = () => fn(this.getSessionSnapshot());
1385
+ const unsubs = [
1386
+ this.commands$.subscribe(emit),
1387
+ this.interceptors$.subscribe(emit),
1388
+ this.isRecording$.subscribe(emit),
1389
+ this.isPaused$.subscribe(emit)
1390
+ ];
1391
+ return () => unsubs.forEach((u) => u());
1392
+ }
1106
1393
  onSelectorNotFound(fn) {
1107
1394
  return this.selectorNotFound$.subscribe((v) => {
1108
1395
  if (v) fn(v.target, v.action);
@@ -1334,6 +1621,7 @@ var RecordingService = class {
1334
1621
 
1335
1622
  // src/services/persistence.service.ts
1336
1623
  import { openDB } from "idb";
1624
+ var ACTIVE_SESSION_ID = 1;
1337
1625
  var PersistenceService = class {
1338
1626
  // dbName is overridable so tests can use unique names for isolation.
1339
1627
  constructor(dbName = DB_SCHEMA.name) {
@@ -1362,9 +1650,9 @@ var PersistenceService = class {
1362
1650
  return this._db;
1363
1651
  }
1364
1652
  // ── Tests ─────────────────────────────────────────────────────────────────
1365
- async insertTest(name, commands = [], interceptors = [], tags = []) {
1653
+ async insertTest(name, commands = [], interceptors = [], tags = [], notes) {
1366
1654
  const db = await this.getDB();
1367
- const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {} };
1655
+ const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {}, ...notes ? { notes } : {} };
1368
1656
  const id = await db.add("tests", record);
1369
1657
  if (commands.length) await this.insertCommands(commands, id);
1370
1658
  if (interceptors.length) await this.insertInterceptors(interceptors, id);
@@ -1387,10 +1675,10 @@ var PersistenceService = class {
1387
1675
  if (!record) return null;
1388
1676
  const commands = await this.getCommandStrings(testId);
1389
1677
  const interceptors = await this.getInterceptorStrings(testId);
1390
- const itBlock = `it('${record.name}', () => {
1391
- ${commands.join("\n ")}
1678
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
1679
+ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1392
1680
  });`;
1393
- const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => " " + i).join("\n") + "\n" : "";
1681
+ const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
1394
1682
  return { ...record, commands, interceptors, cypressCommands: commands, itBlock, interceptorsBlock };
1395
1683
  }
1396
1684
  async deleteTest(id) {
@@ -1468,6 +1756,25 @@ var PersistenceService = class {
1468
1756
  const records = await db.getAll("configuration");
1469
1757
  return records[0] ?? null;
1470
1758
  }
1759
+ // ── Active recording session ──────────────────────────────────────────────
1760
+ /** Upserts the live recording session (single record, fixed key). */
1761
+ async saveActiveSession(state) {
1762
+ const db = await this.getDB();
1763
+ await db.put("activeSession", { ...state, id: ACTIVE_SESSION_ID });
1764
+ }
1765
+ /** Returns the persisted live session, or null when none is stored. */
1766
+ async getActiveSession() {
1767
+ const db = await this.getDB();
1768
+ const record = await db.get("activeSession", ACTIVE_SESSION_ID);
1769
+ if (!record) return null;
1770
+ const { id: _id, ...state } = record;
1771
+ return state;
1772
+ }
1773
+ /** Removes the live session record. Safe to call when none exists. */
1774
+ async clearActiveSession() {
1775
+ const db = await this.getDB();
1776
+ await db.delete("activeSession", ACTIVE_SESSION_ID);
1777
+ }
1471
1778
  // ── Bulk operations ───────────────────────────────────────────────────────
1472
1779
  async clearAllData() {
1473
1780
  const db = await this.getDB();
@@ -1478,10 +1785,18 @@ var PersistenceService = class {
1478
1785
  ]);
1479
1786
  }
1480
1787
  async ingestFileData(tests, interceptors) {
1481
- await Promise.all([
1482
- this.bulkInsertWithoutId("tests", tests),
1483
- this.bulkInsertWithoutId("interceptors", interceptors)
1484
- ]);
1788
+ if (Array.isArray(tests)) {
1789
+ const db = await this.getDB();
1790
+ for (const test of tests) {
1791
+ const { id: _id, commands, interceptors: testInterceptors, ...record } = test;
1792
+ const newId = await db.add("tests", record);
1793
+ const cmds = Array.isArray(commands) ? commands : [];
1794
+ const icps = Array.isArray(testInterceptors) ? testInterceptors : [];
1795
+ if (cmds.length) await this.insertCommands(cmds, newId);
1796
+ if (icps.length) await this.insertInterceptors(icps, newId);
1797
+ }
1798
+ }
1799
+ await this.bulkInsertWithoutId("interceptors", interceptors);
1485
1800
  }
1486
1801
  async requestDirectoryPermissions() {
1487
1802
  if (!("showDirectoryPicker" in window)) {
@@ -1924,6 +2239,25 @@ function showToast(message, isSuccess = true) {
1924
2239
  setTimeout(() => toast.remove(), 3e3);
1925
2240
  }
1926
2241
 
2242
+ // src/utils/export-selection.utils.ts
2243
+ function selectTestsForExport(tests, mode, opts = {}) {
2244
+ if (mode === "all") return [...tests];
2245
+ if (mode === "manual") {
2246
+ const ids = new Set(opts.ids ?? []);
2247
+ return tests.filter((t) => ids.has(t.id));
2248
+ }
2249
+ const tags = new Set(opts.tags ?? []);
2250
+ if (tags.size === 0) return [];
2251
+ return tests.filter((t) => (t.tags ?? []).some((tag) => tags.has(tag)));
2252
+ }
2253
+
2254
+ // src/utils/host.utils.ts
2255
+ function isLocalHost(hostname) {
2256
+ if (!hostname) return true;
2257
+ const h = hostname.toLowerCase();
2258
+ return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
2259
+ }
2260
+
1927
2261
  // src/components/test-previsualizer/test-previsualizer.styles.ts
1928
2262
  var TEST_PREVISUALIZER_STYLES = `
1929
2263
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -2129,7 +2463,8 @@ if (!customElements.get("test-previsualizer")) {
2129
2463
  var SAVE_TEST_STYLES = `
2130
2464
  :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; }
2131
2465
  * { box-sizing: border-box; }
2132
- .container { padding: 24px 28px; text-align: center; }
2466
+ .container { padding: 24px 28px; text-align: left; }
2467
+ .btn-row, p { text-align: center; }
2133
2468
  p { margin: 0 0 20px; font-size: 14px; color: #8b949e; line-height: 1.5; }
2134
2469
  .btn-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
2135
2470
  button {
@@ -2151,6 +2486,15 @@ var SAVE_TEST_STYLES = `
2151
2486
  }
2152
2487
  input[type="text"]:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.15); }
2153
2488
  input[type="text"]::placeholder { color: #484f58; }
2489
+ textarea {
2490
+ width: 100%; padding: 10px 14px; border: 1px solid #30363d;
2491
+ border-radius: 8px; background: #0d1117; color: #e6edf3;
2492
+ font-size: 14px; outline: none; margin-bottom: 4px;
2493
+ font-family: inherit; resize: vertical; line-height: 1.5;
2494
+ transition: border-color 0.15s, box-shadow 0.15s;
2495
+ }
2496
+ textarea:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.15); }
2497
+ textarea::placeholder { color: #484f58; }
2154
2498
  .tag-label {
2155
2499
  font-size: 11px; color: #484f58; text-align: left; margin-bottom: 5px; display: block;
2156
2500
  }
@@ -2189,7 +2533,7 @@ function renderSaveTestAsk(t) {
2189
2533
  </div>
2190
2534
  </div>`;
2191
2535
  }
2192
- function renderSaveTestDesc(description, tags, t) {
2536
+ function renderSaveTestDesc(description, notes, tags, t) {
2193
2537
  const chipsHtml = tags.map(
2194
2538
  (tag) => `<span class="chip">${escHtml(tag)}<button class="chip-del" data-tag="${escAttr(tag)}" title="${t("SAVE_TEST.REMOVE_TAG_TITLE")}">\u2715</button></span>`
2195
2539
  ).join("");
@@ -2198,6 +2542,8 @@ function renderSaveTestDesc(description, tags, t) {
2198
2542
  <p>${t("SAVE_TEST.DESC_LABEL")} (<code>it()</code>):</p>
2199
2543
  <input id="desc-input" type="text" placeholder="${t("SAVE_TEST.DESC_PLACEHOLDER")}"
2200
2544
  value="${escAttr(description)}" autocomplete="off" />
2545
+ <span class="tag-label">${t("SAVE_TEST.NOTES_LABEL")}</span>
2546
+ <textarea id="notes-input" rows="3" placeholder="${escAttr(t("SAVE_TEST.NOTES_PLACEHOLDER"))}">${escHtml(notes)}</textarea>
2201
2547
  <span class="tag-label">${t("SAVE_TEST.TAGS_LABEL")}</span>
2202
2548
  <div class="tag-input-row">
2203
2549
  <input id="tag-input" type="text" placeholder="${t("SAVE_TEST.TAGS_PLACEHOLDER")}" autocomplete="off" />
@@ -2217,6 +2563,7 @@ var SaveTestElement = class extends HTMLElement {
2217
2563
  shadow;
2218
2564
  _step = "ask";
2219
2565
  description = "";
2566
+ notes = "";
2220
2567
  tags = [];
2221
2568
  translation = translationService;
2222
2569
  constructor() {
@@ -2234,17 +2581,18 @@ var SaveTestElement = class extends HTMLElement {
2234
2581
  this.render();
2235
2582
  }
2236
2583
  confirmSave() {
2237
- this.dispatch("savetest", { description: this.description.trim(), tags: [...this.tags] });
2584
+ this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
2238
2585
  }
2239
2586
  confirmSaveAndExport() {
2240
- this.dispatch("saveandexport", { description: this.description.trim(), tags: [...this.tags] });
2587
+ this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
2241
2588
  }
2242
2589
  cancel() {
2243
- this.dispatch("savetest", { description: null, tags: [] });
2590
+ this.dispatch("savetest", { description: null, notes: "", tags: [] });
2244
2591
  }
2245
2592
  restartComponent() {
2246
2593
  this._step = "ask";
2247
2594
  this.description = "";
2595
+ this.notes = "";
2248
2596
  this.tags = [];
2249
2597
  this.render();
2250
2598
  }
@@ -2271,12 +2619,16 @@ var SaveTestElement = class extends HTMLElement {
2271
2619
  this.shadow.getElementById("btn-yes")?.addEventListener("click", () => this.askSave());
2272
2620
  this.shadow.getElementById("btn-no")?.addEventListener("click", () => this.cancel());
2273
2621
  } else {
2274
- this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.tags, this.t.bind(this))}`;
2622
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.t.bind(this))}`;
2275
2623
  const descInput = this.shadow.getElementById("desc-input");
2624
+ const notesInput = this.shadow.getElementById("notes-input");
2276
2625
  const tagInput = this.shadow.getElementById("tag-input");
2277
2626
  descInput.addEventListener("input", () => {
2278
2627
  this.description = descInput.value;
2279
2628
  });
2629
+ notesInput.addEventListener("input", () => {
2630
+ this.notes = notesInput.value;
2631
+ });
2280
2632
  const tryAddTag = () => {
2281
2633
  this.addTag(tagInput.value);
2282
2634
  tagInput.value = "";
@@ -2382,18 +2734,127 @@ var TEST_EDITOR_STYLES = `
2382
2734
  background: #0d1117; padding: 10px 14px;
2383
2735
  border-top: 1px solid #21262d;
2384
2736
  }
2385
- .section-title { font-size: 10px; color: #484f58; text-transform: uppercase;
2386
- letter-spacing: 0.8px; margin-bottom: 6px; font-weight: 600; }
2387
- .cmd-list { font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
2388
- font-size: 11px; color: #c9d1d9; line-height: 1.8; }
2389
- .icp-list { font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
2390
- font-size: 11px; color: #3fb950; line-height: 1.8; margin-top: 10px; }
2391
- .copy-row { display: flex; gap: 6px; margin-top: 10px; }
2737
+ .test-notes {
2738
+ margin: 0 0 10px; padding: 8px 12px;
2739
+ background: rgba(47,129,247,0.06); border-left: 3px solid rgba(47,129,247,0.4);
2740
+ border-radius: 0 6px 6px 0; font-size: 12px; color: #8b949e;
2741
+ line-height: 1.6; white-space: pre-wrap; word-break: break-word;
2742
+ }
2743
+ .code-preview {
2744
+ margin: 0; padding: 10px 12px; background: #161b22;
2745
+ border-radius: 6px; border: 1px solid #21262d;
2746
+ font-size: 11px; color: #c9d1d9; line-height: 1.7;
2747
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
2748
+ overflow-x: auto; white-space: pre;
2749
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
2750
+ }
2751
+ .code-preview::-webkit-scrollbar { height: 4px; }
2752
+ .code-preview::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
2753
+ .code-preview-icp { margin-top: 8px; }
2754
+ .sh-kw { color: #ff7b72; }
2755
+ .sh-bi { color: #d2a8ff; }
2756
+ .sh-str { color: #a5d6ff; }
2757
+ .sh-cmt { color: #8b949e; font-style: italic; }
2758
+ .sh-num { color: #79c0ff; }
2392
2759
  `;
2393
2760
 
2761
+ // src/utils/syntax-highlight.utils.ts
2762
+ var KEYWORDS = /* @__PURE__ */ new Set([
2763
+ "describe",
2764
+ "it",
2765
+ "xit",
2766
+ "fit",
2767
+ "xdescribe",
2768
+ "fdescribe",
2769
+ "beforeEach",
2770
+ "afterEach",
2771
+ "before",
2772
+ "after",
2773
+ "context",
2774
+ "const",
2775
+ "let",
2776
+ "var",
2777
+ "function",
2778
+ "return",
2779
+ "if",
2780
+ "else",
2781
+ "import",
2782
+ "export",
2783
+ "from",
2784
+ "new",
2785
+ "this",
2786
+ "null",
2787
+ "undefined",
2788
+ "true",
2789
+ "false",
2790
+ "async",
2791
+ "await",
2792
+ "of",
2793
+ "in",
2794
+ "typeof",
2795
+ "void"
2796
+ ]);
2797
+ var BUILTINS = /* @__PURE__ */ new Set(["cy", "expect", "assert", "Cypress", "chai"]);
2798
+ function syntaxHighlight(code) {
2799
+ return code.split("\n").map(highlightLine).join("\n");
2800
+ }
2801
+ function highlightLine(line) {
2802
+ let result = "";
2803
+ let i = 0;
2804
+ const len = line.length;
2805
+ while (i < len) {
2806
+ if (line[i] === "/" && i + 1 < len && line[i + 1] === "/") {
2807
+ result += `<span class="sh-cmt">${escHtml(line.slice(i))}</span>`;
2808
+ return result;
2809
+ }
2810
+ const q = line[i];
2811
+ if (q === "'" || q === '"' || q === "`") {
2812
+ let j = i + 1;
2813
+ while (j < len) {
2814
+ if (line[j] === "\\") {
2815
+ j += 2;
2816
+ continue;
2817
+ }
2818
+ if (line[j] === q) {
2819
+ j++;
2820
+ break;
2821
+ }
2822
+ j++;
2823
+ }
2824
+ result += `<span class="sh-str">${escHtml(line.slice(i, j))}</span>`;
2825
+ i = j;
2826
+ continue;
2827
+ }
2828
+ if (/[0-9]/.test(line[i]) && (i === 0 || !/[a-zA-Z_$]/.test(line[i - 1]))) {
2829
+ let j = i;
2830
+ while (j < len && /[0-9.]/.test(line[j])) j++;
2831
+ result += `<span class="sh-num">${escHtml(line.slice(i, j))}</span>`;
2832
+ i = j;
2833
+ continue;
2834
+ }
2835
+ if (/[a-zA-Z_$]/.test(line[i])) {
2836
+ let j = i;
2837
+ while (j < len && /[a-zA-Z0-9_$]/.test(line[j])) j++;
2838
+ const word = line.slice(i, j);
2839
+ if (KEYWORDS.has(word)) {
2840
+ result += `<span class="sh-kw">${escHtml(word)}</span>`;
2841
+ } else if (BUILTINS.has(word)) {
2842
+ result += `<span class="sh-bi">${escHtml(word)}</span>`;
2843
+ } else {
2844
+ result += escHtml(word);
2845
+ }
2846
+ i = j;
2847
+ continue;
2848
+ }
2849
+ result += escHtml(line[i]);
2850
+ i++;
2851
+ }
2852
+ return result;
2853
+ }
2854
+
2394
2855
  // src/components/test-editor/test-editor.template.ts
2395
2856
  function renderTestEditor(state, t) {
2396
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
2857
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
2397
2858
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
2398
2859
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
2399
2860
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -2404,22 +2865,24 @@ function renderTestEditor(state, t) {
2404
2865
  </div>` : "";
2405
2866
  const rows = visible.map((test, i) => {
2406
2867
  const expanded = expandedIndex === i;
2407
- const date = new Date(test.createdAt).toLocaleString("es-ES", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
2868
+ const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
2408
2869
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
2409
2870
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
2410
2871
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
2872
+ const hasIcps = (test.interceptors ?? []).length > 0;
2873
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
2874
+ ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
2875
+ });`;
2876
+ const icpBlockCode = icps.length ? `beforeEach(() => {
2877
+ // Auto-generated Cypress interceptors
2878
+ ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
2879
+ });` : "";
2880
+ const notesHtml = expanded && test.notes ? `<p class="test-notes">${escHtml(test.notes)}</p>` : "";
2411
2881
  const body = expanded ? `
2412
2882
  <div class="row-body">
2413
- <div class="section-title">${t("TEST_EDITOR.SECTION_COMMANDS")} (${(test.commands ?? []).length})</div>
2414
- <div class="cmd-list">${(test.commands ?? []).map(escHtml).join("<br>")}</div>
2415
- ${icps.length ? `<div class="icp-list">
2416
- <div class="section-title" style="margin-top:8px">${t("TEST_EDITOR.SECTION_INTERCEPTORS")}</div>
2417
- ${icps.map(escHtml).join("<br>")}
2418
- </div>` : ""}
2419
- <div class="copy-row">
2420
- <button class="btn-icon" data-action="copy-cmds" data-idx="${i}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
2421
- ${icps.length ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
2422
- </div>
2883
+ ${notesHtml}
2884
+ <pre class="code-preview">${syntaxHighlight(itBlockCode)}</pre>
2885
+ ${icpBlockCode ? `<pre class="code-preview code-preview-icp">${syntaxHighlight(icpBlockCode)}</pre>` : ""}
2423
2886
  </div>` : "";
2424
2887
  return `
2425
2888
  <div class="row${selectedIds.has(test.id) ? " selected-row" : ""}">
@@ -2428,6 +2891,8 @@ function renderTestEditor(state, t) {
2428
2891
  <span class="test-name">${escHtml(test.name)}</span>
2429
2892
  ${tagsHtml}
2430
2893
  <span class="test-date">${date}</span>
2894
+ <button class="btn-icon" data-action="copy-cmds" data-idx="${i}" title="${t("TEST_EDITOR.COPY_CMDS_BTN")}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
2895
+ ${hasIcps ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}" title="${t("TEST_EDITOR.COPY_ICPS_BTN")}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
2431
2896
  <button class="btn-icon btn-del" data-action="delete" data-id="${test.id}" title="${t("TEST_EDITOR.DELETE_TITLE")}">\u{1F5D1}</button>
2432
2897
  </div>
2433
2898
  ${body}
@@ -2513,11 +2978,11 @@ var TestEditorElement = class extends HTMLElement {
2513
2978
  ` : "";
2514
2979
  const itBlocks = selected.map((t) => {
2515
2980
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
2516
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
2981
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
2517
2982
  ${cmds}
2518
2983
  });`;
2519
2984
  }).join("\n\n");
2520
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
2985
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
2521
2986
  ${beforeEach}${itBlocks}
2522
2987
  });`;
2523
2988
  navigator.clipboard?.writeText(block);
@@ -2551,7 +3016,8 @@ ${beforeEach}${itBlocks}
2551
3016
  selectedIds: this.selectedIds,
2552
3017
  describeName: this.describeName,
2553
3018
  expandedIndex: this.expandedIndex,
2554
- interceptorsByTest: this.interceptorsByTest
3019
+ interceptorsByTest: this.interceptorsByTest,
3020
+ locale: localeForLang(this.translation.getLang())
2555
3021
  }, this.t.bind(this))}`;
2556
3022
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
2557
3023
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -2606,7 +3072,7 @@ ${beforeEach}${itBlocks}
2606
3072
  ev.stopPropagation();
2607
3073
  const idx = Number(el.dataset["idx"]);
2608
3074
  const t = visible[idx];
2609
- this.copyToClipboard((this.interceptorsByTest[t?.id] ?? []).join("\n"));
3075
+ this.copyToClipboard((this.interceptorsByTest[t?.id] ?? t?.interceptors ?? []).join("\n"));
2610
3076
  });
2611
3077
  });
2612
3078
  }
@@ -2708,6 +3174,71 @@ var CONFIGURATION_STYLES = `
2708
3174
 
2709
3175
  /* \u2500\u2500 Data section desc \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2710
3176
  .data-desc { font-size: 11px; color: #484f58; margin-bottom: 10px; line-height: 1.5; }
3177
+
3178
+ /* \u2500\u2500 Export overlay (modal) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3179
+ .export-overlay {
3180
+ position: fixed; inset: 0; z-index: 100000;
3181
+ background: rgba(1,4,9,0.7);
3182
+ display: flex; align-items: center; justify-content: center; padding: 20px;
3183
+ }
3184
+ .export-modal {
3185
+ width: 640px; max-width: 100%; max-height: 86vh;
3186
+ display: flex; flex-direction: column;
3187
+ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
3188
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5); overflow: hidden;
3189
+ }
3190
+ .export-hd {
3191
+ padding: 14px 18px; font-size: 13px; font-weight: 700;
3192
+ border-bottom: 1px solid #21262d; color: #e6edf3;
3193
+ }
3194
+ .export-modes { display: flex; gap: 6px; padding: 12px 18px 0; }
3195
+ .export-mode {
3196
+ flex: 1; padding: 7px 10px; font-size: 11px; border-radius: 6px;
3197
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3198
+ }
3199
+ .export-mode:hover { background: #21262d; color: #e6edf3; }
3200
+ .export-mode.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3201
+ .export-body {
3202
+ padding: 14px 18px; overflow-y: auto; flex: 1; min-height: 160px;
3203
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3204
+ }
3205
+ .export-tag-hint { font-size: 12px; color: #484f58; margin-top: 12px; }
3206
+ .export-result-label {
3207
+ font-size: 10px; color: #484f58; text-transform: uppercase; letter-spacing: 0.6px;
3208
+ font-weight: 600; margin: 14px 0 6px;
3209
+ }
3210
+ .export-row-static { cursor: default; }
3211
+ .export-row-static:hover { background: transparent; }
3212
+ .export-all-desc { font-size: 12px; color: #8b949e; }
3213
+ .export-empty { font-size: 12px; color: #484f58; text-align: center; padding: 20px; }
3214
+ .export-list { display: flex; flex-direction: column; gap: 4px; }
3215
+ .export-row {
3216
+ display: flex; align-items: center; gap: 8px;
3217
+ padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: #c9d1d9;
3218
+ }
3219
+ .export-row:hover { background: #0d1117; }
3220
+ .export-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3221
+ .export-row-tag {
3222
+ font-size: 9px; color: #8b949e; background: #0d1117;
3223
+ border: 1px solid #30363d; border-radius: 10px; padding: 1px 7px; flex-shrink: 0;
3224
+ }
3225
+ .export-tags { display: flex; flex-wrap: wrap; gap: 6px; }
3226
+ .export-tag {
3227
+ padding: 4px 10px; font-size: 11px; border-radius: 12px;
3228
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3229
+ }
3230
+ .export-tag:hover { background: #21262d; color: #e6edf3; }
3231
+ .export-tag.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3232
+ .export-ft {
3233
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
3234
+ padding: 12px 18px; border-top: 1px solid #21262d;
3235
+ }
3236
+ .export-count { font-size: 11px; color: #8b949e; }
3237
+ .export-count b { color: #e6edf3; }
3238
+ .export-ft-actions { display: flex; gap: 8px; }
3239
+ .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
3240
+ .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
3241
+ .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
2711
3242
  `;
2712
3243
 
2713
3244
  // src/components/configuration/configuration.template.ts
@@ -2718,8 +3249,64 @@ var LANGS = [
2718
3249
  { value: "it", label: "Italiano" },
2719
3250
  { value: "de", label: "Deutsch" }
2720
3251
  ];
3252
+ function renderExportOverlay(state, t) {
3253
+ const { isExporting, exportMode, exportTests, exportSelectedIds, exportSelectedTags } = state;
3254
+ if (!isExporting) return "";
3255
+ const count = selectTestsForExport(exportTests, exportMode, {
3256
+ ids: exportSelectedIds,
3257
+ tags: exportSelectedTags
3258
+ }).length;
3259
+ const modeBtn = (mode, key) => `<button class="export-mode ${exportMode === mode ? "active" : ""}" data-export-mode="${mode}">${t(key)}</button>`;
3260
+ let body;
3261
+ if (exportTests.length === 0) {
3262
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_EMPTY")}</div>`;
3263
+ } else if (exportMode === "all") {
3264
+ body = `<div class="export-all-desc">${t("CONFIG.EXPORT_ALL_DESC")}</div>`;
3265
+ } else if (exportMode === "manual") {
3266
+ body = `<div class="export-list">${exportTests.map((test) => `
3267
+ <label class="export-row">
3268
+ <input type="checkbox" data-export-test="${test.id}" ${exportSelectedIds.has(test.id) ? "checked" : ""} />
3269
+ <span class="export-row-name">${escHtml(test.name)}</span>
3270
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3271
+ </label>`).join("")}</div>`;
3272
+ } else {
3273
+ const allTags = [...new Set(exportTests.flatMap((test) => test.tags ?? []))].sort();
3274
+ if (!allTags.length) {
3275
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_NO_TAGS")}</div>`;
3276
+ } else {
3277
+ const chips = `<div class="export-tags">${allTags.map((tag) => `<button class="export-tag ${exportSelectedTags.has(tag) ? "active" : ""}" data-export-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}</div>`;
3278
+ const matched = selectTestsForExport(exportTests, "tags", { tags: exportSelectedTags });
3279
+ const preview = exportSelectedTags.size === 0 ? `<div class="export-tag-hint">${t("CONFIG.EXPORT_TAGS_HINT")}</div>` : `<div class="export-result-label">${t("CONFIG.EXPORT_RESULT_LABEL")}</div>
3280
+ <div class="export-list">${matched.map((test) => `
3281
+ <div class="export-row export-row-static">
3282
+ <span class="export-row-name">${escHtml(test.name)}</span>
3283
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3284
+ </div>`).join("")}</div>`;
3285
+ body = chips + preview;
3286
+ }
3287
+ }
3288
+ return `
3289
+ <div class="export-overlay" id="export-overlay">
3290
+ <div class="export-modal">
3291
+ <div class="export-hd">${t("CONFIG.EXPORT_DIALOG_TITLE")}</div>
3292
+ ${exportTests.length ? `<div class="export-modes">
3293
+ ${modeBtn("all", "CONFIG.EXPORT_MODE_ALL")}
3294
+ ${modeBtn("manual", "CONFIG.EXPORT_MODE_MANUAL")}
3295
+ ${modeBtn("tags", "CONFIG.EXPORT_MODE_TAGS")}
3296
+ </div>` : ""}
3297
+ <div class="export-body">${body}</div>
3298
+ <div class="export-ft">
3299
+ <span class="export-count">${t("CONFIG.EXPORT_COUNT")} <b>${count}</b></span>
3300
+ <span class="export-ft-actions">
3301
+ <button id="btn-export-confirm" class="btn-export-confirm" ${count === 0 ? "disabled" : ""}>${t("CONFIG.EXPORT_CONFIRM")}</button>
3302
+ <button id="btn-export-cancel">${t("CONFIG.EXPORT_CANCEL")}</button>
3303
+ </span>
3304
+ </div>
3305
+ </div>
3306
+ </div>`;
3307
+ }
2721
3308
  function renderConfiguration(state, t) {
2722
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled } = state;
3309
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
2723
3310
  const langOptions = LANGS.map(
2724
3311
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
2725
3312
  ).join("");
@@ -2759,6 +3346,29 @@ function renderConfiguration(state, t) {
2759
3346
  </label>
2760
3347
  </div>
2761
3348
 
3349
+ <!-- Start Hidden -->
3350
+ <div class="card">
3351
+ <div class="card-hd">${t("CONFIG.START_HIDDEN_SECTION")}</div>
3352
+ <label class="check-row">
3353
+ <input type="checkbox" id="start-hidden-toggle" ${startHidden ? "checked" : ""} />
3354
+ <div>
3355
+ <div class="check-title">${t("CONFIG.START_HIDDEN_TITLE")}</div>
3356
+ <div class="check-sub">${t("CONFIG.START_HIDDEN_SUB")}</div>
3357
+ </div>
3358
+ </label>
3359
+ </div>
3360
+
3361
+ <!-- Recording continuity (cross-app resume TTL) -->
3362
+ <div class="card">
3363
+ <div class="card-hd">${t("CONFIG.RESUME_TTL_SECTION")}</div>
3364
+ <div class="field-row">
3365
+ <span class="field-label">${t("CONFIG.RESUME_TTL_LABEL")}</span>
3366
+ <input type="number" id="resume-ttl-input" min="1" step="1" value="${resumeTtlMinutes}"
3367
+ style="width:80px;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none" />
3368
+ </div>
3369
+ <div class="check-sub" style="margin-top:8px">${t("CONFIG.RESUME_TTL_HINT")}</div>
3370
+ </div>
3371
+
2762
3372
  <!-- Selector Strategy -->
2763
3373
  <div class="card card-wide">
2764
3374
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -2778,13 +3388,13 @@ function renderConfiguration(state, t) {
2778
3388
  <div class="card card-wide">
2779
3389
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
2780
3390
  <div class="fs-layout">
2781
- <pre class="fs-tree">cypress/ <span style="color:#484f58">\u2190 selecciona</span>
3391
+ <pre class="fs-tree">cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
2782
3392
  \u2514\u2500\u2500 e2e/
2783
3393
  \u2514\u2500\u2500 *.cy.ts</pre>
2784
3394
  <div class="fs-right">
2785
3395
  <div class="fs-status">
2786
3396
  <span class="fs-dot ${filesystemGranted ? "on" : "off"}"></span>
2787
- ${filesystemGranted && cypressFolderName ? `<span>${t("CONFIG.FOLDER_CONNECTED")}</span>&nbsp;<span class="fs-folder">\u{1F4C1} ${cypressFolderName}</span>` : `<span>${t("CONFIG.FOLDER_NOT_SET")}</span>`}
3397
+ ${filesystemGranted && cypressFolderName ? `<span>${t("CONFIG.FOLDER_CONNECTED")}</span>&nbsp;<span class="fs-folder">\u{1F4C1} ${escHtml(cypressFolderName)}</span>` : `<span>${t("CONFIG.FOLDER_NOT_SET")}</span>`}
2788
3398
  </div>
2789
3399
  <div class="btn-row">
2790
3400
  <button id="btn-change-folder">
@@ -2809,7 +3419,7 @@ function renderConfiguration(state, t) {
2809
3419
  </div>
2810
3420
  </div>
2811
3421
 
2812
- </div>`;
3422
+ </div>${renderExportOverlay(state, t)}`;
2813
3423
  }
2814
3424
 
2815
3425
  // src/components/configuration/configuration.ts
@@ -2821,6 +3431,13 @@ var ConfigurationElement = class extends HTMLElement {
2821
3431
  advancedHttpConfig = false;
2822
3432
  selectorStrategy = "data-cy";
2823
3433
  smartSelectorEnabled = true;
3434
+ startHidden = false;
3435
+ resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
3436
+ isExporting = false;
3437
+ exportMode = "all";
3438
+ exportTests = [];
3439
+ exportSelectedIds = /* @__PURE__ */ new Set();
3440
+ exportSelectedTags = /* @__PURE__ */ new Set();
2824
3441
  filesystemGranted = false;
2825
3442
  cypressFolderName = null;
2826
3443
  constructor() {
@@ -2846,6 +3463,12 @@ var ConfigurationElement = class extends HTMLElement {
2846
3463
  this.advancedHttpConfig = localStorage.getItem("extendedHttpCommands") === "true";
2847
3464
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
2848
3465
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
3466
+ this.startHidden = config?.["startHidden"] === "true";
3467
+ const ttlRaw = config?.[RESUME_TTL_CONFIG_KEY];
3468
+ if (ttlRaw !== void 0 && ttlRaw !== null) {
3469
+ const ttl = Number(ttlRaw);
3470
+ this.resumeTtlMinutes = Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_RESUME_TTL_MINUTES;
3471
+ }
2849
3472
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
2850
3473
  const handle = config?.["cypressDirectoryHandle"];
2851
3474
  this.cypressFolderName = handle?.name ?? null;
@@ -2863,6 +3486,18 @@ var ConfigurationElement = class extends HTMLElement {
2863
3486
  this.persistence.setConfig({ extendedHttpCommands: checked ? "true" : "false" });
2864
3487
  this.render();
2865
3488
  }
3489
+ async onStartHiddenChange(checked) {
3490
+ this.startHidden = checked;
3491
+ await this.persistence.setConfig({ startHidden: checked ? "true" : "false" });
3492
+ this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
3493
+ this.render();
3494
+ }
3495
+ async onResumeTtlChange(minutes) {
3496
+ const safe = Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes) : DEFAULT_RESUME_TTL_MINUTES;
3497
+ this.resumeTtlMinutes = safe;
3498
+ await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
3499
+ this.render();
3500
+ }
2866
3501
  async onSmartSelectorChange(enabled) {
2867
3502
  this.smartSelectorEnabled = enabled;
2868
3503
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -2892,8 +3527,7 @@ var ConfigurationElement = class extends HTMLElement {
2892
3527
  this.cypressFolderName = null;
2893
3528
  this.render();
2894
3529
  }
2895
- async exportAllData() {
2896
- const tests = await this.persistence.getAllTests();
3530
+ downloadTests(tests) {
2897
3531
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
2898
3532
  const url = URL.createObjectURL(blob);
2899
3533
  const a = document.createElement("a");
@@ -2902,6 +3536,53 @@ var ConfigurationElement = class extends HTMLElement {
2902
3536
  a.click();
2903
3537
  URL.revokeObjectURL(url);
2904
3538
  }
3539
+ /** Downloads every saved test (used by the "Todo" mode and as a direct API). */
3540
+ async exportAllData() {
3541
+ const tests = await this.persistence.getAllTests();
3542
+ this.downloadTests(tests);
3543
+ }
3544
+ /** Opens the export selection dialog, loading the current tests. */
3545
+ async openExportDialog() {
3546
+ this.exportTests = await this.persistence.getAllTests();
3547
+ this.exportMode = "all";
3548
+ this.exportSelectedIds = /* @__PURE__ */ new Set();
3549
+ this.exportSelectedTags = /* @__PURE__ */ new Set();
3550
+ this.isExporting = true;
3551
+ this.render();
3552
+ }
3553
+ cancelExport() {
3554
+ this.isExporting = false;
3555
+ this.exportSelectedIds.clear();
3556
+ this.exportSelectedTags.clear();
3557
+ this.render();
3558
+ }
3559
+ setExportMode(mode) {
3560
+ this.exportMode = mode;
3561
+ this.render();
3562
+ }
3563
+ toggleExportTest(id) {
3564
+ if (this.exportSelectedIds.has(id)) this.exportSelectedIds.delete(id);
3565
+ else this.exportSelectedIds.add(id);
3566
+ this.render();
3567
+ }
3568
+ toggleExportTag(tag) {
3569
+ if (this.exportSelectedTags.has(tag)) this.exportSelectedTags.delete(tag);
3570
+ else this.exportSelectedTags.add(tag);
3571
+ this.render();
3572
+ }
3573
+ /** Downloads the tests resolved by the current mode + selection. No-op if empty. */
3574
+ confirmExport() {
3575
+ const subset = selectTestsForExport(this.exportTests, this.exportMode, {
3576
+ ids: this.exportSelectedIds,
3577
+ tags: this.exportSelectedTags
3578
+ });
3579
+ if (!subset.length) return;
3580
+ this.downloadTests(subset);
3581
+ this.isExporting = false;
3582
+ this.exportSelectedIds.clear();
3583
+ this.exportSelectedTags.clear();
3584
+ this.render();
3585
+ }
2905
3586
  async importAllData(file) {
2906
3587
  const text = await file.text();
2907
3588
  let data;
@@ -2913,7 +3594,6 @@ var ConfigurationElement = class extends HTMLElement {
2913
3594
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
2914
3595
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
2915
3596
  }
2916
- await this.persistence.clearAllData();
2917
3597
  await this.persistence.ingestFileData(data.tests, data.interceptors);
2918
3598
  }
2919
3599
  render() {
@@ -2923,7 +3603,14 @@ var ConfigurationElement = class extends HTMLElement {
2923
3603
  selectorStrategy: this.selectorStrategy,
2924
3604
  filesystemGranted: this.filesystemGranted,
2925
3605
  cypressFolderName: this.cypressFolderName,
2926
- smartSelectorEnabled: this.smartSelectorEnabled
3606
+ smartSelectorEnabled: this.smartSelectorEnabled,
3607
+ startHidden: this.startHidden,
3608
+ resumeTtlMinutes: this.resumeTtlMinutes,
3609
+ isExporting: this.isExporting,
3610
+ exportMode: this.exportMode,
3611
+ exportTests: this.exportTests,
3612
+ exportSelectedIds: this.exportSelectedIds,
3613
+ exportSelectedTags: this.exportSelectedTags
2927
3614
  }, this.t.bind(this))}`;
2928
3615
  ;
2929
3616
  this.shadow.getElementById("lang-select").addEventListener(
@@ -2938,13 +3625,32 @@ var ConfigurationElement = class extends HTMLElement {
2938
3625
  "change",
2939
3626
  (e) => this.onSmartSelectorChange(e.target.checked)
2940
3627
  );
3628
+ this.shadow.getElementById("start-hidden-toggle").addEventListener(
3629
+ "change",
3630
+ (e) => this.onStartHiddenChange(e.target.checked)
3631
+ );
3632
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
3633
+ "change",
3634
+ (e) => this.onResumeTtlChange(Number(e.target.value))
3635
+ );
2941
3636
  this.shadow.getElementById("selector-strategy").addEventListener(
2942
3637
  "change",
2943
3638
  (e) => this.onSelectorStrategyChange(e.target.value)
2944
3639
  );
2945
3640
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
2946
3641
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
2947
- this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.exportAllData());
3642
+ this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.openExportDialog());
3643
+ this.shadow.getElementById("btn-export-confirm")?.addEventListener("click", () => this.confirmExport());
3644
+ this.shadow.getElementById("btn-export-cancel")?.addEventListener("click", () => this.cancelExport());
3645
+ this.shadow.querySelectorAll("[data-export-mode]").forEach(
3646
+ (el) => el.addEventListener("click", () => this.setExportMode(el.dataset["exportMode"]))
3647
+ );
3648
+ this.shadow.querySelectorAll("[data-export-test]").forEach(
3649
+ (el) => el.addEventListener("change", () => this.toggleExportTest(Number(el.dataset["exportTest"])))
3650
+ );
3651
+ this.shadow.querySelectorAll("[data-export-tag]").forEach(
3652
+ (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
3653
+ );
2948
3654
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
2949
3655
  const file = e.target.files?.[0];
2950
3656
  if (!file) return;
@@ -3010,12 +3716,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3010
3716
  .file-name { font-size: 11px; color: #484f58; margin-bottom: 8px; font-family: monospace; }
3011
3717
  pre {
3012
3718
  background: #0d1117; padding: 12px; border-radius: 8px;
3013
- font-size: 11px; color: #c9d1d9; overflow-x: auto;
3014
- white-space: pre-wrap; word-break: break-all; margin: 0;
3719
+ font-size: 11px; color: #c9d1d9; margin: 0;
3015
3720
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
3016
- line-height: 1.7; overflow-y: auto;
3017
- border: 1px solid #21262d;
3721
+ line-height: 1.7; border: 1px solid #21262d;
3722
+ white-space: pre; overflow-x: auto; overflow-y: auto;
3723
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3018
3724
  }
3725
+ pre::-webkit-scrollbar { width: 4px; height: 4px; }
3726
+ pre::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
3727
+ .pre-block { font-size: 10.5px; }
3728
+ .pre-icp .sh-str { color: #85e89d; }
3729
+ .sh-kw { color: #ff7b72; }
3730
+ .sh-bi { color: #d2a8ff; }
3731
+ .sh-str { color: #a5d6ff; }
3732
+ .sh-cmt { color: #8b949e; font-style: italic; }
3733
+ .sh-num { color: #79c0ff; }
3019
3734
  .footer {
3020
3735
  padding: 10px 14px; border-top: 1px solid #21262d;
3021
3736
  display: flex; gap: 8px; justify-content: flex-end; background: #161b22;
@@ -3031,6 +3746,12 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3031
3746
  .btn-save:hover { background: #1f6feb; border-color: #1f6feb; color: #fff; }
3032
3747
  .btn-save:disabled { background: #21262d; border-color: #30363d; color: #8b949e; }
3033
3748
  .placeholder { color: #484f58; font-size: 13px; padding: 28px; text-align: center; }
3749
+ .test-notes {
3750
+ margin: 0 0 10px; padding: 8px 12px;
3751
+ background: rgba(47,129,247,0.06); border-left: 3px solid rgba(47,129,247,0.4);
3752
+ border-radius: 0 6px 6px 0; font-size: 12px; color: #8b949e;
3753
+ line-height: 1.6; white-space: pre-wrap; word-break: break-word;
3754
+ }
3034
3755
  .block-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
3035
3756
  .btn-copy {
3036
3757
  padding: 3px 10px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
@@ -3039,18 +3760,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3039
3760
  }
3040
3761
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3041
3762
  .sidebar-toolbar {
3042
- display: flex; gap: 4px; padding: 6px 6px 4px;
3763
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3043
3764
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3044
3765
  }
3045
3766
  .btn-toolbar {
3046
- flex: 1; padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3767
+ flex: 1 1 calc(50% - 2px); padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3047
3768
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3048
3769
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3049
3770
  text-overflow: ellipsis;
3050
3771
  }
3772
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3051
3773
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3052
3774
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3053
3775
  .btn-toolbar.btn-new:hover { background: #238636; color: #fff; }
3776
+ .btn-toolbar.btn-new-folder { color: #d29922; border-color: #9e6a03; }
3777
+ .btn-toolbar.btn-new-folder:hover { background: #9e6a03; color: #fff; }
3054
3778
  .new-file-form {
3055
3779
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3056
3780
  }
@@ -3083,23 +3807,24 @@ function renderNoPermission(needsReauth, t) {
3083
3807
  </div>`;
3084
3808
  }
3085
3809
  function renderAdvancedEditor(state, t) {
3086
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
3810
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3087
3811
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3088
3812
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3089
- <pre>${escHtml(selectedFileContent.slice(0, 4e3))}${selectedFileContent.length > 4e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
3813
+ <pre class="pre-file">${syntaxHighlight(selectedFileContent.slice(0, 6e3))}${selectedFileContent.length > 6e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
3814
+ const notesHtml = testItBlock && testNotes ? `<p class="test-notes">${escHtml(testNotes)}</p>` : "";
3090
3815
  const itHtml = testItBlock ? `<div style="margin-top:10px">
3091
3816
  <div class="file-name block-header">
3092
3817
  <span>${t("ADVANCED_EDITOR.IT_LABEL")}</span>
3093
3818
  <button id="btn-copy-it" class="btn-copy">${t("ADVANCED_EDITOR.COPY_IT_BTN")}</button>
3094
3819
  </div>
3095
- <pre style="max-height:120px;font-size:10.5px">${escHtml(testItBlock.slice(0, 500))}</pre>
3820
+ <pre class="pre-block">${syntaxHighlight(testItBlock.slice(0, 3e3))}</pre>
3096
3821
  </div>` : "";
3097
3822
  const interceptorsHtml = interceptorsBlock ? `<div style="margin-top:10px">
3098
3823
  <div class="file-name block-header">
3099
3824
  <span>${t("ADVANCED_EDITOR.BEFORE_EACH_LABEL")}</span>
3100
3825
  <button id="btn-copy-interceptors" class="btn-copy">${t("ADVANCED_EDITOR.COPY_ICP_BTN")}</button>
3101
3826
  </div>
3102
- <pre style="max-height:120px;font-size:10.5px;color:#3fb950">${escHtml(interceptorsBlock.slice(0, 500))}</pre>
3827
+ <pre class="pre-block pre-icp">${syntaxHighlight(interceptorsBlock.slice(0, 3e3))}</pre>
3103
3828
  </div>` : "";
3104
3829
  const newFileForm = isCreatingFile ? `<div class="new-file-form">
3105
3830
  <input id="input-new-file" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FILE_PLACEHOLDER"))}" autocomplete="off" />
@@ -3109,21 +3834,30 @@ function renderAdvancedEditor(state, t) {
3109
3834
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3110
3835
  </div>
3111
3836
  </div>` : "";
3837
+ const newFolderForm = isCreatingFolder ? `<div class="new-file-form">
3838
+ <input id="input-new-folder" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FOLDER_PLACEHOLDER"))}" autocomplete="off" />
3839
+ <div class="new-file-actions">
3840
+ <button id="btn-new-folder-confirm" class="btn-confirm">${t("ADVANCED_EDITOR.NEW_FILE_CONFIRM")}</button>
3841
+ <button id="btn-new-folder-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3842
+ </div>
3843
+ </div>` : "";
3112
3844
  const toolbar = `
3113
3845
  <div class="sidebar-toolbar">
3114
3846
  <button id="btn-new-file" class="btn-toolbar btn-new">${t("ADVANCED_EDITOR.NEW_FILE_BTN")}</button>
3115
- <button id="btn-refresh" class="btn-toolbar">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
3847
+ <button id="btn-new-folder" class="btn-toolbar btn-new-folder">${t("ADVANCED_EDITOR.NEW_FOLDER_BTN")}</button>
3848
+ <button id="btn-refresh" class="btn-toolbar btn-full">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
3116
3849
  </div>`;
3117
3850
  return `
3118
3851
  <div class="layout">
3119
3852
  <div class="sidebar" style="width:${sidebarWidth}px">
3120
3853
  ${toolbar}
3121
3854
  ${newFileForm}
3855
+ ${newFolderForm}
3122
3856
  <div class="tree-scroll">${treeHtml}</div>
3123
3857
  </div>
3124
3858
  <div id="resize-handle" class="resize-handle"></div>
3125
3859
  <div class="main">
3126
- <div class="content-area">${contentHtml}${itHtml}${interceptorsHtml}</div>
3860
+ <div class="content-area">${contentHtml}${notesHtml}${itHtml}${interceptorsHtml}</div>
3127
3861
  <div class="footer">
3128
3862
  <button id="btn-save" class="btn-save"
3129
3863
  ${!saveButtonEnabled || !testItBlock ? "disabled" : ""}>
@@ -3184,10 +3918,12 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3184
3918
  selectedFileContent = null;
3185
3919
  testItBlock = "";
3186
3920
  interceptorsBlock = "";
3921
+ testNotes = "";
3187
3922
  isPreviewMode = false;
3188
3923
  previewFileName = null;
3189
3924
  previewFileContent = null;
3190
3925
  isCreatingFile = false;
3926
+ isCreatingFolder = false;
3191
3927
  collapsedDirs = /* @__PURE__ */ new Set();
3192
3928
  sidebarWidth = 220;
3193
3929
  hasPermission = false;
@@ -3250,7 +3986,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3250
3986
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3251
3987
  if (!name) return;
3252
3988
  const fileName = `${name}.cy.ts`;
3253
- const template = `describe('${name}', () => {
3989
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3254
3990
 
3255
3991
  it('should ', () => {
3256
3992
 
@@ -3268,6 +4004,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3268
4004
  this.isCreatingFile = false;
3269
4005
  await this.getFoldersData();
3270
4006
  }
4007
+ async createNewFolder(rawName) {
4008
+ if (!this._e2eHandle) return;
4009
+ const name = rawName.trim().replace(/[/\\]/g, "");
4010
+ if (!name) return;
4011
+ try {
4012
+ await this._e2eHandle.getDirectoryHandle(name, { create: true });
4013
+ } catch {
4014
+ }
4015
+ this.isCreatingFolder = false;
4016
+ await this.getFoldersData();
4017
+ }
3271
4018
  async refreshTree() {
3272
4019
  await this.getFoldersData();
3273
4020
  }
@@ -3277,7 +4024,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3277
4024
  if (this.interceptorsBlock) {
3278
4025
  content = this.transformationService.insertBeforeEach(content, this.interceptorsBlock);
3279
4026
  }
3280
- content = this.transformationService.insertItBlock(content, this.testItBlock);
4027
+ const comment = this.testNotes ? this.transformationService.buildBlockComment(this.testNotes) + "\n" : "";
4028
+ content = this.transformationService.insertItBlock(content, comment + this.testItBlock);
3281
4029
  if (!content) return;
3282
4030
  const writable = await this.selectedFileHandle.createWritable();
3283
4031
  await writable.write(content);
@@ -3304,9 +4052,11 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3304
4052
  if (test) {
3305
4053
  this.testItBlock = test.itBlock ?? "";
3306
4054
  this.interceptorsBlock = test.interceptorsBlock ?? "";
4055
+ this.testNotes = test.notes ?? "";
3307
4056
  } else {
3308
4057
  this.testItBlock = "";
3309
4058
  this.interceptorsBlock = "";
4059
+ this.testNotes = "";
3310
4060
  }
3311
4061
  this.render();
3312
4062
  }
@@ -3322,7 +4072,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3322
4072
  fileName: this.selectedFile?.name ?? "",
3323
4073
  testId: this.testId,
3324
4074
  itBlock: this.testItBlock,
3325
- interceptorsBlock: this.interceptorsBlock
4075
+ interceptorsBlock: this.interceptorsBlock,
4076
+ notes: this.testNotes
3326
4077
  },
3327
4078
  bubbles: true,
3328
4079
  composed: true
@@ -3396,8 +4147,10 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3396
4147
  selectedFileContent: this.selectedFileContent,
3397
4148
  testItBlock: this.testItBlock,
3398
4149
  interceptorsBlock: this.interceptorsBlock,
4150
+ testNotes: this.testNotes,
3399
4151
  saveButtonEnabled: this.saveButtonEnabled,
3400
4152
  isCreatingFile: this.isCreatingFile,
4153
+ isCreatingFolder: this.isCreatingFolder,
3401
4154
  collapsedDirs: this.collapsedDirs,
3402
4155
  sidebarWidth: this.sidebarWidth
3403
4156
  }, this.t.bind(this))}`;
@@ -3421,9 +4174,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3421
4174
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
3422
4175
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
3423
4176
  this.isCreatingFile = !this.isCreatingFile;
4177
+ this.isCreatingFolder = false;
3424
4178
  this.render();
3425
4179
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
3426
4180
  });
4181
+ this.shadow.getElementById("btn-new-folder")?.addEventListener("click", () => {
4182
+ this.isCreatingFolder = !this.isCreatingFolder;
4183
+ this.isCreatingFile = false;
4184
+ this.render();
4185
+ if (this.isCreatingFolder) this.shadow.getElementById("input-new-folder")?.focus();
4186
+ });
3427
4187
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
3428
4188
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
3429
4189
  const input2 = this.shadow.getElementById("input-new-file");
@@ -3442,6 +4202,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3442
4202
  this.render();
3443
4203
  }
3444
4204
  });
4205
+ this.shadow.getElementById("btn-new-folder-confirm")?.addEventListener("click", () => {
4206
+ const folderInput2 = this.shadow.getElementById("input-new-folder");
4207
+ this.createNewFolder(folderInput2?.value ?? "");
4208
+ });
4209
+ this.shadow.getElementById("btn-new-folder-cancel")?.addEventListener("click", () => {
4210
+ this.isCreatingFolder = false;
4211
+ this.render();
4212
+ });
4213
+ const folderInput = this.shadow.getElementById("input-new-folder");
4214
+ folderInput?.addEventListener("keydown", (e) => {
4215
+ if (e.key === "Enter") {
4216
+ this.createNewFolder(folderInput.value);
4217
+ } else if (e.key === "Escape") {
4218
+ this.isCreatingFolder = false;
4219
+ this.render();
4220
+ }
4221
+ });
3445
4222
  }
3446
4223
  };
3447
4224
  if (!customElements.get("advanced-test-editor")) {
@@ -3527,7 +4304,30 @@ var FILE_PREVIEW_STYLES = `
3527
4304
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
3528
4305
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
3529
4306
  .btn-launch:hover { background: rgba(227,179,65,0.1); }
4307
+ .btn-launch:disabled { opacity: 0.45; cursor: not-allowed; }
4308
+ .btn-launch:disabled:hover { background: transparent; }
4309
+ .launch-hint { font-size: 10px; color: #8b6d3b; align-self: center; }
4310
+ .btn-insert { background: transparent; border-color: #a371f7; color: #a371f7; }
4311
+ .btn-insert:hover { background: rgba(163,113,247,0.12); color: #c8a8ff; }
3530
4312
  .btn-diff-active { background: rgba(47,129,247,0.15); border-color: rgba(47,129,247,0.4); color: #2f81f7; }
4313
+
4314
+ /* \u2500\u2500 Run result panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4315
+ .run-result {
4316
+ border-top: 1px solid #21262d; padding: 8px 12px; background: #0d1117;
4317
+ max-height: 220px; overflow-y: auto; flex-shrink: 0;
4318
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
4319
+ }
4320
+ .run-status { font-size: 12px; font-weight: 600; }
4321
+ .run-passed .run-status { color: #3fb950; }
4322
+ .run-failed .run-status { color: #f85149; }
4323
+ .run-running .run-status { color: #e3b341; }
4324
+ .run-error .run-status { color: #f85149; }
4325
+ .run-output {
4326
+ margin: 6px 0 0; padding: 8px 10px; background: #161b22; border: 1px solid #21262d;
4327
+ border-radius: 5px; font-size: 10px; color: #c9d1d9; line-height: 1.5;
4328
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
4329
+ white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto;
4330
+ }
3531
4331
  `;
3532
4332
 
3533
4333
  // src/components/file-preview/file-preview.template.ts
@@ -3584,8 +4384,14 @@ function buildDiffHtml(original, current, t) {
3584
4384
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
3585
4385
  }).join("");
3586
4386
  }
4387
+ var RUN_STATUS_KEY = {
4388
+ running: "FILE_PREVIEW.LAUNCH_RUNNING",
4389
+ passed: "FILE_PREVIEW.LAUNCH_PASSED",
4390
+ failed: "FILE_PREVIEW.LAUNCH_FAILED",
4391
+ error: "FILE_PREVIEW.LAUNCH_NO_RUNNER"
4392
+ };
3587
4393
  function renderFilePreview(state, t) {
3588
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4394
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
3589
4395
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
3590
4396
  <div class="blocks-panel">
3591
4397
  ${itBlock ? `
@@ -3606,6 +4412,13 @@ function renderFilePreview(state, t) {
3606
4412
  </div>` : ""}
3607
4413
  </div>` : "";
3608
4414
  const hasChanges = originalContent !== null && originalContent !== (fileContent ?? "");
4415
+ const hasBlocks = !!(itBlock || interceptorsBlock);
4416
+ const launchBtnHtml = isLocal ? `<button id="btn-launch" class="btn-launch" ${runState === "running" ? "disabled" : ""}>${runState === "running" ? t("FILE_PREVIEW.LAUNCH_RUNNING") : t("FILE_PREVIEW.LAUNCH_BTN")}</button>` : `<button class="btn-launch" disabled title="${t("FILE_PREVIEW.LAUNCH_LOCAL_ONLY")}">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4417
+ <span class="launch-hint">${t("FILE_PREVIEW.LAUNCH_LOCAL_ONLY")}</span>`;
4418
+ const runResultHtml = runState !== "idle" ? `<div class="run-result run-${runState}">
4419
+ <span class="run-status">${t(RUN_STATUS_KEY[runState])}</span>
4420
+ ${runOutput ? `<pre class="run-output">${escHtml(runOutput.slice(0, 8e3))}</pre>` : ""}
4421
+ </div>` : "";
3609
4422
  return `
3610
4423
  <div class="container">
3611
4424
  <div class="header">
@@ -3616,9 +4429,11 @@ function renderFilePreview(state, t) {
3616
4429
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
3617
4430
  ${blocksPanelHtml}
3618
4431
  </div>
4432
+ ${runResultHtml}
3619
4433
  <div class="footer">
3620
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4434
+ ${launchBtnHtml}
3621
4435
  ${hasChanges ? `<button id="btn-diff" class="${showDiff ? "btn-diff-active" : ""}">${t("FILE_PREVIEW.DIFF_BTN")}</button>` : ""}
4436
+ ${hasBlocks && !showDiff ? `<button id="btn-insert" class="btn-insert" title="${t("FILE_PREVIEW.INSERT_TITLE")}">${t("FILE_PREVIEW.INSERT_BTN")}</button>` : ""}
3622
4437
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
3623
4438
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
3624
4439
  </div>
@@ -3634,11 +4449,18 @@ var FilePreviewElement = class extends HTMLElement {
3634
4449
  translation = translationService;
3635
4450
  itBlock = "";
3636
4451
  interceptorsBlock = "";
4452
+ notes = "";
3637
4453
  commands = [];
3638
4454
  interceptors = [];
4455
+ /** Endpoint of the local Cypress runner. Configurable; defaults to the bundled runner's port. */
4456
+ runnerUrl = "http://localhost:8123/run-test";
4457
+ /** Whether the app is served locally — gates the launch button. Overridable for tests. */
4458
+ isLocal = isLocalHost(window.location.hostname);
3639
4459
  _fileContent = null;
3640
4460
  _originalContent = null;
3641
4461
  _showDiff = false;
4462
+ _runState = "idle";
4463
+ _runOutput = "";
3642
4464
  constructor() {
3643
4465
  super();
3644
4466
  this.shadow = this.attachShadow({ mode: "open" });
@@ -3666,15 +4488,34 @@ var FilePreviewElement = class extends HTMLElement {
3666
4488
  onClose() {
3667
4489
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
3668
4490
  }
4491
+ /**
4492
+ * Runs the current spec headless via the local runner and shows the result.
4493
+ * No-op off localhost. Never throws — connection failures surface as an
4494
+ * "error" state + toast instead of an unhandled rejection.
4495
+ */
3669
4496
  async launchTest(specPath) {
3670
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
4497
+ if (!this.isLocal) return;
4498
+ const path = specPath ?? this.fileName ?? "";
3671
4499
  if (!path) return;
3672
- const response = await fetch("http://localhost:8123/run-test", {
3673
- method: "POST",
3674
- headers: { "Content-Type": "application/json" },
3675
- body: JSON.stringify({ specPath: path })
3676
- });
3677
- await response.json();
4500
+ this._runState = "running";
4501
+ this._runOutput = "";
4502
+ this.render();
4503
+ try {
4504
+ const response = await fetch(this.runnerUrl, {
4505
+ method: "POST",
4506
+ headers: { "Content-Type": "application/json" },
4507
+ body: JSON.stringify({ specPath: path })
4508
+ });
4509
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4510
+ const result = await response.json();
4511
+ this._runOutput = result.output ?? "";
4512
+ this._runState = result.success ? "passed" : "failed";
4513
+ } catch {
4514
+ this._runState = "error";
4515
+ this._runOutput = "";
4516
+ showToast(this.t("FILE_PREVIEW.LAUNCH_NO_RUNNER"), false);
4517
+ }
4518
+ this.render();
3678
4519
  }
3679
4520
  copyToClipboard(text) {
3680
4521
  navigator.clipboard?.writeText(text);
@@ -3683,6 +4524,32 @@ var FilePreviewElement = class extends HTMLElement {
3683
4524
  this._showDiff = !this._showDiff;
3684
4525
  this.render();
3685
4526
  }
4527
+ /**
4528
+ * Merges the it() and beforeEach() blocks into the current editor content
4529
+ * using the same logic as the automatic "Insert into file" action, so the
4530
+ * user does not have to copy/paste manually. Leaves the content untouched
4531
+ * (and warns) when the file has no valid describe() block to insert into.
4532
+ */
4533
+ insertBlocks() {
4534
+ const base = this.textarea?.value ?? this._fileContent ?? "";
4535
+ let content = base;
4536
+ if (this.interceptorsBlock) {
4537
+ const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
4538
+ if (merged) content = merged;
4539
+ }
4540
+ if (this.itBlock) {
4541
+ const comment = this.notes ? advancedTestTransformationService.buildBlockComment(this.notes) + "\n" : "";
4542
+ const merged = advancedTestTransformationService.insertItBlock(content, comment + this.itBlock);
4543
+ if (merged) content = merged;
4544
+ }
4545
+ if (content === base) {
4546
+ showToast(this.t("FILE_PREVIEW.INSERT_ERROR"), false);
4547
+ return;
4548
+ }
4549
+ this._fileContent = content;
4550
+ this.render();
4551
+ showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
4552
+ }
3686
4553
  render() {
3687
4554
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
3688
4555
  fileName: this.fileName,
@@ -3692,7 +4559,10 @@ var FilePreviewElement = class extends HTMLElement {
3692
4559
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
3693
4560
  itBlock: this.itBlock,
3694
4561
  interceptorsBlock: this.interceptorsBlock,
3695
- closeLabel: this.closeLabel
4562
+ closeLabel: this.closeLabel,
4563
+ isLocal: this.isLocal,
4564
+ runState: this._runState,
4565
+ runOutput: this._runOutput
3696
4566
  }, this.t.bind(this))}`;
3697
4567
  this.textarea = this.shadow.getElementById("editor");
3698
4568
  if (this.textarea) {
@@ -3708,6 +4578,7 @@ var FilePreviewElement = class extends HTMLElement {
3708
4578
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
3709
4579
  });
3710
4580
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
4581
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
3711
4582
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
3712
4583
  this.copyToClipboard(this.itBlock);
3713
4584
  });
@@ -3955,6 +4826,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
3955
4826
  interceptorsUnsub;
3956
4827
  pauseUnsub;
3957
4828
  selectorNotFoundUnsub;
4829
+ langUnsub;
4830
+ sessionUnsub;
4831
+ sessionSaveTimer;
3958
4832
  controlFirstTimeData = true;
3959
4833
  _previsualizerRef = null;
3960
4834
  httpMonitor;
@@ -3962,6 +4836,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
3962
4836
  recording;
3963
4837
  persistence;
3964
4838
  translation;
4839
+ isVisible = false;
3965
4840
  isRecording = false;
3966
4841
  isPaused = false;
3967
4842
  cypressCommands = [];
@@ -3996,17 +4871,22 @@ var LibE2eRecorderElement = class extends HTMLElement {
3996
4871
  this.initSelectorStrategy();
3997
4872
  this.initSubscriptions();
3998
4873
  this.render();
4874
+ this.initVisibility();
4875
+ this.initSessionContinuity();
3999
4876
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4000
4877
  window.addEventListener("keydown", this.keydownHandler);
4001
4878
  }
4002
4879
  disconnectedCallback() {
4003
4880
  if (this._isDisabled) return;
4004
4881
  window.removeEventListener("keydown", this.keydownHandler);
4882
+ this.flushActiveSessionOnDisconnect();
4005
4883
  this.recordingUnsub?.();
4006
4884
  this.commandsUnsub?.();
4007
4885
  this.interceptorsUnsub?.();
4008
4886
  this.pauseUnsub?.();
4009
4887
  this.selectorNotFoundUnsub?.();
4888
+ this.langUnsub?.();
4889
+ this.sessionUnsub?.();
4010
4890
  this.httpMonitor?.uninstall();
4011
4891
  this.recording.destroy();
4012
4892
  }
@@ -4029,11 +4909,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4029
4909
  this.isRecording = val;
4030
4910
  if (!val && !this.controlFirstTimeData) {
4031
4911
  this.saveRecordingHistory();
4912
+ this.clearSessionPersistence();
4032
4913
  this.showSaveTestDialog();
4033
4914
  }
4034
4915
  this.controlFirstTimeData = false;
4035
4916
  this.render();
4036
4917
  });
4918
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4037
4919
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4038
4920
  this.cypressCommands = cmds;
4039
4921
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4049,9 +4931,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
4049
4931
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4050
4932
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4051
4933
  });
4934
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4052
4935
  }
4053
4936
  showSelectorPicker(target) {
4054
- import("./selector-picker-VJOLGZ5H.js").then(() => {
4937
+ import("./selector-picker-NKYFFFGK.js").then(() => {
4055
4938
  const existing = document.querySelector("selector-picker");
4056
4939
  if (existing) existing.remove();
4057
4940
  const picker = document.createElement("selector-picker");
@@ -4061,12 +4944,135 @@ var LibE2eRecorderElement = class extends HTMLElement {
4061
4944
  document.body.appendChild(picker);
4062
4945
  });
4063
4946
  }
4947
+ async initVisibility() {
4948
+ if (this.hasAttribute("start-hidden")) {
4949
+ this.isVisible = this.getAttribute("start-hidden") === "false";
4950
+ this.style.display = this.isVisible ? "" : "none";
4951
+ return;
4952
+ }
4953
+ const config = await this.persistence.getGeneralConfig();
4954
+ this.isVisible = config?.["startHidden"] !== "true";
4955
+ this.style.display = this.isVisible ? "" : "none";
4956
+ }
4064
4957
  async initSelectorStrategy() {
4065
4958
  const config = await this.persistence.getGeneralConfig();
4066
4959
  const strategy = config?.["selectorStrategy"];
4067
4960
  if (strategy) this.recording.selectorStrategy = strategy;
4068
4961
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4069
4962
  }
4963
+ // ── cross-app session continuity (spec 006) ───────────────────────────────
4964
+ /**
4965
+ * On mount, detect a persisted live recording session and either resume it
4966
+ * silently (recent) or prompt continue/discard (stale). No-op when there is
4967
+ * no actively-recording session.
4968
+ */
4969
+ async initSessionContinuity() {
4970
+ if (this._isDisabled) return;
4971
+ const session = await this.persistence.getActiveSession();
4972
+ if (!session || !session.isRecording) return;
4973
+ const ttlMin = await this.getResumeTtlMinutes();
4974
+ const ageMs = Date.now() - session.updatedAt;
4975
+ if (ageMs <= ttlMin * 6e4) {
4976
+ this.resumeSessionState(session);
4977
+ } else {
4978
+ this.promptResumeOrDiscard(session);
4979
+ }
4980
+ }
4981
+ async getResumeTtlMinutes() {
4982
+ const config = await this.persistence.getConfig(RESUME_TTL_CONFIG_KEY);
4983
+ const raw = config?.[RESUME_TTL_CONFIG_KEY];
4984
+ const n = typeof raw === "number" ? raw : Number(raw);
4985
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_RESUME_TTL_MINUTES;
4986
+ }
4987
+ /** Rehydrates the recorder from a persisted session (no bootstrap re-emitted). */
4988
+ resumeSessionState(session) {
4989
+ this.recording.restoreSession(session);
4990
+ this.cypressCommands = session.commands;
4991
+ this.interceptors = session.interceptors;
4992
+ this.controlFirstTimeData = false;
4993
+ this.render();
4994
+ }
4995
+ async promptResumeOrDiscard(session) {
4996
+ const t = this.translation.translate.bind(this.translation);
4997
+ const result = await Swal.fire({
4998
+ title: t("RECORDER.SESSION_RESUME_TITLE"),
4999
+ html: `<div style="padding:8px 4px;color:#8b949e;font-size:13px;line-height:1.6"><p>${t("RECORDER.SESSION_RESUME_TEXT")}</p><p style="margin-top:6px;color:#c9d1d9"><b>${session.commands.length}</b> ${t("RECORDER.SESSION_RESUME_COUNT")}</p></div>`,
5000
+ showCancelButton: true,
5001
+ confirmButtonText: t("RECORDER.SESSION_CONTINUE_BTN"),
5002
+ cancelButtonText: t("RECORDER.SESSION_DISCARD_BTN"),
5003
+ color: "#e6edf3"
5004
+ });
5005
+ if (result && result.isConfirmed) {
5006
+ this.resumeSessionState(session);
5007
+ } else {
5008
+ this.discardSession();
5009
+ }
5010
+ }
5011
+ /** Debounced persistence of the live session; clears it the moment recording stops. */
5012
+ persistActiveSession(state) {
5013
+ if (this._isDisabled) return;
5014
+ if (!state.isRecording) {
5015
+ this.clearSessionPersistence();
5016
+ return;
5017
+ }
5018
+ this.writeSessionBreadcrumb(state);
5019
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5020
+ this.sessionSaveTimer = setTimeout(() => {
5021
+ const snap = this.recording.getSessionSnapshot();
5022
+ if (snap.isRecording) this.persistence.saveActiveSession(snap).catch(() => {
5023
+ });
5024
+ }, 300);
5025
+ }
5026
+ writeSessionBreadcrumb(state) {
5027
+ try {
5028
+ localStorage.setItem(ACTIVE_SESSION_BREADCRUMB_KEY, JSON.stringify({
5029
+ sessionId: state.sessionId,
5030
+ isRecording: state.isRecording,
5031
+ updatedAt: state.updatedAt
5032
+ }));
5033
+ } catch {
5034
+ }
5035
+ }
5036
+ flushActiveSessionOnDisconnect() {
5037
+ const snap = this.recording.getSessionSnapshot();
5038
+ if (!snap.isRecording) return;
5039
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5040
+ this.writeSessionBreadcrumb(snap);
5041
+ this.persistence.saveActiveSession(snap).catch(() => {
5042
+ });
5043
+ }
5044
+ clearSessionPersistence() {
5045
+ if (this.sessionSaveTimer) {
5046
+ clearTimeout(this.sessionSaveTimer);
5047
+ this.sessionSaveTimer = void 0;
5048
+ }
5049
+ try {
5050
+ localStorage.removeItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5051
+ } catch {
5052
+ }
5053
+ this.persistence.clearActiveSession().catch(() => {
5054
+ });
5055
+ }
5056
+ /** True when a synchronous breadcrumb marks an active recording session. */
5057
+ hasActiveSession() {
5058
+ try {
5059
+ const raw = localStorage.getItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5060
+ if (!raw) return false;
5061
+ return !!JSON.parse(raw).isRecording;
5062
+ } catch {
5063
+ return false;
5064
+ }
5065
+ }
5066
+ /** Programmatically resume the persisted session, if any. */
5067
+ resumeSession() {
5068
+ this.persistence.getActiveSession().then((s) => {
5069
+ if (s) this.resumeSessionState(s);
5070
+ });
5071
+ }
5072
+ /** Discard the persisted live session (breadcrumb + DB record). */
5073
+ discardSession() {
5074
+ this.clearSessionPersistence();
5075
+ }
4070
5076
  toggle() {
4071
5077
  this.recording.toggleRecording();
4072
5078
  }
@@ -4080,6 +5086,12 @@ var LibE2eRecorderElement = class extends HTMLElement {
4080
5086
  handleKeyboardEvent(event) {
4081
5087
  if (!event.ctrlKey) return;
4082
5088
  const key = event.key.toLowerCase();
5089
+ if (key === "e" && event.shiftKey) {
5090
+ event.preventDefault();
5091
+ this.toggleVisibility();
5092
+ return;
5093
+ }
5094
+ if (!this.isVisible) return;
4083
5095
  if (key === "r") {
4084
5096
  event.preventDefault();
4085
5097
  this.toggle();
@@ -4097,6 +5109,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
4097
5109
  this.showSettingsDialog();
4098
5110
  }
4099
5111
  }
5112
+ toggleVisibility() {
5113
+ this.isVisible = !this.isVisible;
5114
+ this.style.display = this.isVisible ? "" : "none";
5115
+ }
4100
5116
  // ── recording history (task 5) ────────────────────────────────────────────
4101
5117
  saveRecordingHistory() {
4102
5118
  if (!this.cypressCommands.length) return;
@@ -4332,13 +5348,13 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4332
5348
  child.translation = this.translation;
4333
5349
  container.appendChild(child);
4334
5350
  child.addEventListener("savetest", (e) => {
4335
- const { description, tags } = e.detail ?? {};
4336
- this.onSaveTest(description ?? null, tags ?? []);
5351
+ const { description, notes, tags } = e.detail ?? {};
5352
+ this.onSaveTest(description ?? null, tags ?? [], notes ?? "");
4337
5353
  Swal.close();
4338
5354
  });
4339
5355
  child.addEventListener("saveandexport", (e) => {
4340
- const { description, tags } = e.detail ?? {};
4341
- this.onSaveAndExportTest(description ?? null, tags ?? []);
5356
+ const { description, notes, tags } = e.detail ?? {};
5357
+ this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "");
4342
5358
  Swal.close();
4343
5359
  });
4344
5360
  },
@@ -4369,6 +5385,10 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4369
5385
  child.addEventListener("smartselectorchange", (e) => {
4370
5386
  this.smartSelectorEnabled = e.detail;
4371
5387
  });
5388
+ child.addEventListener("starthiddenchange", (e) => {
5389
+ this.isVisible = !e.detail;
5390
+ this.style.display = this.isVisible ? "" : "none";
5391
+ });
4372
5392
  },
4373
5393
  willClose: () => {
4374
5394
  this.isSettingsDialogOpen = false;
@@ -4423,7 +5443,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4423
5443
  e.detail.fileName,
4424
5444
  e.detail.testId,
4425
5445
  e.detail.itBlock,
4426
- e.detail.interceptorsBlock
5446
+ e.detail.interceptorsBlock,
5447
+ e.detail.notes
4427
5448
  ), 150);
4428
5449
  });
4429
5450
  },
@@ -4437,7 +5458,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4437
5458
  }, 0);
4438
5459
  });
4439
5460
  }
4440
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
5461
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
4441
5462
  Swal.fire({
4442
5463
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
4443
5464
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -4458,6 +5479,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4458
5479
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
4459
5480
  child.itBlock = itBlock;
4460
5481
  child.interceptorsBlock = interceptorsBlock;
5482
+ child.notes = notes;
4461
5483
  container.appendChild(child);
4462
5484
  child.addEventListener("close", () => {
4463
5485
  Swal.close();
@@ -4480,18 +5502,18 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4480
5502
  this.resizePopup();
4481
5503
  }
4482
5504
  // ── save handlers ─────────────────────────────────────────────────────────
4483
- async onSaveTest(description, tags = []) {
5505
+ async onSaveTest(description, tags = [], notes = "") {
4484
5506
  if (description) {
4485
- await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags);
5507
+ await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
4486
5508
  this.recording.clearCommands();
4487
5509
  this.clearRecordingHistory();
4488
5510
  this.cypressCommands = [];
4489
5511
  this.interceptors = [];
4490
5512
  }
4491
5513
  }
4492
- async onSaveAndExportTest(description, tags = []) {
5514
+ async onSaveAndExportTest(description, tags = [], notes = "") {
4493
5515
  if (description) {
4494
- const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags);
5516
+ const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
4495
5517
  this.recording.clearCommands();
4496
5518
  this.clearRecordingHistory();
4497
5519
  this.cypressCommands = [];
@@ -4518,6 +5540,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4518
5540
  render() {
4519
5541
  const rec = this.isRecording;
4520
5542
  const paused = this.isPaused;
5543
+ this.style.display = this.isVisible ? "" : "none";
4521
5544
  this.shadow.innerHTML = `<style>${getRecorderStyles(rec, paused)}</style>${renderRecorderWidget(rec, paused, this.translation.translate.bind(this.translation))}`;
4522
5545
  this.shadow.querySelector('[data-action="toggle"]')?.addEventListener("click", () => this.toggle());
4523
5546
  this.shadow.querySelector('[data-action="pause"]')?.addEventListener("click", () => this.togglePause());
@@ -4532,23 +5555,28 @@ if (!customElements.get("lib-e2e-recorder")) {
4532
5555
  }
4533
5556
 
4534
5557
  // src/index.ts
4535
- var VERSION = "0.1.0";
5558
+ var VERSION = "0.5.0";
4536
5559
  export {
5560
+ ACTIVE_SESSION_BREADCRUMB_KEY,
4537
5561
  AdvancedTestEditorElement,
4538
5562
  AdvancedTestTransformationService,
4539
5563
  ConfigurationElement,
4540
5564
  DB_SCHEMA,
4541
5565
  DB_STORE_NAMES,
5566
+ DEFAULT_RESUME_TTL_MINUTES,
4542
5567
  FilePreviewElement,
4543
5568
  HttpMonitor,
4544
5569
  INPUT_TYPES,
4545
5570
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
5571
+ LOCALE_BY_LANG,
4546
5572
  LibE2eRecorderElement,
4547
5573
  PersistenceService,
5574
+ RESUME_TTL_CONFIG_KEY,
4548
5575
  RecordingService,
4549
5576
  SCROLLBAR_STYLES,
4550
5577
  SUPPORTED_LANGS,
4551
5578
  SaveTestElement,
5579
+ SelectorPickerElement,
4552
5580
  Subject,
4553
5581
  TestEditorElement,
4554
5582
  TestPrevisualizerElement,
@@ -4559,10 +5587,13 @@ export {
4559
5587
  generateAlias,
4560
5588
  injectStyles,
4561
5589
  isLang,
5590
+ isLocalHost,
5591
+ localeForLang,
4562
5592
  makeModalResizable,
4563
5593
  makeSwalDraggable,
4564
5594
  makeSwalDraggableByContentId,
4565
5595
  persistenceService,
5596
+ selectTestsForExport,
4566
5597
  setSwal2DataCyAttribute,
4567
5598
  showToast,
4568
5599
  transformationService,