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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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: {
@@ -578,7 +636,9 @@ var I18N_ES = {
578
636
  NO_TAGS: "Sin etiquetas",
579
637
  SAVE_BTN: "\u{1F4BE} Guardar",
580
638
  SAVE_AND_EDIT: "\u{1F4DD} Guardar y editar",
581
- REMOVE_TAG_TITLE: "Quitar"
639
+ REMOVE_TAG_TITLE: "Quitar",
640
+ NOTES_LABEL: "Descripci\xF3n / Notas (opcional):",
641
+ NOTES_PLACEHOLDER: "Qu\xE9 valida este test, condiciones previas, etc."
582
642
  },
583
643
  TEST_EDITOR: {
584
644
  NO_TAGS: "Sin etiquetas",
@@ -588,8 +648,6 @@ var I18N_ES = {
588
648
  COPY_DESCRIBE: "\u{1F5C2} Copiar describe()",
589
649
  MULTI_SELECT: "\u{1F5C2} Multi-select",
590
650
  CANCEL_SELECT: "\u2715 Cancelar",
591
- SECTION_COMMANDS: "Comandos",
592
- SECTION_INTERCEPTORS: "Interceptores",
593
651
  COPY_CMDS_BTN: "\u{1F4CB} Copiar comandos",
594
652
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
595
653
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -618,6 +676,18 @@ var I18N_ES = {
618
676
  DATA_SECTION: "\u{1F4BE} Datos",
619
677
  DATA_DESC: "Exporta todos tus tests a JSON o importa una copia de seguridad.",
620
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:",
621
691
  IMPORT_BTN: "\u2B07\uFE0F Importar tests",
622
692
  FOLDER_UPDATED_TOAST: "\u2713 Carpeta de Cypress actualizada",
623
693
  FOLDER_ERROR_TOAST: "Error al acceder a la carpeta",
@@ -627,7 +697,13 @@ var I18N_ES = {
627
697
  JSON_BAD_FORMAT: "El archivo no tiene el formato esperado.",
628
698
  SMART_SELECTOR_SECTION: "\u{1F50D} Selector inteligente",
629
699
  SMART_SELECTOR_TITLE: "Selector inteligente",
630
- SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido."
700
+ SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido.",
701
+ START_HIDDEN_SECTION: "\u{1F441} Visibilidad del widget",
702
+ START_HIDDEN_TITLE: "Iniciar oculto",
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."
631
707
  },
632
708
  SELECTOR_PICKER: {
633
709
  TITLE: "Selecciona un elemento del DOM",
@@ -668,8 +744,10 @@ var I18N_ES = {
668
744
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
669
745
  CLOSE_BTN: "\u2715 Cerrar",
670
746
  NEW_FILE_BTN: "+ Nuevo archivo",
747
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
671
748
  REFRESH_BTN: "\u21BB Actualizar",
672
749
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
750
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
673
751
  NEW_FILE_CONFIRM: "Crear",
674
752
  NEW_FILE_CANCEL: "Cancelar"
675
753
  },
@@ -682,10 +760,19 @@ var I18N_ES = {
682
760
  NO_CHANGES: "Sin cambios respecto al original",
683
761
  NO_CHANGES_SHORT: "Sin cambios",
684
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?)",
685
768
  DIFF_BTN: "\u{1F4CA} Diff",
686
769
  SAVE_BTN: "\u{1F4BE} Guardar",
687
770
  CLOSE_BTN: "\u2715 Cerrar",
688
- 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"
689
776
  },
690
777
  RECORDER: {
691
778
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -714,7 +801,12 @@ var I18N_ES = {
714
801
  BADGE_PAUSED: "\u23F8 PAUSA",
715
802
  BADGE_REC: "\u25CF REC",
716
803
  STOP_TITLE: "Detener (Ctrl+R)",
717
- 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"
718
810
  }
719
811
  };
720
812
 
@@ -740,7 +832,9 @@ var I18N_EN = {
740
832
  NO_TAGS: "No tags",
741
833
  SAVE_BTN: "\u{1F4BE} Save",
742
834
  SAVE_AND_EDIT: "\u{1F4DD} Save and edit",
743
- REMOVE_TAG_TITLE: "Remove"
835
+ REMOVE_TAG_TITLE: "Remove",
836
+ NOTES_LABEL: "Description / Notes (optional):",
837
+ NOTES_PLACEHOLDER: "What this test validates, preconditions, etc."
744
838
  },
745
839
  TEST_EDITOR: {
746
840
  NO_TAGS: "No tags",
@@ -750,8 +844,6 @@ var I18N_EN = {
750
844
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
751
845
  MULTI_SELECT: "\u{1F5C2} Multi-select",
752
846
  CANCEL_SELECT: "\u2715 Cancel",
753
- SECTION_COMMANDS: "Commands",
754
- SECTION_INTERCEPTORS: "Interceptors",
755
847
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
756
848
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
757
849
  DEFAULT_DESCRIBE: "Test suite",
@@ -780,6 +872,18 @@ var I18N_EN = {
780
872
  DATA_SECTION: "\u{1F4BE} Data",
781
873
  DATA_DESC: "Export all your tests to JSON or import a backup.",
782
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:",
783
887
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
784
888
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
785
889
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -789,7 +893,13 @@ var I18N_EN = {
789
893
  JSON_BAD_FORMAT: "The file does not have the expected format.",
790
894
  SMART_SELECTOR_SECTION: "\u{1F50D} Smart selector",
791
895
  SMART_SELECTOR_TITLE: "Smart selector",
792
- SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector."
896
+ SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
897
+ START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
898
+ START_HIDDEN_TITLE: "Start hidden",
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."
793
903
  },
794
904
  SELECTOR_PICKER: {
795
905
  TITLE: "Select a DOM element",
@@ -830,8 +940,10 @@ var I18N_EN = {
830
940
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
831
941
  CLOSE_BTN: "\u2715 Close",
832
942
  NEW_FILE_BTN: "+ New file",
943
+ NEW_FOLDER_BTN: "+ New folder",
833
944
  REFRESH_BTN: "\u21BB Refresh",
834
945
  NEW_FILE_PLACEHOLDER: "test-name",
946
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
835
947
  NEW_FILE_CONFIRM: "Create",
836
948
  NEW_FILE_CANCEL: "Cancel"
837
949
  },
@@ -844,10 +956,19 @@ var I18N_EN = {
844
956
  NO_CHANGES: "No changes from original",
845
957
  NO_CHANGES_SHORT: "No changes",
846
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?)",
847
964
  DIFF_BTN: "\u{1F4CA} Diff",
848
965
  SAVE_BTN: "\u{1F4BE} Save",
849
966
  CLOSE_BTN: "\u2715 Close",
850
- 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"
851
972
  },
852
973
  RECORDER: {
853
974
  FS_TITLE: "\u{1F4C1} File access",
@@ -876,7 +997,12 @@ var I18N_EN = {
876
997
  BADGE_PAUSED: "\u23F8 PAUSED",
877
998
  BADGE_REC: "\u25CF REC",
878
999
  STOP_TITLE: "Stop (Ctrl+R)",
879
- 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"
880
1006
  }
881
1007
  };
882
1008
 
@@ -902,7 +1028,9 @@ var I18N_FR = {
902
1028
  NO_TAGS: "Sans \xE9tiquettes",
903
1029
  SAVE_BTN: "\u{1F4BE} Enregistrer",
904
1030
  SAVE_AND_EDIT: "\u{1F4DD} Enregistrer et \xE9diter",
905
- REMOVE_TAG_TITLE: "Retirer"
1031
+ REMOVE_TAG_TITLE: "Retirer",
1032
+ NOTES_LABEL: "Description / Notes (optionnel) :",
1033
+ NOTES_PLACEHOLDER: "Ce que ce test valide, conditions pr\xE9alables, etc."
906
1034
  },
907
1035
  TEST_EDITOR: {
908
1036
  NO_TAGS: "Sans \xE9tiquettes",
@@ -912,8 +1040,6 @@ var I18N_FR = {
912
1040
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
913
1041
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
914
1042
  CANCEL_SELECT: "\u2715 Annuler",
915
- SECTION_COMMANDS: "Commandes",
916
- SECTION_INTERCEPTORS: "Intercepteurs",
917
1043
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
918
1044
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
919
1045
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -942,6 +1068,18 @@ var I18N_FR = {
942
1068
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
943
1069
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
944
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 :",
945
1083
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
946
1084
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
947
1085
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -951,7 +1089,13 @@ var I18N_FR = {
951
1089
  JSON_BAD_FORMAT: "Le fichier n'a pas le format attendu.",
952
1090
  SMART_SELECTOR_SECTION: "\u{1F50D} S\xE9lecteur intelligent",
953
1091
  SMART_SELECTOR_TITLE: "S\xE9lecteur intelligent",
954
- SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide."
1092
+ SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
1093
+ START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
1094
+ START_HIDDEN_TITLE: "D\xE9marrer masqu\xE9",
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."
955
1099
  },
956
1100
  SELECTOR_PICKER: {
957
1101
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -992,8 +1136,10 @@ var I18N_FR = {
992
1136
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
993
1137
  CLOSE_BTN: "\u2715 Fermer",
994
1138
  NEW_FILE_BTN: "+ Nouveau fichier",
1139
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
995
1140
  REFRESH_BTN: "\u21BB Actualiser",
996
1141
  NEW_FILE_PLACEHOLDER: "nom-du-test",
1142
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
997
1143
  NEW_FILE_CONFIRM: "Cr\xE9er",
998
1144
  NEW_FILE_CANCEL: "Annuler"
999
1145
  },
@@ -1006,10 +1152,19 @@ var I18N_FR = {
1006
1152
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
1007
1153
  NO_CHANGES_SHORT: "Aucun changement",
1008
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 ?)",
1009
1160
  DIFF_BTN: "\u{1F4CA} Diff",
1010
1161
  SAVE_BTN: "\u{1F4BE} Enregistrer",
1011
1162
  CLOSE_BTN: "\u2715 Fermer",
1012
- 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"
1013
1168
  },
1014
1169
  RECORDER: {
1015
1170
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -1038,7 +1193,12 @@ var I18N_FR = {
1038
1193
  BADGE_PAUSED: "\u23F8 PAUSE",
1039
1194
  BADGE_REC: "\u25CF REC",
1040
1195
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
1041
- 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"
1042
1202
  }
1043
1203
  };
1044
1204
 
@@ -1064,7 +1224,9 @@ var I18N_IT = {
1064
1224
  NO_TAGS: "Nessuna etichetta",
1065
1225
  SAVE_BTN: "\u{1F4BE} Salva",
1066
1226
  SAVE_AND_EDIT: "\u{1F4DD} Salva e modifica",
1067
- REMOVE_TAG_TITLE: "Rimuovi"
1227
+ REMOVE_TAG_TITLE: "Rimuovi",
1228
+ NOTES_LABEL: "Descrizione / Note (opzionale):",
1229
+ NOTES_PLACEHOLDER: "Cosa valida questo test, condizioni preliminari, ecc."
1068
1230
  },
1069
1231
  TEST_EDITOR: {
1070
1232
  NO_TAGS: "Nessuna etichetta",
@@ -1074,8 +1236,6 @@ var I18N_IT = {
1074
1236
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
1075
1237
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
1076
1238
  CANCEL_SELECT: "\u2715 Annulla",
1077
- SECTION_COMMANDS: "Comandi",
1078
- SECTION_INTERCEPTORS: "Interceptor",
1079
1239
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
1080
1240
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
1081
1241
  DEFAULT_DESCRIBE: "Suite di test",
@@ -1104,6 +1264,18 @@ var I18N_IT = {
1104
1264
  DATA_SECTION: "\u{1F4BE} Dati",
1105
1265
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
1106
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:",
1107
1279
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
1108
1280
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
1109
1281
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -1113,7 +1285,13 @@ var I18N_IT = {
1113
1285
  JSON_BAD_FORMAT: "Il file non ha il formato previsto.",
1114
1286
  SMART_SELECTOR_SECTION: "\u{1F50D} Selettore intelligente",
1115
1287
  SMART_SELECTOR_TITLE: "Selettore intelligente",
1116
- SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido."
1288
+ SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
1289
+ START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
1290
+ START_HIDDEN_TITLE: "Avvia nascosto",
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."
1117
1295
  },
1118
1296
  SELECTOR_PICKER: {
1119
1297
  TITLE: "Seleziona un elemento DOM",
@@ -1154,8 +1332,10 @@ var I18N_IT = {
1154
1332
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
1155
1333
  CLOSE_BTN: "\u2715 Chiudi",
1156
1334
  NEW_FILE_BTN: "+ Nuovo file",
1335
+ NEW_FOLDER_BTN: "+ Nuova cartella",
1157
1336
  REFRESH_BTN: "\u21BB Aggiorna",
1158
1337
  NEW_FILE_PLACEHOLDER: "nome-del-test",
1338
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
1159
1339
  NEW_FILE_CONFIRM: "Crea",
1160
1340
  NEW_FILE_CANCEL: "Annulla"
1161
1341
  },
@@ -1168,10 +1348,19 @@ var I18N_IT = {
1168
1348
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
1169
1349
  NO_CHANGES_SHORT: "Nessuna modifica",
1170
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?)",
1171
1356
  DIFF_BTN: "\u{1F4CA} Diff",
1172
1357
  SAVE_BTN: "\u{1F4BE} Salva",
1173
1358
  CLOSE_BTN: "\u2715 Chiudi",
1174
- 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"
1175
1364
  },
1176
1365
  RECORDER: {
1177
1366
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -1200,7 +1389,12 @@ var I18N_IT = {
1200
1389
  BADGE_PAUSED: "\u23F8 PAUSA",
1201
1390
  BADGE_REC: "\u25CF REC",
1202
1391
  STOP_TITLE: "Ferma (Ctrl+R)",
1203
- 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"
1204
1398
  }
1205
1399
  };
1206
1400
 
@@ -1226,7 +1420,9 @@ var I18N_DE = {
1226
1420
  NO_TAGS: "Keine Tags",
1227
1421
  SAVE_BTN: "\u{1F4BE} Speichern",
1228
1422
  SAVE_AND_EDIT: "\u{1F4DD} Speichern und bearbeiten",
1229
- REMOVE_TAG_TITLE: "Entfernen"
1423
+ REMOVE_TAG_TITLE: "Entfernen",
1424
+ NOTES_LABEL: "Beschreibung / Notizen (optional):",
1425
+ NOTES_PLACEHOLDER: "Was dieser Test validiert, Voraussetzungen usw."
1230
1426
  },
1231
1427
  TEST_EDITOR: {
1232
1428
  NO_TAGS: "Keine Tags",
@@ -1236,8 +1432,6 @@ var I18N_DE = {
1236
1432
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
1237
1433
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
1238
1434
  CANCEL_SELECT: "\u2715 Abbrechen",
1239
- SECTION_COMMANDS: "Befehle",
1240
- SECTION_INTERCEPTORS: "Interceptors",
1241
1435
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
1242
1436
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
1243
1437
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -1266,6 +1460,18 @@ var I18N_DE = {
1266
1460
  DATA_SECTION: "\u{1F4BE} Daten",
1267
1461
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
1268
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:",
1269
1475
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
1270
1476
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
1271
1477
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -1275,7 +1481,13 @@ var I18N_DE = {
1275
1481
  JSON_BAD_FORMAT: "Die Datei hat nicht das erwartete Format.",
1276
1482
  SMART_SELECTOR_SECTION: "\u{1F50D} Intelligenter Selektor",
1277
1483
  SMART_SELECTOR_TITLE: "Intelligenter Selektor",
1278
- SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird."
1484
+ SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
1485
+ START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
1486
+ START_HIDDEN_TITLE: "Versteckt starten",
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."
1279
1491
  },
1280
1492
  SELECTOR_PICKER: {
1281
1493
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -1316,8 +1528,10 @@ var I18N_DE = {
1316
1528
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
1317
1529
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1318
1530
  NEW_FILE_BTN: "+ Neue Datei",
1531
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
1319
1532
  REFRESH_BTN: "\u21BB Aktualisieren",
1320
1533
  NEW_FILE_PLACEHOLDER: "test-name",
1534
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
1321
1535
  NEW_FILE_CONFIRM: "Erstellen",
1322
1536
  NEW_FILE_CANCEL: "Abbrechen"
1323
1537
  },
@@ -1330,10 +1544,19 @@ var I18N_DE = {
1330
1544
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
1331
1545
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
1332
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?)",
1333
1552
  DIFF_BTN: "\u{1F4CA} Diff",
1334
1553
  SAVE_BTN: "\u{1F4BE} Speichern",
1335
1554
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1336
- 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"
1337
1560
  },
1338
1561
  RECORDER: {
1339
1562
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -1362,13 +1585,18 @@ var I18N_DE = {
1362
1585
  BADGE_PAUSED: "\u23F8 PAUSE",
1363
1586
  BADGE_REC: "\u25CF REC",
1364
1587
  STOP_TITLE: "Stopp (Ctrl+R)",
1365
- 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"
1366
1594
  }
1367
1595
  };
1368
1596
 
1369
1597
  // src/services/translation.service.ts
1370
1598
  var TranslationService = class {
1371
- lang;
1599
+ lang$;
1372
1600
  translations = {
1373
1601
  es: I18N_ES,
1374
1602
  en: I18N_EN,
@@ -1377,17 +1605,21 @@ var TranslationService = class {
1377
1605
  de: I18N_DE
1378
1606
  };
1379
1607
  constructor() {
1380
- this.lang = this.detectLang();
1608
+ this.lang$ = new Subject(this.detectLang());
1381
1609
  }
1382
1610
  setLang(lang) {
1383
- this.lang = lang;
1611
+ this.lang$.next(lang);
1384
1612
  }
1385
1613
  getLang() {
1386
- 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);
1387
1619
  }
1388
1620
  translate(key) {
1389
1621
  const keys = key.split(".");
1390
- let value = this.translations[this.lang];
1622
+ let value = this.translations[this.lang$.getValue()];
1391
1623
  for (const k of keys) {
1392
1624
  value = value?.[k];
1393
1625
  if (value === void 0) return key;
@@ -1401,14 +1633,41 @@ var TranslationService = class {
1401
1633
  };
1402
1634
  var translationService = new TranslationService();
1403
1635
 
1636
+ // src/utils/code-format.utils.ts
1637
+ function gcd(a, b) {
1638
+ while (b) {
1639
+ const t = b;
1640
+ b = a % b;
1641
+ a = t;
1642
+ }
1643
+ return a;
1644
+ }
1645
+ function escapeSingleQuotes(value) {
1646
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1647
+ }
1648
+ function normalizeBlock(code, baseIndent) {
1649
+ const lines = code.split("\n").map((l) => l.trimEnd());
1650
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
1651
+ if (!nonEmpty.length) return "";
1652
+ const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
1653
+ const minIndent = Math.min(...indents);
1654
+ const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
1655
+ const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
1656
+ return lines.map((l) => {
1657
+ if (!l.trim()) return "";
1658
+ const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
1659
+ return baseIndent + " ".repeat(level) + l.trimStart();
1660
+ }).join("\n");
1661
+ }
1662
+
1404
1663
  // src/services/transformation.service.ts
1405
1664
  var TransformationService = class {
1406
1665
  toLang(lang) {
1407
1666
  return isLang(lang) ? lang : "en";
1408
1667
  }
1409
1668
  generateItDescription(description, commands) {
1410
- const commandsBlock = commands.map((cmd) => ` ${cmd}`).join("\n");
1411
- return `it('${description}', () => {
1669
+ const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
1670
+ return `it('${escapeSingleQuotes(description)}', () => {
1412
1671
  ${commandsBlock}
1413
1672
  });`;
1414
1673
  }
@@ -1438,6 +1697,13 @@ ${interceptors} })
1438
1697
  }
1439
1698
  return content.slice(0, idx) + "\n" + itBlock + "\n" + content.slice(idx);
1440
1699
  }
1700
+ buildBlockComment(notes) {
1701
+ if (!notes.trim()) return "";
1702
+ const lines = notes.split("\n").map((l) => ` * ${l}`).join("\n");
1703
+ return `/**
1704
+ ${lines}
1705
+ */`;
1706
+ }
1441
1707
  isFile(file) {
1442
1708
  return !!file && file.kind === "file";
1443
1709
  }
@@ -1455,29 +1721,15 @@ ${interceptors} })
1455
1721
  };
1456
1722
  var advancedTestTransformationService = new AdvancedTestTransformationService();
1457
1723
 
1458
- // src/utils/subject.ts
1459
- var Subject = class {
1460
- _value;
1461
- listeners = /* @__PURE__ */ new Set();
1462
- constructor(initialValue) {
1463
- this._value = initialValue;
1464
- }
1465
- next(value) {
1466
- this._value = value;
1467
- this.listeners.forEach((l) => l(value));
1468
- }
1469
- getValue() {
1470
- return this._value;
1471
- }
1472
- subscribe(fn) {
1473
- this.listeners.add(fn);
1474
- return () => this.listeners.delete(fn);
1475
- }
1476
- };
1477
-
1478
1724
  // src/services/recording.service.ts
1479
1725
  init_selector_quality_utils();
1480
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
+ }
1481
1733
  var RecordingService = class {
1482
1734
  commands$ = new Subject([]);
1483
1735
  interceptors$ = new Subject([]);
@@ -1487,6 +1739,9 @@ var RecordingService = class {
1487
1739
  inputDebounceTimers = /* @__PURE__ */ new Map();
1488
1740
  abort = new AbortController();
1489
1741
  selectorStrategy = "data-cy";
1742
+ /** Stable id of the current live session (null when none has started). */
1743
+ sessionId = null;
1744
+ startedAt = 0;
1490
1745
  // Stored originals for history patching cleanup
1491
1746
  origPushState = history.pushState.bind(history);
1492
1747
  origReplaceState = history.replaceState.bind(history);
@@ -1498,12 +1753,42 @@ var RecordingService = class {
1498
1753
  }
1499
1754
  // ── Public API ────────────────────────────────────────────────────────────
1500
1755
  startRecording() {
1756
+ this.sessionId = createSessionId();
1757
+ this.startedAt = Date.now();
1501
1758
  this.isPaused$.next(false);
1502
1759
  this.isRecording$.next(true);
1503
1760
  this.addCommand(`cy.viewport(1900, 1200)`);
1504
1761
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1505
1762
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1506
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
+ }
1507
1792
  stopRecording() {
1508
1793
  this.isPaused$.next(false);
1509
1794
  this.isRecording$.next(false);
@@ -1590,6 +1875,21 @@ var RecordingService = class {
1590
1875
  onPauseChange(fn) {
1591
1876
  return this.isPaused$.subscribe(fn);
1592
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
+ }
1593
1893
  onSelectorNotFound(fn) {
1594
1894
  return this.selectorNotFound$.subscribe((v) => {
1595
1895
  if (v) fn(v.target, v.action);
@@ -1821,6 +2121,7 @@ var RecordingService = class {
1821
2121
 
1822
2122
  // src/services/persistence.service.ts
1823
2123
  var import_idb = require("idb");
2124
+ var ACTIVE_SESSION_ID = 1;
1824
2125
  var PersistenceService = class {
1825
2126
  // dbName is overridable so tests can use unique names for isolation.
1826
2127
  constructor(dbName = DB_SCHEMA.name) {
@@ -1849,9 +2150,9 @@ var PersistenceService = class {
1849
2150
  return this._db;
1850
2151
  }
1851
2152
  // ── Tests ─────────────────────────────────────────────────────────────────
1852
- async insertTest(name, commands = [], interceptors = [], tags = []) {
2153
+ async insertTest(name, commands = [], interceptors = [], tags = [], notes) {
1853
2154
  const db = await this.getDB();
1854
- const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {} };
2155
+ const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {}, ...notes ? { notes } : {} };
1855
2156
  const id = await db.add("tests", record);
1856
2157
  if (commands.length) await this.insertCommands(commands, id);
1857
2158
  if (interceptors.length) await this.insertInterceptors(interceptors, id);
@@ -1874,10 +2175,10 @@ var PersistenceService = class {
1874
2175
  if (!record) return null;
1875
2176
  const commands = await this.getCommandStrings(testId);
1876
2177
  const interceptors = await this.getInterceptorStrings(testId);
1877
- const itBlock = `it('${record.name}', () => {
1878
- ${commands.join("\n ")}
2178
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
2179
+ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1879
2180
  });`;
1880
- const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => " " + i).join("\n") + "\n" : "";
2181
+ const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
1881
2182
  return { ...record, commands, interceptors, cypressCommands: commands, itBlock, interceptorsBlock };
1882
2183
  }
1883
2184
  async deleteTest(id) {
@@ -1955,6 +2256,25 @@ var PersistenceService = class {
1955
2256
  const records = await db.getAll("configuration");
1956
2257
  return records[0] ?? null;
1957
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
+ }
1958
2278
  // ── Bulk operations ───────────────────────────────────────────────────────
1959
2279
  async clearAllData() {
1960
2280
  const db = await this.getDB();
@@ -1965,10 +2285,18 @@ var PersistenceService = class {
1965
2285
  ]);
1966
2286
  }
1967
2287
  async ingestFileData(tests, interceptors) {
1968
- await Promise.all([
1969
- this.bulkInsertWithoutId("tests", tests),
1970
- this.bulkInsertWithoutId("interceptors", interceptors)
1971
- ]);
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);
1972
2300
  }
1973
2301
  async requestDirectoryPermissions() {
1974
2302
  if (!("showDirectoryPicker" in window)) {
@@ -2411,6 +2739,28 @@ function showToast(message, isSuccess = true) {
2411
2739
  setTimeout(() => toast.remove(), 3e3);
2412
2740
  }
2413
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
+
2414
2764
  // src/components/test-previsualizer/test-previsualizer.styles.ts
2415
2765
  var TEST_PREVISUALIZER_STYLES = `
2416
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; }
@@ -2617,7 +2967,8 @@ if (!customElements.get("test-previsualizer")) {
2617
2967
  var SAVE_TEST_STYLES = `
2618
2968
  :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; }
2619
2969
  * { box-sizing: border-box; }
2620
- .container { padding: 24px 28px; text-align: center; }
2970
+ .container { padding: 24px 28px; text-align: left; }
2971
+ .btn-row, p { text-align: center; }
2621
2972
  p { margin: 0 0 20px; font-size: 14px; color: #8b949e; line-height: 1.5; }
2622
2973
  .btn-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
2623
2974
  button {
@@ -2639,6 +2990,15 @@ var SAVE_TEST_STYLES = `
2639
2990
  }
2640
2991
  input[type="text"]:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.15); }
2641
2992
  input[type="text"]::placeholder { color: #484f58; }
2993
+ textarea {
2994
+ width: 100%; padding: 10px 14px; border: 1px solid #30363d;
2995
+ border-radius: 8px; background: #0d1117; color: #e6edf3;
2996
+ font-size: 14px; outline: none; margin-bottom: 4px;
2997
+ font-family: inherit; resize: vertical; line-height: 1.5;
2998
+ transition: border-color 0.15s, box-shadow 0.15s;
2999
+ }
3000
+ textarea:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.15); }
3001
+ textarea::placeholder { color: #484f58; }
2642
3002
  .tag-label {
2643
3003
  font-size: 11px; color: #484f58; text-align: left; margin-bottom: 5px; display: block;
2644
3004
  }
@@ -2678,7 +3038,7 @@ function renderSaveTestAsk(t) {
2678
3038
  </div>
2679
3039
  </div>`;
2680
3040
  }
2681
- function renderSaveTestDesc(description, tags, t) {
3041
+ function renderSaveTestDesc(description, notes, tags, t) {
2682
3042
  const chipsHtml = tags.map(
2683
3043
  (tag) => `<span class="chip">${escHtml(tag)}<button class="chip-del" data-tag="${escAttr(tag)}" title="${t("SAVE_TEST.REMOVE_TAG_TITLE")}">\u2715</button></span>`
2684
3044
  ).join("");
@@ -2687,6 +3047,8 @@ function renderSaveTestDesc(description, tags, t) {
2687
3047
  <p>${t("SAVE_TEST.DESC_LABEL")} (<code>it()</code>):</p>
2688
3048
  <input id="desc-input" type="text" placeholder="${t("SAVE_TEST.DESC_PLACEHOLDER")}"
2689
3049
  value="${escAttr(description)}" autocomplete="off" />
3050
+ <span class="tag-label">${t("SAVE_TEST.NOTES_LABEL")}</span>
3051
+ <textarea id="notes-input" rows="3" placeholder="${escAttr(t("SAVE_TEST.NOTES_PLACEHOLDER"))}">${escHtml(notes)}</textarea>
2690
3052
  <span class="tag-label">${t("SAVE_TEST.TAGS_LABEL")}</span>
2691
3053
  <div class="tag-input-row">
2692
3054
  <input id="tag-input" type="text" placeholder="${t("SAVE_TEST.TAGS_PLACEHOLDER")}" autocomplete="off" />
@@ -2706,6 +3068,7 @@ var SaveTestElement = class extends HTMLElement {
2706
3068
  shadow;
2707
3069
  _step = "ask";
2708
3070
  description = "";
3071
+ notes = "";
2709
3072
  tags = [];
2710
3073
  translation = translationService;
2711
3074
  constructor() {
@@ -2723,17 +3086,18 @@ var SaveTestElement = class extends HTMLElement {
2723
3086
  this.render();
2724
3087
  }
2725
3088
  confirmSave() {
2726
- this.dispatch("savetest", { description: this.description.trim(), tags: [...this.tags] });
3089
+ this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
2727
3090
  }
2728
3091
  confirmSaveAndExport() {
2729
- this.dispatch("saveandexport", { description: this.description.trim(), tags: [...this.tags] });
3092
+ this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
2730
3093
  }
2731
3094
  cancel() {
2732
- this.dispatch("savetest", { description: null, tags: [] });
3095
+ this.dispatch("savetest", { description: null, notes: "", tags: [] });
2733
3096
  }
2734
3097
  restartComponent() {
2735
3098
  this._step = "ask";
2736
3099
  this.description = "";
3100
+ this.notes = "";
2737
3101
  this.tags = [];
2738
3102
  this.render();
2739
3103
  }
@@ -2760,12 +3124,16 @@ var SaveTestElement = class extends HTMLElement {
2760
3124
  this.shadow.getElementById("btn-yes")?.addEventListener("click", () => this.askSave());
2761
3125
  this.shadow.getElementById("btn-no")?.addEventListener("click", () => this.cancel());
2762
3126
  } else {
2763
- this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.tags, this.t.bind(this))}`;
3127
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.t.bind(this))}`;
2764
3128
  const descInput = this.shadow.getElementById("desc-input");
3129
+ const notesInput = this.shadow.getElementById("notes-input");
2765
3130
  const tagInput = this.shadow.getElementById("tag-input");
2766
3131
  descInput.addEventListener("input", () => {
2767
3132
  this.description = descInput.value;
2768
3133
  });
3134
+ notesInput.addEventListener("input", () => {
3135
+ this.notes = notesInput.value;
3136
+ });
2769
3137
  const tryAddTag = () => {
2770
3138
  this.addTag(tagInput.value);
2771
3139
  tagInput.value = "";
@@ -2871,19 +3239,131 @@ var TEST_EDITOR_STYLES = `
2871
3239
  background: #0d1117; padding: 10px 14px;
2872
3240
  border-top: 1px solid #21262d;
2873
3241
  }
2874
- .section-title { font-size: 10px; color: #484f58; text-transform: uppercase;
2875
- letter-spacing: 0.8px; margin-bottom: 6px; font-weight: 600; }
2876
- .cmd-list { font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
2877
- font-size: 11px; color: #c9d1d9; line-height: 1.8; }
2878
- .icp-list { font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
2879
- font-size: 11px; color: #3fb950; line-height: 1.8; margin-top: 10px; }
2880
- .copy-row { display: flex; gap: 6px; margin-top: 10px; }
3242
+ .test-notes {
3243
+ margin: 0 0 10px; padding: 8px 12px;
3244
+ background: rgba(47,129,247,0.06); border-left: 3px solid rgba(47,129,247,0.4);
3245
+ border-radius: 0 6px 6px 0; font-size: 12px; color: #8b949e;
3246
+ line-height: 1.6; white-space: pre-wrap; word-break: break-word;
3247
+ }
3248
+ .code-preview {
3249
+ margin: 0; padding: 10px 12px; background: #161b22;
3250
+ border-radius: 6px; border: 1px solid #21262d;
3251
+ font-size: 11px; color: #c9d1d9; line-height: 1.7;
3252
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
3253
+ overflow-x: auto; white-space: pre;
3254
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3255
+ }
3256
+ .code-preview::-webkit-scrollbar { height: 4px; }
3257
+ .code-preview::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
3258
+ .code-preview-icp { margin-top: 8px; }
3259
+ .sh-kw { color: #ff7b72; }
3260
+ .sh-bi { color: #d2a8ff; }
3261
+ .sh-str { color: #a5d6ff; }
3262
+ .sh-cmt { color: #8b949e; font-style: italic; }
3263
+ .sh-num { color: #79c0ff; }
2881
3264
  `;
2882
3265
 
2883
3266
  // src/components/test-editor/test-editor.template.ts
2884
3267
  init_html_utils();
3268
+
3269
+ // src/utils/syntax-highlight.utils.ts
3270
+ init_html_utils();
3271
+ var KEYWORDS = /* @__PURE__ */ new Set([
3272
+ "describe",
3273
+ "it",
3274
+ "xit",
3275
+ "fit",
3276
+ "xdescribe",
3277
+ "fdescribe",
3278
+ "beforeEach",
3279
+ "afterEach",
3280
+ "before",
3281
+ "after",
3282
+ "context",
3283
+ "const",
3284
+ "let",
3285
+ "var",
3286
+ "function",
3287
+ "return",
3288
+ "if",
3289
+ "else",
3290
+ "import",
3291
+ "export",
3292
+ "from",
3293
+ "new",
3294
+ "this",
3295
+ "null",
3296
+ "undefined",
3297
+ "true",
3298
+ "false",
3299
+ "async",
3300
+ "await",
3301
+ "of",
3302
+ "in",
3303
+ "typeof",
3304
+ "void"
3305
+ ]);
3306
+ var BUILTINS = /* @__PURE__ */ new Set(["cy", "expect", "assert", "Cypress", "chai"]);
3307
+ function syntaxHighlight(code) {
3308
+ return code.split("\n").map(highlightLine).join("\n");
3309
+ }
3310
+ function highlightLine(line) {
3311
+ let result = "";
3312
+ let i = 0;
3313
+ const len = line.length;
3314
+ while (i < len) {
3315
+ if (line[i] === "/" && i + 1 < len && line[i + 1] === "/") {
3316
+ result += `<span class="sh-cmt">${escHtml(line.slice(i))}</span>`;
3317
+ return result;
3318
+ }
3319
+ const q = line[i];
3320
+ if (q === "'" || q === '"' || q === "`") {
3321
+ let j = i + 1;
3322
+ while (j < len) {
3323
+ if (line[j] === "\\") {
3324
+ j += 2;
3325
+ continue;
3326
+ }
3327
+ if (line[j] === q) {
3328
+ j++;
3329
+ break;
3330
+ }
3331
+ j++;
3332
+ }
3333
+ result += `<span class="sh-str">${escHtml(line.slice(i, j))}</span>`;
3334
+ i = j;
3335
+ continue;
3336
+ }
3337
+ if (/[0-9]/.test(line[i]) && (i === 0 || !/[a-zA-Z_$]/.test(line[i - 1]))) {
3338
+ let j = i;
3339
+ while (j < len && /[0-9.]/.test(line[j])) j++;
3340
+ result += `<span class="sh-num">${escHtml(line.slice(i, j))}</span>`;
3341
+ i = j;
3342
+ continue;
3343
+ }
3344
+ if (/[a-zA-Z_$]/.test(line[i])) {
3345
+ let j = i;
3346
+ while (j < len && /[a-zA-Z0-9_$]/.test(line[j])) j++;
3347
+ const word = line.slice(i, j);
3348
+ if (KEYWORDS.has(word)) {
3349
+ result += `<span class="sh-kw">${escHtml(word)}</span>`;
3350
+ } else if (BUILTINS.has(word)) {
3351
+ result += `<span class="sh-bi">${escHtml(word)}</span>`;
3352
+ } else {
3353
+ result += escHtml(word);
3354
+ }
3355
+ i = j;
3356
+ continue;
3357
+ }
3358
+ result += escHtml(line[i]);
3359
+ i++;
3360
+ }
3361
+ return result;
3362
+ }
3363
+
3364
+ // src/components/test-editor/test-editor.template.ts
2885
3365
  function renderTestEditor(state, t) {
2886
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
3366
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
2887
3367
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
2888
3368
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
2889
3369
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -2894,22 +3374,24 @@ function renderTestEditor(state, t) {
2894
3374
  </div>` : "";
2895
3375
  const rows = visible.map((test, i) => {
2896
3376
  const expanded = expandedIndex === i;
2897
- 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" });
2898
3378
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
2899
3379
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
2900
3380
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
3381
+ const hasIcps = (test.interceptors ?? []).length > 0;
3382
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
3383
+ ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
3384
+ });`;
3385
+ const icpBlockCode = icps.length ? `beforeEach(() => {
3386
+ // Auto-generated Cypress interceptors
3387
+ ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
3388
+ });` : "";
3389
+ const notesHtml = expanded && test.notes ? `<p class="test-notes">${escHtml(test.notes)}</p>` : "";
2901
3390
  const body = expanded ? `
2902
3391
  <div class="row-body">
2903
- <div class="section-title">${t("TEST_EDITOR.SECTION_COMMANDS")} (${(test.commands ?? []).length})</div>
2904
- <div class="cmd-list">${(test.commands ?? []).map(escHtml).join("<br>")}</div>
2905
- ${icps.length ? `<div class="icp-list">
2906
- <div class="section-title" style="margin-top:8px">${t("TEST_EDITOR.SECTION_INTERCEPTORS")}</div>
2907
- ${icps.map(escHtml).join("<br>")}
2908
- </div>` : ""}
2909
- <div class="copy-row">
2910
- <button class="btn-icon" data-action="copy-cmds" data-idx="${i}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
2911
- ${icps.length ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
2912
- </div>
3392
+ ${notesHtml}
3393
+ <pre class="code-preview">${syntaxHighlight(itBlockCode)}</pre>
3394
+ ${icpBlockCode ? `<pre class="code-preview code-preview-icp">${syntaxHighlight(icpBlockCode)}</pre>` : ""}
2913
3395
  </div>` : "";
2914
3396
  return `
2915
3397
  <div class="row${selectedIds.has(test.id) ? " selected-row" : ""}">
@@ -2918,6 +3400,8 @@ function renderTestEditor(state, t) {
2918
3400
  <span class="test-name">${escHtml(test.name)}</span>
2919
3401
  ${tagsHtml}
2920
3402
  <span class="test-date">${date}</span>
3403
+ <button class="btn-icon" data-action="copy-cmds" data-idx="${i}" title="${t("TEST_EDITOR.COPY_CMDS_BTN")}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
3404
+ ${hasIcps ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}" title="${t("TEST_EDITOR.COPY_ICPS_BTN")}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
2921
3405
  <button class="btn-icon btn-del" data-action="delete" data-id="${test.id}" title="${t("TEST_EDITOR.DELETE_TITLE")}">\u{1F5D1}</button>
2922
3406
  </div>
2923
3407
  ${body}
@@ -3003,11 +3487,11 @@ var TestEditorElement = class extends HTMLElement {
3003
3487
  ` : "";
3004
3488
  const itBlocks = selected.map((t) => {
3005
3489
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
3006
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
3490
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
3007
3491
  ${cmds}
3008
3492
  });`;
3009
3493
  }).join("\n\n");
3010
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
3494
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
3011
3495
  ${beforeEach}${itBlocks}
3012
3496
  });`;
3013
3497
  navigator.clipboard?.writeText(block);
@@ -3041,7 +3525,8 @@ ${beforeEach}${itBlocks}
3041
3525
  selectedIds: this.selectedIds,
3042
3526
  describeName: this.describeName,
3043
3527
  expandedIndex: this.expandedIndex,
3044
- interceptorsByTest: this.interceptorsByTest
3528
+ interceptorsByTest: this.interceptorsByTest,
3529
+ locale: localeForLang(this.translation.getLang())
3045
3530
  }, this.t.bind(this))}`;
3046
3531
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
3047
3532
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -3096,7 +3581,7 @@ ${beforeEach}${itBlocks}
3096
3581
  ev.stopPropagation();
3097
3582
  const idx = Number(el.dataset["idx"]);
3098
3583
  const t = visible[idx];
3099
- this.copyToClipboard((this.interceptorsByTest[t?.id] ?? []).join("\n"));
3584
+ this.copyToClipboard((this.interceptorsByTest[t?.id] ?? t?.interceptors ?? []).join("\n"));
3100
3585
  });
3101
3586
  });
3102
3587
  }
@@ -3198,9 +3683,75 @@ var CONFIGURATION_STYLES = `
3198
3683
 
3199
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 */
3200
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; }
3201
3751
  `;
3202
3752
 
3203
3753
  // src/components/configuration/configuration.template.ts
3754
+ init_html_utils();
3204
3755
  var LANGS = [
3205
3756
  { value: "es", label: "Espa\xF1ol" },
3206
3757
  { value: "en", label: "English" },
@@ -3208,8 +3759,64 @@ var LANGS = [
3208
3759
  { value: "it", label: "Italiano" },
3209
3760
  { value: "de", label: "Deutsch" }
3210
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
+ }
3211
3818
  function renderConfiguration(state, t) {
3212
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled } = state;
3819
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
3213
3820
  const langOptions = LANGS.map(
3214
3821
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
3215
3822
  ).join("");
@@ -3249,6 +3856,29 @@ function renderConfiguration(state, t) {
3249
3856
  </label>
3250
3857
  </div>
3251
3858
 
3859
+ <!-- Start Hidden -->
3860
+ <div class="card">
3861
+ <div class="card-hd">${t("CONFIG.START_HIDDEN_SECTION")}</div>
3862
+ <label class="check-row">
3863
+ <input type="checkbox" id="start-hidden-toggle" ${startHidden ? "checked" : ""} />
3864
+ <div>
3865
+ <div class="check-title">${t("CONFIG.START_HIDDEN_TITLE")}</div>
3866
+ <div class="check-sub">${t("CONFIG.START_HIDDEN_SUB")}</div>
3867
+ </div>
3868
+ </label>
3869
+ </div>
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
+
3252
3882
  <!-- Selector Strategy -->
3253
3883
  <div class="card card-wide">
3254
3884
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -3268,13 +3898,13 @@ function renderConfiguration(state, t) {
3268
3898
  <div class="card card-wide">
3269
3899
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
3270
3900
  <div class="fs-layout">
3271
- <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>
3272
3902
  \u2514\u2500\u2500 e2e/
3273
3903
  \u2514\u2500\u2500 *.cy.ts</pre>
3274
3904
  <div class="fs-right">
3275
3905
  <div class="fs-status">
3276
3906
  <span class="fs-dot ${filesystemGranted ? "on" : "off"}"></span>
3277
- ${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>`}
3278
3908
  </div>
3279
3909
  <div class="btn-row">
3280
3910
  <button id="btn-change-folder">
@@ -3299,7 +3929,7 @@ function renderConfiguration(state, t) {
3299
3929
  </div>
3300
3930
  </div>
3301
3931
 
3302
- </div>`;
3932
+ </div>${renderExportOverlay(state, t)}`;
3303
3933
  }
3304
3934
 
3305
3935
  // src/components/configuration/configuration.ts
@@ -3311,6 +3941,13 @@ var ConfigurationElement = class extends HTMLElement {
3311
3941
  advancedHttpConfig = false;
3312
3942
  selectorStrategy = "data-cy";
3313
3943
  smartSelectorEnabled = true;
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();
3314
3951
  filesystemGranted = false;
3315
3952
  cypressFolderName = null;
3316
3953
  constructor() {
@@ -3336,6 +3973,12 @@ var ConfigurationElement = class extends HTMLElement {
3336
3973
  this.advancedHttpConfig = localStorage.getItem("extendedHttpCommands") === "true";
3337
3974
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
3338
3975
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
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
+ }
3339
3982
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
3340
3983
  const handle = config?.["cypressDirectoryHandle"];
3341
3984
  this.cypressFolderName = handle?.name ?? null;
@@ -3353,6 +3996,18 @@ var ConfigurationElement = class extends HTMLElement {
3353
3996
  this.persistence.setConfig({ extendedHttpCommands: checked ? "true" : "false" });
3354
3997
  this.render();
3355
3998
  }
3999
+ async onStartHiddenChange(checked) {
4000
+ this.startHidden = checked;
4001
+ await this.persistence.setConfig({ startHidden: checked ? "true" : "false" });
4002
+ this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
4003
+ this.render();
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
+ }
3356
4011
  async onSmartSelectorChange(enabled) {
3357
4012
  this.smartSelectorEnabled = enabled;
3358
4013
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -3382,8 +4037,7 @@ var ConfigurationElement = class extends HTMLElement {
3382
4037
  this.cypressFolderName = null;
3383
4038
  this.render();
3384
4039
  }
3385
- async exportAllData() {
3386
- const tests = await this.persistence.getAllTests();
4040
+ downloadTests(tests) {
3387
4041
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
3388
4042
  const url = URL.createObjectURL(blob);
3389
4043
  const a = document.createElement("a");
@@ -3392,6 +4046,53 @@ var ConfigurationElement = class extends HTMLElement {
3392
4046
  a.click();
3393
4047
  URL.revokeObjectURL(url);
3394
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
+ }
3395
4096
  async importAllData(file) {
3396
4097
  const text = await file.text();
3397
4098
  let data;
@@ -3403,7 +4104,6 @@ var ConfigurationElement = class extends HTMLElement {
3403
4104
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
3404
4105
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
3405
4106
  }
3406
- await this.persistence.clearAllData();
3407
4107
  await this.persistence.ingestFileData(data.tests, data.interceptors);
3408
4108
  }
3409
4109
  render() {
@@ -3413,7 +4113,14 @@ var ConfigurationElement = class extends HTMLElement {
3413
4113
  selectorStrategy: this.selectorStrategy,
3414
4114
  filesystemGranted: this.filesystemGranted,
3415
4115
  cypressFolderName: this.cypressFolderName,
3416
- smartSelectorEnabled: this.smartSelectorEnabled
4116
+ smartSelectorEnabled: this.smartSelectorEnabled,
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
3417
4124
  }, this.t.bind(this))}`;
3418
4125
  ;
3419
4126
  this.shadow.getElementById("lang-select").addEventListener(
@@ -3428,13 +4135,32 @@ var ConfigurationElement = class extends HTMLElement {
3428
4135
  "change",
3429
4136
  (e) => this.onSmartSelectorChange(e.target.checked)
3430
4137
  );
4138
+ this.shadow.getElementById("start-hidden-toggle").addEventListener(
4139
+ "change",
4140
+ (e) => this.onStartHiddenChange(e.target.checked)
4141
+ );
4142
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
4143
+ "change",
4144
+ (e) => this.onResumeTtlChange(Number(e.target.value))
4145
+ );
3431
4146
  this.shadow.getElementById("selector-strategy").addEventListener(
3432
4147
  "change",
3433
4148
  (e) => this.onSelectorStrategyChange(e.target.value)
3434
4149
  );
3435
4150
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
3436
4151
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
3437
- 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
+ );
3438
4164
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
3439
4165
  const file = e.target.files?.[0];
3440
4166
  if (!file) return;
@@ -3500,12 +4226,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3500
4226
  .file-name { font-size: 11px; color: #484f58; margin-bottom: 8px; font-family: monospace; }
3501
4227
  pre {
3502
4228
  background: #0d1117; padding: 12px; border-radius: 8px;
3503
- font-size: 11px; color: #c9d1d9; overflow-x: auto;
3504
- white-space: pre-wrap; word-break: break-all; margin: 0;
4229
+ font-size: 11px; color: #c9d1d9; margin: 0;
3505
4230
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
3506
- line-height: 1.7; overflow-y: auto;
3507
- border: 1px solid #21262d;
4231
+ line-height: 1.7; border: 1px solid #21262d;
4232
+ white-space: pre; overflow-x: auto; overflow-y: auto;
4233
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3508
4234
  }
4235
+ pre::-webkit-scrollbar { width: 4px; height: 4px; }
4236
+ pre::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
4237
+ .pre-block { font-size: 10.5px; }
4238
+ .pre-icp .sh-str { color: #85e89d; }
4239
+ .sh-kw { color: #ff7b72; }
4240
+ .sh-bi { color: #d2a8ff; }
4241
+ .sh-str { color: #a5d6ff; }
4242
+ .sh-cmt { color: #8b949e; font-style: italic; }
4243
+ .sh-num { color: #79c0ff; }
3509
4244
  .footer {
3510
4245
  padding: 10px 14px; border-top: 1px solid #21262d;
3511
4246
  display: flex; gap: 8px; justify-content: flex-end; background: #161b22;
@@ -3521,6 +4256,12 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3521
4256
  .btn-save:hover { background: #1f6feb; border-color: #1f6feb; color: #fff; }
3522
4257
  .btn-save:disabled { background: #21262d; border-color: #30363d; color: #8b949e; }
3523
4258
  .placeholder { color: #484f58; font-size: 13px; padding: 28px; text-align: center; }
4259
+ .test-notes {
4260
+ margin: 0 0 10px; padding: 8px 12px;
4261
+ background: rgba(47,129,247,0.06); border-left: 3px solid rgba(47,129,247,0.4);
4262
+ border-radius: 0 6px 6px 0; font-size: 12px; color: #8b949e;
4263
+ line-height: 1.6; white-space: pre-wrap; word-break: break-word;
4264
+ }
3524
4265
  .block-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
3525
4266
  .btn-copy {
3526
4267
  padding: 3px 10px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
@@ -3529,18 +4270,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3529
4270
  }
3530
4271
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3531
4272
  .sidebar-toolbar {
3532
- display: flex; gap: 4px; padding: 6px 6px 4px;
4273
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3533
4274
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3534
4275
  }
3535
4276
  .btn-toolbar {
3536
- 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;
3537
4278
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3538
4279
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3539
4280
  text-overflow: ellipsis;
3540
4281
  }
4282
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3541
4283
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3542
4284
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3543
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; }
3544
4288
  .new-file-form {
3545
4289
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3546
4290
  }
@@ -3574,23 +4318,24 @@ function renderNoPermission(needsReauth, t) {
3574
4318
  </div>`;
3575
4319
  }
3576
4320
  function renderAdvancedEditor(state, t) {
3577
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
4321
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3578
4322
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3579
4323
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3580
- <pre>${escHtml(selectedFileContent.slice(0, 4e3))}${selectedFileContent.length > 4e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
4324
+ <pre class="pre-file">${syntaxHighlight(selectedFileContent.slice(0, 6e3))}${selectedFileContent.length > 6e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
4325
+ const notesHtml = testItBlock && testNotes ? `<p class="test-notes">${escHtml(testNotes)}</p>` : "";
3581
4326
  const itHtml = testItBlock ? `<div style="margin-top:10px">
3582
4327
  <div class="file-name block-header">
3583
4328
  <span>${t("ADVANCED_EDITOR.IT_LABEL")}</span>
3584
4329
  <button id="btn-copy-it" class="btn-copy">${t("ADVANCED_EDITOR.COPY_IT_BTN")}</button>
3585
4330
  </div>
3586
- <pre style="max-height:120px;font-size:10.5px">${escHtml(testItBlock.slice(0, 500))}</pre>
4331
+ <pre class="pre-block">${syntaxHighlight(testItBlock.slice(0, 3e3))}</pre>
3587
4332
  </div>` : "";
3588
4333
  const interceptorsHtml = interceptorsBlock ? `<div style="margin-top:10px">
3589
4334
  <div class="file-name block-header">
3590
4335
  <span>${t("ADVANCED_EDITOR.BEFORE_EACH_LABEL")}</span>
3591
4336
  <button id="btn-copy-interceptors" class="btn-copy">${t("ADVANCED_EDITOR.COPY_ICP_BTN")}</button>
3592
4337
  </div>
3593
- <pre style="max-height:120px;font-size:10.5px;color:#3fb950">${escHtml(interceptorsBlock.slice(0, 500))}</pre>
4338
+ <pre class="pre-block pre-icp">${syntaxHighlight(interceptorsBlock.slice(0, 3e3))}</pre>
3594
4339
  </div>` : "";
3595
4340
  const newFileForm = isCreatingFile ? `<div class="new-file-form">
3596
4341
  <input id="input-new-file" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FILE_PLACEHOLDER"))}" autocomplete="off" />
@@ -3600,21 +4345,30 @@ function renderAdvancedEditor(state, t) {
3600
4345
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3601
4346
  </div>
3602
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>` : "";
3603
4355
  const toolbar = `
3604
4356
  <div class="sidebar-toolbar">
3605
4357
  <button id="btn-new-file" class="btn-toolbar btn-new">${t("ADVANCED_EDITOR.NEW_FILE_BTN")}</button>
3606
- <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>
3607
4360
  </div>`;
3608
4361
  return `
3609
4362
  <div class="layout">
3610
4363
  <div class="sidebar" style="width:${sidebarWidth}px">
3611
4364
  ${toolbar}
3612
4365
  ${newFileForm}
4366
+ ${newFolderForm}
3613
4367
  <div class="tree-scroll">${treeHtml}</div>
3614
4368
  </div>
3615
4369
  <div id="resize-handle" class="resize-handle"></div>
3616
4370
  <div class="main">
3617
- <div class="content-area">${contentHtml}${itHtml}${interceptorsHtml}</div>
4371
+ <div class="content-area">${contentHtml}${notesHtml}${itHtml}${interceptorsHtml}</div>
3618
4372
  <div class="footer">
3619
4373
  <button id="btn-save" class="btn-save"
3620
4374
  ${!saveButtonEnabled || !testItBlock ? "disabled" : ""}>
@@ -3675,10 +4429,12 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3675
4429
  selectedFileContent = null;
3676
4430
  testItBlock = "";
3677
4431
  interceptorsBlock = "";
4432
+ testNotes = "";
3678
4433
  isPreviewMode = false;
3679
4434
  previewFileName = null;
3680
4435
  previewFileContent = null;
3681
4436
  isCreatingFile = false;
4437
+ isCreatingFolder = false;
3682
4438
  collapsedDirs = /* @__PURE__ */ new Set();
3683
4439
  sidebarWidth = 220;
3684
4440
  hasPermission = false;
@@ -3741,7 +4497,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3741
4497
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3742
4498
  if (!name) return;
3743
4499
  const fileName = `${name}.cy.ts`;
3744
- const template = `describe('${name}', () => {
4500
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3745
4501
 
3746
4502
  it('should ', () => {
3747
4503
 
@@ -3759,6 +4515,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3759
4515
  this.isCreatingFile = false;
3760
4516
  await this.getFoldersData();
3761
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
+ }
3762
4529
  async refreshTree() {
3763
4530
  await this.getFoldersData();
3764
4531
  }
@@ -3768,7 +4535,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3768
4535
  if (this.interceptorsBlock) {
3769
4536
  content = this.transformationService.insertBeforeEach(content, this.interceptorsBlock);
3770
4537
  }
3771
- content = this.transformationService.insertItBlock(content, this.testItBlock);
4538
+ const comment = this.testNotes ? this.transformationService.buildBlockComment(this.testNotes) + "\n" : "";
4539
+ content = this.transformationService.insertItBlock(content, comment + this.testItBlock);
3772
4540
  if (!content) return;
3773
4541
  const writable = await this.selectedFileHandle.createWritable();
3774
4542
  await writable.write(content);
@@ -3795,9 +4563,11 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3795
4563
  if (test) {
3796
4564
  this.testItBlock = test.itBlock ?? "";
3797
4565
  this.interceptorsBlock = test.interceptorsBlock ?? "";
4566
+ this.testNotes = test.notes ?? "";
3798
4567
  } else {
3799
4568
  this.testItBlock = "";
3800
4569
  this.interceptorsBlock = "";
4570
+ this.testNotes = "";
3801
4571
  }
3802
4572
  this.render();
3803
4573
  }
@@ -3813,7 +4583,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3813
4583
  fileName: this.selectedFile?.name ?? "",
3814
4584
  testId: this.testId,
3815
4585
  itBlock: this.testItBlock,
3816
- interceptorsBlock: this.interceptorsBlock
4586
+ interceptorsBlock: this.interceptorsBlock,
4587
+ notes: this.testNotes
3817
4588
  },
3818
4589
  bubbles: true,
3819
4590
  composed: true
@@ -3887,8 +4658,10 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3887
4658
  selectedFileContent: this.selectedFileContent,
3888
4659
  testItBlock: this.testItBlock,
3889
4660
  interceptorsBlock: this.interceptorsBlock,
4661
+ testNotes: this.testNotes,
3890
4662
  saveButtonEnabled: this.saveButtonEnabled,
3891
4663
  isCreatingFile: this.isCreatingFile,
4664
+ isCreatingFolder: this.isCreatingFolder,
3892
4665
  collapsedDirs: this.collapsedDirs,
3893
4666
  sidebarWidth: this.sidebarWidth
3894
4667
  }, this.t.bind(this))}`;
@@ -3912,9 +4685,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3912
4685
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
3913
4686
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
3914
4687
  this.isCreatingFile = !this.isCreatingFile;
4688
+ this.isCreatingFolder = false;
3915
4689
  this.render();
3916
4690
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
3917
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
+ });
3918
4698
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
3919
4699
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
3920
4700
  const input2 = this.shadow.getElementById("input-new-file");
@@ -3933,6 +4713,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3933
4713
  this.render();
3934
4714
  }
3935
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
+ });
3936
4733
  }
3937
4734
  };
3938
4735
  if (!customElements.get("advanced-test-editor")) {
@@ -4018,7 +4815,30 @@ var FILE_PREVIEW_STYLES = `
4018
4815
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
4019
4816
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
4020
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; }
4021
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
+ }
4022
4842
  `;
4023
4843
 
4024
4844
  // src/components/file-preview/file-preview.template.ts
@@ -4076,8 +4896,14 @@ function buildDiffHtml(original, current, t) {
4076
4896
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
4077
4897
  }).join("");
4078
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
+ };
4079
4905
  function renderFilePreview(state, t) {
4080
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4906
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
4081
4907
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
4082
4908
  <div class="blocks-panel">
4083
4909
  ${itBlock ? `
@@ -4098,6 +4924,13 @@ function renderFilePreview(state, t) {
4098
4924
  </div>` : ""}
4099
4925
  </div>` : "";
4100
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>` : "";
4101
4934
  return `
4102
4935
  <div class="container">
4103
4936
  <div class="header">
@@ -4108,9 +4941,11 @@ function renderFilePreview(state, t) {
4108
4941
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
4109
4942
  ${blocksPanelHtml}
4110
4943
  </div>
4944
+ ${runResultHtml}
4111
4945
  <div class="footer">
4112
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
4946
+ ${launchBtnHtml}
4113
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>` : ""}
4114
4949
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
4115
4950
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
4116
4951
  </div>
@@ -4126,11 +4961,18 @@ var FilePreviewElement = class extends HTMLElement {
4126
4961
  translation = translationService;
4127
4962
  itBlock = "";
4128
4963
  interceptorsBlock = "";
4964
+ notes = "";
4129
4965
  commands = [];
4130
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);
4131
4971
  _fileContent = null;
4132
4972
  _originalContent = null;
4133
4973
  _showDiff = false;
4974
+ _runState = "idle";
4975
+ _runOutput = "";
4134
4976
  constructor() {
4135
4977
  super();
4136
4978
  this.shadow = this.attachShadow({ mode: "open" });
@@ -4158,15 +5000,34 @@ var FilePreviewElement = class extends HTMLElement {
4158
5000
  onClose() {
4159
5001
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
4160
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
+ */
4161
5008
  async launchTest(specPath) {
4162
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
5009
+ if (!this.isLocal) return;
5010
+ const path = specPath ?? this.fileName ?? "";
4163
5011
  if (!path) return;
4164
- const response = await fetch("http://localhost:8123/run-test", {
4165
- method: "POST",
4166
- headers: { "Content-Type": "application/json" },
4167
- body: JSON.stringify({ specPath: path })
4168
- });
4169
- 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();
4170
5031
  }
4171
5032
  copyToClipboard(text) {
4172
5033
  navigator.clipboard?.writeText(text);
@@ -4175,6 +5036,32 @@ var FilePreviewElement = class extends HTMLElement {
4175
5036
  this._showDiff = !this._showDiff;
4176
5037
  this.render();
4177
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
+ }
4178
5065
  render() {
4179
5066
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
4180
5067
  fileName: this.fileName,
@@ -4184,7 +5071,10 @@ var FilePreviewElement = class extends HTMLElement {
4184
5071
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
4185
5072
  itBlock: this.itBlock,
4186
5073
  interceptorsBlock: this.interceptorsBlock,
4187
- closeLabel: this.closeLabel
5074
+ closeLabel: this.closeLabel,
5075
+ isLocal: this.isLocal,
5076
+ runState: this._runState,
5077
+ runOutput: this._runOutput
4188
5078
  }, this.t.bind(this))}`;
4189
5079
  this.textarea = this.shadow.getElementById("editor");
4190
5080
  if (this.textarea) {
@@ -4200,6 +5090,7 @@ var FilePreviewElement = class extends HTMLElement {
4200
5090
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
4201
5091
  });
4202
5092
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
5093
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
4203
5094
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
4204
5095
  this.copyToClipboard(this.itBlock);
4205
5096
  });
@@ -4447,6 +5338,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
4447
5338
  interceptorsUnsub;
4448
5339
  pauseUnsub;
4449
5340
  selectorNotFoundUnsub;
5341
+ langUnsub;
5342
+ sessionUnsub;
5343
+ sessionSaveTimer;
4450
5344
  controlFirstTimeData = true;
4451
5345
  _previsualizerRef = null;
4452
5346
  httpMonitor;
@@ -4454,6 +5348,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
4454
5348
  recording;
4455
5349
  persistence;
4456
5350
  translation;
5351
+ isVisible = false;
4457
5352
  isRecording = false;
4458
5353
  isPaused = false;
4459
5354
  cypressCommands = [];
@@ -4488,17 +5383,22 @@ var LibE2eRecorderElement = class extends HTMLElement {
4488
5383
  this.initSelectorStrategy();
4489
5384
  this.initSubscriptions();
4490
5385
  this.render();
5386
+ this.initVisibility();
5387
+ this.initSessionContinuity();
4491
5388
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4492
5389
  window.addEventListener("keydown", this.keydownHandler);
4493
5390
  }
4494
5391
  disconnectedCallback() {
4495
5392
  if (this._isDisabled) return;
4496
5393
  window.removeEventListener("keydown", this.keydownHandler);
5394
+ this.flushActiveSessionOnDisconnect();
4497
5395
  this.recordingUnsub?.();
4498
5396
  this.commandsUnsub?.();
4499
5397
  this.interceptorsUnsub?.();
4500
5398
  this.pauseUnsub?.();
4501
5399
  this.selectorNotFoundUnsub?.();
5400
+ this.langUnsub?.();
5401
+ this.sessionUnsub?.();
4502
5402
  this.httpMonitor?.uninstall();
4503
5403
  this.recording.destroy();
4504
5404
  }
@@ -4521,11 +5421,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4521
5421
  this.isRecording = val;
4522
5422
  if (!val && !this.controlFirstTimeData) {
4523
5423
  this.saveRecordingHistory();
5424
+ this.clearSessionPersistence();
4524
5425
  this.showSaveTestDialog();
4525
5426
  }
4526
5427
  this.controlFirstTimeData = false;
4527
5428
  this.render();
4528
5429
  });
5430
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4529
5431
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4530
5432
  this.cypressCommands = cmds;
4531
5433
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4541,6 +5443,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
4541
5443
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4542
5444
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4543
5445
  });
5446
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4544
5447
  }
4545
5448
  showSelectorPicker(target) {
4546
5449
  Promise.resolve().then(() => (init_selector_picker(), selector_picker_exports)).then(() => {
@@ -4553,12 +5456,135 @@ var LibE2eRecorderElement = class extends HTMLElement {
4553
5456
  document.body.appendChild(picker);
4554
5457
  });
4555
5458
  }
5459
+ async initVisibility() {
5460
+ if (this.hasAttribute("start-hidden")) {
5461
+ this.isVisible = this.getAttribute("start-hidden") === "false";
5462
+ this.style.display = this.isVisible ? "" : "none";
5463
+ return;
5464
+ }
5465
+ const config = await this.persistence.getGeneralConfig();
5466
+ this.isVisible = config?.["startHidden"] !== "true";
5467
+ this.style.display = this.isVisible ? "" : "none";
5468
+ }
4556
5469
  async initSelectorStrategy() {
4557
5470
  const config = await this.persistence.getGeneralConfig();
4558
5471
  const strategy = config?.["selectorStrategy"];
4559
5472
  if (strategy) this.recording.selectorStrategy = strategy;
4560
5473
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4561
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
+ }
4562
5588
  toggle() {
4563
5589
  this.recording.toggleRecording();
4564
5590
  }
@@ -4572,6 +5598,12 @@ var LibE2eRecorderElement = class extends HTMLElement {
4572
5598
  handleKeyboardEvent(event) {
4573
5599
  if (!event.ctrlKey) return;
4574
5600
  const key = event.key.toLowerCase();
5601
+ if (key === "e" && event.shiftKey) {
5602
+ event.preventDefault();
5603
+ this.toggleVisibility();
5604
+ return;
5605
+ }
5606
+ if (!this.isVisible) return;
4575
5607
  if (key === "r") {
4576
5608
  event.preventDefault();
4577
5609
  this.toggle();
@@ -4589,6 +5621,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
4589
5621
  this.showSettingsDialog();
4590
5622
  }
4591
5623
  }
5624
+ toggleVisibility() {
5625
+ this.isVisible = !this.isVisible;
5626
+ this.style.display = this.isVisible ? "" : "none";
5627
+ }
4592
5628
  // ── recording history (task 5) ────────────────────────────────────────────
4593
5629
  saveRecordingHistory() {
4594
5630
  if (!this.cypressCommands.length) return;
@@ -4824,13 +5860,13 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4824
5860
  child.translation = this.translation;
4825
5861
  container.appendChild(child);
4826
5862
  child.addEventListener("savetest", (e) => {
4827
- const { description, tags } = e.detail ?? {};
4828
- this.onSaveTest(description ?? null, tags ?? []);
5863
+ const { description, notes, tags } = e.detail ?? {};
5864
+ this.onSaveTest(description ?? null, tags ?? [], notes ?? "");
4829
5865
  import_sweetalert2.default.close();
4830
5866
  });
4831
5867
  child.addEventListener("saveandexport", (e) => {
4832
- const { description, tags } = e.detail ?? {};
4833
- this.onSaveAndExportTest(description ?? null, tags ?? []);
5868
+ const { description, notes, tags } = e.detail ?? {};
5869
+ this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "");
4834
5870
  import_sweetalert2.default.close();
4835
5871
  });
4836
5872
  },
@@ -4861,6 +5897,10 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4861
5897
  child.addEventListener("smartselectorchange", (e) => {
4862
5898
  this.smartSelectorEnabled = e.detail;
4863
5899
  });
5900
+ child.addEventListener("starthiddenchange", (e) => {
5901
+ this.isVisible = !e.detail;
5902
+ this.style.display = this.isVisible ? "" : "none";
5903
+ });
4864
5904
  },
4865
5905
  willClose: () => {
4866
5906
  this.isSettingsDialogOpen = false;
@@ -4915,7 +5955,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4915
5955
  e.detail.fileName,
4916
5956
  e.detail.testId,
4917
5957
  e.detail.itBlock,
4918
- e.detail.interceptorsBlock
5958
+ e.detail.interceptorsBlock,
5959
+ e.detail.notes
4919
5960
  ), 150);
4920
5961
  });
4921
5962
  },
@@ -4929,7 +5970,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4929
5970
  }, 0);
4930
5971
  });
4931
5972
  }
4932
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
5973
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
4933
5974
  import_sweetalert2.default.fire({
4934
5975
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
4935
5976
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -4950,6 +5991,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4950
5991
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
4951
5992
  child.itBlock = itBlock;
4952
5993
  child.interceptorsBlock = interceptorsBlock;
5994
+ child.notes = notes;
4953
5995
  container.appendChild(child);
4954
5996
  child.addEventListener("close", () => {
4955
5997
  import_sweetalert2.default.close();
@@ -4972,18 +6014,18 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
4972
6014
  this.resizePopup();
4973
6015
  }
4974
6016
  // ── save handlers ─────────────────────────────────────────────────────────
4975
- async onSaveTest(description, tags = []) {
6017
+ async onSaveTest(description, tags = [], notes = "") {
4976
6018
  if (description) {
4977
- await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags);
6019
+ await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
4978
6020
  this.recording.clearCommands();
4979
6021
  this.clearRecordingHistory();
4980
6022
  this.cypressCommands = [];
4981
6023
  this.interceptors = [];
4982
6024
  }
4983
6025
  }
4984
- async onSaveAndExportTest(description, tags = []) {
6026
+ async onSaveAndExportTest(description, tags = [], notes = "") {
4985
6027
  if (description) {
4986
- const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags);
6028
+ const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
4987
6029
  this.recording.clearCommands();
4988
6030
  this.clearRecordingHistory();
4989
6031
  this.cypressCommands = [];
@@ -5010,6 +6052,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5010
6052
  render() {
5011
6053
  const rec = this.isRecording;
5012
6054
  const paused = this.isPaused;
6055
+ this.style.display = this.isVisible ? "" : "none";
5013
6056
  this.shadow.innerHTML = `<style>${getRecorderStyles(rec, paused)}</style>${renderRecorderWidget(rec, paused, this.translation.translate.bind(this.translation))}`;
5014
6057
  this.shadow.querySelector('[data-action="toggle"]')?.addEventListener("click", () => this.toggle());
5015
6058
  this.shadow.querySelector('[data-action="pause"]')?.addEventListener("click", () => this.togglePause());
@@ -5024,24 +6067,29 @@ if (!customElements.get("lib-e2e-recorder")) {
5024
6067
  }
5025
6068
 
5026
6069
  // src/index.ts
5027
- var VERSION = "0.1.0";
6070
+ var VERSION = "0.5.0";
5028
6071
  // Annotate the CommonJS export names for ESM import in node:
5029
6072
  0 && (module.exports = {
6073
+ ACTIVE_SESSION_BREADCRUMB_KEY,
5030
6074
  AdvancedTestEditorElement,
5031
6075
  AdvancedTestTransformationService,
5032
6076
  ConfigurationElement,
5033
6077
  DB_SCHEMA,
5034
6078
  DB_STORE_NAMES,
6079
+ DEFAULT_RESUME_TTL_MINUTES,
5035
6080
  FilePreviewElement,
5036
6081
  HttpMonitor,
5037
6082
  INPUT_TYPES,
5038
6083
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
6084
+ LOCALE_BY_LANG,
5039
6085
  LibE2eRecorderElement,
5040
6086
  PersistenceService,
6087
+ RESUME_TTL_CONFIG_KEY,
5041
6088
  RecordingService,
5042
6089
  SCROLLBAR_STYLES,
5043
6090
  SUPPORTED_LANGS,
5044
6091
  SaveTestElement,
6092
+ SelectorPickerElement,
5045
6093
  Subject,
5046
6094
  TestEditorElement,
5047
6095
  TestPrevisualizerElement,
@@ -5052,10 +6100,13 @@ var VERSION = "0.1.0";
5052
6100
  generateAlias,
5053
6101
  injectStyles,
5054
6102
  isLang,
6103
+ isLocalHost,
6104
+ localeForLang,
5055
6105
  makeModalResizable,
5056
6106
  makeSwalDraggable,
5057
6107
  makeSwalDraggableByContentId,
5058
6108
  persistenceService,
6109
+ selectTestsForExport,
5059
6110
  setSwal2DataCyAttribute,
5060
6111
  showToast,
5061
6112
  transformationService,