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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -61,6 +61,25 @@ function buildPickerSelector(el) {
61
61
  }
62
62
  return el.tagName.toLowerCase();
63
63
  }
64
+ function keyAttrDisplay(el) {
65
+ for (const attr of ["data-cy", "data-testid", "aria-label"]) {
66
+ const v = el.getAttribute(attr);
67
+ if (v) return `${attr}="${v}"`;
68
+ }
69
+ if (el.id) return `id="${el.id}"`;
70
+ if (el.className && el.className.trim()) {
71
+ return el.className.trim().split(/\s+/).map((c) => `.${c}`).join("");
72
+ }
73
+ return "";
74
+ }
75
+ function describePickerRow(el) {
76
+ return {
77
+ quality: getSelectorQuality(el),
78
+ selector: buildPickerSelector(el),
79
+ tag: el.tagName.toLowerCase(),
80
+ attr: keyAttrDisplay(el)
81
+ };
82
+ }
64
83
  var FORBIDDEN_ID_PREFIXES;
65
84
  var init_selector_quality_utils = __esm({
66
85
  "src/utils/selector-quality.utils.ts"() {
@@ -80,19 +99,6 @@ var init_selector_quality_utils = __esm({
80
99
  }
81
100
  });
82
101
 
83
- // src/utils/html.utils.ts
84
- function escHtml(s) {
85
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
86
- }
87
- function escAttr(s) {
88
- return s.replace(/"/g, "&quot;").replace(/</g, "&lt;");
89
- }
90
- var init_html_utils = __esm({
91
- "src/utils/html.utils.ts"() {
92
- "use strict";
93
- }
94
- });
95
-
96
102
  // src/components/selector-picker/selector-picker.styles.ts
97
103
  var SELECTOR_PICKER_STYLES;
98
104
  var init_selector_picker_styles = __esm({
@@ -234,42 +240,42 @@ var init_selector_picker_styles = __esm({
234
240
  }
235
241
  });
236
242
 
237
- // src/components/selector-picker/selector-picker.template.ts
238
- function keyAttrDisplay(el) {
239
- for (const attr of ["data-cy", "data-testid", "aria-label"]) {
240
- const v = el.getAttribute(attr);
241
- if (v) return `${attr}="${escHtml(v)}"`;
242
- }
243
- if (el.id) return `id="${escHtml(el.id)}"`;
244
- if (el.className && el.className.trim()) return escHtml(el.className.trim().split(/\s+/).map((c) => `.${c}`).join(""));
245
- return "";
243
+ // src/utils/html.utils.ts
244
+ function escHtml(s) {
245
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
246
+ }
247
+ function escAttr(s) {
248
+ return s.replace(/"/g, "&quot;").replace(/</g, "&lt;");
246
249
  }
247
- function renderPickerRows(ancestors, selectedIndex, t) {
248
- return ancestors.map((el, i) => {
249
- const quality = getSelectorQuality(el);
250
- const selector = buildPickerSelector(el);
251
- const tag = el.tagName.toLowerCase();
252
- const attr = keyAttrDisplay(el);
250
+ var init_html_utils = __esm({
251
+ "src/utils/html.utils.ts"() {
252
+ "use strict";
253
+ }
254
+ });
255
+
256
+ // src/components/selector-picker/selector-picker.template.ts
257
+ function renderPickerRows(rows, selectedIndex, t) {
258
+ return rows.map((row, i) => {
253
259
  const isSelected = i === selectedIndex;
254
- const isPoor = quality === "poor";
260
+ const isPoor = row.quality === "poor";
255
261
  return `
256
262
  <div class="row${isSelected ? " selected" : ""}" data-idx="${i}" role="option" aria-selected="${isSelected}">
257
- <span class="quality-badge ${QUALITY_CLASS[quality]}" title="${escHtml(t(`SELECTOR_PICKER.QUALITY_${quality.toUpperCase()}`))}">
258
- ${QUALITY_EMOJI[quality]}
263
+ <span class="quality-badge ${QUALITY_CLASS[row.quality]}" title="${escHtml(t(`SELECTOR_PICKER.QUALITY_${row.quality.toUpperCase()}`))}">
264
+ ${QUALITY_EMOJI[row.quality]}
259
265
  </span>
260
- <span class="tag-name">&lt;${escHtml(tag)}&gt;</span>
261
- ${attr ? `<span class="attr-value">${attr}</span>` : '<span class="attr-value"></span>'}
262
- <span class="selector-preview">${escHtml(selector)}</span>
266
+ <span class="tag-name">&lt;${escHtml(row.tag)}&gt;</span>
267
+ ${row.attr ? `<span class="attr-value">${escHtml(row.attr)}</span>` : '<span class="attr-value"></span>'}
268
+ <span class="selector-preview">${escHtml(row.selector)}</span>
263
269
  ${isPoor ? `<span class="poor-warning" title="${escHtml(t("SELECTOR_PICKER.QUALITY_POOR"))}">\u26A0</span>` : ""}
264
270
  </div>`;
265
271
  }).join("");
266
272
  }
267
- function renderPicker(ancestors, selectedIndex, t) {
273
+ function renderPicker(rows, selectedIndex, t) {
268
274
  return `
269
275
  <div class="overlay" role="listbox">
270
276
  <div class="header">${escHtml(t("SELECTOR_PICKER.TITLE"))}</div>
271
277
  <div class="list">
272
- ${renderPickerRows(ancestors, selectedIndex, t)}
278
+ ${renderPickerRows(rows, selectedIndex, t)}
273
279
  </div>
274
280
  <div class="footer">
275
281
  <span class="hint"><kbd>\u2191\u2193</kbd> ${escHtml(t("SELECTOR_PICKER.PREVIEW_LABEL"))}</span>
@@ -282,7 +288,6 @@ var QUALITY_EMOJI, QUALITY_CLASS;
282
288
  var init_selector_picker_template = __esm({
283
289
  "src/components/selector-picker/selector-picker.template.ts"() {
284
290
  "use strict";
285
- init_selector_quality_utils();
286
291
  init_html_utils();
287
292
  QUALITY_EMOJI = {
288
293
  excellent: "\u{1F7E2}",
@@ -319,6 +324,7 @@ var init_selector_picker = __esm({
319
324
  translation;
320
325
  shadow;
321
326
  ancestors = [];
327
+ rows = [];
322
328
  selectedIndex = 0;
323
329
  keyHandler = null;
324
330
  unsubRecording = null;
@@ -330,6 +336,7 @@ var init_selector_picker = __esm({
330
336
  connectedCallback() {
331
337
  this.setAttribute("data-cy", "lib-e2e-cypress-for-dummys");
332
338
  this.ancestors = this.buildAncestors();
339
+ this.rows = this.ancestors.map(describePickerRow);
333
340
  this.selectedIndex = this.findBestIndex();
334
341
  this.render();
335
342
  this.attachKeyListener();
@@ -359,8 +366,8 @@ var init_selector_picker = __esm({
359
366
  return chain;
360
367
  }
361
368
  findBestIndex() {
362
- for (let i = 0; i < this.ancestors.length; i++) {
363
- const q = getSelectorQuality(this.ancestors[i]);
369
+ for (let i = 0; i < this.rows.length; i++) {
370
+ const q = this.rows[i].quality;
364
371
  if (q === "excellent" || q === "good" || q === "acceptable") return i;
365
372
  }
366
373
  return 0;
@@ -368,7 +375,7 @@ var init_selector_picker = __esm({
368
375
  // ── Rendering ───────────────────────────────────────────────────────────────
369
376
  render() {
370
377
  this.shadow.innerHTML = `<style>${SELECTOR_PICKER_STYLES}</style>${renderPicker(
371
- this.ancestors,
378
+ this.rows,
372
379
  this.selectedIndex,
373
380
  this.t.bind(this)
374
381
  )}`;
@@ -424,16 +431,15 @@ var init_selector_picker = __esm({
424
431
  }
425
432
  // ── Actions ─────────────────────────────────────────────────────────────────
426
433
  confirm(index) {
427
- const el = this.ancestors[index];
428
- if (!el) return;
429
- const selector = buildPickerSelector(el);
430
- const command = `cy.get('${selector}').click()`;
434
+ const row = this.rows[index];
435
+ if (!row) return;
436
+ const command = `cy.get('${row.selector}').click()`;
431
437
  this.recording.appendCommand(command);
432
- this.dispatchEvent(new CustomEvent("selectorchosen", { detail: command, bubbles: true }));
438
+ this.dispatchEvent(new CustomEvent("selectorchosen", { detail: command, bubbles: true, composed: true }));
433
439
  this.closeSilently();
434
440
  }
435
441
  cancel() {
436
- this.dispatchEvent(new CustomEvent("pickercancelled", { bubbles: true }));
442
+ this.dispatchEvent(new CustomEvent("pickercancelled", { bubbles: true, composed: true }));
437
443
  this.closeSilently();
438
444
  }
439
445
  closeSilently() {
@@ -454,21 +460,26 @@ var init_selector_picker = __esm({
454
460
  // src/index.ts
455
461
  var index_exports = {};
456
462
  __export(index_exports, {
463
+ ACTIVE_SESSION_BREADCRUMB_KEY: () => ACTIVE_SESSION_BREADCRUMB_KEY,
457
464
  AdvancedTestEditorElement: () => AdvancedTestEditorElement,
458
465
  AdvancedTestTransformationService: () => AdvancedTestTransformationService,
459
466
  ConfigurationElement: () => ConfigurationElement,
460
467
  DB_SCHEMA: () => DB_SCHEMA,
461
468
  DB_STORE_NAMES: () => DB_STORE_NAMES,
469
+ DEFAULT_RESUME_TTL_MINUTES: () => DEFAULT_RESUME_TTL_MINUTES,
462
470
  FilePreviewElement: () => FilePreviewElement,
463
471
  HttpMonitor: () => HttpMonitor,
464
472
  INPUT_TYPES: () => INPUT_TYPES,
465
473
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES: () => LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
474
+ LOCALE_BY_LANG: () => LOCALE_BY_LANG,
466
475
  LibE2eRecorderElement: () => LibE2eRecorderElement,
467
476
  PersistenceService: () => PersistenceService,
477
+ RESUME_TTL_CONFIG_KEY: () => RESUME_TTL_CONFIG_KEY,
468
478
  RecordingService: () => RecordingService,
469
479
  SCROLLBAR_STYLES: () => SCROLLBAR_STYLES,
470
480
  SUPPORTED_LANGS: () => SUPPORTED_LANGS,
471
481
  SaveTestElement: () => SaveTestElement,
482
+ SelectorPickerElement: () => SelectorPickerElement,
472
483
  Subject: () => Subject,
473
484
  TestEditorElement: () => TestEditorElement,
474
485
  TestPrevisualizerElement: () => TestPrevisualizerElement,
@@ -479,10 +490,13 @@ __export(index_exports, {
479
490
  generateAlias: () => generateAlias,
480
491
  injectStyles: () => injectStyles,
481
492
  isLang: () => isLang,
493
+ isLocalHost: () => isLocalHost,
494
+ localeForLang: () => localeForLang,
482
495
  makeModalResizable: () => makeModalResizable,
483
496
  makeSwalDraggable: () => makeSwalDraggable,
484
497
  makeSwalDraggableByContentId: () => makeSwalDraggableByContentId,
485
498
  persistenceService: () => persistenceService,
499
+ selectTestsForExport: () => selectTestsForExport,
486
500
  setSwal2DataCyAttribute: () => setSwal2DataCyAttribute,
487
501
  showToast: () => showToast,
488
502
  transformationService: () => transformationService,
@@ -495,6 +509,16 @@ var SUPPORTED_LANGS = ["es", "en", "fr", "it", "de"];
495
509
  function isLang(value) {
496
510
  return SUPPORTED_LANGS.includes(value);
497
511
  }
512
+ var LOCALE_BY_LANG = {
513
+ es: "es-ES",
514
+ en: "en-GB",
515
+ fr: "fr-FR",
516
+ it: "it-IT",
517
+ de: "de-DE"
518
+ };
519
+ function localeForLang(lang) {
520
+ return isLang(lang) ? LOCALE_BY_LANG[lang] : "es-ES";
521
+ }
498
522
 
499
523
  // src/models/input-types.model.ts
500
524
  var INPUT_TYPES = [
@@ -511,7 +535,7 @@ var INPUT_TYPES = [
511
535
  // src/models/db-schema.model.ts
512
536
  var DB_SCHEMA = {
513
537
  name: "E2ECypressDB",
514
- version: 10,
538
+ version: 11,
515
539
  stores: [
516
540
  {
517
541
  name: "tests",
@@ -551,11 +575,45 @@ var DB_SCHEMA = {
551
575
  { name: "extendedHttpCommands", keyPath: "extendedHttpCommands", unique: false },
552
576
  { name: "allowReadWriteFiles", keyPath: "allowReadWriteFiles", unique: false }
553
577
  ]
578
+ },
579
+ {
580
+ // Single-record store (fixed key id=1) holding the live, in-progress
581
+ // recording session so it survives a micro-frontend crossing or reload.
582
+ // See docs/specs/006-cross-app-recording-continuity.md.
583
+ name: "activeSession",
584
+ keyPath: "id",
585
+ autoIncrement: false,
586
+ indexes: []
554
587
  }
555
588
  ]
556
589
  };
557
590
  var DB_STORE_NAMES = DB_SCHEMA.stores.map((s) => s.name);
558
591
 
592
+ // src/models/active-session.model.ts
593
+ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
594
+ var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
595
+ var DEFAULT_RESUME_TTL_MINUTES = 30;
596
+
597
+ // src/utils/subject.ts
598
+ var Subject = class {
599
+ _value;
600
+ listeners = /* @__PURE__ */ new Set();
601
+ constructor(initialValue) {
602
+ this._value = initialValue;
603
+ }
604
+ next(value) {
605
+ this._value = value;
606
+ this.listeners.forEach((l) => l(value));
607
+ }
608
+ getValue() {
609
+ return this._value;
610
+ }
611
+ subscribe(fn) {
612
+ this.listeners.add(fn);
613
+ return () => this.listeners.delete(fn);
614
+ }
615
+ };
616
+
559
617
  // src/i18n/es.ts
560
618
  var I18N_ES = {
561
619
  MAIN_FRAME: {
@@ -590,8 +648,6 @@ var I18N_ES = {
590
648
  COPY_DESCRIBE: "\u{1F5C2} Copiar describe()",
591
649
  MULTI_SELECT: "\u{1F5C2} Multi-select",
592
650
  CANCEL_SELECT: "\u2715 Cancelar",
593
- SECTION_COMMANDS: "Comandos",
594
- SECTION_INTERCEPTORS: "Interceptores",
595
651
  COPY_CMDS_BTN: "\u{1F4CB} Copiar comandos",
596
652
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
597
653
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -620,6 +676,18 @@ var I18N_ES = {
620
676
  DATA_SECTION: "\u{1F4BE} Datos",
621
677
  DATA_DESC: "Exporta todos tus tests a JSON o importa una copia de seguridad.",
622
678
  EXPORT_BTN: "\u2B06\uFE0F Exportar tests",
679
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exportar tests",
680
+ EXPORT_MODE_ALL: "Todo",
681
+ EXPORT_MODE_MANUAL: "Selecci\xF3n manual",
682
+ EXPORT_MODE_TAGS: "Por tags",
683
+ EXPORT_ALL_DESC: "Se exportar\xE1n todos los tests guardados.",
684
+ EXPORT_COUNT: "A exportar:",
685
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportar",
686
+ EXPORT_CANCEL: "Cancelar",
687
+ EXPORT_EMPTY: "No hay tests para exportar.",
688
+ EXPORT_NO_TAGS: "No hay tags disponibles.",
689
+ EXPORT_TAGS_HINT: "Selecciona uno o m\xE1s tags para ver qu\xE9 pruebas se exportar\xE1n.",
690
+ EXPORT_RESULT_LABEL: "Pruebas que se exportar\xE1n:",
623
691
  IMPORT_BTN: "\u2B07\uFE0F Importar tests",
624
692
  FOLDER_UPDATED_TOAST: "\u2713 Carpeta de Cypress actualizada",
625
693
  FOLDER_ERROR_TOAST: "Error al acceder a la carpeta",
@@ -632,7 +700,10 @@ var I18N_ES = {
632
700
  SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido.",
633
701
  START_HIDDEN_SECTION: "\u{1F441} Visibilidad del widget",
634
702
  START_HIDDEN_TITLE: "Iniciar oculto",
635
- START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo."
703
+ START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo.",
704
+ RESUME_TTL_SECTION: "\u23F1 Continuidad de grabaci\xF3n",
705
+ RESUME_TTL_LABEL: "Reanudar sin preguntar (minutos)",
706
+ 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."
636
707
  },
637
708
  SELECTOR_PICKER: {
638
709
  TITLE: "Selecciona un elemento del DOM",
@@ -673,8 +744,10 @@ var I18N_ES = {
673
744
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
674
745
  CLOSE_BTN: "\u2715 Cerrar",
675
746
  NEW_FILE_BTN: "+ Nuevo archivo",
747
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
676
748
  REFRESH_BTN: "\u21BB Actualizar",
677
749
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
750
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
678
751
  NEW_FILE_CONFIRM: "Crear",
679
752
  NEW_FILE_CANCEL: "Cancelar"
680
753
  },
@@ -687,10 +760,19 @@ var I18N_ES = {
687
760
  NO_CHANGES: "Sin cambios respecto al original",
688
761
  NO_CHANGES_SHORT: "Sin cambios",
689
762
  LAUNCH_BTN: "\u25B6 Lanzar test",
763
+ LAUNCH_LOCAL_ONLY: "Mu\xE9velo a local para poder probar",
764
+ LAUNCH_RUNNING: "\u23F3 Ejecutando\u2026",
765
+ LAUNCH_PASSED: "\u2713 Prueba superada",
766
+ LAUNCH_FAILED: "\u2717 Prueba fallida",
767
+ LAUNCH_NO_RUNNER: "No se detect\xF3 el runner local (\xBFlo has arrancado?)",
690
768
  DIFF_BTN: "\u{1F4CA} Diff",
691
769
  SAVE_BTN: "\u{1F4BE} Guardar",
692
770
  CLOSE_BTN: "\u2715 Cerrar",
693
- BACK_TO_EDITOR: "\u2190 Volver al editor"
771
+ BACK_TO_EDITOR: "\u2190 Volver al editor",
772
+ INSERT_BTN: "\u{1FA84} Insertar bloques",
773
+ INSERT_TITLE: "Insertar it() y beforeEach() en el contenido del editor",
774
+ INSERT_DONE: "Bloques insertados en el editor",
775
+ INSERT_ERROR: "No se encontr\xF3 un bloque describe() v\xE1lido en el archivo"
694
776
  },
695
777
  RECORDER: {
696
778
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -719,7 +801,12 @@ var I18N_ES = {
719
801
  BADGE_PAUSED: "\u23F8 PAUSA",
720
802
  BADGE_REC: "\u25CF REC",
721
803
  STOP_TITLE: "Detener (Ctrl+R)",
722
- START_TITLE: "Grabar (Ctrl+R)"
804
+ START_TITLE: "Grabar (Ctrl+R)",
805
+ SESSION_RESUME_TITLE: "\u23FA Grabaci\xF3n en curso",
806
+ SESSION_RESUME_TEXT: "Hay una grabaci\xF3n activa sin terminar.",
807
+ SESSION_RESUME_COUNT: "comandos capturados",
808
+ SESSION_CONTINUE_BTN: "Continuar grabando",
809
+ SESSION_DISCARD_BTN: "Descartar"
723
810
  }
724
811
  };
725
812
 
@@ -757,8 +844,6 @@ var I18N_EN = {
757
844
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
758
845
  MULTI_SELECT: "\u{1F5C2} Multi-select",
759
846
  CANCEL_SELECT: "\u2715 Cancel",
760
- SECTION_COMMANDS: "Commands",
761
- SECTION_INTERCEPTORS: "Interceptors",
762
847
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
763
848
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
764
849
  DEFAULT_DESCRIBE: "Test suite",
@@ -787,6 +872,18 @@ var I18N_EN = {
787
872
  DATA_SECTION: "\u{1F4BE} Data",
788
873
  DATA_DESC: "Export all your tests to JSON or import a backup.",
789
874
  EXPORT_BTN: "\u2B06\uFE0F Export tests",
875
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Export tests",
876
+ EXPORT_MODE_ALL: "All",
877
+ EXPORT_MODE_MANUAL: "Manual selection",
878
+ EXPORT_MODE_TAGS: "By tags",
879
+ EXPORT_ALL_DESC: "All saved tests will be exported.",
880
+ EXPORT_COUNT: "To export:",
881
+ EXPORT_CONFIRM: "\u2B06\uFE0F Export",
882
+ EXPORT_CANCEL: "Cancel",
883
+ EXPORT_EMPTY: "No tests to export.",
884
+ EXPORT_NO_TAGS: "No tags available.",
885
+ EXPORT_TAGS_HINT: "Select one or more tags to preview which tests will be exported.",
886
+ EXPORT_RESULT_LABEL: "Tests that will be exported:",
790
887
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
791
888
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
792
889
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -799,7 +896,10 @@ var I18N_EN = {
799
896
  SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
800
897
  START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
801
898
  START_HIDDEN_TITLE: "Start hidden",
802
- START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it."
899
+ START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it.",
900
+ RESUME_TTL_SECTION: "\u23F1 Recording continuity",
901
+ RESUME_TTL_LABEL: "Resume without asking (minutes)",
902
+ 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."
803
903
  },
804
904
  SELECTOR_PICKER: {
805
905
  TITLE: "Select a DOM element",
@@ -840,8 +940,10 @@ var I18N_EN = {
840
940
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
841
941
  CLOSE_BTN: "\u2715 Close",
842
942
  NEW_FILE_BTN: "+ New file",
943
+ NEW_FOLDER_BTN: "+ New folder",
843
944
  REFRESH_BTN: "\u21BB Refresh",
844
945
  NEW_FILE_PLACEHOLDER: "test-name",
946
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
845
947
  NEW_FILE_CONFIRM: "Create",
846
948
  NEW_FILE_CANCEL: "Cancel"
847
949
  },
@@ -854,10 +956,19 @@ var I18N_EN = {
854
956
  NO_CHANGES: "No changes from original",
855
957
  NO_CHANGES_SHORT: "No changes",
856
958
  LAUNCH_BTN: "\u25B6 Run test",
959
+ LAUNCH_LOCAL_ONLY: "Move it to localhost to run tests",
960
+ LAUNCH_RUNNING: "\u23F3 Running\u2026",
961
+ LAUNCH_PASSED: "\u2713 Test passed",
962
+ LAUNCH_FAILED: "\u2717 Test failed",
963
+ LAUNCH_NO_RUNNER: "No local runner detected (did you start it?)",
857
964
  DIFF_BTN: "\u{1F4CA} Diff",
858
965
  SAVE_BTN: "\u{1F4BE} Save",
859
966
  CLOSE_BTN: "\u2715 Close",
860
- BACK_TO_EDITOR: "\u2190 Back to editor"
967
+ BACK_TO_EDITOR: "\u2190 Back to editor",
968
+ INSERT_BTN: "\u{1FA84} Insert blocks",
969
+ INSERT_TITLE: "Insert it() and beforeEach() into the editor content",
970
+ INSERT_DONE: "Blocks inserted into the editor",
971
+ INSERT_ERROR: "No valid describe() block found in the file"
861
972
  },
862
973
  RECORDER: {
863
974
  FS_TITLE: "\u{1F4C1} File access",
@@ -886,7 +997,12 @@ var I18N_EN = {
886
997
  BADGE_PAUSED: "\u23F8 PAUSED",
887
998
  BADGE_REC: "\u25CF REC",
888
999
  STOP_TITLE: "Stop (Ctrl+R)",
889
- START_TITLE: "Record (Ctrl+R)"
1000
+ START_TITLE: "Record (Ctrl+R)",
1001
+ SESSION_RESUME_TITLE: "\u23FA Recording in progress",
1002
+ SESSION_RESUME_TEXT: "There is an unfinished active recording.",
1003
+ SESSION_RESUME_COUNT: "commands captured",
1004
+ SESSION_CONTINUE_BTN: "Keep recording",
1005
+ SESSION_DISCARD_BTN: "Discard"
890
1006
  }
891
1007
  };
892
1008
 
@@ -924,8 +1040,6 @@ var I18N_FR = {
924
1040
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
925
1041
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
926
1042
  CANCEL_SELECT: "\u2715 Annuler",
927
- SECTION_COMMANDS: "Commandes",
928
- SECTION_INTERCEPTORS: "Intercepteurs",
929
1043
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
930
1044
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
931
1045
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -954,6 +1068,18 @@ var I18N_FR = {
954
1068
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
955
1069
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
956
1070
  EXPORT_BTN: "\u2B06\uFE0F Exporter les tests",
1071
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exporter les tests",
1072
+ EXPORT_MODE_ALL: "Tout",
1073
+ EXPORT_MODE_MANUAL: "S\xE9lection manuelle",
1074
+ EXPORT_MODE_TAGS: "Par tags",
1075
+ EXPORT_ALL_DESC: "Tous les tests enregistr\xE9s seront export\xE9s.",
1076
+ EXPORT_COUNT: "\xC0 exporter :",
1077
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exporter",
1078
+ EXPORT_CANCEL: "Annuler",
1079
+ EXPORT_EMPTY: "Aucun test \xE0 exporter.",
1080
+ EXPORT_NO_TAGS: "Aucun tag disponible.",
1081
+ EXPORT_TAGS_HINT: "S\xE9lectionnez un ou plusieurs tags pour voir les tests qui seront export\xE9s.",
1082
+ EXPORT_RESULT_LABEL: "Tests qui seront export\xE9s :",
957
1083
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
958
1084
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
959
1085
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -966,7 +1092,10 @@ var I18N_FR = {
966
1092
  SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
967
1093
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
968
1094
  START_HIDDEN_TITLE: "D\xE9marrer masqu\xE9",
969
- START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer."
1095
+ START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer.",
1096
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE9 d'enregistrement",
1097
+ RESUME_TTL_LABEL: "Reprendre sans demander (minutes)",
1098
+ 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."
970
1099
  },
971
1100
  SELECTOR_PICKER: {
972
1101
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -1007,8 +1136,10 @@ var I18N_FR = {
1007
1136
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
1008
1137
  CLOSE_BTN: "\u2715 Fermer",
1009
1138
  NEW_FILE_BTN: "+ Nouveau fichier",
1139
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
1010
1140
  REFRESH_BTN: "\u21BB Actualiser",
1011
1141
  NEW_FILE_PLACEHOLDER: "nom-du-test",
1142
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
1012
1143
  NEW_FILE_CONFIRM: "Cr\xE9er",
1013
1144
  NEW_FILE_CANCEL: "Annuler"
1014
1145
  },
@@ -1021,10 +1152,19 @@ var I18N_FR = {
1021
1152
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
1022
1153
  NO_CHANGES_SHORT: "Aucun changement",
1023
1154
  LAUNCH_BTN: "\u25B6 Lancer le test",
1155
+ LAUNCH_LOCAL_ONLY: "Passe en local pour pouvoir tester",
1156
+ LAUNCH_RUNNING: "\u23F3 Ex\xE9cution\u2026",
1157
+ LAUNCH_PASSED: "\u2713 Test r\xE9ussi",
1158
+ LAUNCH_FAILED: "\u2717 Test \xE9chou\xE9",
1159
+ LAUNCH_NO_RUNNER: "Aucun runner local d\xE9tect\xE9 (l'avez-vous d\xE9marr\xE9 ?)",
1024
1160
  DIFF_BTN: "\u{1F4CA} Diff",
1025
1161
  SAVE_BTN: "\u{1F4BE} Enregistrer",
1026
1162
  CLOSE_BTN: "\u2715 Fermer",
1027
- BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur"
1163
+ BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur",
1164
+ INSERT_BTN: "\u{1FA84} Ins\xE9rer les blocs",
1165
+ INSERT_TITLE: "Ins\xE9rer it() et beforeEach() dans le contenu de l'\xE9diteur",
1166
+ INSERT_DONE: "Blocs ins\xE9r\xE9s dans l'\xE9diteur",
1167
+ INSERT_ERROR: "Aucun bloc describe() valide trouv\xE9 dans le fichier"
1028
1168
  },
1029
1169
  RECORDER: {
1030
1170
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -1053,7 +1193,12 @@ var I18N_FR = {
1053
1193
  BADGE_PAUSED: "\u23F8 PAUSE",
1054
1194
  BADGE_REC: "\u25CF REC",
1055
1195
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
1056
- START_TITLE: "Enregistrer (Ctrl+R)"
1196
+ START_TITLE: "Enregistrer (Ctrl+R)",
1197
+ SESSION_RESUME_TITLE: "\u23FA Enregistrement en cours",
1198
+ SESSION_RESUME_TEXT: "Un enregistrement actif est inachev\xE9.",
1199
+ SESSION_RESUME_COUNT: "commandes captur\xE9es",
1200
+ SESSION_CONTINUE_BTN: "Continuer l'enregistrement",
1201
+ SESSION_DISCARD_BTN: "Ignorer"
1057
1202
  }
1058
1203
  };
1059
1204
 
@@ -1091,8 +1236,6 @@ var I18N_IT = {
1091
1236
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
1092
1237
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
1093
1238
  CANCEL_SELECT: "\u2715 Annulla",
1094
- SECTION_COMMANDS: "Comandi",
1095
- SECTION_INTERCEPTORS: "Interceptor",
1096
1239
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
1097
1240
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
1098
1241
  DEFAULT_DESCRIBE: "Suite di test",
@@ -1121,6 +1264,18 @@ var I18N_IT = {
1121
1264
  DATA_SECTION: "\u{1F4BE} Dati",
1122
1265
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
1123
1266
  EXPORT_BTN: "\u2B06\uFE0F Esporta test",
1267
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Esporta test",
1268
+ EXPORT_MODE_ALL: "Tutto",
1269
+ EXPORT_MODE_MANUAL: "Selezione manuale",
1270
+ EXPORT_MODE_TAGS: "Per tag",
1271
+ EXPORT_ALL_DESC: "Verranno esportati tutti i test salvati.",
1272
+ EXPORT_COUNT: "Da esportare:",
1273
+ EXPORT_CONFIRM: "\u2B06\uFE0F Esporta",
1274
+ EXPORT_CANCEL: "Annulla",
1275
+ EXPORT_EMPTY: "Nessun test da esportare.",
1276
+ EXPORT_NO_TAGS: "Nessun tag disponibile.",
1277
+ EXPORT_TAGS_HINT: "Seleziona uno o pi\xF9 tag per vedere quali test verranno esportati.",
1278
+ EXPORT_RESULT_LABEL: "Test che verranno esportati:",
1124
1279
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
1125
1280
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
1126
1281
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -1133,7 +1288,10 @@ var I18N_IT = {
1133
1288
  SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
1134
1289
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
1135
1290
  START_HIDDEN_TITLE: "Avvia nascosto",
1136
- START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo."
1291
+ START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo.",
1292
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE0 di registrazione",
1293
+ RESUME_TTL_LABEL: "Riprendi senza chiedere (minuti)",
1294
+ 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."
1137
1295
  },
1138
1296
  SELECTOR_PICKER: {
1139
1297
  TITLE: "Seleziona un elemento DOM",
@@ -1174,8 +1332,10 @@ var I18N_IT = {
1174
1332
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
1175
1333
  CLOSE_BTN: "\u2715 Chiudi",
1176
1334
  NEW_FILE_BTN: "+ Nuovo file",
1335
+ NEW_FOLDER_BTN: "+ Nuova cartella",
1177
1336
  REFRESH_BTN: "\u21BB Aggiorna",
1178
1337
  NEW_FILE_PLACEHOLDER: "nome-del-test",
1338
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
1179
1339
  NEW_FILE_CONFIRM: "Crea",
1180
1340
  NEW_FILE_CANCEL: "Annulla"
1181
1341
  },
@@ -1188,10 +1348,19 @@ var I18N_IT = {
1188
1348
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
1189
1349
  NO_CHANGES_SHORT: "Nessuna modifica",
1190
1350
  LAUNCH_BTN: "\u25B6 Esegui test",
1351
+ LAUNCH_LOCAL_ONLY: "Spostati in locale per poter testare",
1352
+ LAUNCH_RUNNING: "\u23F3 Esecuzione\u2026",
1353
+ LAUNCH_PASSED: "\u2713 Test superato",
1354
+ LAUNCH_FAILED: "\u2717 Test fallito",
1355
+ LAUNCH_NO_RUNNER: "Nessun runner locale rilevato (l'hai avviato?)",
1191
1356
  DIFF_BTN: "\u{1F4CA} Diff",
1192
1357
  SAVE_BTN: "\u{1F4BE} Salva",
1193
1358
  CLOSE_BTN: "\u2715 Chiudi",
1194
- BACK_TO_EDITOR: "\u2190 Torna all'editor"
1359
+ BACK_TO_EDITOR: "\u2190 Torna all'editor",
1360
+ INSERT_BTN: "\u{1FA84} Inserisci blocchi",
1361
+ INSERT_TITLE: "Inserisci it() e beforeEach() nel contenuto dell'editor",
1362
+ INSERT_DONE: "Blocchi inseriti nell'editor",
1363
+ INSERT_ERROR: "Nessun blocco describe() valido trovato nel file"
1195
1364
  },
1196
1365
  RECORDER: {
1197
1366
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -1220,7 +1389,12 @@ var I18N_IT = {
1220
1389
  BADGE_PAUSED: "\u23F8 PAUSA",
1221
1390
  BADGE_REC: "\u25CF REC",
1222
1391
  STOP_TITLE: "Ferma (Ctrl+R)",
1223
- START_TITLE: "Registra (Ctrl+R)"
1392
+ START_TITLE: "Registra (Ctrl+R)",
1393
+ SESSION_RESUME_TITLE: "\u23FA Registrazione in corso",
1394
+ SESSION_RESUME_TEXT: "C'\xE8 una registrazione attiva non terminata.",
1395
+ SESSION_RESUME_COUNT: "comandi catturati",
1396
+ SESSION_CONTINUE_BTN: "Continua a registrare",
1397
+ SESSION_DISCARD_BTN: "Annulla"
1224
1398
  }
1225
1399
  };
1226
1400
 
@@ -1258,8 +1432,6 @@ var I18N_DE = {
1258
1432
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
1259
1433
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
1260
1434
  CANCEL_SELECT: "\u2715 Abbrechen",
1261
- SECTION_COMMANDS: "Befehle",
1262
- SECTION_INTERCEPTORS: "Interceptors",
1263
1435
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
1264
1436
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
1265
1437
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -1288,6 +1460,18 @@ var I18N_DE = {
1288
1460
  DATA_SECTION: "\u{1F4BE} Daten",
1289
1461
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
1290
1462
  EXPORT_BTN: "\u2B06\uFE0F Tests exportieren",
1463
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Tests exportieren",
1464
+ EXPORT_MODE_ALL: "Alle",
1465
+ EXPORT_MODE_MANUAL: "Manuelle Auswahl",
1466
+ EXPORT_MODE_TAGS: "Nach Tags",
1467
+ EXPORT_ALL_DESC: "Alle gespeicherten Tests werden exportiert.",
1468
+ EXPORT_COUNT: "Zu exportieren:",
1469
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportieren",
1470
+ EXPORT_CANCEL: "Abbrechen",
1471
+ EXPORT_EMPTY: "Keine Tests zum Exportieren.",
1472
+ EXPORT_NO_TAGS: "Keine Tags verf\xFCgbar.",
1473
+ EXPORT_TAGS_HINT: "W\xE4hle ein oder mehrere Tags, um zu sehen, welche Tests exportiert werden.",
1474
+ EXPORT_RESULT_LABEL: "Diese Tests werden exportiert:",
1291
1475
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
1292
1476
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
1293
1477
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -1300,7 +1484,10 @@ var I18N_DE = {
1300
1484
  SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
1301
1485
  START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
1302
1486
  START_HIDDEN_TITLE: "Versteckt starten",
1303
- START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden."
1487
+ START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden.",
1488
+ RESUME_TTL_SECTION: "\u23F1 Aufzeichnungskontinuit\xE4t",
1489
+ RESUME_TTL_LABEL: "Ohne Nachfrage fortsetzen (Minuten)",
1490
+ 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."
1304
1491
  },
1305
1492
  SELECTOR_PICKER: {
1306
1493
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -1341,8 +1528,10 @@ var I18N_DE = {
1341
1528
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
1342
1529
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1343
1530
  NEW_FILE_BTN: "+ Neue Datei",
1531
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
1344
1532
  REFRESH_BTN: "\u21BB Aktualisieren",
1345
1533
  NEW_FILE_PLACEHOLDER: "test-name",
1534
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
1346
1535
  NEW_FILE_CONFIRM: "Erstellen",
1347
1536
  NEW_FILE_CANCEL: "Abbrechen"
1348
1537
  },
@@ -1355,10 +1544,19 @@ var I18N_DE = {
1355
1544
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
1356
1545
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
1357
1546
  LAUNCH_BTN: "\u25B6 Test starten",
1547
+ LAUNCH_LOCAL_ONLY: "Wechsle zu localhost, um Tests auszuf\xFChren",
1548
+ LAUNCH_RUNNING: "\u23F3 L\xE4uft\u2026",
1549
+ LAUNCH_PASSED: "\u2713 Test bestanden",
1550
+ LAUNCH_FAILED: "\u2717 Test fehlgeschlagen",
1551
+ LAUNCH_NO_RUNNER: "Kein lokaler Runner erkannt (gestartet?)",
1358
1552
  DIFF_BTN: "\u{1F4CA} Diff",
1359
1553
  SAVE_BTN: "\u{1F4BE} Speichern",
1360
1554
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1361
- BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor"
1555
+ BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor",
1556
+ INSERT_BTN: "\u{1FA84} Bl\xF6cke einf\xFCgen",
1557
+ INSERT_TITLE: "it() und beforeEach() in den Editor-Inhalt einf\xFCgen",
1558
+ INSERT_DONE: "Bl\xF6cke in den Editor eingef\xFCgt",
1559
+ INSERT_ERROR: "Kein g\xFCltiger describe()-Block in der Datei gefunden"
1362
1560
  },
1363
1561
  RECORDER: {
1364
1562
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -1387,13 +1585,18 @@ var I18N_DE = {
1387
1585
  BADGE_PAUSED: "\u23F8 PAUSE",
1388
1586
  BADGE_REC: "\u25CF REC",
1389
1587
  STOP_TITLE: "Stopp (Ctrl+R)",
1390
- START_TITLE: "Aufzeichnen (Ctrl+R)"
1588
+ START_TITLE: "Aufzeichnen (Ctrl+R)",
1589
+ SESSION_RESUME_TITLE: "\u23FA Aufzeichnung l\xE4uft",
1590
+ SESSION_RESUME_TEXT: "Es gibt eine nicht abgeschlossene aktive Aufzeichnung.",
1591
+ SESSION_RESUME_COUNT: "erfasste Befehle",
1592
+ SESSION_CONTINUE_BTN: "Weiter aufzeichnen",
1593
+ SESSION_DISCARD_BTN: "Verwerfen"
1391
1594
  }
1392
1595
  };
1393
1596
 
1394
1597
  // src/services/translation.service.ts
1395
1598
  var TranslationService = class {
1396
- lang;
1599
+ lang$;
1397
1600
  translations = {
1398
1601
  es: I18N_ES,
1399
1602
  en: I18N_EN,
@@ -1402,17 +1605,21 @@ var TranslationService = class {
1402
1605
  de: I18N_DE
1403
1606
  };
1404
1607
  constructor() {
1405
- this.lang = this.detectLang();
1608
+ this.lang$ = new Subject(this.detectLang());
1406
1609
  }
1407
1610
  setLang(lang) {
1408
- this.lang = lang;
1611
+ this.lang$.next(lang);
1409
1612
  }
1410
1613
  getLang() {
1411
- return this.lang;
1614
+ return this.lang$.getValue();
1615
+ }
1616
+ /** Subscribe to language changes; returns an unsubscribe function. */
1617
+ onLangChange(fn) {
1618
+ return this.lang$.subscribe(fn);
1412
1619
  }
1413
1620
  translate(key) {
1414
1621
  const keys = key.split(".");
1415
- let value = this.translations[this.lang];
1622
+ let value = this.translations[this.lang$.getValue()];
1416
1623
  for (const k of keys) {
1417
1624
  value = value?.[k];
1418
1625
  if (value === void 0) return key;
@@ -1435,6 +1642,9 @@ function gcd(a, b) {
1435
1642
  }
1436
1643
  return a;
1437
1644
  }
1645
+ function escapeSingleQuotes(value) {
1646
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1647
+ }
1438
1648
  function normalizeBlock(code, baseIndent) {
1439
1649
  const lines = code.split("\n").map((l) => l.trimEnd());
1440
1650
  const nonEmpty = lines.filter((l) => l.trim().length > 0);
@@ -1457,7 +1667,7 @@ var TransformationService = class {
1457
1667
  }
1458
1668
  generateItDescription(description, commands) {
1459
1669
  const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
1460
- return `it('${description}', () => {
1670
+ return `it('${escapeSingleQuotes(description)}', () => {
1461
1671
  ${commandsBlock}
1462
1672
  });`;
1463
1673
  }
@@ -1511,29 +1721,15 @@ ${lines}
1511
1721
  };
1512
1722
  var advancedTestTransformationService = new AdvancedTestTransformationService();
1513
1723
 
1514
- // src/utils/subject.ts
1515
- var Subject = class {
1516
- _value;
1517
- listeners = /* @__PURE__ */ new Set();
1518
- constructor(initialValue) {
1519
- this._value = initialValue;
1520
- }
1521
- next(value) {
1522
- this._value = value;
1523
- this.listeners.forEach((l) => l(value));
1524
- }
1525
- getValue() {
1526
- return this._value;
1527
- }
1528
- subscribe(fn) {
1529
- this.listeners.add(fn);
1530
- return () => this.listeners.delete(fn);
1531
- }
1532
- };
1533
-
1534
1724
  // src/services/recording.service.ts
1535
1725
  init_selector_quality_utils();
1536
1726
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
1727
+ function createSessionId() {
1728
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1729
+ return crypto.randomUUID();
1730
+ }
1731
+ return `sess-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
1732
+ }
1537
1733
  var RecordingService = class {
1538
1734
  commands$ = new Subject([]);
1539
1735
  interceptors$ = new Subject([]);
@@ -1543,6 +1739,9 @@ var RecordingService = class {
1543
1739
  inputDebounceTimers = /* @__PURE__ */ new Map();
1544
1740
  abort = new AbortController();
1545
1741
  selectorStrategy = "data-cy";
1742
+ /** Stable id of the current live session (null when none has started). */
1743
+ sessionId = null;
1744
+ startedAt = 0;
1546
1745
  // Stored originals for history patching cleanup
1547
1746
  origPushState = history.pushState.bind(history);
1548
1747
  origReplaceState = history.replaceState.bind(history);
@@ -1554,12 +1753,42 @@ var RecordingService = class {
1554
1753
  }
1555
1754
  // ── Public API ────────────────────────────────────────────────────────────
1556
1755
  startRecording() {
1756
+ this.sessionId = createSessionId();
1757
+ this.startedAt = Date.now();
1557
1758
  this.isPaused$.next(false);
1558
1759
  this.isRecording$.next(true);
1559
1760
  this.addCommand(`cy.viewport(1900, 1200)`);
1560
1761
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1561
1762
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1562
1763
  }
1764
+ /**
1765
+ * Rehydrates a previously persisted session WITHOUT running the startRecording
1766
+ * bootstrap (no viewport/visit/hide). Used to continue a recording across a
1767
+ * micro-frontend crossing or a same-origin reload.
1768
+ * See docs/specs/006-cross-app-recording-continuity.md.
1769
+ */
1770
+ restoreSession(state) {
1771
+ this.sessionId = state.sessionId;
1772
+ this.startedAt = state.startedAt;
1773
+ this.selectorStrategy = state.selectorStrategy;
1774
+ this.commands$.next([...state.commands]);
1775
+ this.interceptors$.next([...state.interceptors]);
1776
+ this.isPaused$.next(state.isPaused);
1777
+ this.isRecording$.next(state.isRecording);
1778
+ }
1779
+ /** Full snapshot of the live session for persistence. */
1780
+ getSessionSnapshot() {
1781
+ return {
1782
+ sessionId: this.sessionId ?? createSessionId(),
1783
+ isRecording: this.isRecording$.getValue(),
1784
+ isPaused: this.isPaused$.getValue(),
1785
+ commands: this.commands$.getValue(),
1786
+ interceptors: this.interceptors$.getValue(),
1787
+ selectorStrategy: this.selectorStrategy,
1788
+ startedAt: this.startedAt,
1789
+ updatedAt: Date.now()
1790
+ };
1791
+ }
1563
1792
  stopRecording() {
1564
1793
  this.isPaused$.next(false);
1565
1794
  this.isRecording$.next(false);
@@ -1646,6 +1875,21 @@ var RecordingService = class {
1646
1875
  onPauseChange(fn) {
1647
1876
  return this.isPaused$.subscribe(fn);
1648
1877
  }
1878
+ /**
1879
+ * Fires a full session snapshot whenever any persisted field changes
1880
+ * (commands, interceptors, recording or paused state). Drives the debounced
1881
+ * persistence of the live session. Returns a combined unsubscribe.
1882
+ */
1883
+ onSessionChange(fn) {
1884
+ const emit = () => fn(this.getSessionSnapshot());
1885
+ const unsubs = [
1886
+ this.commands$.subscribe(emit),
1887
+ this.interceptors$.subscribe(emit),
1888
+ this.isRecording$.subscribe(emit),
1889
+ this.isPaused$.subscribe(emit)
1890
+ ];
1891
+ return () => unsubs.forEach((u) => u());
1892
+ }
1649
1893
  onSelectorNotFound(fn) {
1650
1894
  return this.selectorNotFound$.subscribe((v) => {
1651
1895
  if (v) fn(v.target, v.action);
@@ -1877,6 +2121,7 @@ var RecordingService = class {
1877
2121
 
1878
2122
  // src/services/persistence.service.ts
1879
2123
  var import_idb = require("idb");
2124
+ var ACTIVE_SESSION_ID = 1;
1880
2125
  var PersistenceService = class {
1881
2126
  // dbName is overridable so tests can use unique names for isolation.
1882
2127
  constructor(dbName = DB_SCHEMA.name) {
@@ -1930,7 +2175,7 @@ var PersistenceService = class {
1930
2175
  if (!record) return null;
1931
2176
  const commands = await this.getCommandStrings(testId);
1932
2177
  const interceptors = await this.getInterceptorStrings(testId);
1933
- const itBlock = `it('${record.name}', () => {
2178
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
1934
2179
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1935
2180
  });`;
1936
2181
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
@@ -2011,6 +2256,25 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2011
2256
  const records = await db.getAll("configuration");
2012
2257
  return records[0] ?? null;
2013
2258
  }
2259
+ // ── Active recording session ──────────────────────────────────────────────
2260
+ /** Upserts the live recording session (single record, fixed key). */
2261
+ async saveActiveSession(state) {
2262
+ const db = await this.getDB();
2263
+ await db.put("activeSession", { ...state, id: ACTIVE_SESSION_ID });
2264
+ }
2265
+ /** Returns the persisted live session, or null when none is stored. */
2266
+ async getActiveSession() {
2267
+ const db = await this.getDB();
2268
+ const record = await db.get("activeSession", ACTIVE_SESSION_ID);
2269
+ if (!record) return null;
2270
+ const { id: _id, ...state } = record;
2271
+ return state;
2272
+ }
2273
+ /** Removes the live session record. Safe to call when none exists. */
2274
+ async clearActiveSession() {
2275
+ const db = await this.getDB();
2276
+ await db.delete("activeSession", ACTIVE_SESSION_ID);
2277
+ }
2014
2278
  // ── Bulk operations ───────────────────────────────────────────────────────
2015
2279
  async clearAllData() {
2016
2280
  const db = await this.getDB();
@@ -2021,10 +2285,18 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2021
2285
  ]);
2022
2286
  }
2023
2287
  async ingestFileData(tests, interceptors) {
2024
- await Promise.all([
2025
- this.bulkInsertWithoutId("tests", tests),
2026
- this.bulkInsertWithoutId("interceptors", interceptors)
2027
- ]);
2288
+ if (Array.isArray(tests)) {
2289
+ const db = await this.getDB();
2290
+ for (const test of tests) {
2291
+ const { id: _id, commands, interceptors: testInterceptors, ...record } = test;
2292
+ const newId = await db.add("tests", record);
2293
+ const cmds = Array.isArray(commands) ? commands : [];
2294
+ const icps = Array.isArray(testInterceptors) ? testInterceptors : [];
2295
+ if (cmds.length) await this.insertCommands(cmds, newId);
2296
+ if (icps.length) await this.insertInterceptors(icps, newId);
2297
+ }
2298
+ }
2299
+ await this.bulkInsertWithoutId("interceptors", interceptors);
2028
2300
  }
2029
2301
  async requestDirectoryPermissions() {
2030
2302
  if (!("showDirectoryPicker" in window)) {
@@ -2467,6 +2739,28 @@ function showToast(message, isSuccess = true) {
2467
2739
  setTimeout(() => toast.remove(), 3e3);
2468
2740
  }
2469
2741
 
2742
+ // src/utils/export-selection.utils.ts
2743
+ function selectTestsForExport(tests, mode, opts = {}) {
2744
+ if (mode === "all") return [...tests];
2745
+ if (mode === "manual") {
2746
+ const ids = new Set(opts.ids ?? []);
2747
+ return tests.filter((t) => ids.has(t.id));
2748
+ }
2749
+ const tags = new Set(opts.tags ?? []);
2750
+ if (tags.size === 0) return [];
2751
+ return tests.filter((t) => (t.tags ?? []).some((tag) => tags.has(tag)));
2752
+ }
2753
+
2754
+ // src/utils/host.utils.ts
2755
+ function isLocalHost(hostname) {
2756
+ if (!hostname) return true;
2757
+ const h = hostname.toLowerCase();
2758
+ return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
2759
+ }
2760
+
2761
+ // src/index.ts
2762
+ init_selector_picker();
2763
+
2470
2764
  // src/components/test-previsualizer/test-previsualizer.styles.ts
2471
2765
  var TEST_PREVISUALIZER_STYLES = `
2472
2766
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -3069,7 +3363,7 @@ function highlightLine(line) {
3069
3363
 
3070
3364
  // src/components/test-editor/test-editor.template.ts
3071
3365
  function renderTestEditor(state, t) {
3072
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
3366
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
3073
3367
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
3074
3368
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
3075
3369
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -3080,12 +3374,12 @@ function renderTestEditor(state, t) {
3080
3374
  </div>` : "";
3081
3375
  const rows = visible.map((test, i) => {
3082
3376
  const expanded = expandedIndex === i;
3083
- const date = new Date(test.createdAt).toLocaleString("es-ES", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
3377
+ const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
3084
3378
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
3085
3379
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
3086
3380
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
3087
3381
  const hasIcps = (test.interceptors ?? []).length > 0;
3088
- const itBlockCode = `it('${test.name}', () => {
3382
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
3089
3383
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
3090
3384
  });`;
3091
3385
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -3193,11 +3487,11 @@ var TestEditorElement = class extends HTMLElement {
3193
3487
  ` : "";
3194
3488
  const itBlocks = selected.map((t) => {
3195
3489
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
3196
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
3490
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
3197
3491
  ${cmds}
3198
3492
  });`;
3199
3493
  }).join("\n\n");
3200
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
3494
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
3201
3495
  ${beforeEach}${itBlocks}
3202
3496
  });`;
3203
3497
  navigator.clipboard?.writeText(block);
@@ -3231,7 +3525,8 @@ ${beforeEach}${itBlocks}
3231
3525
  selectedIds: this.selectedIds,
3232
3526
  describeName: this.describeName,
3233
3527
  expandedIndex: this.expandedIndex,
3234
- interceptorsByTest: this.interceptorsByTest
3528
+ interceptorsByTest: this.interceptorsByTest,
3529
+ locale: localeForLang(this.translation.getLang())
3235
3530
  }, this.t.bind(this))}`;
3236
3531
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
3237
3532
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -3388,9 +3683,75 @@ var CONFIGURATION_STYLES = `
3388
3683
 
3389
3684
  /* \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 */
3390
3685
  .data-desc { font-size: 11px; color: #484f58; margin-bottom: 10px; line-height: 1.5; }
3686
+
3687
+ /* \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 */
3688
+ .export-overlay {
3689
+ position: fixed; inset: 0; z-index: 100000;
3690
+ background: rgba(1,4,9,0.7);
3691
+ display: flex; align-items: center; justify-content: center; padding: 20px;
3692
+ }
3693
+ .export-modal {
3694
+ width: 640px; max-width: 100%; max-height: 86vh;
3695
+ display: flex; flex-direction: column;
3696
+ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
3697
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5); overflow: hidden;
3698
+ }
3699
+ .export-hd {
3700
+ padding: 14px 18px; font-size: 13px; font-weight: 700;
3701
+ border-bottom: 1px solid #21262d; color: #e6edf3;
3702
+ }
3703
+ .export-modes { display: flex; gap: 6px; padding: 12px 18px 0; }
3704
+ .export-mode {
3705
+ flex: 1; padding: 7px 10px; font-size: 11px; border-radius: 6px;
3706
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3707
+ }
3708
+ .export-mode:hover { background: #21262d; color: #e6edf3; }
3709
+ .export-mode.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3710
+ .export-body {
3711
+ padding: 14px 18px; overflow-y: auto; flex: 1; min-height: 160px;
3712
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3713
+ }
3714
+ .export-tag-hint { font-size: 12px; color: #484f58; margin-top: 12px; }
3715
+ .export-result-label {
3716
+ font-size: 10px; color: #484f58; text-transform: uppercase; letter-spacing: 0.6px;
3717
+ font-weight: 600; margin: 14px 0 6px;
3718
+ }
3719
+ .export-row-static { cursor: default; }
3720
+ .export-row-static:hover { background: transparent; }
3721
+ .export-all-desc { font-size: 12px; color: #8b949e; }
3722
+ .export-empty { font-size: 12px; color: #484f58; text-align: center; padding: 20px; }
3723
+ .export-list { display: flex; flex-direction: column; gap: 4px; }
3724
+ .export-row {
3725
+ display: flex; align-items: center; gap: 8px;
3726
+ padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: #c9d1d9;
3727
+ }
3728
+ .export-row:hover { background: #0d1117; }
3729
+ .export-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3730
+ .export-row-tag {
3731
+ font-size: 9px; color: #8b949e; background: #0d1117;
3732
+ border: 1px solid #30363d; border-radius: 10px; padding: 1px 7px; flex-shrink: 0;
3733
+ }
3734
+ .export-tags { display: flex; flex-wrap: wrap; gap: 6px; }
3735
+ .export-tag {
3736
+ padding: 4px 10px; font-size: 11px; border-radius: 12px;
3737
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3738
+ }
3739
+ .export-tag:hover { background: #21262d; color: #e6edf3; }
3740
+ .export-tag.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3741
+ .export-ft {
3742
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
3743
+ padding: 12px 18px; border-top: 1px solid #21262d;
3744
+ }
3745
+ .export-count { font-size: 11px; color: #8b949e; }
3746
+ .export-count b { color: #e6edf3; }
3747
+ .export-ft-actions { display: flex; gap: 8px; }
3748
+ .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
3749
+ .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
3750
+ .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
3391
3751
  `;
3392
3752
 
3393
3753
  // src/components/configuration/configuration.template.ts
3754
+ init_html_utils();
3394
3755
  var LANGS = [
3395
3756
  { value: "es", label: "Espa\xF1ol" },
3396
3757
  { value: "en", label: "English" },
@@ -3398,8 +3759,64 @@ var LANGS = [
3398
3759
  { value: "it", label: "Italiano" },
3399
3760
  { value: "de", label: "Deutsch" }
3400
3761
  ];
3762
+ function renderExportOverlay(state, t) {
3763
+ const { isExporting, exportMode, exportTests, exportSelectedIds, exportSelectedTags } = state;
3764
+ if (!isExporting) return "";
3765
+ const count = selectTestsForExport(exportTests, exportMode, {
3766
+ ids: exportSelectedIds,
3767
+ tags: exportSelectedTags
3768
+ }).length;
3769
+ const modeBtn = (mode, key) => `<button class="export-mode ${exportMode === mode ? "active" : ""}" data-export-mode="${mode}">${t(key)}</button>`;
3770
+ let body;
3771
+ if (exportTests.length === 0) {
3772
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_EMPTY")}</div>`;
3773
+ } else if (exportMode === "all") {
3774
+ body = `<div class="export-all-desc">${t("CONFIG.EXPORT_ALL_DESC")}</div>`;
3775
+ } else if (exportMode === "manual") {
3776
+ body = `<div class="export-list">${exportTests.map((test) => `
3777
+ <label class="export-row">
3778
+ <input type="checkbox" data-export-test="${test.id}" ${exportSelectedIds.has(test.id) ? "checked" : ""} />
3779
+ <span class="export-row-name">${escHtml(test.name)}</span>
3780
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3781
+ </label>`).join("")}</div>`;
3782
+ } else {
3783
+ const allTags = [...new Set(exportTests.flatMap((test) => test.tags ?? []))].sort();
3784
+ if (!allTags.length) {
3785
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_NO_TAGS")}</div>`;
3786
+ } else {
3787
+ 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>`;
3788
+ const matched = selectTestsForExport(exportTests, "tags", { tags: exportSelectedTags });
3789
+ 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>
3790
+ <div class="export-list">${matched.map((test) => `
3791
+ <div class="export-row export-row-static">
3792
+ <span class="export-row-name">${escHtml(test.name)}</span>
3793
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3794
+ </div>`).join("")}</div>`;
3795
+ body = chips + preview;
3796
+ }
3797
+ }
3798
+ return `
3799
+ <div class="export-overlay" id="export-overlay">
3800
+ <div class="export-modal">
3801
+ <div class="export-hd">${t("CONFIG.EXPORT_DIALOG_TITLE")}</div>
3802
+ ${exportTests.length ? `<div class="export-modes">
3803
+ ${modeBtn("all", "CONFIG.EXPORT_MODE_ALL")}
3804
+ ${modeBtn("manual", "CONFIG.EXPORT_MODE_MANUAL")}
3805
+ ${modeBtn("tags", "CONFIG.EXPORT_MODE_TAGS")}
3806
+ </div>` : ""}
3807
+ <div class="export-body">${body}</div>
3808
+ <div class="export-ft">
3809
+ <span class="export-count">${t("CONFIG.EXPORT_COUNT")} <b>${count}</b></span>
3810
+ <span class="export-ft-actions">
3811
+ <button id="btn-export-confirm" class="btn-export-confirm" ${count === 0 ? "disabled" : ""}>${t("CONFIG.EXPORT_CONFIRM")}</button>
3812
+ <button id="btn-export-cancel">${t("CONFIG.EXPORT_CANCEL")}</button>
3813
+ </span>
3814
+ </div>
3815
+ </div>
3816
+ </div>`;
3817
+ }
3401
3818
  function renderConfiguration(state, t) {
3402
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden } = state;
3819
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
3403
3820
  const langOptions = LANGS.map(
3404
3821
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
3405
3822
  ).join("");
@@ -3451,6 +3868,17 @@ function renderConfiguration(state, t) {
3451
3868
  </label>
3452
3869
  </div>
3453
3870
 
3871
+ <!-- Recording continuity (cross-app resume TTL) -->
3872
+ <div class="card">
3873
+ <div class="card-hd">${t("CONFIG.RESUME_TTL_SECTION")}</div>
3874
+ <div class="field-row">
3875
+ <span class="field-label">${t("CONFIG.RESUME_TTL_LABEL")}</span>
3876
+ <input type="number" id="resume-ttl-input" min="1" step="1" value="${resumeTtlMinutes}"
3877
+ style="width:80px;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none" />
3878
+ </div>
3879
+ <div class="check-sub" style="margin-top:8px">${t("CONFIG.RESUME_TTL_HINT")}</div>
3880
+ </div>
3881
+
3454
3882
  <!-- Selector Strategy -->
3455
3883
  <div class="card card-wide">
3456
3884
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -3470,13 +3898,13 @@ function renderConfiguration(state, t) {
3470
3898
  <div class="card card-wide">
3471
3899
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
3472
3900
  <div class="fs-layout">
3473
- <pre class="fs-tree">cypress/ <span style="color:#484f58">\u2190 selecciona</span>
3901
+ <pre class="fs-tree">cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
3474
3902
  \u2514\u2500\u2500 e2e/
3475
3903
  \u2514\u2500\u2500 *.cy.ts</pre>
3476
3904
  <div class="fs-right">
3477
3905
  <div class="fs-status">
3478
3906
  <span class="fs-dot ${filesystemGranted ? "on" : "off"}"></span>
3479
- ${filesystemGranted && cypressFolderName ? `<span>${t("CONFIG.FOLDER_CONNECTED")}</span>&nbsp;<span class="fs-folder">\u{1F4C1} ${cypressFolderName}</span>` : `<span>${t("CONFIG.FOLDER_NOT_SET")}</span>`}
3907
+ ${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>`}
3480
3908
  </div>
3481
3909
  <div class="btn-row">
3482
3910
  <button id="btn-change-folder">
@@ -3501,7 +3929,7 @@ function renderConfiguration(state, t) {
3501
3929
  </div>
3502
3930
  </div>
3503
3931
 
3504
- </div>`;
3932
+ </div>${renderExportOverlay(state, t)}`;
3505
3933
  }
3506
3934
 
3507
3935
  // src/components/configuration/configuration.ts
@@ -3514,6 +3942,12 @@ var ConfigurationElement = class extends HTMLElement {
3514
3942
  selectorStrategy = "data-cy";
3515
3943
  smartSelectorEnabled = true;
3516
3944
  startHidden = false;
3945
+ resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
3946
+ isExporting = false;
3947
+ exportMode = "all";
3948
+ exportTests = [];
3949
+ exportSelectedIds = /* @__PURE__ */ new Set();
3950
+ exportSelectedTags = /* @__PURE__ */ new Set();
3517
3951
  filesystemGranted = false;
3518
3952
  cypressFolderName = null;
3519
3953
  constructor() {
@@ -3540,6 +3974,11 @@ var ConfigurationElement = class extends HTMLElement {
3540
3974
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
3541
3975
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
3542
3976
  this.startHidden = config?.["startHidden"] === "true";
3977
+ const ttlRaw = config?.[RESUME_TTL_CONFIG_KEY];
3978
+ if (ttlRaw !== void 0 && ttlRaw !== null) {
3979
+ const ttl = Number(ttlRaw);
3980
+ this.resumeTtlMinutes = Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_RESUME_TTL_MINUTES;
3981
+ }
3543
3982
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
3544
3983
  const handle = config?.["cypressDirectoryHandle"];
3545
3984
  this.cypressFolderName = handle?.name ?? null;
@@ -3563,6 +4002,12 @@ var ConfigurationElement = class extends HTMLElement {
3563
4002
  this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
3564
4003
  this.render();
3565
4004
  }
4005
+ async onResumeTtlChange(minutes) {
4006
+ const safe = Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes) : DEFAULT_RESUME_TTL_MINUTES;
4007
+ this.resumeTtlMinutes = safe;
4008
+ await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
4009
+ this.render();
4010
+ }
3566
4011
  async onSmartSelectorChange(enabled) {
3567
4012
  this.smartSelectorEnabled = enabled;
3568
4013
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -3592,8 +4037,7 @@ var ConfigurationElement = class extends HTMLElement {
3592
4037
  this.cypressFolderName = null;
3593
4038
  this.render();
3594
4039
  }
3595
- async exportAllData() {
3596
- const tests = await this.persistence.getAllTests();
4040
+ downloadTests(tests) {
3597
4041
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
3598
4042
  const url = URL.createObjectURL(blob);
3599
4043
  const a = document.createElement("a");
@@ -3602,6 +4046,53 @@ var ConfigurationElement = class extends HTMLElement {
3602
4046
  a.click();
3603
4047
  URL.revokeObjectURL(url);
3604
4048
  }
4049
+ /** Downloads every saved test (used by the "Todo" mode and as a direct API). */
4050
+ async exportAllData() {
4051
+ const tests = await this.persistence.getAllTests();
4052
+ this.downloadTests(tests);
4053
+ }
4054
+ /** Opens the export selection dialog, loading the current tests. */
4055
+ async openExportDialog() {
4056
+ this.exportTests = await this.persistence.getAllTests();
4057
+ this.exportMode = "all";
4058
+ this.exportSelectedIds = /* @__PURE__ */ new Set();
4059
+ this.exportSelectedTags = /* @__PURE__ */ new Set();
4060
+ this.isExporting = true;
4061
+ this.render();
4062
+ }
4063
+ cancelExport() {
4064
+ this.isExporting = false;
4065
+ this.exportSelectedIds.clear();
4066
+ this.exportSelectedTags.clear();
4067
+ this.render();
4068
+ }
4069
+ setExportMode(mode) {
4070
+ this.exportMode = mode;
4071
+ this.render();
4072
+ }
4073
+ toggleExportTest(id) {
4074
+ if (this.exportSelectedIds.has(id)) this.exportSelectedIds.delete(id);
4075
+ else this.exportSelectedIds.add(id);
4076
+ this.render();
4077
+ }
4078
+ toggleExportTag(tag) {
4079
+ if (this.exportSelectedTags.has(tag)) this.exportSelectedTags.delete(tag);
4080
+ else this.exportSelectedTags.add(tag);
4081
+ this.render();
4082
+ }
4083
+ /** Downloads the tests resolved by the current mode + selection. No-op if empty. */
4084
+ confirmExport() {
4085
+ const subset = selectTestsForExport(this.exportTests, this.exportMode, {
4086
+ ids: this.exportSelectedIds,
4087
+ tags: this.exportSelectedTags
4088
+ });
4089
+ if (!subset.length) return;
4090
+ this.downloadTests(subset);
4091
+ this.isExporting = false;
4092
+ this.exportSelectedIds.clear();
4093
+ this.exportSelectedTags.clear();
4094
+ this.render();
4095
+ }
3605
4096
  async importAllData(file) {
3606
4097
  const text = await file.text();
3607
4098
  let data;
@@ -3613,7 +4104,6 @@ var ConfigurationElement = class extends HTMLElement {
3613
4104
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
3614
4105
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
3615
4106
  }
3616
- await this.persistence.clearAllData();
3617
4107
  await this.persistence.ingestFileData(data.tests, data.interceptors);
3618
4108
  }
3619
4109
  render() {
@@ -3624,7 +4114,13 @@ var ConfigurationElement = class extends HTMLElement {
3624
4114
  filesystemGranted: this.filesystemGranted,
3625
4115
  cypressFolderName: this.cypressFolderName,
3626
4116
  smartSelectorEnabled: this.smartSelectorEnabled,
3627
- startHidden: this.startHidden
4117
+ startHidden: this.startHidden,
4118
+ resumeTtlMinutes: this.resumeTtlMinutes,
4119
+ isExporting: this.isExporting,
4120
+ exportMode: this.exportMode,
4121
+ exportTests: this.exportTests,
4122
+ exportSelectedIds: this.exportSelectedIds,
4123
+ exportSelectedTags: this.exportSelectedTags
3628
4124
  }, this.t.bind(this))}`;
3629
4125
  ;
3630
4126
  this.shadow.getElementById("lang-select").addEventListener(
@@ -3643,13 +4139,28 @@ var ConfigurationElement = class extends HTMLElement {
3643
4139
  "change",
3644
4140
  (e) => this.onStartHiddenChange(e.target.checked)
3645
4141
  );
4142
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
4143
+ "change",
4144
+ (e) => this.onResumeTtlChange(Number(e.target.value))
4145
+ );
3646
4146
  this.shadow.getElementById("selector-strategy").addEventListener(
3647
4147
  "change",
3648
4148
  (e) => this.onSelectorStrategyChange(e.target.value)
3649
4149
  );
3650
4150
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
3651
4151
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
3652
- this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.exportAllData());
4152
+ this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.openExportDialog());
4153
+ this.shadow.getElementById("btn-export-confirm")?.addEventListener("click", () => this.confirmExport());
4154
+ this.shadow.getElementById("btn-export-cancel")?.addEventListener("click", () => this.cancelExport());
4155
+ this.shadow.querySelectorAll("[data-export-mode]").forEach(
4156
+ (el) => el.addEventListener("click", () => this.setExportMode(el.dataset["exportMode"]))
4157
+ );
4158
+ this.shadow.querySelectorAll("[data-export-test]").forEach(
4159
+ (el) => el.addEventListener("change", () => this.toggleExportTest(Number(el.dataset["exportTest"])))
4160
+ );
4161
+ this.shadow.querySelectorAll("[data-export-tag]").forEach(
4162
+ (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
4163
+ );
3653
4164
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
3654
4165
  const file = e.target.files?.[0];
3655
4166
  if (!file) return;
@@ -3759,18 +4270,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3759
4270
  }
3760
4271
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3761
4272
  .sidebar-toolbar {
3762
- display: flex; gap: 4px; padding: 6px 6px 4px;
4273
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3763
4274
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3764
4275
  }
3765
4276
  .btn-toolbar {
3766
- flex: 1; padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
4277
+ flex: 1 1 calc(50% - 2px); padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3767
4278
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3768
4279
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3769
4280
  text-overflow: ellipsis;
3770
4281
  }
4282
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3771
4283
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3772
4284
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3773
4285
  .btn-toolbar.btn-new:hover { background: #238636; color: #fff; }
4286
+ .btn-toolbar.btn-new-folder { color: #d29922; border-color: #9e6a03; }
4287
+ .btn-toolbar.btn-new-folder:hover { background: #9e6a03; color: #fff; }
3774
4288
  .new-file-form {
3775
4289
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3776
4290
  }
@@ -3804,7 +4318,7 @@ function renderNoPermission(needsReauth, t) {
3804
4318
  </div>`;
3805
4319
  }
3806
4320
  function renderAdvancedEditor(state, t) {
3807
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
4321
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3808
4322
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3809
4323
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3810
4324
  <pre class="pre-file">${syntaxHighlight(selectedFileContent.slice(0, 6e3))}${selectedFileContent.length > 6e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
@@ -3831,16 +4345,25 @@ function renderAdvancedEditor(state, t) {
3831
4345
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3832
4346
  </div>
3833
4347
  </div>` : "";
4348
+ const newFolderForm = isCreatingFolder ? `<div class="new-file-form">
4349
+ <input id="input-new-folder" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FOLDER_PLACEHOLDER"))}" autocomplete="off" />
4350
+ <div class="new-file-actions">
4351
+ <button id="btn-new-folder-confirm" class="btn-confirm">${t("ADVANCED_EDITOR.NEW_FILE_CONFIRM")}</button>
4352
+ <button id="btn-new-folder-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
4353
+ </div>
4354
+ </div>` : "";
3834
4355
  const toolbar = `
3835
4356
  <div class="sidebar-toolbar">
3836
4357
  <button id="btn-new-file" class="btn-toolbar btn-new">${t("ADVANCED_EDITOR.NEW_FILE_BTN")}</button>
3837
- <button id="btn-refresh" class="btn-toolbar">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
4358
+ <button id="btn-new-folder" class="btn-toolbar btn-new-folder">${t("ADVANCED_EDITOR.NEW_FOLDER_BTN")}</button>
4359
+ <button id="btn-refresh" class="btn-toolbar btn-full">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
3838
4360
  </div>`;
3839
4361
  return `
3840
4362
  <div class="layout">
3841
4363
  <div class="sidebar" style="width:${sidebarWidth}px">
3842
4364
  ${toolbar}
3843
4365
  ${newFileForm}
4366
+ ${newFolderForm}
3844
4367
  <div class="tree-scroll">${treeHtml}</div>
3845
4368
  </div>
3846
4369
  <div id="resize-handle" class="resize-handle"></div>
@@ -3911,6 +4434,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3911
4434
  previewFileName = null;
3912
4435
  previewFileContent = null;
3913
4436
  isCreatingFile = false;
4437
+ isCreatingFolder = false;
3914
4438
  collapsedDirs = /* @__PURE__ */ new Set();
3915
4439
  sidebarWidth = 220;
3916
4440
  hasPermission = false;
@@ -3973,7 +4497,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3973
4497
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3974
4498
  if (!name) return;
3975
4499
  const fileName = `${name}.cy.ts`;
3976
- const template = `describe('${name}', () => {
4500
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3977
4501
 
3978
4502
  it('should ', () => {
3979
4503
 
@@ -3991,6 +4515,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3991
4515
  this.isCreatingFile = false;
3992
4516
  await this.getFoldersData();
3993
4517
  }
4518
+ async createNewFolder(rawName) {
4519
+ if (!this._e2eHandle) return;
4520
+ const name = rawName.trim().replace(/[/\\]/g, "");
4521
+ if (!name) return;
4522
+ try {
4523
+ await this._e2eHandle.getDirectoryHandle(name, { create: true });
4524
+ } catch {
4525
+ }
4526
+ this.isCreatingFolder = false;
4527
+ await this.getFoldersData();
4528
+ }
3994
4529
  async refreshTree() {
3995
4530
  await this.getFoldersData();
3996
4531
  }
@@ -4048,7 +4583,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4048
4583
  fileName: this.selectedFile?.name ?? "",
4049
4584
  testId: this.testId,
4050
4585
  itBlock: this.testItBlock,
4051
- interceptorsBlock: this.interceptorsBlock
4586
+ interceptorsBlock: this.interceptorsBlock,
4587
+ notes: this.testNotes
4052
4588
  },
4053
4589
  bubbles: true,
4054
4590
  composed: true
@@ -4125,6 +4661,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4125
4661
  testNotes: this.testNotes,
4126
4662
  saveButtonEnabled: this.saveButtonEnabled,
4127
4663
  isCreatingFile: this.isCreatingFile,
4664
+ isCreatingFolder: this.isCreatingFolder,
4128
4665
  collapsedDirs: this.collapsedDirs,
4129
4666
  sidebarWidth: this.sidebarWidth
4130
4667
  }, this.t.bind(this))}`;
@@ -4148,9 +4685,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4148
4685
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
4149
4686
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
4150
4687
  this.isCreatingFile = !this.isCreatingFile;
4688
+ this.isCreatingFolder = false;
4151
4689
  this.render();
4152
4690
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
4153
4691
  });
4692
+ this.shadow.getElementById("btn-new-folder")?.addEventListener("click", () => {
4693
+ this.isCreatingFolder = !this.isCreatingFolder;
4694
+ this.isCreatingFile = false;
4695
+ this.render();
4696
+ if (this.isCreatingFolder) this.shadow.getElementById("input-new-folder")?.focus();
4697
+ });
4154
4698
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
4155
4699
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
4156
4700
  const input2 = this.shadow.getElementById("input-new-file");
@@ -4169,6 +4713,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4169
4713
  this.render();
4170
4714
  }
4171
4715
  });
4716
+ this.shadow.getElementById("btn-new-folder-confirm")?.addEventListener("click", () => {
4717
+ const folderInput2 = this.shadow.getElementById("input-new-folder");
4718
+ this.createNewFolder(folderInput2?.value ?? "");
4719
+ });
4720
+ this.shadow.getElementById("btn-new-folder-cancel")?.addEventListener("click", () => {
4721
+ this.isCreatingFolder = false;
4722
+ this.render();
4723
+ });
4724
+ const folderInput = this.shadow.getElementById("input-new-folder");
4725
+ folderInput?.addEventListener("keydown", (e) => {
4726
+ if (e.key === "Enter") {
4727
+ this.createNewFolder(folderInput.value);
4728
+ } else if (e.key === "Escape") {
4729
+ this.isCreatingFolder = false;
4730
+ this.render();
4731
+ }
4732
+ });
4172
4733
  }
4173
4734
  };
4174
4735
  if (!customElements.get("advanced-test-editor")) {
@@ -4254,7 +4815,30 @@ var FILE_PREVIEW_STYLES = `
4254
4815
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
4255
4816
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
4256
4817
  .btn-launch:hover { background: rgba(227,179,65,0.1); }
4818
+ .btn-launch:disabled { opacity: 0.45; cursor: not-allowed; }
4819
+ .btn-launch:disabled:hover { background: transparent; }
4820
+ .launch-hint { font-size: 10px; color: #8b6d3b; align-self: center; }
4821
+ .btn-insert { background: transparent; border-color: #a371f7; color: #a371f7; }
4822
+ .btn-insert:hover { background: rgba(163,113,247,0.12); color: #c8a8ff; }
4257
4823
  .btn-diff-active { background: rgba(47,129,247,0.15); border-color: rgba(47,129,247,0.4); color: #2f81f7; }
4824
+
4825
+ /* \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 */
4826
+ .run-result {
4827
+ border-top: 1px solid #21262d; padding: 8px 12px; background: #0d1117;
4828
+ max-height: 220px; overflow-y: auto; flex-shrink: 0;
4829
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
4830
+ }
4831
+ .run-status { font-size: 12px; font-weight: 600; }
4832
+ .run-passed .run-status { color: #3fb950; }
4833
+ .run-failed .run-status { color: #f85149; }
4834
+ .run-running .run-status { color: #e3b341; }
4835
+ .run-error .run-status { color: #f85149; }
4836
+ .run-output {
4837
+ margin: 6px 0 0; padding: 8px 10px; background: #161b22; border: 1px solid #21262d;
4838
+ border-radius: 5px; font-size: 10px; color: #c9d1d9; line-height: 1.5;
4839
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
4840
+ white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto;
4841
+ }
4258
4842
  `;
4259
4843
 
4260
4844
  // src/components/file-preview/file-preview.template.ts
@@ -4312,8 +4896,14 @@ function buildDiffHtml(original, current, t) {
4312
4896
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
4313
4897
  }).join("");
4314
4898
  }
4899
+ var RUN_STATUS_KEY = {
4900
+ running: "FILE_PREVIEW.LAUNCH_RUNNING",
4901
+ passed: "FILE_PREVIEW.LAUNCH_PASSED",
4902
+ failed: "FILE_PREVIEW.LAUNCH_FAILED",
4903
+ error: "FILE_PREVIEW.LAUNCH_NO_RUNNER"
4904
+ };
4315
4905
  function renderFilePreview(state, t) {
4316
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4906
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
4317
4907
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
4318
4908
  <div class="blocks-panel">
4319
4909
  ${itBlock ? `
@@ -4334,6 +4924,13 @@ function renderFilePreview(state, t) {
4334
4924
  </div>` : ""}
4335
4925
  </div>` : "";
4336
4926
  const hasChanges = originalContent !== null && originalContent !== (fileContent ?? "");
4927
+ const hasBlocks = !!(itBlock || interceptorsBlock);
4928
+ 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>
4929
+ <span class="launch-hint">${t("FILE_PREVIEW.LAUNCH_LOCAL_ONLY")}</span>`;
4930
+ const runResultHtml = runState !== "idle" ? `<div class="run-result run-${runState}">
4931
+ <span class="run-status">${t(RUN_STATUS_KEY[runState])}</span>
4932
+ ${runOutput ? `<pre class="run-output">${escHtml(runOutput.slice(0, 8e3))}</pre>` : ""}
4933
+ </div>` : "";
4337
4934
  return `
4338
4935
  <div class="container">
4339
4936
  <div class="header">
@@ -4344,9 +4941,11 @@ function renderFilePreview(state, t) {
4344
4941
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
4345
4942
  ${blocksPanelHtml}
4346
4943
  </div>
4944
+ ${runResultHtml}
4347
4945
  <div class="footer">
4348
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4946
+ ${launchBtnHtml}
4349
4947
  ${hasChanges ? `<button id="btn-diff" class="${showDiff ? "btn-diff-active" : ""}">${t("FILE_PREVIEW.DIFF_BTN")}</button>` : ""}
4948
+ ${hasBlocks && !showDiff ? `<button id="btn-insert" class="btn-insert" title="${t("FILE_PREVIEW.INSERT_TITLE")}">${t("FILE_PREVIEW.INSERT_BTN")}</button>` : ""}
4350
4949
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
4351
4950
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
4352
4951
  </div>
@@ -4362,11 +4961,18 @@ var FilePreviewElement = class extends HTMLElement {
4362
4961
  translation = translationService;
4363
4962
  itBlock = "";
4364
4963
  interceptorsBlock = "";
4964
+ notes = "";
4365
4965
  commands = [];
4366
4966
  interceptors = [];
4967
+ /** Endpoint of the local Cypress runner. Configurable; defaults to the bundled runner's port. */
4968
+ runnerUrl = "http://localhost:8123/run-test";
4969
+ /** Whether the app is served locally — gates the launch button. Overridable for tests. */
4970
+ isLocal = isLocalHost(window.location.hostname);
4367
4971
  _fileContent = null;
4368
4972
  _originalContent = null;
4369
4973
  _showDiff = false;
4974
+ _runState = "idle";
4975
+ _runOutput = "";
4370
4976
  constructor() {
4371
4977
  super();
4372
4978
  this.shadow = this.attachShadow({ mode: "open" });
@@ -4394,15 +5000,34 @@ var FilePreviewElement = class extends HTMLElement {
4394
5000
  onClose() {
4395
5001
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
4396
5002
  }
5003
+ /**
5004
+ * Runs the current spec headless via the local runner and shows the result.
5005
+ * No-op off localhost. Never throws — connection failures surface as an
5006
+ * "error" state + toast instead of an unhandled rejection.
5007
+ */
4397
5008
  async launchTest(specPath) {
4398
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
5009
+ if (!this.isLocal) return;
5010
+ const path = specPath ?? this.fileName ?? "";
4399
5011
  if (!path) return;
4400
- const response = await fetch("http://localhost:8123/run-test", {
4401
- method: "POST",
4402
- headers: { "Content-Type": "application/json" },
4403
- body: JSON.stringify({ specPath: path })
4404
- });
4405
- await response.json();
5012
+ this._runState = "running";
5013
+ this._runOutput = "";
5014
+ this.render();
5015
+ try {
5016
+ const response = await fetch(this.runnerUrl, {
5017
+ method: "POST",
5018
+ headers: { "Content-Type": "application/json" },
5019
+ body: JSON.stringify({ specPath: path })
5020
+ });
5021
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
5022
+ const result = await response.json();
5023
+ this._runOutput = result.output ?? "";
5024
+ this._runState = result.success ? "passed" : "failed";
5025
+ } catch {
5026
+ this._runState = "error";
5027
+ this._runOutput = "";
5028
+ showToast(this.t("FILE_PREVIEW.LAUNCH_NO_RUNNER"), false);
5029
+ }
5030
+ this.render();
4406
5031
  }
4407
5032
  copyToClipboard(text) {
4408
5033
  navigator.clipboard?.writeText(text);
@@ -4411,6 +5036,32 @@ var FilePreviewElement = class extends HTMLElement {
4411
5036
  this._showDiff = !this._showDiff;
4412
5037
  this.render();
4413
5038
  }
5039
+ /**
5040
+ * Merges the it() and beforeEach() blocks into the current editor content
5041
+ * using the same logic as the automatic "Insert into file" action, so the
5042
+ * user does not have to copy/paste manually. Leaves the content untouched
5043
+ * (and warns) when the file has no valid describe() block to insert into.
5044
+ */
5045
+ insertBlocks() {
5046
+ const base = this.textarea?.value ?? this._fileContent ?? "";
5047
+ let content = base;
5048
+ if (this.interceptorsBlock) {
5049
+ const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
5050
+ if (merged) content = merged;
5051
+ }
5052
+ if (this.itBlock) {
5053
+ const comment = this.notes ? advancedTestTransformationService.buildBlockComment(this.notes) + "\n" : "";
5054
+ const merged = advancedTestTransformationService.insertItBlock(content, comment + this.itBlock);
5055
+ if (merged) content = merged;
5056
+ }
5057
+ if (content === base) {
5058
+ showToast(this.t("FILE_PREVIEW.INSERT_ERROR"), false);
5059
+ return;
5060
+ }
5061
+ this._fileContent = content;
5062
+ this.render();
5063
+ showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
5064
+ }
4414
5065
  render() {
4415
5066
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
4416
5067
  fileName: this.fileName,
@@ -4420,7 +5071,10 @@ var FilePreviewElement = class extends HTMLElement {
4420
5071
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
4421
5072
  itBlock: this.itBlock,
4422
5073
  interceptorsBlock: this.interceptorsBlock,
4423
- closeLabel: this.closeLabel
5074
+ closeLabel: this.closeLabel,
5075
+ isLocal: this.isLocal,
5076
+ runState: this._runState,
5077
+ runOutput: this._runOutput
4424
5078
  }, this.t.bind(this))}`;
4425
5079
  this.textarea = this.shadow.getElementById("editor");
4426
5080
  if (this.textarea) {
@@ -4436,6 +5090,7 @@ var FilePreviewElement = class extends HTMLElement {
4436
5090
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
4437
5091
  });
4438
5092
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
5093
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
4439
5094
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
4440
5095
  this.copyToClipboard(this.itBlock);
4441
5096
  });
@@ -4683,6 +5338,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
4683
5338
  interceptorsUnsub;
4684
5339
  pauseUnsub;
4685
5340
  selectorNotFoundUnsub;
5341
+ langUnsub;
5342
+ sessionUnsub;
5343
+ sessionSaveTimer;
4686
5344
  controlFirstTimeData = true;
4687
5345
  _previsualizerRef = null;
4688
5346
  httpMonitor;
@@ -4726,17 +5384,21 @@ var LibE2eRecorderElement = class extends HTMLElement {
4726
5384
  this.initSubscriptions();
4727
5385
  this.render();
4728
5386
  this.initVisibility();
5387
+ this.initSessionContinuity();
4729
5388
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4730
5389
  window.addEventListener("keydown", this.keydownHandler);
4731
5390
  }
4732
5391
  disconnectedCallback() {
4733
5392
  if (this._isDisabled) return;
4734
5393
  window.removeEventListener("keydown", this.keydownHandler);
5394
+ this.flushActiveSessionOnDisconnect();
4735
5395
  this.recordingUnsub?.();
4736
5396
  this.commandsUnsub?.();
4737
5397
  this.interceptorsUnsub?.();
4738
5398
  this.pauseUnsub?.();
4739
5399
  this.selectorNotFoundUnsub?.();
5400
+ this.langUnsub?.();
5401
+ this.sessionUnsub?.();
4740
5402
  this.httpMonitor?.uninstall();
4741
5403
  this.recording.destroy();
4742
5404
  }
@@ -4759,11 +5421,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4759
5421
  this.isRecording = val;
4760
5422
  if (!val && !this.controlFirstTimeData) {
4761
5423
  this.saveRecordingHistory();
5424
+ this.clearSessionPersistence();
4762
5425
  this.showSaveTestDialog();
4763
5426
  }
4764
5427
  this.controlFirstTimeData = false;
4765
5428
  this.render();
4766
5429
  });
5430
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4767
5431
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4768
5432
  this.cypressCommands = cmds;
4769
5433
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4779,6 +5443,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
4779
5443
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4780
5444
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4781
5445
  });
5446
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4782
5447
  }
4783
5448
  showSelectorPicker(target) {
4784
5449
  Promise.resolve().then(() => (init_selector_picker(), selector_picker_exports)).then(() => {
@@ -4807,6 +5472,119 @@ var LibE2eRecorderElement = class extends HTMLElement {
4807
5472
  if (strategy) this.recording.selectorStrategy = strategy;
4808
5473
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4809
5474
  }
5475
+ // ── cross-app session continuity (spec 006) ───────────────────────────────
5476
+ /**
5477
+ * On mount, detect a persisted live recording session and either resume it
5478
+ * silently (recent) or prompt continue/discard (stale). No-op when there is
5479
+ * no actively-recording session.
5480
+ */
5481
+ async initSessionContinuity() {
5482
+ if (this._isDisabled) return;
5483
+ const session = await this.persistence.getActiveSession();
5484
+ if (!session || !session.isRecording) return;
5485
+ const ttlMin = await this.getResumeTtlMinutes();
5486
+ const ageMs = Date.now() - session.updatedAt;
5487
+ if (ageMs <= ttlMin * 6e4) {
5488
+ this.resumeSessionState(session);
5489
+ } else {
5490
+ this.promptResumeOrDiscard(session);
5491
+ }
5492
+ }
5493
+ async getResumeTtlMinutes() {
5494
+ const config = await this.persistence.getConfig(RESUME_TTL_CONFIG_KEY);
5495
+ const raw = config?.[RESUME_TTL_CONFIG_KEY];
5496
+ const n = typeof raw === "number" ? raw : Number(raw);
5497
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_RESUME_TTL_MINUTES;
5498
+ }
5499
+ /** Rehydrates the recorder from a persisted session (no bootstrap re-emitted). */
5500
+ resumeSessionState(session) {
5501
+ this.recording.restoreSession(session);
5502
+ this.cypressCommands = session.commands;
5503
+ this.interceptors = session.interceptors;
5504
+ this.controlFirstTimeData = false;
5505
+ this.render();
5506
+ }
5507
+ async promptResumeOrDiscard(session) {
5508
+ const t = this.translation.translate.bind(this.translation);
5509
+ const result = await import_sweetalert2.default.fire({
5510
+ title: t("RECORDER.SESSION_RESUME_TITLE"),
5511
+ 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>`,
5512
+ showCancelButton: true,
5513
+ confirmButtonText: t("RECORDER.SESSION_CONTINUE_BTN"),
5514
+ cancelButtonText: t("RECORDER.SESSION_DISCARD_BTN"),
5515
+ color: "#e6edf3"
5516
+ });
5517
+ if (result && result.isConfirmed) {
5518
+ this.resumeSessionState(session);
5519
+ } else {
5520
+ this.discardSession();
5521
+ }
5522
+ }
5523
+ /** Debounced persistence of the live session; clears it the moment recording stops. */
5524
+ persistActiveSession(state) {
5525
+ if (this._isDisabled) return;
5526
+ if (!state.isRecording) {
5527
+ this.clearSessionPersistence();
5528
+ return;
5529
+ }
5530
+ this.writeSessionBreadcrumb(state);
5531
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5532
+ this.sessionSaveTimer = setTimeout(() => {
5533
+ const snap = this.recording.getSessionSnapshot();
5534
+ if (snap.isRecording) this.persistence.saveActiveSession(snap).catch(() => {
5535
+ });
5536
+ }, 300);
5537
+ }
5538
+ writeSessionBreadcrumb(state) {
5539
+ try {
5540
+ localStorage.setItem(ACTIVE_SESSION_BREADCRUMB_KEY, JSON.stringify({
5541
+ sessionId: state.sessionId,
5542
+ isRecording: state.isRecording,
5543
+ updatedAt: state.updatedAt
5544
+ }));
5545
+ } catch {
5546
+ }
5547
+ }
5548
+ flushActiveSessionOnDisconnect() {
5549
+ const snap = this.recording.getSessionSnapshot();
5550
+ if (!snap.isRecording) return;
5551
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5552
+ this.writeSessionBreadcrumb(snap);
5553
+ this.persistence.saveActiveSession(snap).catch(() => {
5554
+ });
5555
+ }
5556
+ clearSessionPersistence() {
5557
+ if (this.sessionSaveTimer) {
5558
+ clearTimeout(this.sessionSaveTimer);
5559
+ this.sessionSaveTimer = void 0;
5560
+ }
5561
+ try {
5562
+ localStorage.removeItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5563
+ } catch {
5564
+ }
5565
+ this.persistence.clearActiveSession().catch(() => {
5566
+ });
5567
+ }
5568
+ /** True when a synchronous breadcrumb marks an active recording session. */
5569
+ hasActiveSession() {
5570
+ try {
5571
+ const raw = localStorage.getItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5572
+ if (!raw) return false;
5573
+ return !!JSON.parse(raw).isRecording;
5574
+ } catch {
5575
+ return false;
5576
+ }
5577
+ }
5578
+ /** Programmatically resume the persisted session, if any. */
5579
+ resumeSession() {
5580
+ this.persistence.getActiveSession().then((s) => {
5581
+ if (s) this.resumeSessionState(s);
5582
+ });
5583
+ }
5584
+ /** Discard the persisted live session (breadcrumb + DB record). */
5585
+ discardSession() {
5586
+ this.clearSessionPersistence();
5587
+ }
4810
5588
  toggle() {
4811
5589
  this.recording.toggleRecording();
4812
5590
  }
@@ -5177,7 +5955,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5177
5955
  e.detail.fileName,
5178
5956
  e.detail.testId,
5179
5957
  e.detail.itBlock,
5180
- e.detail.interceptorsBlock
5958
+ e.detail.interceptorsBlock,
5959
+ e.detail.notes
5181
5960
  ), 150);
5182
5961
  });
5183
5962
  },
@@ -5191,7 +5970,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5191
5970
  }, 0);
5192
5971
  });
5193
5972
  }
5194
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
5973
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
5195
5974
  import_sweetalert2.default.fire({
5196
5975
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
5197
5976
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -5212,6 +5991,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5212
5991
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
5213
5992
  child.itBlock = itBlock;
5214
5993
  child.interceptorsBlock = interceptorsBlock;
5994
+ child.notes = notes;
5215
5995
  container.appendChild(child);
5216
5996
  child.addEventListener("close", () => {
5217
5997
  import_sweetalert2.default.close();
@@ -5287,24 +6067,29 @@ if (!customElements.get("lib-e2e-recorder")) {
5287
6067
  }
5288
6068
 
5289
6069
  // src/index.ts
5290
- var VERSION = "0.1.0";
6070
+ var VERSION = "0.5.0";
5291
6071
  // Annotate the CommonJS export names for ESM import in node:
5292
6072
  0 && (module.exports = {
6073
+ ACTIVE_SESSION_BREADCRUMB_KEY,
5293
6074
  AdvancedTestEditorElement,
5294
6075
  AdvancedTestTransformationService,
5295
6076
  ConfigurationElement,
5296
6077
  DB_SCHEMA,
5297
6078
  DB_STORE_NAMES,
6079
+ DEFAULT_RESUME_TTL_MINUTES,
5298
6080
  FilePreviewElement,
5299
6081
  HttpMonitor,
5300
6082
  INPUT_TYPES,
5301
6083
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
6084
+ LOCALE_BY_LANG,
5302
6085
  LibE2eRecorderElement,
5303
6086
  PersistenceService,
6087
+ RESUME_TTL_CONFIG_KEY,
5304
6088
  RecordingService,
5305
6089
  SCROLLBAR_STYLES,
5306
6090
  SUPPORTED_LANGS,
5307
6091
  SaveTestElement,
6092
+ SelectorPickerElement,
5308
6093
  Subject,
5309
6094
  TestEditorElement,
5310
6095
  TestPrevisualizerElement,
@@ -5315,10 +6100,13 @@ var VERSION = "0.1.0";
5315
6100
  generateAlias,
5316
6101
  injectStyles,
5317
6102
  isLang,
6103
+ isLocalHost,
6104
+ localeForLang,
5318
6105
  makeModalResizable,
5319
6106
  makeSwalDraggable,
5320
6107
  makeSwalDraggableByContentId,
5321
6108
  persistenceService,
6109
+ selectTestsForExport,
5322
6110
  setSwal2DataCyAttribute,
5323
6111
  showToast,
5324
6112
  transformationService,