lib-e2e-cypress-for-dummys-ts 0.3.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: {
@@ -104,8 +149,6 @@ var I18N_ES = {
104
149
  COPY_DESCRIBE: "\u{1F5C2} Copiar describe()",
105
150
  MULTI_SELECT: "\u{1F5C2} Multi-select",
106
151
  CANCEL_SELECT: "\u2715 Cancelar",
107
- SECTION_COMMANDS: "Comandos",
108
- SECTION_INTERCEPTORS: "Interceptores",
109
152
  COPY_CMDS_BTN: "\u{1F4CB} Copiar comandos",
110
153
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
111
154
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -134,6 +177,18 @@ var I18N_ES = {
134
177
  DATA_SECTION: "\u{1F4BE} Datos",
135
178
  DATA_DESC: "Exporta todos tus tests a JSON o importa una copia de seguridad.",
136
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:",
137
192
  IMPORT_BTN: "\u2B07\uFE0F Importar tests",
138
193
  FOLDER_UPDATED_TOAST: "\u2713 Carpeta de Cypress actualizada",
139
194
  FOLDER_ERROR_TOAST: "Error al acceder a la carpeta",
@@ -146,7 +201,10 @@ var I18N_ES = {
146
201
  SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido.",
147
202
  START_HIDDEN_SECTION: "\u{1F441} Visibilidad del widget",
148
203
  START_HIDDEN_TITLE: "Iniciar oculto",
149
- START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo."
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."
150
208
  },
151
209
  SELECTOR_PICKER: {
152
210
  TITLE: "Selecciona un elemento del DOM",
@@ -187,8 +245,10 @@ var I18N_ES = {
187
245
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
188
246
  CLOSE_BTN: "\u2715 Cerrar",
189
247
  NEW_FILE_BTN: "+ Nuevo archivo",
248
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
190
249
  REFRESH_BTN: "\u21BB Actualizar",
191
250
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
251
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
192
252
  NEW_FILE_CONFIRM: "Crear",
193
253
  NEW_FILE_CANCEL: "Cancelar"
194
254
  },
@@ -201,10 +261,19 @@ var I18N_ES = {
201
261
  NO_CHANGES: "Sin cambios respecto al original",
202
262
  NO_CHANGES_SHORT: "Sin cambios",
203
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?)",
204
269
  DIFF_BTN: "\u{1F4CA} Diff",
205
270
  SAVE_BTN: "\u{1F4BE} Guardar",
206
271
  CLOSE_BTN: "\u2715 Cerrar",
207
- 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"
208
277
  },
209
278
  RECORDER: {
210
279
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -233,7 +302,12 @@ var I18N_ES = {
233
302
  BADGE_PAUSED: "\u23F8 PAUSA",
234
303
  BADGE_REC: "\u25CF REC",
235
304
  STOP_TITLE: "Detener (Ctrl+R)",
236
- 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"
237
311
  }
238
312
  };
239
313
 
@@ -271,8 +345,6 @@ var I18N_EN = {
271
345
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
272
346
  MULTI_SELECT: "\u{1F5C2} Multi-select",
273
347
  CANCEL_SELECT: "\u2715 Cancel",
274
- SECTION_COMMANDS: "Commands",
275
- SECTION_INTERCEPTORS: "Interceptors",
276
348
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
277
349
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
278
350
  DEFAULT_DESCRIBE: "Test suite",
@@ -301,6 +373,18 @@ var I18N_EN = {
301
373
  DATA_SECTION: "\u{1F4BE} Data",
302
374
  DATA_DESC: "Export all your tests to JSON or import a backup.",
303
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:",
304
388
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
305
389
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
306
390
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -313,7 +397,10 @@ var I18N_EN = {
313
397
  SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
314
398
  START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
315
399
  START_HIDDEN_TITLE: "Start hidden",
316
- START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it."
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."
317
404
  },
318
405
  SELECTOR_PICKER: {
319
406
  TITLE: "Select a DOM element",
@@ -354,8 +441,10 @@ var I18N_EN = {
354
441
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
355
442
  CLOSE_BTN: "\u2715 Close",
356
443
  NEW_FILE_BTN: "+ New file",
444
+ NEW_FOLDER_BTN: "+ New folder",
357
445
  REFRESH_BTN: "\u21BB Refresh",
358
446
  NEW_FILE_PLACEHOLDER: "test-name",
447
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
359
448
  NEW_FILE_CONFIRM: "Create",
360
449
  NEW_FILE_CANCEL: "Cancel"
361
450
  },
@@ -368,10 +457,19 @@ var I18N_EN = {
368
457
  NO_CHANGES: "No changes from original",
369
458
  NO_CHANGES_SHORT: "No changes",
370
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?)",
371
465
  DIFF_BTN: "\u{1F4CA} Diff",
372
466
  SAVE_BTN: "\u{1F4BE} Save",
373
467
  CLOSE_BTN: "\u2715 Close",
374
- 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"
375
473
  },
376
474
  RECORDER: {
377
475
  FS_TITLE: "\u{1F4C1} File access",
@@ -400,7 +498,12 @@ var I18N_EN = {
400
498
  BADGE_PAUSED: "\u23F8 PAUSED",
401
499
  BADGE_REC: "\u25CF REC",
402
500
  STOP_TITLE: "Stop (Ctrl+R)",
403
- 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"
404
507
  }
405
508
  };
406
509
 
@@ -438,8 +541,6 @@ var I18N_FR = {
438
541
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
439
542
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
440
543
  CANCEL_SELECT: "\u2715 Annuler",
441
- SECTION_COMMANDS: "Commandes",
442
- SECTION_INTERCEPTORS: "Intercepteurs",
443
544
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
444
545
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
445
546
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -468,6 +569,18 @@ var I18N_FR = {
468
569
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
469
570
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
470
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 :",
471
584
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
472
585
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
473
586
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -480,7 +593,10 @@ var I18N_FR = {
480
593
  SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
481
594
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
482
595
  START_HIDDEN_TITLE: "D\xE9marrer masqu\xE9",
483
- START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer."
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."
484
600
  },
485
601
  SELECTOR_PICKER: {
486
602
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -521,8 +637,10 @@ var I18N_FR = {
521
637
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
522
638
  CLOSE_BTN: "\u2715 Fermer",
523
639
  NEW_FILE_BTN: "+ Nouveau fichier",
640
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
524
641
  REFRESH_BTN: "\u21BB Actualiser",
525
642
  NEW_FILE_PLACEHOLDER: "nom-du-test",
643
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
526
644
  NEW_FILE_CONFIRM: "Cr\xE9er",
527
645
  NEW_FILE_CANCEL: "Annuler"
528
646
  },
@@ -535,10 +653,19 @@ var I18N_FR = {
535
653
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
536
654
  NO_CHANGES_SHORT: "Aucun changement",
537
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 ?)",
538
661
  DIFF_BTN: "\u{1F4CA} Diff",
539
662
  SAVE_BTN: "\u{1F4BE} Enregistrer",
540
663
  CLOSE_BTN: "\u2715 Fermer",
541
- 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"
542
669
  },
543
670
  RECORDER: {
544
671
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -567,7 +694,12 @@ var I18N_FR = {
567
694
  BADGE_PAUSED: "\u23F8 PAUSE",
568
695
  BADGE_REC: "\u25CF REC",
569
696
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
570
- 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"
571
703
  }
572
704
  };
573
705
 
@@ -605,8 +737,6 @@ var I18N_IT = {
605
737
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
606
738
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
607
739
  CANCEL_SELECT: "\u2715 Annulla",
608
- SECTION_COMMANDS: "Comandi",
609
- SECTION_INTERCEPTORS: "Interceptor",
610
740
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
611
741
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
612
742
  DEFAULT_DESCRIBE: "Suite di test",
@@ -635,6 +765,18 @@ var I18N_IT = {
635
765
  DATA_SECTION: "\u{1F4BE} Dati",
636
766
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
637
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:",
638
780
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
639
781
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
640
782
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -647,7 +789,10 @@ var I18N_IT = {
647
789
  SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
648
790
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
649
791
  START_HIDDEN_TITLE: "Avvia nascosto",
650
- START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo."
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."
651
796
  },
652
797
  SELECTOR_PICKER: {
653
798
  TITLE: "Seleziona un elemento DOM",
@@ -688,8 +833,10 @@ var I18N_IT = {
688
833
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
689
834
  CLOSE_BTN: "\u2715 Chiudi",
690
835
  NEW_FILE_BTN: "+ Nuovo file",
836
+ NEW_FOLDER_BTN: "+ Nuova cartella",
691
837
  REFRESH_BTN: "\u21BB Aggiorna",
692
838
  NEW_FILE_PLACEHOLDER: "nome-del-test",
839
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
693
840
  NEW_FILE_CONFIRM: "Crea",
694
841
  NEW_FILE_CANCEL: "Annulla"
695
842
  },
@@ -702,10 +849,19 @@ var I18N_IT = {
702
849
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
703
850
  NO_CHANGES_SHORT: "Nessuna modifica",
704
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?)",
705
857
  DIFF_BTN: "\u{1F4CA} Diff",
706
858
  SAVE_BTN: "\u{1F4BE} Salva",
707
859
  CLOSE_BTN: "\u2715 Chiudi",
708
- 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"
709
865
  },
710
866
  RECORDER: {
711
867
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -734,7 +890,12 @@ var I18N_IT = {
734
890
  BADGE_PAUSED: "\u23F8 PAUSA",
735
891
  BADGE_REC: "\u25CF REC",
736
892
  STOP_TITLE: "Ferma (Ctrl+R)",
737
- 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"
738
899
  }
739
900
  };
740
901
 
@@ -772,8 +933,6 @@ var I18N_DE = {
772
933
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
773
934
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
774
935
  CANCEL_SELECT: "\u2715 Abbrechen",
775
- SECTION_COMMANDS: "Befehle",
776
- SECTION_INTERCEPTORS: "Interceptors",
777
936
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
778
937
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
779
938
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -802,6 +961,18 @@ var I18N_DE = {
802
961
  DATA_SECTION: "\u{1F4BE} Daten",
803
962
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
804
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:",
805
976
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
806
977
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
807
978
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -814,7 +985,10 @@ var I18N_DE = {
814
985
  SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
815
986
  START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
816
987
  START_HIDDEN_TITLE: "Versteckt starten",
817
- START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden."
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."
818
992
  },
819
993
  SELECTOR_PICKER: {
820
994
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -855,8 +1029,10 @@ var I18N_DE = {
855
1029
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
856
1030
  CLOSE_BTN: "\u2715 Schlie\xDFen",
857
1031
  NEW_FILE_BTN: "+ Neue Datei",
1032
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
858
1033
  REFRESH_BTN: "\u21BB Aktualisieren",
859
1034
  NEW_FILE_PLACEHOLDER: "test-name",
1035
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
860
1036
  NEW_FILE_CONFIRM: "Erstellen",
861
1037
  NEW_FILE_CANCEL: "Abbrechen"
862
1038
  },
@@ -869,10 +1045,19 @@ var I18N_DE = {
869
1045
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
870
1046
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
871
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?)",
872
1053
  DIFF_BTN: "\u{1F4CA} Diff",
873
1054
  SAVE_BTN: "\u{1F4BE} Speichern",
874
1055
  CLOSE_BTN: "\u2715 Schlie\xDFen",
875
- 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"
876
1061
  },
877
1062
  RECORDER: {
878
1063
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -901,13 +1086,18 @@ var I18N_DE = {
901
1086
  BADGE_PAUSED: "\u23F8 PAUSE",
902
1087
  BADGE_REC: "\u25CF REC",
903
1088
  STOP_TITLE: "Stopp (Ctrl+R)",
904
- 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"
905
1095
  }
906
1096
  };
907
1097
 
908
1098
  // src/services/translation.service.ts
909
1099
  var TranslationService = class {
910
- lang;
1100
+ lang$;
911
1101
  translations = {
912
1102
  es: I18N_ES,
913
1103
  en: I18N_EN,
@@ -916,17 +1106,21 @@ var TranslationService = class {
916
1106
  de: I18N_DE
917
1107
  };
918
1108
  constructor() {
919
- this.lang = this.detectLang();
1109
+ this.lang$ = new Subject(this.detectLang());
920
1110
  }
921
1111
  setLang(lang) {
922
- this.lang = lang;
1112
+ this.lang$.next(lang);
923
1113
  }
924
1114
  getLang() {
925
- 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);
926
1120
  }
927
1121
  translate(key) {
928
1122
  const keys = key.split(".");
929
- let value = this.translations[this.lang];
1123
+ let value = this.translations[this.lang$.getValue()];
930
1124
  for (const k of keys) {
931
1125
  value = value?.[k];
932
1126
  if (value === void 0) return key;
@@ -949,6 +1143,9 @@ function gcd(a, b) {
949
1143
  }
950
1144
  return a;
951
1145
  }
1146
+ function escapeSingleQuotes(value) {
1147
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1148
+ }
952
1149
  function normalizeBlock(code, baseIndent) {
953
1150
  const lines = code.split("\n").map((l) => l.trimEnd());
954
1151
  const nonEmpty = lines.filter((l) => l.trim().length > 0);
@@ -971,7 +1168,7 @@ var TransformationService = class {
971
1168
  }
972
1169
  generateItDescription(description, commands) {
973
1170
  const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
974
- return `it('${description}', () => {
1171
+ return `it('${escapeSingleQuotes(description)}', () => {
975
1172
  ${commandsBlock}
976
1173
  });`;
977
1174
  }
@@ -1025,28 +1222,14 @@ ${lines}
1025
1222
  };
1026
1223
  var advancedTestTransformationService = new AdvancedTestTransformationService();
1027
1224
 
1028
- // src/utils/subject.ts
1029
- var Subject = class {
1030
- _value;
1031
- listeners = /* @__PURE__ */ new Set();
1032
- constructor(initialValue) {
1033
- this._value = initialValue;
1034
- }
1035
- next(value) {
1036
- this._value = value;
1037
- this.listeners.forEach((l) => l(value));
1038
- }
1039
- getValue() {
1040
- return this._value;
1041
- }
1042
- subscribe(fn) {
1043
- this.listeners.add(fn);
1044
- return () => this.listeners.delete(fn);
1045
- }
1046
- };
1047
-
1048
1225
  // src/services/recording.service.ts
1049
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
+ }
1050
1233
  var RecordingService = class {
1051
1234
  commands$ = new Subject([]);
1052
1235
  interceptors$ = new Subject([]);
@@ -1056,6 +1239,9 @@ var RecordingService = class {
1056
1239
  inputDebounceTimers = /* @__PURE__ */ new Map();
1057
1240
  abort = new AbortController();
1058
1241
  selectorStrategy = "data-cy";
1242
+ /** Stable id of the current live session (null when none has started). */
1243
+ sessionId = null;
1244
+ startedAt = 0;
1059
1245
  // Stored originals for history patching cleanup
1060
1246
  origPushState = history.pushState.bind(history);
1061
1247
  origReplaceState = history.replaceState.bind(history);
@@ -1067,12 +1253,42 @@ var RecordingService = class {
1067
1253
  }
1068
1254
  // ── Public API ────────────────────────────────────────────────────────────
1069
1255
  startRecording() {
1256
+ this.sessionId = createSessionId();
1257
+ this.startedAt = Date.now();
1070
1258
  this.isPaused$.next(false);
1071
1259
  this.isRecording$.next(true);
1072
1260
  this.addCommand(`cy.viewport(1900, 1200)`);
1073
1261
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1074
1262
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1075
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
+ }
1076
1292
  stopRecording() {
1077
1293
  this.isPaused$.next(false);
1078
1294
  this.isRecording$.next(false);
@@ -1159,6 +1375,21 @@ var RecordingService = class {
1159
1375
  onPauseChange(fn) {
1160
1376
  return this.isPaused$.subscribe(fn);
1161
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
+ }
1162
1393
  onSelectorNotFound(fn) {
1163
1394
  return this.selectorNotFound$.subscribe((v) => {
1164
1395
  if (v) fn(v.target, v.action);
@@ -1390,6 +1621,7 @@ var RecordingService = class {
1390
1621
 
1391
1622
  // src/services/persistence.service.ts
1392
1623
  import { openDB } from "idb";
1624
+ var ACTIVE_SESSION_ID = 1;
1393
1625
  var PersistenceService = class {
1394
1626
  // dbName is overridable so tests can use unique names for isolation.
1395
1627
  constructor(dbName = DB_SCHEMA.name) {
@@ -1443,7 +1675,7 @@ var PersistenceService = class {
1443
1675
  if (!record) return null;
1444
1676
  const commands = await this.getCommandStrings(testId);
1445
1677
  const interceptors = await this.getInterceptorStrings(testId);
1446
- const itBlock = `it('${record.name}', () => {
1678
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
1447
1679
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1448
1680
  });`;
1449
1681
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
@@ -1524,6 +1756,25 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1524
1756
  const records = await db.getAll("configuration");
1525
1757
  return records[0] ?? null;
1526
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
+ }
1527
1778
  // ── Bulk operations ───────────────────────────────────────────────────────
1528
1779
  async clearAllData() {
1529
1780
  const db = await this.getDB();
@@ -1534,10 +1785,18 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1534
1785
  ]);
1535
1786
  }
1536
1787
  async ingestFileData(tests, interceptors) {
1537
- await Promise.all([
1538
- this.bulkInsertWithoutId("tests", tests),
1539
- this.bulkInsertWithoutId("interceptors", interceptors)
1540
- ]);
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);
1541
1800
  }
1542
1801
  async requestDirectoryPermissions() {
1543
1802
  if (!("showDirectoryPicker" in window)) {
@@ -1980,6 +2239,25 @@ function showToast(message, isSuccess = true) {
1980
2239
  setTimeout(() => toast.remove(), 3e3);
1981
2240
  }
1982
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
+
1983
2261
  // src/components/test-previsualizer/test-previsualizer.styles.ts
1984
2262
  var TEST_PREVISUALIZER_STYLES = `
1985
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; }
@@ -2576,7 +2854,7 @@ function highlightLine(line) {
2576
2854
 
2577
2855
  // src/components/test-editor/test-editor.template.ts
2578
2856
  function renderTestEditor(state, t) {
2579
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
2857
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
2580
2858
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
2581
2859
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
2582
2860
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -2587,12 +2865,12 @@ function renderTestEditor(state, t) {
2587
2865
  </div>` : "";
2588
2866
  const rows = visible.map((test, i) => {
2589
2867
  const expanded = expandedIndex === i;
2590
- 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" });
2591
2869
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
2592
2870
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
2593
2871
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
2594
2872
  const hasIcps = (test.interceptors ?? []).length > 0;
2595
- const itBlockCode = `it('${test.name}', () => {
2873
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
2596
2874
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
2597
2875
  });`;
2598
2876
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -2700,11 +2978,11 @@ var TestEditorElement = class extends HTMLElement {
2700
2978
  ` : "";
2701
2979
  const itBlocks = selected.map((t) => {
2702
2980
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
2703
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
2981
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
2704
2982
  ${cmds}
2705
2983
  });`;
2706
2984
  }).join("\n\n");
2707
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
2985
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
2708
2986
  ${beforeEach}${itBlocks}
2709
2987
  });`;
2710
2988
  navigator.clipboard?.writeText(block);
@@ -2738,7 +3016,8 @@ ${beforeEach}${itBlocks}
2738
3016
  selectedIds: this.selectedIds,
2739
3017
  describeName: this.describeName,
2740
3018
  expandedIndex: this.expandedIndex,
2741
- interceptorsByTest: this.interceptorsByTest
3019
+ interceptorsByTest: this.interceptorsByTest,
3020
+ locale: localeForLang(this.translation.getLang())
2742
3021
  }, this.t.bind(this))}`;
2743
3022
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
2744
3023
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -2895,6 +3174,71 @@ var CONFIGURATION_STYLES = `
2895
3174
 
2896
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 */
2897
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; }
2898
3242
  `;
2899
3243
 
2900
3244
  // src/components/configuration/configuration.template.ts
@@ -2905,8 +3249,64 @@ var LANGS = [
2905
3249
  { value: "it", label: "Italiano" },
2906
3250
  { value: "de", label: "Deutsch" }
2907
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
+ }
2908
3308
  function renderConfiguration(state, t) {
2909
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden } = state;
3309
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
2910
3310
  const langOptions = LANGS.map(
2911
3311
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
2912
3312
  ).join("");
@@ -2958,6 +3358,17 @@ function renderConfiguration(state, t) {
2958
3358
  </label>
2959
3359
  </div>
2960
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
+
2961
3372
  <!-- Selector Strategy -->
2962
3373
  <div class="card card-wide">
2963
3374
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -2977,13 +3388,13 @@ function renderConfiguration(state, t) {
2977
3388
  <div class="card card-wide">
2978
3389
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
2979
3390
  <div class="fs-layout">
2980
- <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>
2981
3392
  \u2514\u2500\u2500 e2e/
2982
3393
  \u2514\u2500\u2500 *.cy.ts</pre>
2983
3394
  <div class="fs-right">
2984
3395
  <div class="fs-status">
2985
3396
  <span class="fs-dot ${filesystemGranted ? "on" : "off"}"></span>
2986
- ${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>`}
2987
3398
  </div>
2988
3399
  <div class="btn-row">
2989
3400
  <button id="btn-change-folder">
@@ -3008,7 +3419,7 @@ function renderConfiguration(state, t) {
3008
3419
  </div>
3009
3420
  </div>
3010
3421
 
3011
- </div>`;
3422
+ </div>${renderExportOverlay(state, t)}`;
3012
3423
  }
3013
3424
 
3014
3425
  // src/components/configuration/configuration.ts
@@ -3021,6 +3432,12 @@ var ConfigurationElement = class extends HTMLElement {
3021
3432
  selectorStrategy = "data-cy";
3022
3433
  smartSelectorEnabled = true;
3023
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();
3024
3441
  filesystemGranted = false;
3025
3442
  cypressFolderName = null;
3026
3443
  constructor() {
@@ -3047,6 +3464,11 @@ var ConfigurationElement = class extends HTMLElement {
3047
3464
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
3048
3465
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
3049
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
+ }
3050
3472
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
3051
3473
  const handle = config?.["cypressDirectoryHandle"];
3052
3474
  this.cypressFolderName = handle?.name ?? null;
@@ -3070,6 +3492,12 @@ var ConfigurationElement = class extends HTMLElement {
3070
3492
  this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
3071
3493
  this.render();
3072
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
+ }
3073
3501
  async onSmartSelectorChange(enabled) {
3074
3502
  this.smartSelectorEnabled = enabled;
3075
3503
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -3099,8 +3527,7 @@ var ConfigurationElement = class extends HTMLElement {
3099
3527
  this.cypressFolderName = null;
3100
3528
  this.render();
3101
3529
  }
3102
- async exportAllData() {
3103
- const tests = await this.persistence.getAllTests();
3530
+ downloadTests(tests) {
3104
3531
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
3105
3532
  const url = URL.createObjectURL(blob);
3106
3533
  const a = document.createElement("a");
@@ -3109,6 +3536,53 @@ var ConfigurationElement = class extends HTMLElement {
3109
3536
  a.click();
3110
3537
  URL.revokeObjectURL(url);
3111
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
+ }
3112
3586
  async importAllData(file) {
3113
3587
  const text = await file.text();
3114
3588
  let data;
@@ -3120,7 +3594,6 @@ var ConfigurationElement = class extends HTMLElement {
3120
3594
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
3121
3595
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
3122
3596
  }
3123
- await this.persistence.clearAllData();
3124
3597
  await this.persistence.ingestFileData(data.tests, data.interceptors);
3125
3598
  }
3126
3599
  render() {
@@ -3131,7 +3604,13 @@ var ConfigurationElement = class extends HTMLElement {
3131
3604
  filesystemGranted: this.filesystemGranted,
3132
3605
  cypressFolderName: this.cypressFolderName,
3133
3606
  smartSelectorEnabled: this.smartSelectorEnabled,
3134
- startHidden: this.startHidden
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
3135
3614
  }, this.t.bind(this))}`;
3136
3615
  ;
3137
3616
  this.shadow.getElementById("lang-select").addEventListener(
@@ -3150,13 +3629,28 @@ var ConfigurationElement = class extends HTMLElement {
3150
3629
  "change",
3151
3630
  (e) => this.onStartHiddenChange(e.target.checked)
3152
3631
  );
3632
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
3633
+ "change",
3634
+ (e) => this.onResumeTtlChange(Number(e.target.value))
3635
+ );
3153
3636
  this.shadow.getElementById("selector-strategy").addEventListener(
3154
3637
  "change",
3155
3638
  (e) => this.onSelectorStrategyChange(e.target.value)
3156
3639
  );
3157
3640
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
3158
3641
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
3159
- 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
+ );
3160
3654
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
3161
3655
  const file = e.target.files?.[0];
3162
3656
  if (!file) return;
@@ -3266,18 +3760,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3266
3760
  }
3267
3761
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3268
3762
  .sidebar-toolbar {
3269
- display: flex; gap: 4px; padding: 6px 6px 4px;
3763
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3270
3764
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3271
3765
  }
3272
3766
  .btn-toolbar {
3273
- 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;
3274
3768
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3275
3769
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3276
3770
  text-overflow: ellipsis;
3277
3771
  }
3772
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3278
3773
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3279
3774
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3280
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; }
3281
3778
  .new-file-form {
3282
3779
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3283
3780
  }
@@ -3310,7 +3807,7 @@ function renderNoPermission(needsReauth, t) {
3310
3807
  </div>`;
3311
3808
  }
3312
3809
  function renderAdvancedEditor(state, t) {
3313
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
3810
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3314
3811
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3315
3812
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3316
3813
  <pre class="pre-file">${syntaxHighlight(selectedFileContent.slice(0, 6e3))}${selectedFileContent.length > 6e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
@@ -3337,16 +3834,25 @@ function renderAdvancedEditor(state, t) {
3337
3834
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3338
3835
  </div>
3339
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>` : "";
3340
3844
  const toolbar = `
3341
3845
  <div class="sidebar-toolbar">
3342
3846
  <button id="btn-new-file" class="btn-toolbar btn-new">${t("ADVANCED_EDITOR.NEW_FILE_BTN")}</button>
3343
- <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>
3344
3849
  </div>`;
3345
3850
  return `
3346
3851
  <div class="layout">
3347
3852
  <div class="sidebar" style="width:${sidebarWidth}px">
3348
3853
  ${toolbar}
3349
3854
  ${newFileForm}
3855
+ ${newFolderForm}
3350
3856
  <div class="tree-scroll">${treeHtml}</div>
3351
3857
  </div>
3352
3858
  <div id="resize-handle" class="resize-handle"></div>
@@ -3417,6 +3923,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3417
3923
  previewFileName = null;
3418
3924
  previewFileContent = null;
3419
3925
  isCreatingFile = false;
3926
+ isCreatingFolder = false;
3420
3927
  collapsedDirs = /* @__PURE__ */ new Set();
3421
3928
  sidebarWidth = 220;
3422
3929
  hasPermission = false;
@@ -3479,7 +3986,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3479
3986
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3480
3987
  if (!name) return;
3481
3988
  const fileName = `${name}.cy.ts`;
3482
- const template = `describe('${name}', () => {
3989
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3483
3990
 
3484
3991
  it('should ', () => {
3485
3992
 
@@ -3497,6 +4004,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3497
4004
  this.isCreatingFile = false;
3498
4005
  await this.getFoldersData();
3499
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
+ }
3500
4018
  async refreshTree() {
3501
4019
  await this.getFoldersData();
3502
4020
  }
@@ -3554,7 +4072,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3554
4072
  fileName: this.selectedFile?.name ?? "",
3555
4073
  testId: this.testId,
3556
4074
  itBlock: this.testItBlock,
3557
- interceptorsBlock: this.interceptorsBlock
4075
+ interceptorsBlock: this.interceptorsBlock,
4076
+ notes: this.testNotes
3558
4077
  },
3559
4078
  bubbles: true,
3560
4079
  composed: true
@@ -3631,6 +4150,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3631
4150
  testNotes: this.testNotes,
3632
4151
  saveButtonEnabled: this.saveButtonEnabled,
3633
4152
  isCreatingFile: this.isCreatingFile,
4153
+ isCreatingFolder: this.isCreatingFolder,
3634
4154
  collapsedDirs: this.collapsedDirs,
3635
4155
  sidebarWidth: this.sidebarWidth
3636
4156
  }, this.t.bind(this))}`;
@@ -3654,9 +4174,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3654
4174
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
3655
4175
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
3656
4176
  this.isCreatingFile = !this.isCreatingFile;
4177
+ this.isCreatingFolder = false;
3657
4178
  this.render();
3658
4179
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
3659
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
+ });
3660
4187
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
3661
4188
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
3662
4189
  const input2 = this.shadow.getElementById("input-new-file");
@@ -3675,6 +4202,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3675
4202
  this.render();
3676
4203
  }
3677
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
+ });
3678
4222
  }
3679
4223
  };
3680
4224
  if (!customElements.get("advanced-test-editor")) {
@@ -3760,7 +4304,30 @@ var FILE_PREVIEW_STYLES = `
3760
4304
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
3761
4305
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
3762
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; }
3763
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
+ }
3764
4331
  `;
3765
4332
 
3766
4333
  // src/components/file-preview/file-preview.template.ts
@@ -3817,8 +4384,14 @@ function buildDiffHtml(original, current, t) {
3817
4384
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
3818
4385
  }).join("");
3819
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
+ };
3820
4393
  function renderFilePreview(state, t) {
3821
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4394
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
3822
4395
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
3823
4396
  <div class="blocks-panel">
3824
4397
  ${itBlock ? `
@@ -3839,6 +4412,13 @@ function renderFilePreview(state, t) {
3839
4412
  </div>` : ""}
3840
4413
  </div>` : "";
3841
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>` : "";
3842
4422
  return `
3843
4423
  <div class="container">
3844
4424
  <div class="header">
@@ -3849,9 +4429,11 @@ function renderFilePreview(state, t) {
3849
4429
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
3850
4430
  ${blocksPanelHtml}
3851
4431
  </div>
4432
+ ${runResultHtml}
3852
4433
  <div class="footer">
3853
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4434
+ ${launchBtnHtml}
3854
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>` : ""}
3855
4437
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
3856
4438
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
3857
4439
  </div>
@@ -3867,11 +4449,18 @@ var FilePreviewElement = class extends HTMLElement {
3867
4449
  translation = translationService;
3868
4450
  itBlock = "";
3869
4451
  interceptorsBlock = "";
4452
+ notes = "";
3870
4453
  commands = [];
3871
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);
3872
4459
  _fileContent = null;
3873
4460
  _originalContent = null;
3874
4461
  _showDiff = false;
4462
+ _runState = "idle";
4463
+ _runOutput = "";
3875
4464
  constructor() {
3876
4465
  super();
3877
4466
  this.shadow = this.attachShadow({ mode: "open" });
@@ -3899,15 +4488,34 @@ var FilePreviewElement = class extends HTMLElement {
3899
4488
  onClose() {
3900
4489
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
3901
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
+ */
3902
4496
  async launchTest(specPath) {
3903
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
4497
+ if (!this.isLocal) return;
4498
+ const path = specPath ?? this.fileName ?? "";
3904
4499
  if (!path) return;
3905
- const response = await fetch("http://localhost:8123/run-test", {
3906
- method: "POST",
3907
- headers: { "Content-Type": "application/json" },
3908
- body: JSON.stringify({ specPath: path })
3909
- });
3910
- 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();
3911
4519
  }
3912
4520
  copyToClipboard(text) {
3913
4521
  navigator.clipboard?.writeText(text);
@@ -3916,6 +4524,32 @@ var FilePreviewElement = class extends HTMLElement {
3916
4524
  this._showDiff = !this._showDiff;
3917
4525
  this.render();
3918
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
+ }
3919
4553
  render() {
3920
4554
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
3921
4555
  fileName: this.fileName,
@@ -3925,7 +4559,10 @@ var FilePreviewElement = class extends HTMLElement {
3925
4559
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
3926
4560
  itBlock: this.itBlock,
3927
4561
  interceptorsBlock: this.interceptorsBlock,
3928
- closeLabel: this.closeLabel
4562
+ closeLabel: this.closeLabel,
4563
+ isLocal: this.isLocal,
4564
+ runState: this._runState,
4565
+ runOutput: this._runOutput
3929
4566
  }, this.t.bind(this))}`;
3930
4567
  this.textarea = this.shadow.getElementById("editor");
3931
4568
  if (this.textarea) {
@@ -3941,6 +4578,7 @@ var FilePreviewElement = class extends HTMLElement {
3941
4578
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
3942
4579
  });
3943
4580
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
4581
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
3944
4582
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
3945
4583
  this.copyToClipboard(this.itBlock);
3946
4584
  });
@@ -4188,6 +4826,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
4188
4826
  interceptorsUnsub;
4189
4827
  pauseUnsub;
4190
4828
  selectorNotFoundUnsub;
4829
+ langUnsub;
4830
+ sessionUnsub;
4831
+ sessionSaveTimer;
4191
4832
  controlFirstTimeData = true;
4192
4833
  _previsualizerRef = null;
4193
4834
  httpMonitor;
@@ -4231,17 +4872,21 @@ var LibE2eRecorderElement = class extends HTMLElement {
4231
4872
  this.initSubscriptions();
4232
4873
  this.render();
4233
4874
  this.initVisibility();
4875
+ this.initSessionContinuity();
4234
4876
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4235
4877
  window.addEventListener("keydown", this.keydownHandler);
4236
4878
  }
4237
4879
  disconnectedCallback() {
4238
4880
  if (this._isDisabled) return;
4239
4881
  window.removeEventListener("keydown", this.keydownHandler);
4882
+ this.flushActiveSessionOnDisconnect();
4240
4883
  this.recordingUnsub?.();
4241
4884
  this.commandsUnsub?.();
4242
4885
  this.interceptorsUnsub?.();
4243
4886
  this.pauseUnsub?.();
4244
4887
  this.selectorNotFoundUnsub?.();
4888
+ this.langUnsub?.();
4889
+ this.sessionUnsub?.();
4245
4890
  this.httpMonitor?.uninstall();
4246
4891
  this.recording.destroy();
4247
4892
  }
@@ -4264,11 +4909,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4264
4909
  this.isRecording = val;
4265
4910
  if (!val && !this.controlFirstTimeData) {
4266
4911
  this.saveRecordingHistory();
4912
+ this.clearSessionPersistence();
4267
4913
  this.showSaveTestDialog();
4268
4914
  }
4269
4915
  this.controlFirstTimeData = false;
4270
4916
  this.render();
4271
4917
  });
4918
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4272
4919
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4273
4920
  this.cypressCommands = cmds;
4274
4921
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4284,9 +4931,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
4284
4931
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4285
4932
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4286
4933
  });
4934
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4287
4935
  }
4288
4936
  showSelectorPicker(target) {
4289
- import("./selector-picker-VJOLGZ5H.js").then(() => {
4937
+ import("./selector-picker-NKYFFFGK.js").then(() => {
4290
4938
  const existing = document.querySelector("selector-picker");
4291
4939
  if (existing) existing.remove();
4292
4940
  const picker = document.createElement("selector-picker");
@@ -4312,6 +4960,119 @@ var LibE2eRecorderElement = class extends HTMLElement {
4312
4960
  if (strategy) this.recording.selectorStrategy = strategy;
4313
4961
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4314
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
+ }
4315
5076
  toggle() {
4316
5077
  this.recording.toggleRecording();
4317
5078
  }
@@ -4682,7 +5443,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4682
5443
  e.detail.fileName,
4683
5444
  e.detail.testId,
4684
5445
  e.detail.itBlock,
4685
- e.detail.interceptorsBlock
5446
+ e.detail.interceptorsBlock,
5447
+ e.detail.notes
4686
5448
  ), 150);
4687
5449
  });
4688
5450
  },
@@ -4696,7 +5458,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4696
5458
  }, 0);
4697
5459
  });
4698
5460
  }
4699
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
5461
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
4700
5462
  Swal.fire({
4701
5463
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
4702
5464
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -4717,6 +5479,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4717
5479
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
4718
5480
  child.itBlock = itBlock;
4719
5481
  child.interceptorsBlock = interceptorsBlock;
5482
+ child.notes = notes;
4720
5483
  container.appendChild(child);
4721
5484
  child.addEventListener("close", () => {
4722
5485
  Swal.close();
@@ -4792,23 +5555,28 @@ if (!customElements.get("lib-e2e-recorder")) {
4792
5555
  }
4793
5556
 
4794
5557
  // src/index.ts
4795
- var VERSION = "0.1.0";
5558
+ var VERSION = "0.5.0";
4796
5559
  export {
5560
+ ACTIVE_SESSION_BREADCRUMB_KEY,
4797
5561
  AdvancedTestEditorElement,
4798
5562
  AdvancedTestTransformationService,
4799
5563
  ConfigurationElement,
4800
5564
  DB_SCHEMA,
4801
5565
  DB_STORE_NAMES,
5566
+ DEFAULT_RESUME_TTL_MINUTES,
4802
5567
  FilePreviewElement,
4803
5568
  HttpMonitor,
4804
5569
  INPUT_TYPES,
4805
5570
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
5571
+ LOCALE_BY_LANG,
4806
5572
  LibE2eRecorderElement,
4807
5573
  PersistenceService,
5574
+ RESUME_TTL_CONFIG_KEY,
4808
5575
  RecordingService,
4809
5576
  SCROLLBAR_STYLES,
4810
5577
  SUPPORTED_LANGS,
4811
5578
  SaveTestElement,
5579
+ SelectorPickerElement,
4812
5580
  Subject,
4813
5581
  TestEditorElement,
4814
5582
  TestPrevisualizerElement,
@@ -4819,10 +5587,13 @@ export {
4819
5587
  generateAlias,
4820
5588
  injectStyles,
4821
5589
  isLang,
5590
+ isLocalHost,
5591
+ localeForLang,
4822
5592
  makeModalResizable,
4823
5593
  makeSwalDraggable,
4824
5594
  makeSwalDraggableByContentId,
4825
5595
  persistenceService,
5596
+ selectTestsForExport,
4826
5597
  setSwal2DataCyAttribute,
4827
5598
  showToast,
4828
5599
  transformationService,