lib-e2e-cypress-for-dummys-ts 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 @@ 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.",
208
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Posici\xF3n del widget",
209
+ WIDGET_POSITION_HINT: "Arrastra el bot\xF3n de grabar para moverlo donde no estorbe. Se recuerda su posici\xF3n.",
210
+ WIDGET_POSITION_RESET_BTN: "\u21BA Restablecer posici\xF3n",
211
+ WIDGET_POSITION_RESET_DONE: "\u2713 Posici\xF3n restablecida"
150
212
  },
151
213
  SELECTOR_PICKER: {
152
214
  TITLE: "Selecciona un elemento del DOM",
@@ -187,8 +249,10 @@ var I18N_ES = {
187
249
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
188
250
  CLOSE_BTN: "\u2715 Cerrar",
189
251
  NEW_FILE_BTN: "+ Nuevo archivo",
252
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
190
253
  REFRESH_BTN: "\u21BB Actualizar",
191
254
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
255
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
192
256
  NEW_FILE_CONFIRM: "Crear",
193
257
  NEW_FILE_CANCEL: "Cancelar"
194
258
  },
@@ -201,10 +265,19 @@ var I18N_ES = {
201
265
  NO_CHANGES: "Sin cambios respecto al original",
202
266
  NO_CHANGES_SHORT: "Sin cambios",
203
267
  LAUNCH_BTN: "\u25B6 Lanzar test",
268
+ LAUNCH_LOCAL_ONLY: "Mu\xE9velo a local para poder probar",
269
+ LAUNCH_RUNNING: "\u23F3 Ejecutando\u2026",
270
+ LAUNCH_PASSED: "\u2713 Prueba superada",
271
+ LAUNCH_FAILED: "\u2717 Prueba fallida",
272
+ LAUNCH_NO_RUNNER: "No se detect\xF3 el runner local (\xBFlo has arrancado?)",
204
273
  DIFF_BTN: "\u{1F4CA} Diff",
205
274
  SAVE_BTN: "\u{1F4BE} Guardar",
206
275
  CLOSE_BTN: "\u2715 Cerrar",
207
- BACK_TO_EDITOR: "\u2190 Volver al editor"
276
+ BACK_TO_EDITOR: "\u2190 Volver al editor",
277
+ INSERT_BTN: "\u{1FA84} Insertar bloques",
278
+ INSERT_TITLE: "Insertar it() y beforeEach() en el contenido del editor",
279
+ INSERT_DONE: "Bloques insertados en el editor",
280
+ INSERT_ERROR: "No se encontr\xF3 un bloque describe() v\xE1lido en el archivo"
208
281
  },
209
282
  RECORDER: {
210
283
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -233,7 +306,12 @@ var I18N_ES = {
233
306
  BADGE_PAUSED: "\u23F8 PAUSA",
234
307
  BADGE_REC: "\u25CF REC",
235
308
  STOP_TITLE: "Detener (Ctrl+R)",
236
- START_TITLE: "Grabar (Ctrl+R)"
309
+ START_TITLE: "Grabar (Ctrl+R)",
310
+ SESSION_RESUME_TITLE: "\u23FA Grabaci\xF3n en curso",
311
+ SESSION_RESUME_TEXT: "Hay una grabaci\xF3n activa sin terminar.",
312
+ SESSION_RESUME_COUNT: "comandos capturados",
313
+ SESSION_CONTINUE_BTN: "Continuar grabando",
314
+ SESSION_DISCARD_BTN: "Descartar"
237
315
  }
238
316
  };
239
317
 
@@ -271,8 +349,6 @@ var I18N_EN = {
271
349
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
272
350
  MULTI_SELECT: "\u{1F5C2} Multi-select",
273
351
  CANCEL_SELECT: "\u2715 Cancel",
274
- SECTION_COMMANDS: "Commands",
275
- SECTION_INTERCEPTORS: "Interceptors",
276
352
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
277
353
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
278
354
  DEFAULT_DESCRIBE: "Test suite",
@@ -301,6 +377,18 @@ var I18N_EN = {
301
377
  DATA_SECTION: "\u{1F4BE} Data",
302
378
  DATA_DESC: "Export all your tests to JSON or import a backup.",
303
379
  EXPORT_BTN: "\u2B06\uFE0F Export tests",
380
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Export tests",
381
+ EXPORT_MODE_ALL: "All",
382
+ EXPORT_MODE_MANUAL: "Manual selection",
383
+ EXPORT_MODE_TAGS: "By tags",
384
+ EXPORT_ALL_DESC: "All saved tests will be exported.",
385
+ EXPORT_COUNT: "To export:",
386
+ EXPORT_CONFIRM: "\u2B06\uFE0F Export",
387
+ EXPORT_CANCEL: "Cancel",
388
+ EXPORT_EMPTY: "No tests to export.",
389
+ EXPORT_NO_TAGS: "No tags available.",
390
+ EXPORT_TAGS_HINT: "Select one or more tags to preview which tests will be exported.",
391
+ EXPORT_RESULT_LABEL: "Tests that will be exported:",
304
392
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
305
393
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
306
394
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -313,7 +401,14 @@ var I18N_EN = {
313
401
  SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
314
402
  START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
315
403
  START_HIDDEN_TITLE: "Start hidden",
316
- START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it."
404
+ START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it.",
405
+ RESUME_TTL_SECTION: "\u23F1 Recording continuity",
406
+ RESUME_TTL_LABEL: "Resume without asking (minutes)",
407
+ 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.",
408
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Widget position",
409
+ WIDGET_POSITION_HINT: "Drag the record button to move it out of the way. Its position is remembered.",
410
+ WIDGET_POSITION_RESET_BTN: "\u21BA Reset position",
411
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position reset"
317
412
  },
318
413
  SELECTOR_PICKER: {
319
414
  TITLE: "Select a DOM element",
@@ -354,8 +449,10 @@ var I18N_EN = {
354
449
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
355
450
  CLOSE_BTN: "\u2715 Close",
356
451
  NEW_FILE_BTN: "+ New file",
452
+ NEW_FOLDER_BTN: "+ New folder",
357
453
  REFRESH_BTN: "\u21BB Refresh",
358
454
  NEW_FILE_PLACEHOLDER: "test-name",
455
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
359
456
  NEW_FILE_CONFIRM: "Create",
360
457
  NEW_FILE_CANCEL: "Cancel"
361
458
  },
@@ -368,10 +465,19 @@ var I18N_EN = {
368
465
  NO_CHANGES: "No changes from original",
369
466
  NO_CHANGES_SHORT: "No changes",
370
467
  LAUNCH_BTN: "\u25B6 Run test",
468
+ LAUNCH_LOCAL_ONLY: "Move it to localhost to run tests",
469
+ LAUNCH_RUNNING: "\u23F3 Running\u2026",
470
+ LAUNCH_PASSED: "\u2713 Test passed",
471
+ LAUNCH_FAILED: "\u2717 Test failed",
472
+ LAUNCH_NO_RUNNER: "No local runner detected (did you start it?)",
371
473
  DIFF_BTN: "\u{1F4CA} Diff",
372
474
  SAVE_BTN: "\u{1F4BE} Save",
373
475
  CLOSE_BTN: "\u2715 Close",
374
- BACK_TO_EDITOR: "\u2190 Back to editor"
476
+ BACK_TO_EDITOR: "\u2190 Back to editor",
477
+ INSERT_BTN: "\u{1FA84} Insert blocks",
478
+ INSERT_TITLE: "Insert it() and beforeEach() into the editor content",
479
+ INSERT_DONE: "Blocks inserted into the editor",
480
+ INSERT_ERROR: "No valid describe() block found in the file"
375
481
  },
376
482
  RECORDER: {
377
483
  FS_TITLE: "\u{1F4C1} File access",
@@ -400,7 +506,12 @@ var I18N_EN = {
400
506
  BADGE_PAUSED: "\u23F8 PAUSED",
401
507
  BADGE_REC: "\u25CF REC",
402
508
  STOP_TITLE: "Stop (Ctrl+R)",
403
- START_TITLE: "Record (Ctrl+R)"
509
+ START_TITLE: "Record (Ctrl+R)",
510
+ SESSION_RESUME_TITLE: "\u23FA Recording in progress",
511
+ SESSION_RESUME_TEXT: "There is an unfinished active recording.",
512
+ SESSION_RESUME_COUNT: "commands captured",
513
+ SESSION_CONTINUE_BTN: "Keep recording",
514
+ SESSION_DISCARD_BTN: "Discard"
404
515
  }
405
516
  };
406
517
 
@@ -438,8 +549,6 @@ var I18N_FR = {
438
549
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
439
550
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
440
551
  CANCEL_SELECT: "\u2715 Annuler",
441
- SECTION_COMMANDS: "Commandes",
442
- SECTION_INTERCEPTORS: "Intercepteurs",
443
552
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
444
553
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
445
554
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -468,6 +577,18 @@ var I18N_FR = {
468
577
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
469
578
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
470
579
  EXPORT_BTN: "\u2B06\uFE0F Exporter les tests",
580
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exporter les tests",
581
+ EXPORT_MODE_ALL: "Tout",
582
+ EXPORT_MODE_MANUAL: "S\xE9lection manuelle",
583
+ EXPORT_MODE_TAGS: "Par tags",
584
+ EXPORT_ALL_DESC: "Tous les tests enregistr\xE9s seront export\xE9s.",
585
+ EXPORT_COUNT: "\xC0 exporter :",
586
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exporter",
587
+ EXPORT_CANCEL: "Annuler",
588
+ EXPORT_EMPTY: "Aucun test \xE0 exporter.",
589
+ EXPORT_NO_TAGS: "Aucun tag disponible.",
590
+ EXPORT_TAGS_HINT: "S\xE9lectionnez un ou plusieurs tags pour voir les tests qui seront export\xE9s.",
591
+ EXPORT_RESULT_LABEL: "Tests qui seront export\xE9s :",
471
592
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
472
593
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
473
594
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -480,7 +601,14 @@ var I18N_FR = {
480
601
  SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
481
602
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
482
603
  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."
604
+ START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer.",
605
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE9 d'enregistrement",
606
+ RESUME_TTL_LABEL: "Reprendre sans demander (minutes)",
607
+ 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.",
608
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Position du widget",
609
+ WIDGET_POSITION_HINT: "Fais glisser le bouton d'enregistrement pour le d\xE9placer. Sa position est m\xE9moris\xE9e.",
610
+ WIDGET_POSITION_RESET_BTN: "\u21BA R\xE9initialiser la position",
611
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position r\xE9initialis\xE9e"
484
612
  },
485
613
  SELECTOR_PICKER: {
486
614
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -521,8 +649,10 @@ var I18N_FR = {
521
649
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
522
650
  CLOSE_BTN: "\u2715 Fermer",
523
651
  NEW_FILE_BTN: "+ Nouveau fichier",
652
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
524
653
  REFRESH_BTN: "\u21BB Actualiser",
525
654
  NEW_FILE_PLACEHOLDER: "nom-du-test",
655
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
526
656
  NEW_FILE_CONFIRM: "Cr\xE9er",
527
657
  NEW_FILE_CANCEL: "Annuler"
528
658
  },
@@ -535,10 +665,19 @@ var I18N_FR = {
535
665
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
536
666
  NO_CHANGES_SHORT: "Aucun changement",
537
667
  LAUNCH_BTN: "\u25B6 Lancer le test",
668
+ LAUNCH_LOCAL_ONLY: "Passe en local pour pouvoir tester",
669
+ LAUNCH_RUNNING: "\u23F3 Ex\xE9cution\u2026",
670
+ LAUNCH_PASSED: "\u2713 Test r\xE9ussi",
671
+ LAUNCH_FAILED: "\u2717 Test \xE9chou\xE9",
672
+ LAUNCH_NO_RUNNER: "Aucun runner local d\xE9tect\xE9 (l'avez-vous d\xE9marr\xE9 ?)",
538
673
  DIFF_BTN: "\u{1F4CA} Diff",
539
674
  SAVE_BTN: "\u{1F4BE} Enregistrer",
540
675
  CLOSE_BTN: "\u2715 Fermer",
541
- BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur"
676
+ BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur",
677
+ INSERT_BTN: "\u{1FA84} Ins\xE9rer les blocs",
678
+ INSERT_TITLE: "Ins\xE9rer it() et beforeEach() dans le contenu de l'\xE9diteur",
679
+ INSERT_DONE: "Blocs ins\xE9r\xE9s dans l'\xE9diteur",
680
+ INSERT_ERROR: "Aucun bloc describe() valide trouv\xE9 dans le fichier"
542
681
  },
543
682
  RECORDER: {
544
683
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -567,7 +706,12 @@ var I18N_FR = {
567
706
  BADGE_PAUSED: "\u23F8 PAUSE",
568
707
  BADGE_REC: "\u25CF REC",
569
708
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
570
- START_TITLE: "Enregistrer (Ctrl+R)"
709
+ START_TITLE: "Enregistrer (Ctrl+R)",
710
+ SESSION_RESUME_TITLE: "\u23FA Enregistrement en cours",
711
+ SESSION_RESUME_TEXT: "Un enregistrement actif est inachev\xE9.",
712
+ SESSION_RESUME_COUNT: "commandes captur\xE9es",
713
+ SESSION_CONTINUE_BTN: "Continuer l'enregistrement",
714
+ SESSION_DISCARD_BTN: "Ignorer"
571
715
  }
572
716
  };
573
717
 
@@ -605,8 +749,6 @@ var I18N_IT = {
605
749
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
606
750
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
607
751
  CANCEL_SELECT: "\u2715 Annulla",
608
- SECTION_COMMANDS: "Comandi",
609
- SECTION_INTERCEPTORS: "Interceptor",
610
752
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
611
753
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
612
754
  DEFAULT_DESCRIBE: "Suite di test",
@@ -635,6 +777,18 @@ var I18N_IT = {
635
777
  DATA_SECTION: "\u{1F4BE} Dati",
636
778
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
637
779
  EXPORT_BTN: "\u2B06\uFE0F Esporta test",
780
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Esporta test",
781
+ EXPORT_MODE_ALL: "Tutto",
782
+ EXPORT_MODE_MANUAL: "Selezione manuale",
783
+ EXPORT_MODE_TAGS: "Per tag",
784
+ EXPORT_ALL_DESC: "Verranno esportati tutti i test salvati.",
785
+ EXPORT_COUNT: "Da esportare:",
786
+ EXPORT_CONFIRM: "\u2B06\uFE0F Esporta",
787
+ EXPORT_CANCEL: "Annulla",
788
+ EXPORT_EMPTY: "Nessun test da esportare.",
789
+ EXPORT_NO_TAGS: "Nessun tag disponibile.",
790
+ EXPORT_TAGS_HINT: "Seleziona uno o pi\xF9 tag per vedere quali test verranno esportati.",
791
+ EXPORT_RESULT_LABEL: "Test che verranno esportati:",
638
792
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
639
793
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
640
794
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -647,7 +801,14 @@ var I18N_IT = {
647
801
  SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
648
802
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
649
803
  START_HIDDEN_TITLE: "Avvia nascosto",
650
- START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo."
804
+ START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo.",
805
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE0 di registrazione",
806
+ RESUME_TTL_LABEL: "Riprendi senza chiedere (minuti)",
807
+ 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.",
808
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Posizione del widget",
809
+ WIDGET_POSITION_HINT: "Trascina il pulsante di registrazione per spostarlo. La posizione viene ricordata.",
810
+ WIDGET_POSITION_RESET_BTN: "\u21BA Reimposta posizione",
811
+ WIDGET_POSITION_RESET_DONE: "\u2713 Posizione reimpostata"
651
812
  },
652
813
  SELECTOR_PICKER: {
653
814
  TITLE: "Seleziona un elemento DOM",
@@ -688,8 +849,10 @@ var I18N_IT = {
688
849
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
689
850
  CLOSE_BTN: "\u2715 Chiudi",
690
851
  NEW_FILE_BTN: "+ Nuovo file",
852
+ NEW_FOLDER_BTN: "+ Nuova cartella",
691
853
  REFRESH_BTN: "\u21BB Aggiorna",
692
854
  NEW_FILE_PLACEHOLDER: "nome-del-test",
855
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
693
856
  NEW_FILE_CONFIRM: "Crea",
694
857
  NEW_FILE_CANCEL: "Annulla"
695
858
  },
@@ -702,10 +865,19 @@ var I18N_IT = {
702
865
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
703
866
  NO_CHANGES_SHORT: "Nessuna modifica",
704
867
  LAUNCH_BTN: "\u25B6 Esegui test",
868
+ LAUNCH_LOCAL_ONLY: "Spostati in locale per poter testare",
869
+ LAUNCH_RUNNING: "\u23F3 Esecuzione\u2026",
870
+ LAUNCH_PASSED: "\u2713 Test superato",
871
+ LAUNCH_FAILED: "\u2717 Test fallito",
872
+ LAUNCH_NO_RUNNER: "Nessun runner locale rilevato (l'hai avviato?)",
705
873
  DIFF_BTN: "\u{1F4CA} Diff",
706
874
  SAVE_BTN: "\u{1F4BE} Salva",
707
875
  CLOSE_BTN: "\u2715 Chiudi",
708
- BACK_TO_EDITOR: "\u2190 Torna all'editor"
876
+ BACK_TO_EDITOR: "\u2190 Torna all'editor",
877
+ INSERT_BTN: "\u{1FA84} Inserisci blocchi",
878
+ INSERT_TITLE: "Inserisci it() e beforeEach() nel contenuto dell'editor",
879
+ INSERT_DONE: "Blocchi inseriti nell'editor",
880
+ INSERT_ERROR: "Nessun blocco describe() valido trovato nel file"
709
881
  },
710
882
  RECORDER: {
711
883
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -734,7 +906,12 @@ var I18N_IT = {
734
906
  BADGE_PAUSED: "\u23F8 PAUSA",
735
907
  BADGE_REC: "\u25CF REC",
736
908
  STOP_TITLE: "Ferma (Ctrl+R)",
737
- START_TITLE: "Registra (Ctrl+R)"
909
+ START_TITLE: "Registra (Ctrl+R)",
910
+ SESSION_RESUME_TITLE: "\u23FA Registrazione in corso",
911
+ SESSION_RESUME_TEXT: "C'\xE8 una registrazione attiva non terminata.",
912
+ SESSION_RESUME_COUNT: "comandi catturati",
913
+ SESSION_CONTINUE_BTN: "Continua a registrare",
914
+ SESSION_DISCARD_BTN: "Annulla"
738
915
  }
739
916
  };
740
917
 
@@ -772,8 +949,6 @@ var I18N_DE = {
772
949
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
773
950
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
774
951
  CANCEL_SELECT: "\u2715 Abbrechen",
775
- SECTION_COMMANDS: "Befehle",
776
- SECTION_INTERCEPTORS: "Interceptors",
777
952
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
778
953
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
779
954
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -802,6 +977,18 @@ var I18N_DE = {
802
977
  DATA_SECTION: "\u{1F4BE} Daten",
803
978
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
804
979
  EXPORT_BTN: "\u2B06\uFE0F Tests exportieren",
980
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Tests exportieren",
981
+ EXPORT_MODE_ALL: "Alle",
982
+ EXPORT_MODE_MANUAL: "Manuelle Auswahl",
983
+ EXPORT_MODE_TAGS: "Nach Tags",
984
+ EXPORT_ALL_DESC: "Alle gespeicherten Tests werden exportiert.",
985
+ EXPORT_COUNT: "Zu exportieren:",
986
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportieren",
987
+ EXPORT_CANCEL: "Abbrechen",
988
+ EXPORT_EMPTY: "Keine Tests zum Exportieren.",
989
+ EXPORT_NO_TAGS: "Keine Tags verf\xFCgbar.",
990
+ EXPORT_TAGS_HINT: "W\xE4hle ein oder mehrere Tags, um zu sehen, welche Tests exportiert werden.",
991
+ EXPORT_RESULT_LABEL: "Diese Tests werden exportiert:",
805
992
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
806
993
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
807
994
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -814,7 +1001,14 @@ var I18N_DE = {
814
1001
  SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
815
1002
  START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
816
1003
  START_HIDDEN_TITLE: "Versteckt starten",
817
- START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden."
1004
+ START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden.",
1005
+ RESUME_TTL_SECTION: "\u23F1 Aufzeichnungskontinuit\xE4t",
1006
+ RESUME_TTL_LABEL: "Ohne Nachfrage fortsetzen (Minuten)",
1007
+ 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.",
1008
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Widget-Position",
1009
+ WIDGET_POSITION_HINT: "Ziehe den Aufnahme-Button, um ihn aus dem Weg zu schieben. Die Position wird gespeichert.",
1010
+ WIDGET_POSITION_RESET_BTN: "\u21BA Position zur\xFCcksetzen",
1011
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position zur\xFCckgesetzt"
818
1012
  },
819
1013
  SELECTOR_PICKER: {
820
1014
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -855,8 +1049,10 @@ var I18N_DE = {
855
1049
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
856
1050
  CLOSE_BTN: "\u2715 Schlie\xDFen",
857
1051
  NEW_FILE_BTN: "+ Neue Datei",
1052
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
858
1053
  REFRESH_BTN: "\u21BB Aktualisieren",
859
1054
  NEW_FILE_PLACEHOLDER: "test-name",
1055
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
860
1056
  NEW_FILE_CONFIRM: "Erstellen",
861
1057
  NEW_FILE_CANCEL: "Abbrechen"
862
1058
  },
@@ -869,10 +1065,19 @@ var I18N_DE = {
869
1065
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
870
1066
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
871
1067
  LAUNCH_BTN: "\u25B6 Test starten",
1068
+ LAUNCH_LOCAL_ONLY: "Wechsle zu localhost, um Tests auszuf\xFChren",
1069
+ LAUNCH_RUNNING: "\u23F3 L\xE4uft\u2026",
1070
+ LAUNCH_PASSED: "\u2713 Test bestanden",
1071
+ LAUNCH_FAILED: "\u2717 Test fehlgeschlagen",
1072
+ LAUNCH_NO_RUNNER: "Kein lokaler Runner erkannt (gestartet?)",
872
1073
  DIFF_BTN: "\u{1F4CA} Diff",
873
1074
  SAVE_BTN: "\u{1F4BE} Speichern",
874
1075
  CLOSE_BTN: "\u2715 Schlie\xDFen",
875
- BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor"
1076
+ BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor",
1077
+ INSERT_BTN: "\u{1FA84} Bl\xF6cke einf\xFCgen",
1078
+ INSERT_TITLE: "it() und beforeEach() in den Editor-Inhalt einf\xFCgen",
1079
+ INSERT_DONE: "Bl\xF6cke in den Editor eingef\xFCgt",
1080
+ INSERT_ERROR: "Kein g\xFCltiger describe()-Block in der Datei gefunden"
876
1081
  },
877
1082
  RECORDER: {
878
1083
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -901,13 +1106,18 @@ var I18N_DE = {
901
1106
  BADGE_PAUSED: "\u23F8 PAUSE",
902
1107
  BADGE_REC: "\u25CF REC",
903
1108
  STOP_TITLE: "Stopp (Ctrl+R)",
904
- START_TITLE: "Aufzeichnen (Ctrl+R)"
1109
+ START_TITLE: "Aufzeichnen (Ctrl+R)",
1110
+ SESSION_RESUME_TITLE: "\u23FA Aufzeichnung l\xE4uft",
1111
+ SESSION_RESUME_TEXT: "Es gibt eine nicht abgeschlossene aktive Aufzeichnung.",
1112
+ SESSION_RESUME_COUNT: "erfasste Befehle",
1113
+ SESSION_CONTINUE_BTN: "Weiter aufzeichnen",
1114
+ SESSION_DISCARD_BTN: "Verwerfen"
905
1115
  }
906
1116
  };
907
1117
 
908
1118
  // src/services/translation.service.ts
909
1119
  var TranslationService = class {
910
- lang;
1120
+ lang$;
911
1121
  translations = {
912
1122
  es: I18N_ES,
913
1123
  en: I18N_EN,
@@ -916,17 +1126,21 @@ var TranslationService = class {
916
1126
  de: I18N_DE
917
1127
  };
918
1128
  constructor() {
919
- this.lang = this.detectLang();
1129
+ this.lang$ = new Subject(this.detectLang());
920
1130
  }
921
1131
  setLang(lang) {
922
- this.lang = lang;
1132
+ this.lang$.next(lang);
923
1133
  }
924
1134
  getLang() {
925
- return this.lang;
1135
+ return this.lang$.getValue();
1136
+ }
1137
+ /** Subscribe to language changes; returns an unsubscribe function. */
1138
+ onLangChange(fn) {
1139
+ return this.lang$.subscribe(fn);
926
1140
  }
927
1141
  translate(key) {
928
1142
  const keys = key.split(".");
929
- let value = this.translations[this.lang];
1143
+ let value = this.translations[this.lang$.getValue()];
930
1144
  for (const k of keys) {
931
1145
  value = value?.[k];
932
1146
  if (value === void 0) return key;
@@ -949,6 +1163,9 @@ function gcd(a, b) {
949
1163
  }
950
1164
  return a;
951
1165
  }
1166
+ function escapeSingleQuotes(value) {
1167
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1168
+ }
952
1169
  function normalizeBlock(code, baseIndent) {
953
1170
  const lines = code.split("\n").map((l) => l.trimEnd());
954
1171
  const nonEmpty = lines.filter((l) => l.trim().length > 0);
@@ -971,7 +1188,7 @@ var TransformationService = class {
971
1188
  }
972
1189
  generateItDescription(description, commands) {
973
1190
  const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
974
- return `it('${description}', () => {
1191
+ return `it('${escapeSingleQuotes(description)}', () => {
975
1192
  ${commandsBlock}
976
1193
  });`;
977
1194
  }
@@ -1025,28 +1242,14 @@ ${lines}
1025
1242
  };
1026
1243
  var advancedTestTransformationService = new AdvancedTestTransformationService();
1027
1244
 
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
1245
  // src/services/recording.service.ts
1049
1246
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
1247
+ function createSessionId() {
1248
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1249
+ return crypto.randomUUID();
1250
+ }
1251
+ return `sess-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
1252
+ }
1050
1253
  var RecordingService = class {
1051
1254
  commands$ = new Subject([]);
1052
1255
  interceptors$ = new Subject([]);
@@ -1056,6 +1259,9 @@ var RecordingService = class {
1056
1259
  inputDebounceTimers = /* @__PURE__ */ new Map();
1057
1260
  abort = new AbortController();
1058
1261
  selectorStrategy = "data-cy";
1262
+ /** Stable id of the current live session (null when none has started). */
1263
+ sessionId = null;
1264
+ startedAt = 0;
1059
1265
  // Stored originals for history patching cleanup
1060
1266
  origPushState = history.pushState.bind(history);
1061
1267
  origReplaceState = history.replaceState.bind(history);
@@ -1067,12 +1273,42 @@ var RecordingService = class {
1067
1273
  }
1068
1274
  // ── Public API ────────────────────────────────────────────────────────────
1069
1275
  startRecording() {
1276
+ this.sessionId = createSessionId();
1277
+ this.startedAt = Date.now();
1070
1278
  this.isPaused$.next(false);
1071
1279
  this.isRecording$.next(true);
1072
1280
  this.addCommand(`cy.viewport(1900, 1200)`);
1073
1281
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1074
1282
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1075
1283
  }
1284
+ /**
1285
+ * Rehydrates a previously persisted session WITHOUT running the startRecording
1286
+ * bootstrap (no viewport/visit/hide). Used to continue a recording across a
1287
+ * micro-frontend crossing or a same-origin reload.
1288
+ * See docs/specs/006-cross-app-recording-continuity.md.
1289
+ */
1290
+ restoreSession(state) {
1291
+ this.sessionId = state.sessionId;
1292
+ this.startedAt = state.startedAt;
1293
+ this.selectorStrategy = state.selectorStrategy;
1294
+ this.commands$.next([...state.commands]);
1295
+ this.interceptors$.next([...state.interceptors]);
1296
+ this.isPaused$.next(state.isPaused);
1297
+ this.isRecording$.next(state.isRecording);
1298
+ }
1299
+ /** Full snapshot of the live session for persistence. */
1300
+ getSessionSnapshot() {
1301
+ return {
1302
+ sessionId: this.sessionId ?? createSessionId(),
1303
+ isRecording: this.isRecording$.getValue(),
1304
+ isPaused: this.isPaused$.getValue(),
1305
+ commands: this.commands$.getValue(),
1306
+ interceptors: this.interceptors$.getValue(),
1307
+ selectorStrategy: this.selectorStrategy,
1308
+ startedAt: this.startedAt,
1309
+ updatedAt: Date.now()
1310
+ };
1311
+ }
1076
1312
  stopRecording() {
1077
1313
  this.isPaused$.next(false);
1078
1314
  this.isRecording$.next(false);
@@ -1159,6 +1395,21 @@ var RecordingService = class {
1159
1395
  onPauseChange(fn) {
1160
1396
  return this.isPaused$.subscribe(fn);
1161
1397
  }
1398
+ /**
1399
+ * Fires a full session snapshot whenever any persisted field changes
1400
+ * (commands, interceptors, recording or paused state). Drives the debounced
1401
+ * persistence of the live session. Returns a combined unsubscribe.
1402
+ */
1403
+ onSessionChange(fn) {
1404
+ const emit = () => fn(this.getSessionSnapshot());
1405
+ const unsubs = [
1406
+ this.commands$.subscribe(emit),
1407
+ this.interceptors$.subscribe(emit),
1408
+ this.isRecording$.subscribe(emit),
1409
+ this.isPaused$.subscribe(emit)
1410
+ ];
1411
+ return () => unsubs.forEach((u) => u());
1412
+ }
1162
1413
  onSelectorNotFound(fn) {
1163
1414
  return this.selectorNotFound$.subscribe((v) => {
1164
1415
  if (v) fn(v.target, v.action);
@@ -1390,6 +1641,7 @@ var RecordingService = class {
1390
1641
 
1391
1642
  // src/services/persistence.service.ts
1392
1643
  import { openDB } from "idb";
1644
+ var ACTIVE_SESSION_ID = 1;
1393
1645
  var PersistenceService = class {
1394
1646
  // dbName is overridable so tests can use unique names for isolation.
1395
1647
  constructor(dbName = DB_SCHEMA.name) {
@@ -1443,7 +1695,7 @@ var PersistenceService = class {
1443
1695
  if (!record) return null;
1444
1696
  const commands = await this.getCommandStrings(testId);
1445
1697
  const interceptors = await this.getInterceptorStrings(testId);
1446
- const itBlock = `it('${record.name}', () => {
1698
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
1447
1699
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1448
1700
  });`;
1449
1701
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
@@ -1524,6 +1776,25 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1524
1776
  const records = await db.getAll("configuration");
1525
1777
  return records[0] ?? null;
1526
1778
  }
1779
+ // ── Active recording session ──────────────────────────────────────────────
1780
+ /** Upserts the live recording session (single record, fixed key). */
1781
+ async saveActiveSession(state) {
1782
+ const db = await this.getDB();
1783
+ await db.put("activeSession", { ...state, id: ACTIVE_SESSION_ID });
1784
+ }
1785
+ /** Returns the persisted live session, or null when none is stored. */
1786
+ async getActiveSession() {
1787
+ const db = await this.getDB();
1788
+ const record = await db.get("activeSession", ACTIVE_SESSION_ID);
1789
+ if (!record) return null;
1790
+ const { id: _id, ...state } = record;
1791
+ return state;
1792
+ }
1793
+ /** Removes the live session record. Safe to call when none exists. */
1794
+ async clearActiveSession() {
1795
+ const db = await this.getDB();
1796
+ await db.delete("activeSession", ACTIVE_SESSION_ID);
1797
+ }
1527
1798
  // ── Bulk operations ───────────────────────────────────────────────────────
1528
1799
  async clearAllData() {
1529
1800
  const db = await this.getDB();
@@ -1534,10 +1805,18 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1534
1805
  ]);
1535
1806
  }
1536
1807
  async ingestFileData(tests, interceptors) {
1537
- await Promise.all([
1538
- this.bulkInsertWithoutId("tests", tests),
1539
- this.bulkInsertWithoutId("interceptors", interceptors)
1540
- ]);
1808
+ if (Array.isArray(tests)) {
1809
+ const db = await this.getDB();
1810
+ for (const test of tests) {
1811
+ const { id: _id, commands, interceptors: testInterceptors, ...record } = test;
1812
+ const newId = await db.add("tests", record);
1813
+ const cmds = Array.isArray(commands) ? commands : [];
1814
+ const icps = Array.isArray(testInterceptors) ? testInterceptors : [];
1815
+ if (cmds.length) await this.insertCommands(cmds, newId);
1816
+ if (icps.length) await this.insertInterceptors(icps, newId);
1817
+ }
1818
+ }
1819
+ await this.bulkInsertWithoutId("interceptors", interceptors);
1541
1820
  }
1542
1821
  async requestDirectoryPermissions() {
1543
1822
  if (!("showDirectoryPicker" in window)) {
@@ -1980,6 +2259,56 @@ function showToast(message, isSuccess = true) {
1980
2259
  setTimeout(() => toast.remove(), 3e3);
1981
2260
  }
1982
2261
 
2262
+ // src/utils/export-selection.utils.ts
2263
+ function selectTestsForExport(tests, mode, opts = {}) {
2264
+ if (mode === "all") return [...tests];
2265
+ if (mode === "manual") {
2266
+ const ids = new Set(opts.ids ?? []);
2267
+ return tests.filter((t) => ids.has(t.id));
2268
+ }
2269
+ const tags = new Set(opts.tags ?? []);
2270
+ if (tags.size === 0) return [];
2271
+ return tests.filter((t) => (t.tags ?? []).some((tag) => tags.has(tag)));
2272
+ }
2273
+
2274
+ // src/utils/host.utils.ts
2275
+ function isLocalHost(hostname) {
2276
+ if (!hostname) return true;
2277
+ const h = hostname.toLowerCase();
2278
+ return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
2279
+ }
2280
+
2281
+ // src/utils/widget-position.utils.ts
2282
+ var TOGGLE_MARGIN = 30;
2283
+ var DRAG_THRESHOLD = 5;
2284
+ var DEFAULT_EDGE_OFFSET = 46;
2285
+ function boxOffsetForDirection(dir) {
2286
+ return {
2287
+ x: dir.endsWith("left") ? 144 : 46,
2288
+ y: dir.startsWith("up") ? 144 : 46
2289
+ };
2290
+ }
2291
+ function clampTogglePosition(x, y, vw, vh, margin = TOGGLE_MARGIN) {
2292
+ const maxX = Math.max(margin, vw - margin);
2293
+ const maxY = Math.max(margin, vh - margin);
2294
+ return {
2295
+ x: Math.min(Math.max(x, margin), maxX),
2296
+ y: Math.min(Math.max(y, margin), maxY)
2297
+ };
2298
+ }
2299
+ function resolveExpandDirection(x, y, vw, vh) {
2300
+ const vertical = y < vh / 2 ? "down" : "up";
2301
+ const horizontal = x < vw / 2 ? "right" : "left";
2302
+ return `${vertical}-${horizontal}`;
2303
+ }
2304
+ function defaultTogglePosition(vw, vh) {
2305
+ return { x: vw - DEFAULT_EDGE_OFFSET, y: vh - DEFAULT_EDGE_OFFSET };
2306
+ }
2307
+ function boxTopLeftFor(toggleCentre, dir) {
2308
+ const off = boxOffsetForDirection(dir);
2309
+ return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
2310
+ }
2311
+
1983
2312
  // src/components/test-previsualizer/test-previsualizer.styles.ts
1984
2313
  var TEST_PREVISUALIZER_STYLES = `
1985
2314
  :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 +2905,7 @@ function highlightLine(line) {
2576
2905
 
2577
2906
  // src/components/test-editor/test-editor.template.ts
2578
2907
  function renderTestEditor(state, t) {
2579
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
2908
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
2580
2909
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
2581
2910
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
2582
2911
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -2587,12 +2916,12 @@ function renderTestEditor(state, t) {
2587
2916
  </div>` : "";
2588
2917
  const rows = visible.map((test, i) => {
2589
2918
  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" });
2919
+ const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
2591
2920
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
2592
2921
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
2593
2922
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
2594
2923
  const hasIcps = (test.interceptors ?? []).length > 0;
2595
- const itBlockCode = `it('${test.name}', () => {
2924
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
2596
2925
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
2597
2926
  });`;
2598
2927
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -2700,11 +3029,11 @@ var TestEditorElement = class extends HTMLElement {
2700
3029
  ` : "";
2701
3030
  const itBlocks = selected.map((t) => {
2702
3031
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
2703
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
3032
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
2704
3033
  ${cmds}
2705
3034
  });`;
2706
3035
  }).join("\n\n");
2707
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
3036
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
2708
3037
  ${beforeEach}${itBlocks}
2709
3038
  });`;
2710
3039
  navigator.clipboard?.writeText(block);
@@ -2738,7 +3067,8 @@ ${beforeEach}${itBlocks}
2738
3067
  selectedIds: this.selectedIds,
2739
3068
  describeName: this.describeName,
2740
3069
  expandedIndex: this.expandedIndex,
2741
- interceptorsByTest: this.interceptorsByTest
3070
+ interceptorsByTest: this.interceptorsByTest,
3071
+ locale: localeForLang(this.translation.getLang())
2742
3072
  }, this.t.bind(this))}`;
2743
3073
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
2744
3074
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -2895,6 +3225,71 @@ var CONFIGURATION_STYLES = `
2895
3225
 
2896
3226
  /* \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
3227
  .data-desc { font-size: 11px; color: #484f58; margin-bottom: 10px; line-height: 1.5; }
3228
+
3229
+ /* \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 */
3230
+ .export-overlay {
3231
+ position: fixed; inset: 0; z-index: 100000;
3232
+ background: rgba(1,4,9,0.7);
3233
+ display: flex; align-items: center; justify-content: center; padding: 20px;
3234
+ }
3235
+ .export-modal {
3236
+ width: 640px; max-width: 100%; max-height: 86vh;
3237
+ display: flex; flex-direction: column;
3238
+ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
3239
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5); overflow: hidden;
3240
+ }
3241
+ .export-hd {
3242
+ padding: 14px 18px; font-size: 13px; font-weight: 700;
3243
+ border-bottom: 1px solid #21262d; color: #e6edf3;
3244
+ }
3245
+ .export-modes { display: flex; gap: 6px; padding: 12px 18px 0; }
3246
+ .export-mode {
3247
+ flex: 1; padding: 7px 10px; font-size: 11px; border-radius: 6px;
3248
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3249
+ }
3250
+ .export-mode:hover { background: #21262d; color: #e6edf3; }
3251
+ .export-mode.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3252
+ .export-body {
3253
+ padding: 14px 18px; overflow-y: auto; flex: 1; min-height: 160px;
3254
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3255
+ }
3256
+ .export-tag-hint { font-size: 12px; color: #484f58; margin-top: 12px; }
3257
+ .export-result-label {
3258
+ font-size: 10px; color: #484f58; text-transform: uppercase; letter-spacing: 0.6px;
3259
+ font-weight: 600; margin: 14px 0 6px;
3260
+ }
3261
+ .export-row-static { cursor: default; }
3262
+ .export-row-static:hover { background: transparent; }
3263
+ .export-all-desc { font-size: 12px; color: #8b949e; }
3264
+ .export-empty { font-size: 12px; color: #484f58; text-align: center; padding: 20px; }
3265
+ .export-list { display: flex; flex-direction: column; gap: 4px; }
3266
+ .export-row {
3267
+ display: flex; align-items: center; gap: 8px;
3268
+ padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: #c9d1d9;
3269
+ }
3270
+ .export-row:hover { background: #0d1117; }
3271
+ .export-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3272
+ .export-row-tag {
3273
+ font-size: 9px; color: #8b949e; background: #0d1117;
3274
+ border: 1px solid #30363d; border-radius: 10px; padding: 1px 7px; flex-shrink: 0;
3275
+ }
3276
+ .export-tags { display: flex; flex-wrap: wrap; gap: 6px; }
3277
+ .export-tag {
3278
+ padding: 4px 10px; font-size: 11px; border-radius: 12px;
3279
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3280
+ }
3281
+ .export-tag:hover { background: #21262d; color: #e6edf3; }
3282
+ .export-tag.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3283
+ .export-ft {
3284
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
3285
+ padding: 12px 18px; border-top: 1px solid #21262d;
3286
+ }
3287
+ .export-count { font-size: 11px; color: #8b949e; }
3288
+ .export-count b { color: #e6edf3; }
3289
+ .export-ft-actions { display: flex; gap: 8px; }
3290
+ .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
3291
+ .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
3292
+ .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
2898
3293
  `;
2899
3294
 
2900
3295
  // src/components/configuration/configuration.template.ts
@@ -2905,8 +3300,64 @@ var LANGS = [
2905
3300
  { value: "it", label: "Italiano" },
2906
3301
  { value: "de", label: "Deutsch" }
2907
3302
  ];
3303
+ function renderExportOverlay(state, t) {
3304
+ const { isExporting, exportMode, exportTests, exportSelectedIds, exportSelectedTags } = state;
3305
+ if (!isExporting) return "";
3306
+ const count = selectTestsForExport(exportTests, exportMode, {
3307
+ ids: exportSelectedIds,
3308
+ tags: exportSelectedTags
3309
+ }).length;
3310
+ const modeBtn = (mode, key) => `<button class="export-mode ${exportMode === mode ? "active" : ""}" data-export-mode="${mode}">${t(key)}</button>`;
3311
+ let body;
3312
+ if (exportTests.length === 0) {
3313
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_EMPTY")}</div>`;
3314
+ } else if (exportMode === "all") {
3315
+ body = `<div class="export-all-desc">${t("CONFIG.EXPORT_ALL_DESC")}</div>`;
3316
+ } else if (exportMode === "manual") {
3317
+ body = `<div class="export-list">${exportTests.map((test) => `
3318
+ <label class="export-row">
3319
+ <input type="checkbox" data-export-test="${test.id}" ${exportSelectedIds.has(test.id) ? "checked" : ""} />
3320
+ <span class="export-row-name">${escHtml(test.name)}</span>
3321
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3322
+ </label>`).join("")}</div>`;
3323
+ } else {
3324
+ const allTags = [...new Set(exportTests.flatMap((test) => test.tags ?? []))].sort();
3325
+ if (!allTags.length) {
3326
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_NO_TAGS")}</div>`;
3327
+ } else {
3328
+ 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>`;
3329
+ const matched = selectTestsForExport(exportTests, "tags", { tags: exportSelectedTags });
3330
+ 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>
3331
+ <div class="export-list">${matched.map((test) => `
3332
+ <div class="export-row export-row-static">
3333
+ <span class="export-row-name">${escHtml(test.name)}</span>
3334
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3335
+ </div>`).join("")}</div>`;
3336
+ body = chips + preview;
3337
+ }
3338
+ }
3339
+ return `
3340
+ <div class="export-overlay" id="export-overlay">
3341
+ <div class="export-modal">
3342
+ <div class="export-hd">${t("CONFIG.EXPORT_DIALOG_TITLE")}</div>
3343
+ ${exportTests.length ? `<div class="export-modes">
3344
+ ${modeBtn("all", "CONFIG.EXPORT_MODE_ALL")}
3345
+ ${modeBtn("manual", "CONFIG.EXPORT_MODE_MANUAL")}
3346
+ ${modeBtn("tags", "CONFIG.EXPORT_MODE_TAGS")}
3347
+ </div>` : ""}
3348
+ <div class="export-body">${body}</div>
3349
+ <div class="export-ft">
3350
+ <span class="export-count">${t("CONFIG.EXPORT_COUNT")} <b>${count}</b></span>
3351
+ <span class="export-ft-actions">
3352
+ <button id="btn-export-confirm" class="btn-export-confirm" ${count === 0 ? "disabled" : ""}>${t("CONFIG.EXPORT_CONFIRM")}</button>
3353
+ <button id="btn-export-cancel">${t("CONFIG.EXPORT_CANCEL")}</button>
3354
+ </span>
3355
+ </div>
3356
+ </div>
3357
+ </div>`;
3358
+ }
2908
3359
  function renderConfiguration(state, t) {
2909
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden } = state;
3360
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
2910
3361
  const langOptions = LANGS.map(
2911
3362
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
2912
3363
  ).join("");
@@ -2958,6 +3409,26 @@ function renderConfiguration(state, t) {
2958
3409
  </label>
2959
3410
  </div>
2960
3411
 
3412
+ <!-- Widget position (draggable widget) -->
3413
+ <div class="card">
3414
+ <div class="card-hd">${t("CONFIG.WIDGET_POSITION_SECTION")}</div>
3415
+ <div class="check-sub" style="margin-bottom:10px">${t("CONFIG.WIDGET_POSITION_HINT")}</div>
3416
+ <div class="btn-row">
3417
+ <button id="btn-reset-position">${t("CONFIG.WIDGET_POSITION_RESET_BTN")}</button>
3418
+ </div>
3419
+ </div>
3420
+
3421
+ <!-- Recording continuity (cross-app resume TTL) -->
3422
+ <div class="card">
3423
+ <div class="card-hd">${t("CONFIG.RESUME_TTL_SECTION")}</div>
3424
+ <div class="field-row">
3425
+ <span class="field-label">${t("CONFIG.RESUME_TTL_LABEL")}</span>
3426
+ <input type="number" id="resume-ttl-input" min="1" step="1" value="${resumeTtlMinutes}"
3427
+ style="width:80px;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none" />
3428
+ </div>
3429
+ <div class="check-sub" style="margin-top:8px">${t("CONFIG.RESUME_TTL_HINT")}</div>
3430
+ </div>
3431
+
2961
3432
  <!-- Selector Strategy -->
2962
3433
  <div class="card card-wide">
2963
3434
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -2977,13 +3448,13 @@ function renderConfiguration(state, t) {
2977
3448
  <div class="card card-wide">
2978
3449
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
2979
3450
  <div class="fs-layout">
2980
- <pre class="fs-tree">cypress/ <span style="color:#484f58">\u2190 selecciona</span>
3451
+ <pre class="fs-tree">cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
2981
3452
  \u2514\u2500\u2500 e2e/
2982
3453
  \u2514\u2500\u2500 *.cy.ts</pre>
2983
3454
  <div class="fs-right">
2984
3455
  <div class="fs-status">
2985
3456
  <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>`}
3457
+ ${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
3458
  </div>
2988
3459
  <div class="btn-row">
2989
3460
  <button id="btn-change-folder">
@@ -3008,7 +3479,7 @@ function renderConfiguration(state, t) {
3008
3479
  </div>
3009
3480
  </div>
3010
3481
 
3011
- </div>`;
3482
+ </div>${renderExportOverlay(state, t)}`;
3012
3483
  }
3013
3484
 
3014
3485
  // src/components/configuration/configuration.ts
@@ -3021,6 +3492,12 @@ var ConfigurationElement = class extends HTMLElement {
3021
3492
  selectorStrategy = "data-cy";
3022
3493
  smartSelectorEnabled = true;
3023
3494
  startHidden = false;
3495
+ resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
3496
+ isExporting = false;
3497
+ exportMode = "all";
3498
+ exportTests = [];
3499
+ exportSelectedIds = /* @__PURE__ */ new Set();
3500
+ exportSelectedTags = /* @__PURE__ */ new Set();
3024
3501
  filesystemGranted = false;
3025
3502
  cypressFolderName = null;
3026
3503
  constructor() {
@@ -3047,6 +3524,11 @@ var ConfigurationElement = class extends HTMLElement {
3047
3524
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
3048
3525
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
3049
3526
  this.startHidden = config?.["startHidden"] === "true";
3527
+ const ttlRaw = config?.[RESUME_TTL_CONFIG_KEY];
3528
+ if (ttlRaw !== void 0 && ttlRaw !== null) {
3529
+ const ttl = Number(ttlRaw);
3530
+ this.resumeTtlMinutes = Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_RESUME_TTL_MINUTES;
3531
+ }
3050
3532
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
3051
3533
  const handle = config?.["cypressDirectoryHandle"];
3052
3534
  this.cypressFolderName = handle?.name ?? null;
@@ -3070,6 +3552,16 @@ var ConfigurationElement = class extends HTMLElement {
3070
3552
  this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
3071
3553
  this.render();
3072
3554
  }
3555
+ async onResumeTtlChange(minutes) {
3556
+ const safe = Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes) : DEFAULT_RESUME_TTL_MINUTES;
3557
+ this.resumeTtlMinutes = safe;
3558
+ await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
3559
+ this.render();
3560
+ }
3561
+ onResetWidgetPosition() {
3562
+ this.dispatchEvent(new CustomEvent("resetwidgetposition", { bubbles: true, composed: true }));
3563
+ showToast(this.t("CONFIG.WIDGET_POSITION_RESET_DONE"));
3564
+ }
3073
3565
  async onSmartSelectorChange(enabled) {
3074
3566
  this.smartSelectorEnabled = enabled;
3075
3567
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -3099,8 +3591,7 @@ var ConfigurationElement = class extends HTMLElement {
3099
3591
  this.cypressFolderName = null;
3100
3592
  this.render();
3101
3593
  }
3102
- async exportAllData() {
3103
- const tests = await this.persistence.getAllTests();
3594
+ downloadTests(tests) {
3104
3595
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
3105
3596
  const url = URL.createObjectURL(blob);
3106
3597
  const a = document.createElement("a");
@@ -3109,6 +3600,53 @@ var ConfigurationElement = class extends HTMLElement {
3109
3600
  a.click();
3110
3601
  URL.revokeObjectURL(url);
3111
3602
  }
3603
+ /** Downloads every saved test (used by the "Todo" mode and as a direct API). */
3604
+ async exportAllData() {
3605
+ const tests = await this.persistence.getAllTests();
3606
+ this.downloadTests(tests);
3607
+ }
3608
+ /** Opens the export selection dialog, loading the current tests. */
3609
+ async openExportDialog() {
3610
+ this.exportTests = await this.persistence.getAllTests();
3611
+ this.exportMode = "all";
3612
+ this.exportSelectedIds = /* @__PURE__ */ new Set();
3613
+ this.exportSelectedTags = /* @__PURE__ */ new Set();
3614
+ this.isExporting = true;
3615
+ this.render();
3616
+ }
3617
+ cancelExport() {
3618
+ this.isExporting = false;
3619
+ this.exportSelectedIds.clear();
3620
+ this.exportSelectedTags.clear();
3621
+ this.render();
3622
+ }
3623
+ setExportMode(mode) {
3624
+ this.exportMode = mode;
3625
+ this.render();
3626
+ }
3627
+ toggleExportTest(id) {
3628
+ if (this.exportSelectedIds.has(id)) this.exportSelectedIds.delete(id);
3629
+ else this.exportSelectedIds.add(id);
3630
+ this.render();
3631
+ }
3632
+ toggleExportTag(tag) {
3633
+ if (this.exportSelectedTags.has(tag)) this.exportSelectedTags.delete(tag);
3634
+ else this.exportSelectedTags.add(tag);
3635
+ this.render();
3636
+ }
3637
+ /** Downloads the tests resolved by the current mode + selection. No-op if empty. */
3638
+ confirmExport() {
3639
+ const subset = selectTestsForExport(this.exportTests, this.exportMode, {
3640
+ ids: this.exportSelectedIds,
3641
+ tags: this.exportSelectedTags
3642
+ });
3643
+ if (!subset.length) return;
3644
+ this.downloadTests(subset);
3645
+ this.isExporting = false;
3646
+ this.exportSelectedIds.clear();
3647
+ this.exportSelectedTags.clear();
3648
+ this.render();
3649
+ }
3112
3650
  async importAllData(file) {
3113
3651
  const text = await file.text();
3114
3652
  let data;
@@ -3120,7 +3658,6 @@ var ConfigurationElement = class extends HTMLElement {
3120
3658
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
3121
3659
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
3122
3660
  }
3123
- await this.persistence.clearAllData();
3124
3661
  await this.persistence.ingestFileData(data.tests, data.interceptors);
3125
3662
  }
3126
3663
  render() {
@@ -3131,7 +3668,13 @@ var ConfigurationElement = class extends HTMLElement {
3131
3668
  filesystemGranted: this.filesystemGranted,
3132
3669
  cypressFolderName: this.cypressFolderName,
3133
3670
  smartSelectorEnabled: this.smartSelectorEnabled,
3134
- startHidden: this.startHidden
3671
+ startHidden: this.startHidden,
3672
+ resumeTtlMinutes: this.resumeTtlMinutes,
3673
+ isExporting: this.isExporting,
3674
+ exportMode: this.exportMode,
3675
+ exportTests: this.exportTests,
3676
+ exportSelectedIds: this.exportSelectedIds,
3677
+ exportSelectedTags: this.exportSelectedTags
3135
3678
  }, this.t.bind(this))}`;
3136
3679
  ;
3137
3680
  this.shadow.getElementById("lang-select").addEventListener(
@@ -3150,13 +3693,29 @@ var ConfigurationElement = class extends HTMLElement {
3150
3693
  "change",
3151
3694
  (e) => this.onStartHiddenChange(e.target.checked)
3152
3695
  );
3696
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
3697
+ "change",
3698
+ (e) => this.onResumeTtlChange(Number(e.target.value))
3699
+ );
3700
+ this.shadow.getElementById("btn-reset-position")?.addEventListener("click", () => this.onResetWidgetPosition());
3153
3701
  this.shadow.getElementById("selector-strategy").addEventListener(
3154
3702
  "change",
3155
3703
  (e) => this.onSelectorStrategyChange(e.target.value)
3156
3704
  );
3157
3705
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
3158
3706
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
3159
- this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.exportAllData());
3707
+ this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.openExportDialog());
3708
+ this.shadow.getElementById("btn-export-confirm")?.addEventListener("click", () => this.confirmExport());
3709
+ this.shadow.getElementById("btn-export-cancel")?.addEventListener("click", () => this.cancelExport());
3710
+ this.shadow.querySelectorAll("[data-export-mode]").forEach(
3711
+ (el) => el.addEventListener("click", () => this.setExportMode(el.dataset["exportMode"]))
3712
+ );
3713
+ this.shadow.querySelectorAll("[data-export-test]").forEach(
3714
+ (el) => el.addEventListener("change", () => this.toggleExportTest(Number(el.dataset["exportTest"])))
3715
+ );
3716
+ this.shadow.querySelectorAll("[data-export-tag]").forEach(
3717
+ (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
3718
+ );
3160
3719
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
3161
3720
  const file = e.target.files?.[0];
3162
3721
  if (!file) return;
@@ -3266,18 +3825,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3266
3825
  }
3267
3826
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3268
3827
  .sidebar-toolbar {
3269
- display: flex; gap: 4px; padding: 6px 6px 4px;
3828
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3270
3829
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3271
3830
  }
3272
3831
  .btn-toolbar {
3273
- flex: 1; padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3832
+ flex: 1 1 calc(50% - 2px); padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3274
3833
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3275
3834
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3276
3835
  text-overflow: ellipsis;
3277
3836
  }
3837
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3278
3838
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3279
3839
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3280
3840
  .btn-toolbar.btn-new:hover { background: #238636; color: #fff; }
3841
+ .btn-toolbar.btn-new-folder { color: #d29922; border-color: #9e6a03; }
3842
+ .btn-toolbar.btn-new-folder:hover { background: #9e6a03; color: #fff; }
3281
3843
  .new-file-form {
3282
3844
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3283
3845
  }
@@ -3310,7 +3872,7 @@ function renderNoPermission(needsReauth, t) {
3310
3872
  </div>`;
3311
3873
  }
3312
3874
  function renderAdvancedEditor(state, t) {
3313
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
3875
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3314
3876
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3315
3877
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3316
3878
  <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 +3899,25 @@ function renderAdvancedEditor(state, t) {
3337
3899
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3338
3900
  </div>
3339
3901
  </div>` : "";
3902
+ const newFolderForm = isCreatingFolder ? `<div class="new-file-form">
3903
+ <input id="input-new-folder" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FOLDER_PLACEHOLDER"))}" autocomplete="off" />
3904
+ <div class="new-file-actions">
3905
+ <button id="btn-new-folder-confirm" class="btn-confirm">${t("ADVANCED_EDITOR.NEW_FILE_CONFIRM")}</button>
3906
+ <button id="btn-new-folder-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3907
+ </div>
3908
+ </div>` : "";
3340
3909
  const toolbar = `
3341
3910
  <div class="sidebar-toolbar">
3342
3911
  <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>
3912
+ <button id="btn-new-folder" class="btn-toolbar btn-new-folder">${t("ADVANCED_EDITOR.NEW_FOLDER_BTN")}</button>
3913
+ <button id="btn-refresh" class="btn-toolbar btn-full">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
3344
3914
  </div>`;
3345
3915
  return `
3346
3916
  <div class="layout">
3347
3917
  <div class="sidebar" style="width:${sidebarWidth}px">
3348
3918
  ${toolbar}
3349
3919
  ${newFileForm}
3920
+ ${newFolderForm}
3350
3921
  <div class="tree-scroll">${treeHtml}</div>
3351
3922
  </div>
3352
3923
  <div id="resize-handle" class="resize-handle"></div>
@@ -3417,6 +3988,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3417
3988
  previewFileName = null;
3418
3989
  previewFileContent = null;
3419
3990
  isCreatingFile = false;
3991
+ isCreatingFolder = false;
3420
3992
  collapsedDirs = /* @__PURE__ */ new Set();
3421
3993
  sidebarWidth = 220;
3422
3994
  hasPermission = false;
@@ -3479,7 +4051,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3479
4051
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3480
4052
  if (!name) return;
3481
4053
  const fileName = `${name}.cy.ts`;
3482
- const template = `describe('${name}', () => {
4054
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3483
4055
 
3484
4056
  it('should ', () => {
3485
4057
 
@@ -3497,6 +4069,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3497
4069
  this.isCreatingFile = false;
3498
4070
  await this.getFoldersData();
3499
4071
  }
4072
+ async createNewFolder(rawName) {
4073
+ if (!this._e2eHandle) return;
4074
+ const name = rawName.trim().replace(/[/\\]/g, "");
4075
+ if (!name) return;
4076
+ try {
4077
+ await this._e2eHandle.getDirectoryHandle(name, { create: true });
4078
+ } catch {
4079
+ }
4080
+ this.isCreatingFolder = false;
4081
+ await this.getFoldersData();
4082
+ }
3500
4083
  async refreshTree() {
3501
4084
  await this.getFoldersData();
3502
4085
  }
@@ -3554,7 +4137,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3554
4137
  fileName: this.selectedFile?.name ?? "",
3555
4138
  testId: this.testId,
3556
4139
  itBlock: this.testItBlock,
3557
- interceptorsBlock: this.interceptorsBlock
4140
+ interceptorsBlock: this.interceptorsBlock,
4141
+ notes: this.testNotes
3558
4142
  },
3559
4143
  bubbles: true,
3560
4144
  composed: true
@@ -3631,6 +4215,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3631
4215
  testNotes: this.testNotes,
3632
4216
  saveButtonEnabled: this.saveButtonEnabled,
3633
4217
  isCreatingFile: this.isCreatingFile,
4218
+ isCreatingFolder: this.isCreatingFolder,
3634
4219
  collapsedDirs: this.collapsedDirs,
3635
4220
  sidebarWidth: this.sidebarWidth
3636
4221
  }, this.t.bind(this))}`;
@@ -3654,9 +4239,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3654
4239
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
3655
4240
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
3656
4241
  this.isCreatingFile = !this.isCreatingFile;
4242
+ this.isCreatingFolder = false;
3657
4243
  this.render();
3658
4244
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
3659
4245
  });
4246
+ this.shadow.getElementById("btn-new-folder")?.addEventListener("click", () => {
4247
+ this.isCreatingFolder = !this.isCreatingFolder;
4248
+ this.isCreatingFile = false;
4249
+ this.render();
4250
+ if (this.isCreatingFolder) this.shadow.getElementById("input-new-folder")?.focus();
4251
+ });
3660
4252
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
3661
4253
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
3662
4254
  const input2 = this.shadow.getElementById("input-new-file");
@@ -3675,6 +4267,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3675
4267
  this.render();
3676
4268
  }
3677
4269
  });
4270
+ this.shadow.getElementById("btn-new-folder-confirm")?.addEventListener("click", () => {
4271
+ const folderInput2 = this.shadow.getElementById("input-new-folder");
4272
+ this.createNewFolder(folderInput2?.value ?? "");
4273
+ });
4274
+ this.shadow.getElementById("btn-new-folder-cancel")?.addEventListener("click", () => {
4275
+ this.isCreatingFolder = false;
4276
+ this.render();
4277
+ });
4278
+ const folderInput = this.shadow.getElementById("input-new-folder");
4279
+ folderInput?.addEventListener("keydown", (e) => {
4280
+ if (e.key === "Enter") {
4281
+ this.createNewFolder(folderInput.value);
4282
+ } else if (e.key === "Escape") {
4283
+ this.isCreatingFolder = false;
4284
+ this.render();
4285
+ }
4286
+ });
3678
4287
  }
3679
4288
  };
3680
4289
  if (!customElements.get("advanced-test-editor")) {
@@ -3760,7 +4369,30 @@ var FILE_PREVIEW_STYLES = `
3760
4369
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
3761
4370
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
3762
4371
  .btn-launch:hover { background: rgba(227,179,65,0.1); }
4372
+ .btn-launch:disabled { opacity: 0.45; cursor: not-allowed; }
4373
+ .btn-launch:disabled:hover { background: transparent; }
4374
+ .launch-hint { font-size: 10px; color: #8b6d3b; align-self: center; }
4375
+ .btn-insert { background: transparent; border-color: #a371f7; color: #a371f7; }
4376
+ .btn-insert:hover { background: rgba(163,113,247,0.12); color: #c8a8ff; }
3763
4377
  .btn-diff-active { background: rgba(47,129,247,0.15); border-color: rgba(47,129,247,0.4); color: #2f81f7; }
4378
+
4379
+ /* \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 */
4380
+ .run-result {
4381
+ border-top: 1px solid #21262d; padding: 8px 12px; background: #0d1117;
4382
+ max-height: 220px; overflow-y: auto; flex-shrink: 0;
4383
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
4384
+ }
4385
+ .run-status { font-size: 12px; font-weight: 600; }
4386
+ .run-passed .run-status { color: #3fb950; }
4387
+ .run-failed .run-status { color: #f85149; }
4388
+ .run-running .run-status { color: #e3b341; }
4389
+ .run-error .run-status { color: #f85149; }
4390
+ .run-output {
4391
+ margin: 6px 0 0; padding: 8px 10px; background: #161b22; border: 1px solid #21262d;
4392
+ border-radius: 5px; font-size: 10px; color: #c9d1d9; line-height: 1.5;
4393
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
4394
+ white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto;
4395
+ }
3764
4396
  `;
3765
4397
 
3766
4398
  // src/components/file-preview/file-preview.template.ts
@@ -3817,8 +4449,14 @@ function buildDiffHtml(original, current, t) {
3817
4449
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
3818
4450
  }).join("");
3819
4451
  }
4452
+ var RUN_STATUS_KEY = {
4453
+ running: "FILE_PREVIEW.LAUNCH_RUNNING",
4454
+ passed: "FILE_PREVIEW.LAUNCH_PASSED",
4455
+ failed: "FILE_PREVIEW.LAUNCH_FAILED",
4456
+ error: "FILE_PREVIEW.LAUNCH_NO_RUNNER"
4457
+ };
3820
4458
  function renderFilePreview(state, t) {
3821
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4459
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
3822
4460
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
3823
4461
  <div class="blocks-panel">
3824
4462
  ${itBlock ? `
@@ -3839,6 +4477,13 @@ function renderFilePreview(state, t) {
3839
4477
  </div>` : ""}
3840
4478
  </div>` : "";
3841
4479
  const hasChanges = originalContent !== null && originalContent !== (fileContent ?? "");
4480
+ const hasBlocks = !!(itBlock || interceptorsBlock);
4481
+ 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>
4482
+ <span class="launch-hint">${t("FILE_PREVIEW.LAUNCH_LOCAL_ONLY")}</span>`;
4483
+ const runResultHtml = runState !== "idle" ? `<div class="run-result run-${runState}">
4484
+ <span class="run-status">${t(RUN_STATUS_KEY[runState])}</span>
4485
+ ${runOutput ? `<pre class="run-output">${escHtml(runOutput.slice(0, 8e3))}</pre>` : ""}
4486
+ </div>` : "";
3842
4487
  return `
3843
4488
  <div class="container">
3844
4489
  <div class="header">
@@ -3849,9 +4494,11 @@ function renderFilePreview(state, t) {
3849
4494
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
3850
4495
  ${blocksPanelHtml}
3851
4496
  </div>
4497
+ ${runResultHtml}
3852
4498
  <div class="footer">
3853
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4499
+ ${launchBtnHtml}
3854
4500
  ${hasChanges ? `<button id="btn-diff" class="${showDiff ? "btn-diff-active" : ""}">${t("FILE_PREVIEW.DIFF_BTN")}</button>` : ""}
4501
+ ${hasBlocks && !showDiff ? `<button id="btn-insert" class="btn-insert" title="${t("FILE_PREVIEW.INSERT_TITLE")}">${t("FILE_PREVIEW.INSERT_BTN")}</button>` : ""}
3855
4502
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
3856
4503
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
3857
4504
  </div>
@@ -3867,11 +4514,18 @@ var FilePreviewElement = class extends HTMLElement {
3867
4514
  translation = translationService;
3868
4515
  itBlock = "";
3869
4516
  interceptorsBlock = "";
4517
+ notes = "";
3870
4518
  commands = [];
3871
4519
  interceptors = [];
4520
+ /** Endpoint of the local Cypress runner. Configurable; defaults to the bundled runner's port. */
4521
+ runnerUrl = "http://localhost:8123/run-test";
4522
+ /** Whether the app is served locally — gates the launch button. Overridable for tests. */
4523
+ isLocal = isLocalHost(window.location.hostname);
3872
4524
  _fileContent = null;
3873
4525
  _originalContent = null;
3874
4526
  _showDiff = false;
4527
+ _runState = "idle";
4528
+ _runOutput = "";
3875
4529
  constructor() {
3876
4530
  super();
3877
4531
  this.shadow = this.attachShadow({ mode: "open" });
@@ -3899,15 +4553,34 @@ var FilePreviewElement = class extends HTMLElement {
3899
4553
  onClose() {
3900
4554
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
3901
4555
  }
4556
+ /**
4557
+ * Runs the current spec headless via the local runner and shows the result.
4558
+ * No-op off localhost. Never throws — connection failures surface as an
4559
+ * "error" state + toast instead of an unhandled rejection.
4560
+ */
3902
4561
  async launchTest(specPath) {
3903
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
4562
+ if (!this.isLocal) return;
4563
+ const path = specPath ?? this.fileName ?? "";
3904
4564
  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();
4565
+ this._runState = "running";
4566
+ this._runOutput = "";
4567
+ this.render();
4568
+ try {
4569
+ const response = await fetch(this.runnerUrl, {
4570
+ method: "POST",
4571
+ headers: { "Content-Type": "application/json" },
4572
+ body: JSON.stringify({ specPath: path })
4573
+ });
4574
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4575
+ const result = await response.json();
4576
+ this._runOutput = result.output ?? "";
4577
+ this._runState = result.success ? "passed" : "failed";
4578
+ } catch {
4579
+ this._runState = "error";
4580
+ this._runOutput = "";
4581
+ showToast(this.t("FILE_PREVIEW.LAUNCH_NO_RUNNER"), false);
4582
+ }
4583
+ this.render();
3911
4584
  }
3912
4585
  copyToClipboard(text) {
3913
4586
  navigator.clipboard?.writeText(text);
@@ -3916,6 +4589,32 @@ var FilePreviewElement = class extends HTMLElement {
3916
4589
  this._showDiff = !this._showDiff;
3917
4590
  this.render();
3918
4591
  }
4592
+ /**
4593
+ * Merges the it() and beforeEach() blocks into the current editor content
4594
+ * using the same logic as the automatic "Insert into file" action, so the
4595
+ * user does not have to copy/paste manually. Leaves the content untouched
4596
+ * (and warns) when the file has no valid describe() block to insert into.
4597
+ */
4598
+ insertBlocks() {
4599
+ const base = this.textarea?.value ?? this._fileContent ?? "";
4600
+ let content = base;
4601
+ if (this.interceptorsBlock) {
4602
+ const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
4603
+ if (merged) content = merged;
4604
+ }
4605
+ if (this.itBlock) {
4606
+ const comment = this.notes ? advancedTestTransformationService.buildBlockComment(this.notes) + "\n" : "";
4607
+ const merged = advancedTestTransformationService.insertItBlock(content, comment + this.itBlock);
4608
+ if (merged) content = merged;
4609
+ }
4610
+ if (content === base) {
4611
+ showToast(this.t("FILE_PREVIEW.INSERT_ERROR"), false);
4612
+ return;
4613
+ }
4614
+ this._fileContent = content;
4615
+ this.render();
4616
+ showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
4617
+ }
3919
4618
  render() {
3920
4619
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
3921
4620
  fileName: this.fileName,
@@ -3925,7 +4624,10 @@ var FilePreviewElement = class extends HTMLElement {
3925
4624
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
3926
4625
  itBlock: this.itBlock,
3927
4626
  interceptorsBlock: this.interceptorsBlock,
3928
- closeLabel: this.closeLabel
4627
+ closeLabel: this.closeLabel,
4628
+ isLocal: this.isLocal,
4629
+ runState: this._runState,
4630
+ runOutput: this._runOutput
3929
4631
  }, this.t.bind(this))}`;
3930
4632
  this.textarea = this.shadow.getElementById("editor");
3931
4633
  if (this.textarea) {
@@ -3941,6 +4643,7 @@ var FilePreviewElement = class extends HTMLElement {
3941
4643
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
3942
4644
  });
3943
4645
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
4646
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
3944
4647
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
3945
4648
  this.copyToClipboard(this.itBlock);
3946
4649
  });
@@ -3957,15 +4660,37 @@ if (!customElements.get("file-preview")) {
3957
4660
  import Swal from "sweetalert2";
3958
4661
 
3959
4662
  // src/components/lib-e2e-recorder/lib-e2e-recorder.styles.ts
4663
+ var DIRECTIONS = ["up-left", "up-right", "down-left", "down-right"];
4664
+ function directionBlocks() {
4665
+ return DIRECTIONS.map((dir) => {
4666
+ const sx = dir.endsWith("left") ? -1 : 1;
4667
+ const sy = dir.startsWith("up") ? -1 : 1;
4668
+ const tv = dir.startsWith("up") ? "bottom" : "top";
4669
+ const th = dir.endsWith("left") ? "right" : "left";
4670
+ const ov = tv === "bottom" ? "top" : "bottom";
4671
+ const oh = th === "right" ? "left" : "right";
4672
+ const sel = `.widget[data-expand="${dir}"]`;
4673
+ const labelGeneral = `${sel} .btn-action::after { ${oh}: calc(100% + 9px); ${th}: auto; top: 50%; bottom: auto; transform: translateY(-50%); }`;
4674
+ const labelEnds = `${sel} .btn-action[data-n="1"]::after, ${sel} .btn-action[data-n="4"]::after { ${ov}: calc(100% + 9px); ${tv}: auto; left: 50%; right: auto; transform: translateX(-50%); }`;
4675
+ return `
4676
+ ${sel} { --sx: ${sx}; --sy: ${sy}; }
4677
+ ${sel} .btn-toggle { ${tv}: 24px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
4678
+ ${sel} .btn-pause { ${tv}: 78px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
4679
+ ${sel} .btn-action { ${tv}: 28px; ${th}: 28px; ${ov}: auto; ${oh}: auto; }
4680
+ ${labelGeneral}
4681
+ ${labelEnds}
4682
+ `;
4683
+ }).join("\n");
4684
+ }
3960
4685
  function getRecorderStyles(rec, paused) {
3961
4686
  return `
3962
4687
  :host { all: initial; }
3963
4688
  *, *::before, *::after { box-sizing: border-box; }
3964
4689
 
3965
4690
  /*
3966
- * Invisible 190\xD7190 hit area anchored at bottom-right.
3967
- * Keeps :hover alive while the cursor travels from the
3968
- * toggle to any of the radial action buttons.
4691
+ * Invisible 190\xD7190 hit area. Position (left/top) is set from JS so the
4692
+ * widget can be dragged; --sx/--sy + data-expand orient the radial menu so
4693
+ * it always expands toward the viewport interior. Defaults to bottom-right.
3969
4694
  */
3970
4695
  .widget {
3971
4696
  position: fixed;
@@ -3974,6 +4699,8 @@ function getRecorderStyles(rec, paused) {
3974
4699
  width: 190px;
3975
4700
  height: 190px;
3976
4701
  z-index: 2147483647;
4702
+ --sx: -1;
4703
+ --sy: -1;
3977
4704
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
3978
4705
  }
3979
4706
 
@@ -3986,7 +4713,8 @@ function getRecorderStyles(rec, paused) {
3986
4713
  height: 44px;
3987
4714
  border-radius: 50%;
3988
4715
  border: none;
3989
- cursor: pointer;
4716
+ cursor: grab;
4717
+ touch-action: none;
3990
4718
  font-size: 19px;
3991
4719
  background: ${rec ? "linear-gradient(135deg,#f85149 0%,#da3633 100%)" : "linear-gradient(135deg,#2f81f7 0%,#1f6feb 100%)"};
3992
4720
  color: #fff;
@@ -3999,7 +4727,7 @@ function getRecorderStyles(rec, paused) {
3999
4727
  ${rec ? "animation: toggle-pulse 2s ease-in-out infinite;" : ""}
4000
4728
  }
4001
4729
  .btn-toggle:hover { transform: scale(1.1); }
4002
- .btn-toggle:active { transform: scale(0.93); }
4730
+ .btn-toggle:active { transform: scale(0.93); cursor: grabbing; }
4003
4731
 
4004
4732
  @keyframes toggle-pulse {
4005
4733
  0%,100% { box-shadow: 0 4px 20px rgba(248,81,73,.55),0 0 0 4px rgba(248,81,73,.13); }
@@ -4030,11 +4758,6 @@ function getRecorderStyles(rec, paused) {
4030
4758
  .btn-pause:active { transform: scale(0.93); }
4031
4759
 
4032
4760
  /* \u2500\u2500 Action buttons \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 */
4033
- /*
4034
- * All three start centered on the toggle button
4035
- * (toggle center = bottom:46px right:46px from widget edge;
4036
- * button half = 18px \u2192 bottom:28px right:28px).
4037
- */
4038
4761
  .btn-action {
4039
4762
  position: absolute;
4040
4763
  bottom: 28px;
@@ -4056,12 +4779,11 @@ function getRecorderStyles(rec, paused) {
4056
4779
  opacity: 0;
4057
4780
  transform: scale(0.35);
4058
4781
  pointer-events: none;
4059
- /* Collapse: fast, no spring */
4060
4782
  transition: opacity .15s, transform .18s ease-in,
4061
4783
  background .15s, color .12s, box-shadow .15s;
4062
4784
  }
4063
4785
 
4064
- /* Label to the left of each button */
4786
+ /* Label chip */
4065
4787
  .btn-action::after {
4066
4788
  content: attr(data-label);
4067
4789
  position: absolute;
@@ -4081,14 +4803,6 @@ function getRecorderStyles(rec, paused) {
4081
4803
  border: 1px solid rgba(48,54,61,.8);
4082
4804
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
4083
4805
  }
4084
- /* Button 1 (top) \u2014 label above instead of left to avoid overlap with btn 2 */
4085
- .btn-action[data-n="1"]::after {
4086
- right: auto;
4087
- left: 50%;
4088
- top: auto;
4089
- bottom: calc(100% + 9px);
4090
- transform: translateX(-50%);
4091
- }
4092
4806
  .btn-action:hover::after { opacity: 1; }
4093
4807
  .btn-action:hover { background: #21262d; color: #e6edf3; }
4094
4808
  .btn-action:active { background: #30363d !important; }
@@ -4101,31 +4815,26 @@ function getRecorderStyles(rec, paused) {
4101
4815
  background .15s, color .12s, box-shadow .15s;
4102
4816
  }
4103
4817
 
4104
- /* Arc positions \u2014 4 buttons, 30\xB0 spacing, radius 90px */
4105
- .widget:hover .btn-action[data-n="1"] { /* 0\xB0 \u2014 straight up */
4106
- transform: translateY(-90px) scale(1);
4818
+ /* Arc positions \u2014 signs come from --sx/--sy (data-expand) */
4819
+ .widget:hover .btn-action[data-n="1"] { /* vertical extreme */
4820
+ transform: translateY(calc(var(--sy) * 90px)) scale(1);
4107
4821
  transition-delay: .03s;
4108
4822
  }
4109
- .widget:hover .btn-action[data-n="2"] { /* 30\xB0 \u2014 upper-left */
4110
- transform: translate(-45px,-78px) scale(1);
4823
+ .widget:hover .btn-action[data-n="2"] { /* diagonal near-vertical */
4824
+ transform: translate(calc(var(--sx) * 45px), calc(var(--sy) * 78px)) scale(1);
4111
4825
  transition-delay: .07s;
4112
4826
  }
4113
- .widget:hover .btn-action[data-n="3"] { /* 60\xB0 \u2014 left-upper */
4114
- transform: translate(-78px,-45px) scale(1);
4827
+ .widget:hover .btn-action[data-n="3"] { /* diagonal near-horizontal */
4828
+ transform: translate(calc(var(--sx) * 78px), calc(var(--sy) * 45px)) scale(1);
4115
4829
  transition-delay: .11s;
4116
4830
  }
4117
- .widget:hover .btn-action[data-n="4"] { /* 90\xB0 \u2014 straight left */
4118
- transform: translateX(-90px) scale(1);
4831
+ .widget:hover .btn-action[data-n="4"] { /* horizontal extreme */
4832
+ transform: translateX(calc(var(--sx) * 90px)) scale(1);
4119
4833
  transition-delay: .15s;
4120
4834
  }
4121
- /* Button 4 (pure left) \u2014 label above to avoid going off-screen */
4122
- .btn-action[data-n="4"]::after {
4123
- right: auto;
4124
- left: 50%;
4125
- top: auto;
4126
- bottom: calc(100% + 9px);
4127
- transform: translateX(-50%);
4128
- }
4835
+
4836
+ /* Per-direction anchors + label sides */
4837
+ ${directionBlocks()}
4129
4838
 
4130
4839
  /* \u2500\u2500 REC / PAUSED badge \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 */
4131
4840
  .rec-badge {
@@ -4188,7 +4897,18 @@ var LibE2eRecorderElement = class extends HTMLElement {
4188
4897
  interceptorsUnsub;
4189
4898
  pauseUnsub;
4190
4899
  selectorNotFoundUnsub;
4900
+ langUnsub;
4901
+ sessionUnsub;
4902
+ sessionSaveTimer;
4191
4903
  controlFirstTimeData = true;
4904
+ // ── draggable widget (spec 007) ──
4905
+ togglePos = null;
4906
+ expandDir = "up-left";
4907
+ dragState;
4908
+ suppressNextToggleClick = false;
4909
+ widgetPointerMove;
4910
+ widgetPointerUp;
4911
+ widgetResize;
4192
4912
  _previsualizerRef = null;
4193
4913
  httpMonitor;
4194
4914
  smartSelectorEnabled = true;
@@ -4231,17 +4951,31 @@ var LibE2eRecorderElement = class extends HTMLElement {
4231
4951
  this.initSubscriptions();
4232
4952
  this.render();
4233
4953
  this.initVisibility();
4954
+ this.initSessionContinuity();
4955
+ this.initWidgetPosition();
4234
4956
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4235
4957
  window.addEventListener("keydown", this.keydownHandler);
4958
+ this.widgetPointerMove = (e) => this.onWidgetPointerMove(e);
4959
+ this.widgetPointerUp = () => this.onWidgetPointerUp();
4960
+ this.widgetResize = () => this.applyWidgetPosition();
4961
+ window.addEventListener("pointermove", this.widgetPointerMove);
4962
+ window.addEventListener("pointerup", this.widgetPointerUp);
4963
+ window.addEventListener("resize", this.widgetResize);
4236
4964
  }
4237
4965
  disconnectedCallback() {
4238
4966
  if (this._isDisabled) return;
4239
4967
  window.removeEventListener("keydown", this.keydownHandler);
4968
+ this.flushActiveSessionOnDisconnect();
4240
4969
  this.recordingUnsub?.();
4241
4970
  this.commandsUnsub?.();
4242
4971
  this.interceptorsUnsub?.();
4243
4972
  this.pauseUnsub?.();
4244
4973
  this.selectorNotFoundUnsub?.();
4974
+ this.langUnsub?.();
4975
+ this.sessionUnsub?.();
4976
+ if (this.widgetPointerMove) window.removeEventListener("pointermove", this.widgetPointerMove);
4977
+ if (this.widgetPointerUp) window.removeEventListener("pointerup", this.widgetPointerUp);
4978
+ if (this.widgetResize) window.removeEventListener("resize", this.widgetResize);
4245
4979
  this.httpMonitor?.uninstall();
4246
4980
  this.recording.destroy();
4247
4981
  }
@@ -4264,11 +4998,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4264
4998
  this.isRecording = val;
4265
4999
  if (!val && !this.controlFirstTimeData) {
4266
5000
  this.saveRecordingHistory();
5001
+ this.clearSessionPersistence();
4267
5002
  this.showSaveTestDialog();
4268
5003
  }
4269
5004
  this.controlFirstTimeData = false;
4270
5005
  this.render();
4271
5006
  });
5007
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4272
5008
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4273
5009
  this.cypressCommands = cmds;
4274
5010
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4284,9 +5020,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
4284
5020
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4285
5021
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4286
5022
  });
5023
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4287
5024
  }
4288
5025
  showSelectorPicker(target) {
4289
- import("./selector-picker-VJOLGZ5H.js").then(() => {
5026
+ import("./selector-picker-NKYFFFGK.js").then(() => {
4290
5027
  const existing = document.querySelector("selector-picker");
4291
5028
  if (existing) existing.remove();
4292
5029
  const picker = document.createElement("selector-picker");
@@ -4312,6 +5049,176 @@ var LibE2eRecorderElement = class extends HTMLElement {
4312
5049
  if (strategy) this.recording.selectorStrategy = strategy;
4313
5050
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4314
5051
  }
5052
+ // ── cross-app session continuity (spec 006) ───────────────────────────────
5053
+ /**
5054
+ * On mount, detect a persisted live recording session and either resume it
5055
+ * silently (recent) or prompt continue/discard (stale). No-op when there is
5056
+ * no actively-recording session.
5057
+ */
5058
+ async initSessionContinuity() {
5059
+ if (this._isDisabled) return;
5060
+ const session = await this.persistence.getActiveSession();
5061
+ if (!session || !session.isRecording) return;
5062
+ const ttlMin = await this.getResumeTtlMinutes();
5063
+ const ageMs = Date.now() - session.updatedAt;
5064
+ if (ageMs <= ttlMin * 6e4) {
5065
+ this.resumeSessionState(session);
5066
+ } else {
5067
+ this.promptResumeOrDiscard(session);
5068
+ }
5069
+ }
5070
+ async getResumeTtlMinutes() {
5071
+ const config = await this.persistence.getConfig(RESUME_TTL_CONFIG_KEY);
5072
+ const raw = config?.[RESUME_TTL_CONFIG_KEY];
5073
+ const n = typeof raw === "number" ? raw : Number(raw);
5074
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_RESUME_TTL_MINUTES;
5075
+ }
5076
+ /** Rehydrates the recorder from a persisted session (no bootstrap re-emitted). */
5077
+ resumeSessionState(session) {
5078
+ this.recording.restoreSession(session);
5079
+ this.cypressCommands = session.commands;
5080
+ this.interceptors = session.interceptors;
5081
+ this.controlFirstTimeData = false;
5082
+ this.render();
5083
+ }
5084
+ async promptResumeOrDiscard(session) {
5085
+ const t = this.translation.translate.bind(this.translation);
5086
+ const result = await Swal.fire({
5087
+ title: t("RECORDER.SESSION_RESUME_TITLE"),
5088
+ 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>`,
5089
+ showCancelButton: true,
5090
+ confirmButtonText: t("RECORDER.SESSION_CONTINUE_BTN"),
5091
+ cancelButtonText: t("RECORDER.SESSION_DISCARD_BTN"),
5092
+ color: "#e6edf3"
5093
+ });
5094
+ if (result && result.isConfirmed) {
5095
+ this.resumeSessionState(session);
5096
+ } else {
5097
+ this.discardSession();
5098
+ }
5099
+ }
5100
+ /** Debounced persistence of the live session; clears it the moment recording stops. */
5101
+ persistActiveSession(state) {
5102
+ if (this._isDisabled) return;
5103
+ if (!state.isRecording) {
5104
+ this.clearSessionPersistence();
5105
+ return;
5106
+ }
5107
+ this.writeSessionBreadcrumb(state);
5108
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5109
+ this.sessionSaveTimer = setTimeout(() => {
5110
+ const snap = this.recording.getSessionSnapshot();
5111
+ if (snap.isRecording) this.persistence.saveActiveSession(snap).catch(() => {
5112
+ });
5113
+ }, 300);
5114
+ }
5115
+ writeSessionBreadcrumb(state) {
5116
+ try {
5117
+ localStorage.setItem(ACTIVE_SESSION_BREADCRUMB_KEY, JSON.stringify({
5118
+ sessionId: state.sessionId,
5119
+ isRecording: state.isRecording,
5120
+ updatedAt: state.updatedAt
5121
+ }));
5122
+ } catch {
5123
+ }
5124
+ }
5125
+ flushActiveSessionOnDisconnect() {
5126
+ const snap = this.recording.getSessionSnapshot();
5127
+ if (!snap.isRecording) return;
5128
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5129
+ this.writeSessionBreadcrumb(snap);
5130
+ this.persistence.saveActiveSession(snap).catch(() => {
5131
+ });
5132
+ }
5133
+ clearSessionPersistence() {
5134
+ if (this.sessionSaveTimer) {
5135
+ clearTimeout(this.sessionSaveTimer);
5136
+ this.sessionSaveTimer = void 0;
5137
+ }
5138
+ try {
5139
+ localStorage.removeItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5140
+ } catch {
5141
+ }
5142
+ this.persistence.clearActiveSession().catch(() => {
5143
+ });
5144
+ }
5145
+ /** True when a synchronous breadcrumb marks an active recording session. */
5146
+ hasActiveSession() {
5147
+ try {
5148
+ const raw = localStorage.getItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5149
+ if (!raw) return false;
5150
+ return !!JSON.parse(raw).isRecording;
5151
+ } catch {
5152
+ return false;
5153
+ }
5154
+ }
5155
+ /** Programmatically resume the persisted session, if any. */
5156
+ resumeSession() {
5157
+ this.persistence.getActiveSession().then((s) => {
5158
+ if (s) this.resumeSessionState(s);
5159
+ });
5160
+ }
5161
+ /** Discard the persisted live session (breadcrumb + DB record). */
5162
+ discardSession() {
5163
+ this.clearSessionPersistence();
5164
+ }
5165
+ // ── draggable widget (spec 007) ────────────────────────────────────────────
5166
+ /** Loads a previously saved widget position and applies it. */
5167
+ async initWidgetPosition() {
5168
+ const config = await this.persistence.getGeneralConfig();
5169
+ const pos = config?.["widgetPosition"];
5170
+ if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
5171
+ this.togglePos = { x: pos.x, y: pos.y };
5172
+ }
5173
+ this.applyWidgetPosition();
5174
+ }
5175
+ /** Positions the `.widget` box from the (clamped) toggle centre and orients the arc. */
5176
+ applyWidgetPosition() {
5177
+ const widget = this.shadow.querySelector(".widget");
5178
+ if (!widget) return;
5179
+ const vw = window.innerWidth;
5180
+ const vh = window.innerHeight;
5181
+ const centre = this.togglePos ?? defaultTogglePosition(vw, vh);
5182
+ const clamped = clampTogglePosition(centre.x, centre.y, vw, vh);
5183
+ this.expandDir = resolveExpandDirection(clamped.x, clamped.y, vw, vh);
5184
+ const topLeft = boxTopLeftFor(clamped, this.expandDir);
5185
+ widget.style.left = `${topLeft.x}px`;
5186
+ widget.style.top = `${topLeft.y}px`;
5187
+ widget.style.right = "auto";
5188
+ widget.style.bottom = "auto";
5189
+ widget.setAttribute("data-expand", this.expandDir);
5190
+ }
5191
+ beginWidgetDrag(e) {
5192
+ const origin = this.togglePos ?? defaultTogglePosition(window.innerWidth, window.innerHeight);
5193
+ this.dragState = { startX: e.clientX, startY: e.clientY, origX: origin.x, origY: origin.y, moved: false };
5194
+ }
5195
+ onWidgetPointerMove(e) {
5196
+ if (!this.dragState) return;
5197
+ const dx = e.clientX - this.dragState.startX;
5198
+ const dy = e.clientY - this.dragState.startY;
5199
+ if (!this.dragState.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
5200
+ this.dragState.moved = true;
5201
+ this.togglePos = { x: this.dragState.origX + dx, y: this.dragState.origY + dy };
5202
+ this.applyWidgetPosition();
5203
+ }
5204
+ onWidgetPointerUp() {
5205
+ if (!this.dragState) return;
5206
+ const moved = this.dragState.moved;
5207
+ this.dragState = void 0;
5208
+ if (!moved || !this.togglePos) return;
5209
+ this.suppressNextToggleClick = true;
5210
+ const clamped = clampTogglePosition(this.togglePos.x, this.togglePos.y, window.innerWidth, window.innerHeight);
5211
+ this.togglePos = clamped;
5212
+ this.persistence.setConfig({ widgetPosition: clamped }).catch(() => {
5213
+ });
5214
+ }
5215
+ /** Resets the widget to its default corner and clears the saved position. */
5216
+ resetWidgetPosition() {
5217
+ this.togglePos = null;
5218
+ this.persistence.setConfig({ widgetPosition: null }).catch(() => {
5219
+ });
5220
+ this.applyWidgetPosition();
5221
+ }
4315
5222
  toggle() {
4316
5223
  this.recording.toggleRecording();
4317
5224
  }
@@ -4628,6 +5535,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4628
5535
  this.isVisible = !e.detail;
4629
5536
  this.style.display = this.isVisible ? "" : "none";
4630
5537
  });
5538
+ child.addEventListener("resetwidgetposition", () => this.resetWidgetPosition());
4631
5539
  },
4632
5540
  willClose: () => {
4633
5541
  this.isSettingsDialogOpen = false;
@@ -4682,7 +5590,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4682
5590
  e.detail.fileName,
4683
5591
  e.detail.testId,
4684
5592
  e.detail.itBlock,
4685
- e.detail.interceptorsBlock
5593
+ e.detail.interceptorsBlock,
5594
+ e.detail.notes
4686
5595
  ), 150);
4687
5596
  });
4688
5597
  },
@@ -4696,7 +5605,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4696
5605
  }, 0);
4697
5606
  });
4698
5607
  }
4699
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
5608
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
4700
5609
  Swal.fire({
4701
5610
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
4702
5611
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -4717,6 +5626,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4717
5626
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
4718
5627
  child.itBlock = itBlock;
4719
5628
  child.interceptorsBlock = interceptorsBlock;
5629
+ child.notes = notes;
4720
5630
  container.appendChild(child);
4721
5631
  child.addEventListener("close", () => {
4722
5632
  Swal.close();
@@ -4779,12 +5689,21 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4779
5689
  const paused = this.isPaused;
4780
5690
  this.style.display = this.isVisible ? "" : "none";
4781
5691
  this.shadow.innerHTML = `<style>${getRecorderStyles(rec, paused)}</style>${renderRecorderWidget(rec, paused, this.translation.translate.bind(this.translation))}`;
4782
- this.shadow.querySelector('[data-action="toggle"]')?.addEventListener("click", () => this.toggle());
5692
+ const toggleBtn = this.shadow.querySelector('[data-action="toggle"]');
5693
+ toggleBtn?.addEventListener("click", () => {
5694
+ if (this.suppressNextToggleClick) {
5695
+ this.suppressNextToggleClick = false;
5696
+ return;
5697
+ }
5698
+ this.toggle();
5699
+ });
5700
+ toggleBtn?.addEventListener("pointerdown", (e) => this.beginWidgetDrag(e));
4783
5701
  this.shadow.querySelector('[data-action="pause"]')?.addEventListener("click", () => this.togglePause());
4784
5702
  this.shadow.querySelector('[data-action="tests"]')?.addEventListener("click", () => this.showSavedTestsDialog());
4785
5703
  this.shadow.querySelector('[data-action="commands"]')?.addEventListener("click", () => this.showCommandsDialog());
4786
5704
  this.shadow.querySelector('[data-action="config"]')?.addEventListener("click", () => this.showSettingsDialog());
4787
5705
  this.shadow.querySelector('[data-action="browse"]')?.addEventListener("click", () => this.showAdvancedEditorDialog());
5706
+ this.applyWidgetPosition();
4788
5707
  }
4789
5708
  };
4790
5709
  if (!customElements.get("lib-e2e-recorder")) {
@@ -4792,37 +5711,52 @@ if (!customElements.get("lib-e2e-recorder")) {
4792
5711
  }
4793
5712
 
4794
5713
  // src/index.ts
4795
- var VERSION = "0.1.0";
5714
+ var VERSION = "0.6.0";
4796
5715
  export {
5716
+ ACTIVE_SESSION_BREADCRUMB_KEY,
4797
5717
  AdvancedTestEditorElement,
4798
5718
  AdvancedTestTransformationService,
4799
5719
  ConfigurationElement,
4800
5720
  DB_SCHEMA,
4801
5721
  DB_STORE_NAMES,
5722
+ DEFAULT_RESUME_TTL_MINUTES,
5723
+ DRAG_THRESHOLD,
4802
5724
  FilePreviewElement,
4803
5725
  HttpMonitor,
4804
5726
  INPUT_TYPES,
4805
5727
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
5728
+ LOCALE_BY_LANG,
4806
5729
  LibE2eRecorderElement,
4807
5730
  PersistenceService,
5731
+ RESUME_TTL_CONFIG_KEY,
4808
5732
  RecordingService,
4809
5733
  SCROLLBAR_STYLES,
4810
5734
  SUPPORTED_LANGS,
4811
5735
  SaveTestElement,
5736
+ SelectorPickerElement,
4812
5737
  Subject,
5738
+ TOGGLE_MARGIN,
4813
5739
  TestEditorElement,
4814
5740
  TestPrevisualizerElement,
4815
5741
  TransformationService,
4816
5742
  TranslationService,
4817
5743
  VERSION,
4818
5744
  advancedTestTransformationService,
5745
+ boxOffsetForDirection,
5746
+ boxTopLeftFor,
5747
+ clampTogglePosition,
5748
+ defaultTogglePosition,
4819
5749
  generateAlias,
4820
5750
  injectStyles,
4821
5751
  isLang,
5752
+ isLocalHost,
5753
+ localeForLang,
4822
5754
  makeModalResizable,
4823
5755
  makeSwalDraggable,
4824
5756
  makeSwalDraggableByContentId,
4825
5757
  persistenceService,
5758
+ resolveExpandDirection,
5759
+ selectTestsForExport,
4826
5760
  setSwal2DataCyAttribute,
4827
5761
  showToast,
4828
5762
  transformationService,