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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
246
  }
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);
247
+ function escAttr(s) {
248
+ return s.replace(/"/g, "&quot;").replace(/</g, "&lt;");
249
+ }
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,35 +460,50 @@ 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,
470
+ DRAG_THRESHOLD: () => DRAG_THRESHOLD,
462
471
  FilePreviewElement: () => FilePreviewElement,
463
472
  HttpMonitor: () => HttpMonitor,
464
473
  INPUT_TYPES: () => INPUT_TYPES,
465
474
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES: () => LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
475
+ LOCALE_BY_LANG: () => LOCALE_BY_LANG,
466
476
  LibE2eRecorderElement: () => LibE2eRecorderElement,
467
477
  PersistenceService: () => PersistenceService,
478
+ RESUME_TTL_CONFIG_KEY: () => RESUME_TTL_CONFIG_KEY,
468
479
  RecordingService: () => RecordingService,
469
480
  SCROLLBAR_STYLES: () => SCROLLBAR_STYLES,
470
481
  SUPPORTED_LANGS: () => SUPPORTED_LANGS,
471
482
  SaveTestElement: () => SaveTestElement,
483
+ SelectorPickerElement: () => SelectorPickerElement,
472
484
  Subject: () => Subject,
485
+ TOGGLE_MARGIN: () => TOGGLE_MARGIN,
473
486
  TestEditorElement: () => TestEditorElement,
474
487
  TestPrevisualizerElement: () => TestPrevisualizerElement,
475
488
  TransformationService: () => TransformationService,
476
489
  TranslationService: () => TranslationService,
477
490
  VERSION: () => VERSION,
478
491
  advancedTestTransformationService: () => advancedTestTransformationService,
492
+ boxOffsetForDirection: () => boxOffsetForDirection,
493
+ boxTopLeftFor: () => boxTopLeftFor,
494
+ clampTogglePosition: () => clampTogglePosition,
495
+ defaultTogglePosition: () => defaultTogglePosition,
479
496
  generateAlias: () => generateAlias,
480
497
  injectStyles: () => injectStyles,
481
498
  isLang: () => isLang,
499
+ isLocalHost: () => isLocalHost,
500
+ localeForLang: () => localeForLang,
482
501
  makeModalResizable: () => makeModalResizable,
483
502
  makeSwalDraggable: () => makeSwalDraggable,
484
503
  makeSwalDraggableByContentId: () => makeSwalDraggableByContentId,
485
504
  persistenceService: () => persistenceService,
505
+ resolveExpandDirection: () => resolveExpandDirection,
506
+ selectTestsForExport: () => selectTestsForExport,
486
507
  setSwal2DataCyAttribute: () => setSwal2DataCyAttribute,
487
508
  showToast: () => showToast,
488
509
  transformationService: () => transformationService,
@@ -495,6 +516,16 @@ var SUPPORTED_LANGS = ["es", "en", "fr", "it", "de"];
495
516
  function isLang(value) {
496
517
  return SUPPORTED_LANGS.includes(value);
497
518
  }
519
+ var LOCALE_BY_LANG = {
520
+ es: "es-ES",
521
+ en: "en-GB",
522
+ fr: "fr-FR",
523
+ it: "it-IT",
524
+ de: "de-DE"
525
+ };
526
+ function localeForLang(lang) {
527
+ return isLang(lang) ? LOCALE_BY_LANG[lang] : "es-ES";
528
+ }
498
529
 
499
530
  // src/models/input-types.model.ts
500
531
  var INPUT_TYPES = [
@@ -511,7 +542,7 @@ var INPUT_TYPES = [
511
542
  // src/models/db-schema.model.ts
512
543
  var DB_SCHEMA = {
513
544
  name: "E2ECypressDB",
514
- version: 10,
545
+ version: 11,
515
546
  stores: [
516
547
  {
517
548
  name: "tests",
@@ -551,11 +582,45 @@ var DB_SCHEMA = {
551
582
  { name: "extendedHttpCommands", keyPath: "extendedHttpCommands", unique: false },
552
583
  { name: "allowReadWriteFiles", keyPath: "allowReadWriteFiles", unique: false }
553
584
  ]
585
+ },
586
+ {
587
+ // Single-record store (fixed key id=1) holding the live, in-progress
588
+ // recording session so it survives a micro-frontend crossing or reload.
589
+ // See docs/specs/006-cross-app-recording-continuity.md.
590
+ name: "activeSession",
591
+ keyPath: "id",
592
+ autoIncrement: false,
593
+ indexes: []
554
594
  }
555
595
  ]
556
596
  };
557
597
  var DB_STORE_NAMES = DB_SCHEMA.stores.map((s) => s.name);
558
598
 
599
+ // src/models/active-session.model.ts
600
+ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
601
+ var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
602
+ var DEFAULT_RESUME_TTL_MINUTES = 30;
603
+
604
+ // src/utils/subject.ts
605
+ var Subject = class {
606
+ _value;
607
+ listeners = /* @__PURE__ */ new Set();
608
+ constructor(initialValue) {
609
+ this._value = initialValue;
610
+ }
611
+ next(value) {
612
+ this._value = value;
613
+ this.listeners.forEach((l) => l(value));
614
+ }
615
+ getValue() {
616
+ return this._value;
617
+ }
618
+ subscribe(fn) {
619
+ this.listeners.add(fn);
620
+ return () => this.listeners.delete(fn);
621
+ }
622
+ };
623
+
559
624
  // src/i18n/es.ts
560
625
  var I18N_ES = {
561
626
  MAIN_FRAME: {
@@ -590,8 +655,6 @@ var I18N_ES = {
590
655
  COPY_DESCRIBE: "\u{1F5C2} Copiar describe()",
591
656
  MULTI_SELECT: "\u{1F5C2} Multi-select",
592
657
  CANCEL_SELECT: "\u2715 Cancelar",
593
- SECTION_COMMANDS: "Comandos",
594
- SECTION_INTERCEPTORS: "Interceptores",
595
658
  COPY_CMDS_BTN: "\u{1F4CB} Copiar comandos",
596
659
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
597
660
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -620,6 +683,18 @@ var I18N_ES = {
620
683
  DATA_SECTION: "\u{1F4BE} Datos",
621
684
  DATA_DESC: "Exporta todos tus tests a JSON o importa una copia de seguridad.",
622
685
  EXPORT_BTN: "\u2B06\uFE0F Exportar tests",
686
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exportar tests",
687
+ EXPORT_MODE_ALL: "Todo",
688
+ EXPORT_MODE_MANUAL: "Selecci\xF3n manual",
689
+ EXPORT_MODE_TAGS: "Por tags",
690
+ EXPORT_ALL_DESC: "Se exportar\xE1n todos los tests guardados.",
691
+ EXPORT_COUNT: "A exportar:",
692
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportar",
693
+ EXPORT_CANCEL: "Cancelar",
694
+ EXPORT_EMPTY: "No hay tests para exportar.",
695
+ EXPORT_NO_TAGS: "No hay tags disponibles.",
696
+ EXPORT_TAGS_HINT: "Selecciona uno o m\xE1s tags para ver qu\xE9 pruebas se exportar\xE1n.",
697
+ EXPORT_RESULT_LABEL: "Pruebas que se exportar\xE1n:",
623
698
  IMPORT_BTN: "\u2B07\uFE0F Importar tests",
624
699
  FOLDER_UPDATED_TOAST: "\u2713 Carpeta de Cypress actualizada",
625
700
  FOLDER_ERROR_TOAST: "Error al acceder a la carpeta",
@@ -632,7 +707,14 @@ var I18N_ES = {
632
707
  SMART_SELECTOR_SUB: "Muestra un picker al hacer click en elementos sin selector v\xE1lido.",
633
708
  START_HIDDEN_SECTION: "\u{1F441} Visibilidad del widget",
634
709
  START_HIDDEN_TITLE: "Iniciar oculto",
635
- START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo."
710
+ START_HIDDEN_SUB: "El widget arranca invisible. Usa Ctrl+Shift+E para mostrarlo u ocultarlo.",
711
+ RESUME_TTL_SECTION: "\u23F1 Continuidad de grabaci\xF3n",
712
+ RESUME_TTL_LABEL: "Reanudar sin preguntar (minutos)",
713
+ 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.",
714
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Posici\xF3n del widget",
715
+ WIDGET_POSITION_HINT: "Arrastra el bot\xF3n de grabar para moverlo donde no estorbe. Se recuerda su posici\xF3n.",
716
+ WIDGET_POSITION_RESET_BTN: "\u21BA Restablecer posici\xF3n",
717
+ WIDGET_POSITION_RESET_DONE: "\u2713 Posici\xF3n restablecida"
636
718
  },
637
719
  SELECTOR_PICKER: {
638
720
  TITLE: "Selecciona un elemento del DOM",
@@ -673,8 +755,10 @@ var I18N_ES = {
673
755
  EDIT_MANUAL_BTN: "\u270F\uFE0F Editar manualmente",
674
756
  CLOSE_BTN: "\u2715 Cerrar",
675
757
  NEW_FILE_BTN: "+ Nuevo archivo",
758
+ NEW_FOLDER_BTN: "+ Nueva carpeta",
676
759
  REFRESH_BTN: "\u21BB Actualizar",
677
760
  NEW_FILE_PLACEHOLDER: "nombre-del-test",
761
+ NEW_FOLDER_PLACEHOLDER: "nombre-de-carpeta",
678
762
  NEW_FILE_CONFIRM: "Crear",
679
763
  NEW_FILE_CANCEL: "Cancelar"
680
764
  },
@@ -687,10 +771,19 @@ var I18N_ES = {
687
771
  NO_CHANGES: "Sin cambios respecto al original",
688
772
  NO_CHANGES_SHORT: "Sin cambios",
689
773
  LAUNCH_BTN: "\u25B6 Lanzar test",
774
+ LAUNCH_LOCAL_ONLY: "Mu\xE9velo a local para poder probar",
775
+ LAUNCH_RUNNING: "\u23F3 Ejecutando\u2026",
776
+ LAUNCH_PASSED: "\u2713 Prueba superada",
777
+ LAUNCH_FAILED: "\u2717 Prueba fallida",
778
+ LAUNCH_NO_RUNNER: "No se detect\xF3 el runner local (\xBFlo has arrancado?)",
690
779
  DIFF_BTN: "\u{1F4CA} Diff",
691
780
  SAVE_BTN: "\u{1F4BE} Guardar",
692
781
  CLOSE_BTN: "\u2715 Cerrar",
693
- BACK_TO_EDITOR: "\u2190 Volver al editor"
782
+ BACK_TO_EDITOR: "\u2190 Volver al editor",
783
+ INSERT_BTN: "\u{1FA84} Insertar bloques",
784
+ INSERT_TITLE: "Insertar it() y beforeEach() en el contenido del editor",
785
+ INSERT_DONE: "Bloques insertados en el editor",
786
+ INSERT_ERROR: "No se encontr\xF3 un bloque describe() v\xE1lido en el archivo"
694
787
  },
695
788
  RECORDER: {
696
789
  FS_TITLE: "\u{1F4C1} Acceso a ficheros",
@@ -719,7 +812,12 @@ var I18N_ES = {
719
812
  BADGE_PAUSED: "\u23F8 PAUSA",
720
813
  BADGE_REC: "\u25CF REC",
721
814
  STOP_TITLE: "Detener (Ctrl+R)",
722
- START_TITLE: "Grabar (Ctrl+R)"
815
+ START_TITLE: "Grabar (Ctrl+R)",
816
+ SESSION_RESUME_TITLE: "\u23FA Grabaci\xF3n en curso",
817
+ SESSION_RESUME_TEXT: "Hay una grabaci\xF3n activa sin terminar.",
818
+ SESSION_RESUME_COUNT: "comandos capturados",
819
+ SESSION_CONTINUE_BTN: "Continuar grabando",
820
+ SESSION_DISCARD_BTN: "Descartar"
723
821
  }
724
822
  };
725
823
 
@@ -757,8 +855,6 @@ var I18N_EN = {
757
855
  COPY_DESCRIBE: "\u{1F5C2} Copy describe()",
758
856
  MULTI_SELECT: "\u{1F5C2} Multi-select",
759
857
  CANCEL_SELECT: "\u2715 Cancel",
760
- SECTION_COMMANDS: "Commands",
761
- SECTION_INTERCEPTORS: "Interceptors",
762
858
  COPY_CMDS_BTN: "\u{1F4CB} Copy commands",
763
859
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
764
860
  DEFAULT_DESCRIBE: "Test suite",
@@ -787,6 +883,18 @@ var I18N_EN = {
787
883
  DATA_SECTION: "\u{1F4BE} Data",
788
884
  DATA_DESC: "Export all your tests to JSON or import a backup.",
789
885
  EXPORT_BTN: "\u2B06\uFE0F Export tests",
886
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Export tests",
887
+ EXPORT_MODE_ALL: "All",
888
+ EXPORT_MODE_MANUAL: "Manual selection",
889
+ EXPORT_MODE_TAGS: "By tags",
890
+ EXPORT_ALL_DESC: "All saved tests will be exported.",
891
+ EXPORT_COUNT: "To export:",
892
+ EXPORT_CONFIRM: "\u2B06\uFE0F Export",
893
+ EXPORT_CANCEL: "Cancel",
894
+ EXPORT_EMPTY: "No tests to export.",
895
+ EXPORT_NO_TAGS: "No tags available.",
896
+ EXPORT_TAGS_HINT: "Select one or more tags to preview which tests will be exported.",
897
+ EXPORT_RESULT_LABEL: "Tests that will be exported:",
790
898
  IMPORT_BTN: "\u2B07\uFE0F Import tests",
791
899
  FOLDER_UPDATED_TOAST: "\u2713 Cypress folder updated",
792
900
  FOLDER_ERROR_TOAST: "Error accessing the folder",
@@ -799,7 +907,14 @@ var I18N_EN = {
799
907
  SMART_SELECTOR_SUB: "Shows a picker when clicking elements with no valid selector.",
800
908
  START_HIDDEN_SECTION: "\u{1F441} Widget visibility",
801
909
  START_HIDDEN_TITLE: "Start hidden",
802
- START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it."
910
+ START_HIDDEN_SUB: "The widget starts invisible. Use Ctrl+Shift+E to show or hide it.",
911
+ RESUME_TTL_SECTION: "\u23F1 Recording continuity",
912
+ RESUME_TTL_LABEL: "Resume without asking (minutes)",
913
+ 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.",
914
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Widget position",
915
+ WIDGET_POSITION_HINT: "Drag the record button to move it out of the way. Its position is remembered.",
916
+ WIDGET_POSITION_RESET_BTN: "\u21BA Reset position",
917
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position reset"
803
918
  },
804
919
  SELECTOR_PICKER: {
805
920
  TITLE: "Select a DOM element",
@@ -840,8 +955,10 @@ var I18N_EN = {
840
955
  EDIT_MANUAL_BTN: "\u270F\uFE0F Edit manually",
841
956
  CLOSE_BTN: "\u2715 Close",
842
957
  NEW_FILE_BTN: "+ New file",
958
+ NEW_FOLDER_BTN: "+ New folder",
843
959
  REFRESH_BTN: "\u21BB Refresh",
844
960
  NEW_FILE_PLACEHOLDER: "test-name",
961
+ NEW_FOLDER_PLACEHOLDER: "folder-name",
845
962
  NEW_FILE_CONFIRM: "Create",
846
963
  NEW_FILE_CANCEL: "Cancel"
847
964
  },
@@ -854,10 +971,19 @@ var I18N_EN = {
854
971
  NO_CHANGES: "No changes from original",
855
972
  NO_CHANGES_SHORT: "No changes",
856
973
  LAUNCH_BTN: "\u25B6 Run test",
974
+ LAUNCH_LOCAL_ONLY: "Move it to localhost to run tests",
975
+ LAUNCH_RUNNING: "\u23F3 Running\u2026",
976
+ LAUNCH_PASSED: "\u2713 Test passed",
977
+ LAUNCH_FAILED: "\u2717 Test failed",
978
+ LAUNCH_NO_RUNNER: "No local runner detected (did you start it?)",
857
979
  DIFF_BTN: "\u{1F4CA} Diff",
858
980
  SAVE_BTN: "\u{1F4BE} Save",
859
981
  CLOSE_BTN: "\u2715 Close",
860
- BACK_TO_EDITOR: "\u2190 Back to editor"
982
+ BACK_TO_EDITOR: "\u2190 Back to editor",
983
+ INSERT_BTN: "\u{1FA84} Insert blocks",
984
+ INSERT_TITLE: "Insert it() and beforeEach() into the editor content",
985
+ INSERT_DONE: "Blocks inserted into the editor",
986
+ INSERT_ERROR: "No valid describe() block found in the file"
861
987
  },
862
988
  RECORDER: {
863
989
  FS_TITLE: "\u{1F4C1} File access",
@@ -886,7 +1012,12 @@ var I18N_EN = {
886
1012
  BADGE_PAUSED: "\u23F8 PAUSED",
887
1013
  BADGE_REC: "\u25CF REC",
888
1014
  STOP_TITLE: "Stop (Ctrl+R)",
889
- START_TITLE: "Record (Ctrl+R)"
1015
+ START_TITLE: "Record (Ctrl+R)",
1016
+ SESSION_RESUME_TITLE: "\u23FA Recording in progress",
1017
+ SESSION_RESUME_TEXT: "There is an unfinished active recording.",
1018
+ SESSION_RESUME_COUNT: "commands captured",
1019
+ SESSION_CONTINUE_BTN: "Keep recording",
1020
+ SESSION_DISCARD_BTN: "Discard"
890
1021
  }
891
1022
  };
892
1023
 
@@ -924,8 +1055,6 @@ var I18N_FR = {
924
1055
  COPY_DESCRIBE: "\u{1F5C2} Copier describe()",
925
1056
  MULTI_SELECT: "\u{1F5C2} Multi-s\xE9lection",
926
1057
  CANCEL_SELECT: "\u2715 Annuler",
927
- SECTION_COMMANDS: "Commandes",
928
- SECTION_INTERCEPTORS: "Intercepteurs",
929
1058
  COPY_CMDS_BTN: "\u{1F4CB} Copier les commandes",
930
1059
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
931
1060
  DEFAULT_DESCRIBE: "Suite de tests",
@@ -954,6 +1083,18 @@ var I18N_FR = {
954
1083
  DATA_SECTION: "\u{1F4BE} Donn\xE9es",
955
1084
  DATA_DESC: "Exportez tous vos tests en JSON ou importez une sauvegarde.",
956
1085
  EXPORT_BTN: "\u2B06\uFE0F Exporter les tests",
1086
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Exporter les tests",
1087
+ EXPORT_MODE_ALL: "Tout",
1088
+ EXPORT_MODE_MANUAL: "S\xE9lection manuelle",
1089
+ EXPORT_MODE_TAGS: "Par tags",
1090
+ EXPORT_ALL_DESC: "Tous les tests enregistr\xE9s seront export\xE9s.",
1091
+ EXPORT_COUNT: "\xC0 exporter :",
1092
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exporter",
1093
+ EXPORT_CANCEL: "Annuler",
1094
+ EXPORT_EMPTY: "Aucun test \xE0 exporter.",
1095
+ EXPORT_NO_TAGS: "Aucun tag disponible.",
1096
+ EXPORT_TAGS_HINT: "S\xE9lectionnez un ou plusieurs tags pour voir les tests qui seront export\xE9s.",
1097
+ EXPORT_RESULT_LABEL: "Tests qui seront export\xE9s :",
957
1098
  IMPORT_BTN: "\u2B07\uFE0F Importer les tests",
958
1099
  FOLDER_UPDATED_TOAST: "\u2713 Dossier Cypress mis \xE0 jour",
959
1100
  FOLDER_ERROR_TOAST: "Erreur lors de l'acc\xE8s au dossier",
@@ -966,7 +1107,14 @@ var I18N_FR = {
966
1107
  SMART_SELECTOR_SUB: "Affiche un s\xE9lecteur lors du clic sur des \xE9l\xE9ments sans s\xE9lecteur valide.",
967
1108
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE9 du widget",
968
1109
  START_HIDDEN_TITLE: "D\xE9marrer masqu\xE9",
969
- START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer."
1110
+ START_HIDDEN_SUB: "Le widget d\xE9marre invisible. Utilisez Ctrl+Shift+E pour l'afficher ou le masquer.",
1111
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE9 d'enregistrement",
1112
+ RESUME_TTL_LABEL: "Reprendre sans demander (minutes)",
1113
+ 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.",
1114
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Position du widget",
1115
+ WIDGET_POSITION_HINT: "Fais glisser le bouton d'enregistrement pour le d\xE9placer. Sa position est m\xE9moris\xE9e.",
1116
+ WIDGET_POSITION_RESET_BTN: "\u21BA R\xE9initialiser la position",
1117
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position r\xE9initialis\xE9e"
970
1118
  },
971
1119
  SELECTOR_PICKER: {
972
1120
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -1007,8 +1155,10 @@ var I18N_FR = {
1007
1155
  EDIT_MANUAL_BTN: "\u270F\uFE0F \xC9diter manuellement",
1008
1156
  CLOSE_BTN: "\u2715 Fermer",
1009
1157
  NEW_FILE_BTN: "+ Nouveau fichier",
1158
+ NEW_FOLDER_BTN: "+ Nouveau dossier",
1010
1159
  REFRESH_BTN: "\u21BB Actualiser",
1011
1160
  NEW_FILE_PLACEHOLDER: "nom-du-test",
1161
+ NEW_FOLDER_PLACEHOLDER: "nom-du-dossier",
1012
1162
  NEW_FILE_CONFIRM: "Cr\xE9er",
1013
1163
  NEW_FILE_CANCEL: "Annuler"
1014
1164
  },
@@ -1021,10 +1171,19 @@ var I18N_FR = {
1021
1171
  NO_CHANGES: "Aucun changement par rapport \xE0 l'original",
1022
1172
  NO_CHANGES_SHORT: "Aucun changement",
1023
1173
  LAUNCH_BTN: "\u25B6 Lancer le test",
1174
+ LAUNCH_LOCAL_ONLY: "Passe en local pour pouvoir tester",
1175
+ LAUNCH_RUNNING: "\u23F3 Ex\xE9cution\u2026",
1176
+ LAUNCH_PASSED: "\u2713 Test r\xE9ussi",
1177
+ LAUNCH_FAILED: "\u2717 Test \xE9chou\xE9",
1178
+ LAUNCH_NO_RUNNER: "Aucun runner local d\xE9tect\xE9 (l'avez-vous d\xE9marr\xE9 ?)",
1024
1179
  DIFF_BTN: "\u{1F4CA} Diff",
1025
1180
  SAVE_BTN: "\u{1F4BE} Enregistrer",
1026
1181
  CLOSE_BTN: "\u2715 Fermer",
1027
- BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur"
1182
+ BACK_TO_EDITOR: "\u2190 Retour \xE0 l'\xE9diteur",
1183
+ INSERT_BTN: "\u{1FA84} Ins\xE9rer les blocs",
1184
+ INSERT_TITLE: "Ins\xE9rer it() et beforeEach() dans le contenu de l'\xE9diteur",
1185
+ INSERT_DONE: "Blocs ins\xE9r\xE9s dans l'\xE9diteur",
1186
+ INSERT_ERROR: "Aucun bloc describe() valide trouv\xE9 dans le fichier"
1028
1187
  },
1029
1188
  RECORDER: {
1030
1189
  FS_TITLE: "\u{1F4C1} Acc\xE8s aux fichiers",
@@ -1053,7 +1212,12 @@ var I18N_FR = {
1053
1212
  BADGE_PAUSED: "\u23F8 PAUSE",
1054
1213
  BADGE_REC: "\u25CF REC",
1055
1214
  STOP_TITLE: "Arr\xEAter (Ctrl+R)",
1056
- START_TITLE: "Enregistrer (Ctrl+R)"
1215
+ START_TITLE: "Enregistrer (Ctrl+R)",
1216
+ SESSION_RESUME_TITLE: "\u23FA Enregistrement en cours",
1217
+ SESSION_RESUME_TEXT: "Un enregistrement actif est inachev\xE9.",
1218
+ SESSION_RESUME_COUNT: "commandes captur\xE9es",
1219
+ SESSION_CONTINUE_BTN: "Continuer l'enregistrement",
1220
+ SESSION_DISCARD_BTN: "Ignorer"
1057
1221
  }
1058
1222
  };
1059
1223
 
@@ -1091,8 +1255,6 @@ var I18N_IT = {
1091
1255
  COPY_DESCRIBE: "\u{1F5C2} Copia describe()",
1092
1256
  MULTI_SELECT: "\u{1F5C2} Multi-selezione",
1093
1257
  CANCEL_SELECT: "\u2715 Annulla",
1094
- SECTION_COMMANDS: "Comandi",
1095
- SECTION_INTERCEPTORS: "Interceptor",
1096
1258
  COPY_CMDS_BTN: "\u{1F4CB} Copia comandi",
1097
1259
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
1098
1260
  DEFAULT_DESCRIBE: "Suite di test",
@@ -1121,6 +1283,18 @@ var I18N_IT = {
1121
1283
  DATA_SECTION: "\u{1F4BE} Dati",
1122
1284
  DATA_DESC: "Esporta tutti i tuoi test in JSON o importa un backup.",
1123
1285
  EXPORT_BTN: "\u2B06\uFE0F Esporta test",
1286
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Esporta test",
1287
+ EXPORT_MODE_ALL: "Tutto",
1288
+ EXPORT_MODE_MANUAL: "Selezione manuale",
1289
+ EXPORT_MODE_TAGS: "Per tag",
1290
+ EXPORT_ALL_DESC: "Verranno esportati tutti i test salvati.",
1291
+ EXPORT_COUNT: "Da esportare:",
1292
+ EXPORT_CONFIRM: "\u2B06\uFE0F Esporta",
1293
+ EXPORT_CANCEL: "Annulla",
1294
+ EXPORT_EMPTY: "Nessun test da esportare.",
1295
+ EXPORT_NO_TAGS: "Nessun tag disponibile.",
1296
+ EXPORT_TAGS_HINT: "Seleziona uno o pi\xF9 tag per vedere quali test verranno esportati.",
1297
+ EXPORT_RESULT_LABEL: "Test che verranno esportati:",
1124
1298
  IMPORT_BTN: "\u2B07\uFE0F Importa test",
1125
1299
  FOLDER_UPDATED_TOAST: "\u2713 Cartella Cypress aggiornata",
1126
1300
  FOLDER_ERROR_TOAST: "Errore durante l'accesso alla cartella",
@@ -1133,7 +1307,14 @@ var I18N_IT = {
1133
1307
  SMART_SELECTOR_SUB: "Mostra un selettore quando si fa clic su elementi senza selettore valido.",
1134
1308
  START_HIDDEN_SECTION: "\u{1F441} Visibilit\xE0 del widget",
1135
1309
  START_HIDDEN_TITLE: "Avvia nascosto",
1136
- START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo."
1310
+ START_HIDDEN_SUB: "Il widget si avvia invisibile. Usa Ctrl+Shift+E per mostrarlo o nasconderlo.",
1311
+ RESUME_TTL_SECTION: "\u23F1 Continuit\xE0 di registrazione",
1312
+ RESUME_TTL_LABEL: "Riprendi senza chiedere (minuti)",
1313
+ 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.",
1314
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Posizione del widget",
1315
+ WIDGET_POSITION_HINT: "Trascina il pulsante di registrazione per spostarlo. La posizione viene ricordata.",
1316
+ WIDGET_POSITION_RESET_BTN: "\u21BA Reimposta posizione",
1317
+ WIDGET_POSITION_RESET_DONE: "\u2713 Posizione reimpostata"
1137
1318
  },
1138
1319
  SELECTOR_PICKER: {
1139
1320
  TITLE: "Seleziona un elemento DOM",
@@ -1174,8 +1355,10 @@ var I18N_IT = {
1174
1355
  EDIT_MANUAL_BTN: "\u270F\uFE0F Modifica manualmente",
1175
1356
  CLOSE_BTN: "\u2715 Chiudi",
1176
1357
  NEW_FILE_BTN: "+ Nuovo file",
1358
+ NEW_FOLDER_BTN: "+ Nuova cartella",
1177
1359
  REFRESH_BTN: "\u21BB Aggiorna",
1178
1360
  NEW_FILE_PLACEHOLDER: "nome-del-test",
1361
+ NEW_FOLDER_PLACEHOLDER: "nome-cartella",
1179
1362
  NEW_FILE_CONFIRM: "Crea",
1180
1363
  NEW_FILE_CANCEL: "Annulla"
1181
1364
  },
@@ -1188,10 +1371,19 @@ var I18N_IT = {
1188
1371
  NO_CHANGES: "Nessuna modifica rispetto all'originale",
1189
1372
  NO_CHANGES_SHORT: "Nessuna modifica",
1190
1373
  LAUNCH_BTN: "\u25B6 Esegui test",
1374
+ LAUNCH_LOCAL_ONLY: "Spostati in locale per poter testare",
1375
+ LAUNCH_RUNNING: "\u23F3 Esecuzione\u2026",
1376
+ LAUNCH_PASSED: "\u2713 Test superato",
1377
+ LAUNCH_FAILED: "\u2717 Test fallito",
1378
+ LAUNCH_NO_RUNNER: "Nessun runner locale rilevato (l'hai avviato?)",
1191
1379
  DIFF_BTN: "\u{1F4CA} Diff",
1192
1380
  SAVE_BTN: "\u{1F4BE} Salva",
1193
1381
  CLOSE_BTN: "\u2715 Chiudi",
1194
- BACK_TO_EDITOR: "\u2190 Torna all'editor"
1382
+ BACK_TO_EDITOR: "\u2190 Torna all'editor",
1383
+ INSERT_BTN: "\u{1FA84} Inserisci blocchi",
1384
+ INSERT_TITLE: "Inserisci it() e beforeEach() nel contenuto dell'editor",
1385
+ INSERT_DONE: "Blocchi inseriti nell'editor",
1386
+ INSERT_ERROR: "Nessun blocco describe() valido trovato nel file"
1195
1387
  },
1196
1388
  RECORDER: {
1197
1389
  FS_TITLE: "\u{1F4C1} Accesso ai file",
@@ -1220,7 +1412,12 @@ var I18N_IT = {
1220
1412
  BADGE_PAUSED: "\u23F8 PAUSA",
1221
1413
  BADGE_REC: "\u25CF REC",
1222
1414
  STOP_TITLE: "Ferma (Ctrl+R)",
1223
- START_TITLE: "Registra (Ctrl+R)"
1415
+ START_TITLE: "Registra (Ctrl+R)",
1416
+ SESSION_RESUME_TITLE: "\u23FA Registrazione in corso",
1417
+ SESSION_RESUME_TEXT: "C'\xE8 una registrazione attiva non terminata.",
1418
+ SESSION_RESUME_COUNT: "comandi catturati",
1419
+ SESSION_CONTINUE_BTN: "Continua a registrare",
1420
+ SESSION_DISCARD_BTN: "Annulla"
1224
1421
  }
1225
1422
  };
1226
1423
 
@@ -1258,8 +1455,6 @@ var I18N_DE = {
1258
1455
  COPY_DESCRIBE: "\u{1F5C2} describe() kopieren",
1259
1456
  MULTI_SELECT: "\u{1F5C2} Mehrfachauswahl",
1260
1457
  CANCEL_SELECT: "\u2715 Abbrechen",
1261
- SECTION_COMMANDS: "Befehle",
1262
- SECTION_INTERCEPTORS: "Interceptors",
1263
1458
  COPY_CMDS_BTN: "\u{1F4CB} Befehle kopieren",
1264
1459
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
1265
1460
  DEFAULT_DESCRIBE: "Test-Suite",
@@ -1288,6 +1483,18 @@ var I18N_DE = {
1288
1483
  DATA_SECTION: "\u{1F4BE} Daten",
1289
1484
  DATA_DESC: "Exportieren Sie alle Tests als JSON oder importieren Sie ein Backup.",
1290
1485
  EXPORT_BTN: "\u2B06\uFE0F Tests exportieren",
1486
+ EXPORT_DIALOG_TITLE: "\u2B06\uFE0F Tests exportieren",
1487
+ EXPORT_MODE_ALL: "Alle",
1488
+ EXPORT_MODE_MANUAL: "Manuelle Auswahl",
1489
+ EXPORT_MODE_TAGS: "Nach Tags",
1490
+ EXPORT_ALL_DESC: "Alle gespeicherten Tests werden exportiert.",
1491
+ EXPORT_COUNT: "Zu exportieren:",
1492
+ EXPORT_CONFIRM: "\u2B06\uFE0F Exportieren",
1493
+ EXPORT_CANCEL: "Abbrechen",
1494
+ EXPORT_EMPTY: "Keine Tests zum Exportieren.",
1495
+ EXPORT_NO_TAGS: "Keine Tags verf\xFCgbar.",
1496
+ EXPORT_TAGS_HINT: "W\xE4hle ein oder mehrere Tags, um zu sehen, welche Tests exportiert werden.",
1497
+ EXPORT_RESULT_LABEL: "Diese Tests werden exportiert:",
1291
1498
  IMPORT_BTN: "\u2B07\uFE0F Tests importieren",
1292
1499
  FOLDER_UPDATED_TOAST: "\u2713 Cypress-Ordner aktualisiert",
1293
1500
  FOLDER_ERROR_TOAST: "Fehler beim Zugriff auf den Ordner",
@@ -1300,7 +1507,14 @@ var I18N_DE = {
1300
1507
  SMART_SELECTOR_SUB: "Zeigt ein Auswahlfeld, wenn auf Elemente ohne g\xFCltigen Selektor geklickt wird.",
1301
1508
  START_HIDDEN_SECTION: "\u{1F441} Widget-Sichtbarkeit",
1302
1509
  START_HIDDEN_TITLE: "Versteckt starten",
1303
- START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden."
1510
+ START_HIDDEN_SUB: "Das Widget startet unsichtbar. Verwenden Sie Ctrl+Shift+E zum Ein-/Ausblenden.",
1511
+ RESUME_TTL_SECTION: "\u23F1 Aufzeichnungskontinuit\xE4t",
1512
+ RESUME_TTL_LABEL: "Ohne Nachfrage fortsetzen (Minuten)",
1513
+ 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.",
1514
+ WIDGET_POSITION_SECTION: "\u{1F9F2} Widget-Position",
1515
+ WIDGET_POSITION_HINT: "Ziehe den Aufnahme-Button, um ihn aus dem Weg zu schieben. Die Position wird gespeichert.",
1516
+ WIDGET_POSITION_RESET_BTN: "\u21BA Position zur\xFCcksetzen",
1517
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position zur\xFCckgesetzt"
1304
1518
  },
1305
1519
  SELECTOR_PICKER: {
1306
1520
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -1341,8 +1555,10 @@ var I18N_DE = {
1341
1555
  EDIT_MANUAL_BTN: "\u270F\uFE0F Manuell bearbeiten",
1342
1556
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1343
1557
  NEW_FILE_BTN: "+ Neue Datei",
1558
+ NEW_FOLDER_BTN: "+ Neuer Ordner",
1344
1559
  REFRESH_BTN: "\u21BB Aktualisieren",
1345
1560
  NEW_FILE_PLACEHOLDER: "test-name",
1561
+ NEW_FOLDER_PLACEHOLDER: "ordnername",
1346
1562
  NEW_FILE_CONFIRM: "Erstellen",
1347
1563
  NEW_FILE_CANCEL: "Abbrechen"
1348
1564
  },
@@ -1355,10 +1571,19 @@ var I18N_DE = {
1355
1571
  NO_CHANGES: "Keine \xC4nderungen gegen\xFCber dem Original",
1356
1572
  NO_CHANGES_SHORT: "Keine \xC4nderungen",
1357
1573
  LAUNCH_BTN: "\u25B6 Test starten",
1574
+ LAUNCH_LOCAL_ONLY: "Wechsle zu localhost, um Tests auszuf\xFChren",
1575
+ LAUNCH_RUNNING: "\u23F3 L\xE4uft\u2026",
1576
+ LAUNCH_PASSED: "\u2713 Test bestanden",
1577
+ LAUNCH_FAILED: "\u2717 Test fehlgeschlagen",
1578
+ LAUNCH_NO_RUNNER: "Kein lokaler Runner erkannt (gestartet?)",
1358
1579
  DIFF_BTN: "\u{1F4CA} Diff",
1359
1580
  SAVE_BTN: "\u{1F4BE} Speichern",
1360
1581
  CLOSE_BTN: "\u2715 Schlie\xDFen",
1361
- BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor"
1582
+ BACK_TO_EDITOR: "\u2190 Zur\xFCck zum Editor",
1583
+ INSERT_BTN: "\u{1FA84} Bl\xF6cke einf\xFCgen",
1584
+ INSERT_TITLE: "it() und beforeEach() in den Editor-Inhalt einf\xFCgen",
1585
+ INSERT_DONE: "Bl\xF6cke in den Editor eingef\xFCgt",
1586
+ INSERT_ERROR: "Kein g\xFCltiger describe()-Block in der Datei gefunden"
1362
1587
  },
1363
1588
  RECORDER: {
1364
1589
  FS_TITLE: "\u{1F4C1} Dateizugriff",
@@ -1387,13 +1612,18 @@ var I18N_DE = {
1387
1612
  BADGE_PAUSED: "\u23F8 PAUSE",
1388
1613
  BADGE_REC: "\u25CF REC",
1389
1614
  STOP_TITLE: "Stopp (Ctrl+R)",
1390
- START_TITLE: "Aufzeichnen (Ctrl+R)"
1615
+ START_TITLE: "Aufzeichnen (Ctrl+R)",
1616
+ SESSION_RESUME_TITLE: "\u23FA Aufzeichnung l\xE4uft",
1617
+ SESSION_RESUME_TEXT: "Es gibt eine nicht abgeschlossene aktive Aufzeichnung.",
1618
+ SESSION_RESUME_COUNT: "erfasste Befehle",
1619
+ SESSION_CONTINUE_BTN: "Weiter aufzeichnen",
1620
+ SESSION_DISCARD_BTN: "Verwerfen"
1391
1621
  }
1392
1622
  };
1393
1623
 
1394
1624
  // src/services/translation.service.ts
1395
1625
  var TranslationService = class {
1396
- lang;
1626
+ lang$;
1397
1627
  translations = {
1398
1628
  es: I18N_ES,
1399
1629
  en: I18N_EN,
@@ -1402,17 +1632,21 @@ var TranslationService = class {
1402
1632
  de: I18N_DE
1403
1633
  };
1404
1634
  constructor() {
1405
- this.lang = this.detectLang();
1635
+ this.lang$ = new Subject(this.detectLang());
1406
1636
  }
1407
1637
  setLang(lang) {
1408
- this.lang = lang;
1638
+ this.lang$.next(lang);
1409
1639
  }
1410
1640
  getLang() {
1411
- return this.lang;
1641
+ return this.lang$.getValue();
1642
+ }
1643
+ /** Subscribe to language changes; returns an unsubscribe function. */
1644
+ onLangChange(fn) {
1645
+ return this.lang$.subscribe(fn);
1412
1646
  }
1413
1647
  translate(key) {
1414
1648
  const keys = key.split(".");
1415
- let value = this.translations[this.lang];
1649
+ let value = this.translations[this.lang$.getValue()];
1416
1650
  for (const k of keys) {
1417
1651
  value = value?.[k];
1418
1652
  if (value === void 0) return key;
@@ -1435,6 +1669,9 @@ function gcd(a, b) {
1435
1669
  }
1436
1670
  return a;
1437
1671
  }
1672
+ function escapeSingleQuotes(value) {
1673
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1674
+ }
1438
1675
  function normalizeBlock(code, baseIndent) {
1439
1676
  const lines = code.split("\n").map((l) => l.trimEnd());
1440
1677
  const nonEmpty = lines.filter((l) => l.trim().length > 0);
@@ -1457,7 +1694,7 @@ var TransformationService = class {
1457
1694
  }
1458
1695
  generateItDescription(description, commands) {
1459
1696
  const commandsBlock = commands.map((cmd) => normalizeBlock(cmd, " ")).join("\n");
1460
- return `it('${description}', () => {
1697
+ return `it('${escapeSingleQuotes(description)}', () => {
1461
1698
  ${commandsBlock}
1462
1699
  });`;
1463
1700
  }
@@ -1511,29 +1748,15 @@ ${lines}
1511
1748
  };
1512
1749
  var advancedTestTransformationService = new AdvancedTestTransformationService();
1513
1750
 
1514
- // src/utils/subject.ts
1515
- var Subject = class {
1516
- _value;
1517
- listeners = /* @__PURE__ */ new Set();
1518
- constructor(initialValue) {
1519
- this._value = initialValue;
1520
- }
1521
- next(value) {
1522
- this._value = value;
1523
- this.listeners.forEach((l) => l(value));
1524
- }
1525
- getValue() {
1526
- return this._value;
1527
- }
1528
- subscribe(fn) {
1529
- this.listeners.add(fn);
1530
- return () => this.listeners.delete(fn);
1531
- }
1532
- };
1533
-
1534
1751
  // src/services/recording.service.ts
1535
1752
  init_selector_quality_utils();
1536
1753
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
1754
+ function createSessionId() {
1755
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1756
+ return crypto.randomUUID();
1757
+ }
1758
+ return `sess-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
1759
+ }
1537
1760
  var RecordingService = class {
1538
1761
  commands$ = new Subject([]);
1539
1762
  interceptors$ = new Subject([]);
@@ -1543,6 +1766,9 @@ var RecordingService = class {
1543
1766
  inputDebounceTimers = /* @__PURE__ */ new Map();
1544
1767
  abort = new AbortController();
1545
1768
  selectorStrategy = "data-cy";
1769
+ /** Stable id of the current live session (null when none has started). */
1770
+ sessionId = null;
1771
+ startedAt = 0;
1546
1772
  // Stored originals for history patching cleanup
1547
1773
  origPushState = history.pushState.bind(history);
1548
1774
  origReplaceState = history.replaceState.bind(history);
@@ -1554,12 +1780,42 @@ var RecordingService = class {
1554
1780
  }
1555
1781
  // ── Public API ────────────────────────────────────────────────────────────
1556
1782
  startRecording() {
1783
+ this.sessionId = createSessionId();
1784
+ this.startedAt = Date.now();
1557
1785
  this.isPaused$.next(false);
1558
1786
  this.isRecording$.next(true);
1559
1787
  this.addCommand(`cy.viewport(1900, 1200)`);
1560
1788
  this.addCommand(`cy.visit('${window.location.pathname}')`);
1561
1789
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
1562
1790
  }
1791
+ /**
1792
+ * Rehydrates a previously persisted session WITHOUT running the startRecording
1793
+ * bootstrap (no viewport/visit/hide). Used to continue a recording across a
1794
+ * micro-frontend crossing or a same-origin reload.
1795
+ * See docs/specs/006-cross-app-recording-continuity.md.
1796
+ */
1797
+ restoreSession(state) {
1798
+ this.sessionId = state.sessionId;
1799
+ this.startedAt = state.startedAt;
1800
+ this.selectorStrategy = state.selectorStrategy;
1801
+ this.commands$.next([...state.commands]);
1802
+ this.interceptors$.next([...state.interceptors]);
1803
+ this.isPaused$.next(state.isPaused);
1804
+ this.isRecording$.next(state.isRecording);
1805
+ }
1806
+ /** Full snapshot of the live session for persistence. */
1807
+ getSessionSnapshot() {
1808
+ return {
1809
+ sessionId: this.sessionId ?? createSessionId(),
1810
+ isRecording: this.isRecording$.getValue(),
1811
+ isPaused: this.isPaused$.getValue(),
1812
+ commands: this.commands$.getValue(),
1813
+ interceptors: this.interceptors$.getValue(),
1814
+ selectorStrategy: this.selectorStrategy,
1815
+ startedAt: this.startedAt,
1816
+ updatedAt: Date.now()
1817
+ };
1818
+ }
1563
1819
  stopRecording() {
1564
1820
  this.isPaused$.next(false);
1565
1821
  this.isRecording$.next(false);
@@ -1646,6 +1902,21 @@ var RecordingService = class {
1646
1902
  onPauseChange(fn) {
1647
1903
  return this.isPaused$.subscribe(fn);
1648
1904
  }
1905
+ /**
1906
+ * Fires a full session snapshot whenever any persisted field changes
1907
+ * (commands, interceptors, recording or paused state). Drives the debounced
1908
+ * persistence of the live session. Returns a combined unsubscribe.
1909
+ */
1910
+ onSessionChange(fn) {
1911
+ const emit = () => fn(this.getSessionSnapshot());
1912
+ const unsubs = [
1913
+ this.commands$.subscribe(emit),
1914
+ this.interceptors$.subscribe(emit),
1915
+ this.isRecording$.subscribe(emit),
1916
+ this.isPaused$.subscribe(emit)
1917
+ ];
1918
+ return () => unsubs.forEach((u) => u());
1919
+ }
1649
1920
  onSelectorNotFound(fn) {
1650
1921
  return this.selectorNotFound$.subscribe((v) => {
1651
1922
  if (v) fn(v.target, v.action);
@@ -1877,6 +2148,7 @@ var RecordingService = class {
1877
2148
 
1878
2149
  // src/services/persistence.service.ts
1879
2150
  var import_idb = require("idb");
2151
+ var ACTIVE_SESSION_ID = 1;
1880
2152
  var PersistenceService = class {
1881
2153
  // dbName is overridable so tests can use unique names for isolation.
1882
2154
  constructor(dbName = DB_SCHEMA.name) {
@@ -1930,7 +2202,7 @@ var PersistenceService = class {
1930
2202
  if (!record) return null;
1931
2203
  const commands = await this.getCommandStrings(testId);
1932
2204
  const interceptors = await this.getInterceptorStrings(testId);
1933
- const itBlock = `it('${record.name}', () => {
2205
+ const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
1934
2206
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
1935
2207
  });`;
1936
2208
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
@@ -2011,6 +2283,25 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2011
2283
  const records = await db.getAll("configuration");
2012
2284
  return records[0] ?? null;
2013
2285
  }
2286
+ // ── Active recording session ──────────────────────────────────────────────
2287
+ /** Upserts the live recording session (single record, fixed key). */
2288
+ async saveActiveSession(state) {
2289
+ const db = await this.getDB();
2290
+ await db.put("activeSession", { ...state, id: ACTIVE_SESSION_ID });
2291
+ }
2292
+ /** Returns the persisted live session, or null when none is stored. */
2293
+ async getActiveSession() {
2294
+ const db = await this.getDB();
2295
+ const record = await db.get("activeSession", ACTIVE_SESSION_ID);
2296
+ if (!record) return null;
2297
+ const { id: _id, ...state } = record;
2298
+ return state;
2299
+ }
2300
+ /** Removes the live session record. Safe to call when none exists. */
2301
+ async clearActiveSession() {
2302
+ const db = await this.getDB();
2303
+ await db.delete("activeSession", ACTIVE_SESSION_ID);
2304
+ }
2014
2305
  // ── Bulk operations ───────────────────────────────────────────────────────
2015
2306
  async clearAllData() {
2016
2307
  const db = await this.getDB();
@@ -2021,10 +2312,18 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2021
2312
  ]);
2022
2313
  }
2023
2314
  async ingestFileData(tests, interceptors) {
2024
- await Promise.all([
2025
- this.bulkInsertWithoutId("tests", tests),
2026
- this.bulkInsertWithoutId("interceptors", interceptors)
2027
- ]);
2315
+ if (Array.isArray(tests)) {
2316
+ const db = await this.getDB();
2317
+ for (const test of tests) {
2318
+ const { id: _id, commands, interceptors: testInterceptors, ...record } = test;
2319
+ const newId = await db.add("tests", record);
2320
+ const cmds = Array.isArray(commands) ? commands : [];
2321
+ const icps = Array.isArray(testInterceptors) ? testInterceptors : [];
2322
+ if (cmds.length) await this.insertCommands(cmds, newId);
2323
+ if (icps.length) await this.insertInterceptors(icps, newId);
2324
+ }
2325
+ }
2326
+ await this.bulkInsertWithoutId("interceptors", interceptors);
2028
2327
  }
2029
2328
  async requestDirectoryPermissions() {
2030
2329
  if (!("showDirectoryPicker" in window)) {
@@ -2467,6 +2766,59 @@ function showToast(message, isSuccess = true) {
2467
2766
  setTimeout(() => toast.remove(), 3e3);
2468
2767
  }
2469
2768
 
2769
+ // src/utils/export-selection.utils.ts
2770
+ function selectTestsForExport(tests, mode, opts = {}) {
2771
+ if (mode === "all") return [...tests];
2772
+ if (mode === "manual") {
2773
+ const ids = new Set(opts.ids ?? []);
2774
+ return tests.filter((t) => ids.has(t.id));
2775
+ }
2776
+ const tags = new Set(opts.tags ?? []);
2777
+ if (tags.size === 0) return [];
2778
+ return tests.filter((t) => (t.tags ?? []).some((tag) => tags.has(tag)));
2779
+ }
2780
+
2781
+ // src/utils/host.utils.ts
2782
+ function isLocalHost(hostname) {
2783
+ if (!hostname) return true;
2784
+ const h = hostname.toLowerCase();
2785
+ return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
2786
+ }
2787
+
2788
+ // src/utils/widget-position.utils.ts
2789
+ var TOGGLE_MARGIN = 30;
2790
+ var DRAG_THRESHOLD = 5;
2791
+ var DEFAULT_EDGE_OFFSET = 46;
2792
+ function boxOffsetForDirection(dir) {
2793
+ return {
2794
+ x: dir.endsWith("left") ? 144 : 46,
2795
+ y: dir.startsWith("up") ? 144 : 46
2796
+ };
2797
+ }
2798
+ function clampTogglePosition(x, y, vw, vh, margin = TOGGLE_MARGIN) {
2799
+ const maxX = Math.max(margin, vw - margin);
2800
+ const maxY = Math.max(margin, vh - margin);
2801
+ return {
2802
+ x: Math.min(Math.max(x, margin), maxX),
2803
+ y: Math.min(Math.max(y, margin), maxY)
2804
+ };
2805
+ }
2806
+ function resolveExpandDirection(x, y, vw, vh) {
2807
+ const vertical = y < vh / 2 ? "down" : "up";
2808
+ const horizontal = x < vw / 2 ? "right" : "left";
2809
+ return `${vertical}-${horizontal}`;
2810
+ }
2811
+ function defaultTogglePosition(vw, vh) {
2812
+ return { x: vw - DEFAULT_EDGE_OFFSET, y: vh - DEFAULT_EDGE_OFFSET };
2813
+ }
2814
+ function boxTopLeftFor(toggleCentre, dir) {
2815
+ const off = boxOffsetForDirection(dir);
2816
+ return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
2817
+ }
2818
+
2819
+ // src/index.ts
2820
+ init_selector_picker();
2821
+
2470
2822
  // src/components/test-previsualizer/test-previsualizer.styles.ts
2471
2823
  var TEST_PREVISUALIZER_STYLES = `
2472
2824
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -3069,7 +3421,7 @@ function highlightLine(line) {
3069
3421
 
3070
3422
  // src/components/test-editor/test-editor.template.ts
3071
3423
  function renderTestEditor(state, t) {
3072
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest } = state;
3424
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
3073
3425
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
3074
3426
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
3075
3427
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -3080,12 +3432,12 @@ function renderTestEditor(state, t) {
3080
3432
  </div>` : "";
3081
3433
  const rows = visible.map((test, i) => {
3082
3434
  const expanded = expandedIndex === i;
3083
- const date = new Date(test.createdAt).toLocaleString("es-ES", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
3435
+ const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
3084
3436
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
3085
3437
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
3086
3438
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
3087
3439
  const hasIcps = (test.interceptors ?? []).length > 0;
3088
- const itBlockCode = `it('${test.name}', () => {
3440
+ const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
3089
3441
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
3090
3442
  });`;
3091
3443
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -3193,11 +3545,11 @@ var TestEditorElement = class extends HTMLElement {
3193
3545
  ` : "";
3194
3546
  const itBlocks = selected.map((t) => {
3195
3547
  const cmds = (t.commands ?? []).map((c) => ` ${c}`).join("\n");
3196
- return ` it('${(t.name ?? "").replace(/'/g, "\\'")}', () => {
3548
+ return ` it('${escapeSingleQuotes(t.name ?? "")}', () => {
3197
3549
  ${cmds}
3198
3550
  });`;
3199
3551
  }).join("\n\n");
3200
- const block = `describe('${name.replace(/'/g, "\\'")}', () => {
3552
+ const block = `describe('${escapeSingleQuotes(name)}', () => {
3201
3553
  ${beforeEach}${itBlocks}
3202
3554
  });`;
3203
3555
  navigator.clipboard?.writeText(block);
@@ -3231,7 +3583,8 @@ ${beforeEach}${itBlocks}
3231
3583
  selectedIds: this.selectedIds,
3232
3584
  describeName: this.describeName,
3233
3585
  expandedIndex: this.expandedIndex,
3234
- interceptorsByTest: this.interceptorsByTest
3586
+ interceptorsByTest: this.interceptorsByTest,
3587
+ locale: localeForLang(this.translation.getLang())
3235
3588
  }, this.t.bind(this))}`;
3236
3589
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
3237
3590
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
@@ -3388,9 +3741,75 @@ var CONFIGURATION_STYLES = `
3388
3741
 
3389
3742
  /* \u2500\u2500 Data section desc \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3390
3743
  .data-desc { font-size: 11px; color: #484f58; margin-bottom: 10px; line-height: 1.5; }
3744
+
3745
+ /* \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 */
3746
+ .export-overlay {
3747
+ position: fixed; inset: 0; z-index: 100000;
3748
+ background: rgba(1,4,9,0.7);
3749
+ display: flex; align-items: center; justify-content: center; padding: 20px;
3750
+ }
3751
+ .export-modal {
3752
+ width: 640px; max-width: 100%; max-height: 86vh;
3753
+ display: flex; flex-direction: column;
3754
+ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
3755
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5); overflow: hidden;
3756
+ }
3757
+ .export-hd {
3758
+ padding: 14px 18px; font-size: 13px; font-weight: 700;
3759
+ border-bottom: 1px solid #21262d; color: #e6edf3;
3760
+ }
3761
+ .export-modes { display: flex; gap: 6px; padding: 12px 18px 0; }
3762
+ .export-mode {
3763
+ flex: 1; padding: 7px 10px; font-size: 11px; border-radius: 6px;
3764
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3765
+ }
3766
+ .export-mode:hover { background: #21262d; color: #e6edf3; }
3767
+ .export-mode.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3768
+ .export-body {
3769
+ padding: 14px 18px; overflow-y: auto; flex: 1; min-height: 160px;
3770
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
3771
+ }
3772
+ .export-tag-hint { font-size: 12px; color: #484f58; margin-top: 12px; }
3773
+ .export-result-label {
3774
+ font-size: 10px; color: #484f58; text-transform: uppercase; letter-spacing: 0.6px;
3775
+ font-weight: 600; margin: 14px 0 6px;
3776
+ }
3777
+ .export-row-static { cursor: default; }
3778
+ .export-row-static:hover { background: transparent; }
3779
+ .export-all-desc { font-size: 12px; color: #8b949e; }
3780
+ .export-empty { font-size: 12px; color: #484f58; text-align: center; padding: 20px; }
3781
+ .export-list { display: flex; flex-direction: column; gap: 4px; }
3782
+ .export-row {
3783
+ display: flex; align-items: center; gap: 8px;
3784
+ padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: #c9d1d9;
3785
+ }
3786
+ .export-row:hover { background: #0d1117; }
3787
+ .export-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3788
+ .export-row-tag {
3789
+ font-size: 9px; color: #8b949e; background: #0d1117;
3790
+ border: 1px solid #30363d; border-radius: 10px; padding: 1px 7px; flex-shrink: 0;
3791
+ }
3792
+ .export-tags { display: flex; flex-wrap: wrap; gap: 6px; }
3793
+ .export-tag {
3794
+ padding: 4px 10px; font-size: 11px; border-radius: 12px;
3795
+ background: #0d1117; border: 1px solid #30363d; color: #8b949e;
3796
+ }
3797
+ .export-tag:hover { background: #21262d; color: #e6edf3; }
3798
+ .export-tag.active { background: rgba(47,129,247,0.15); border-color: #2f81f7; color: #2f81f7; }
3799
+ .export-ft {
3800
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
3801
+ padding: 12px 18px; border-top: 1px solid #21262d;
3802
+ }
3803
+ .export-count { font-size: 11px; color: #8b949e; }
3804
+ .export-count b { color: #e6edf3; }
3805
+ .export-ft-actions { display: flex; gap: 8px; }
3806
+ .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
3807
+ .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
3808
+ .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
3391
3809
  `;
3392
3810
 
3393
3811
  // src/components/configuration/configuration.template.ts
3812
+ init_html_utils();
3394
3813
  var LANGS = [
3395
3814
  { value: "es", label: "Espa\xF1ol" },
3396
3815
  { value: "en", label: "English" },
@@ -3398,8 +3817,64 @@ var LANGS = [
3398
3817
  { value: "it", label: "Italiano" },
3399
3818
  { value: "de", label: "Deutsch" }
3400
3819
  ];
3820
+ function renderExportOverlay(state, t) {
3821
+ const { isExporting, exportMode, exportTests, exportSelectedIds, exportSelectedTags } = state;
3822
+ if (!isExporting) return "";
3823
+ const count = selectTestsForExport(exportTests, exportMode, {
3824
+ ids: exportSelectedIds,
3825
+ tags: exportSelectedTags
3826
+ }).length;
3827
+ const modeBtn = (mode, key) => `<button class="export-mode ${exportMode === mode ? "active" : ""}" data-export-mode="${mode}">${t(key)}</button>`;
3828
+ let body;
3829
+ if (exportTests.length === 0) {
3830
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_EMPTY")}</div>`;
3831
+ } else if (exportMode === "all") {
3832
+ body = `<div class="export-all-desc">${t("CONFIG.EXPORT_ALL_DESC")}</div>`;
3833
+ } else if (exportMode === "manual") {
3834
+ body = `<div class="export-list">${exportTests.map((test) => `
3835
+ <label class="export-row">
3836
+ <input type="checkbox" data-export-test="${test.id}" ${exportSelectedIds.has(test.id) ? "checked" : ""} />
3837
+ <span class="export-row-name">${escHtml(test.name)}</span>
3838
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3839
+ </label>`).join("")}</div>`;
3840
+ } else {
3841
+ const allTags = [...new Set(exportTests.flatMap((test) => test.tags ?? []))].sort();
3842
+ if (!allTags.length) {
3843
+ body = `<div class="export-empty">${t("CONFIG.EXPORT_NO_TAGS")}</div>`;
3844
+ } else {
3845
+ 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>`;
3846
+ const matched = selectTestsForExport(exportTests, "tags", { tags: exportSelectedTags });
3847
+ 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>
3848
+ <div class="export-list">${matched.map((test) => `
3849
+ <div class="export-row export-row-static">
3850
+ <span class="export-row-name">${escHtml(test.name)}</span>
3851
+ ${(test.tags ?? []).map((tag) => `<span class="export-row-tag">${escHtml(tag)}</span>`).join("")}
3852
+ </div>`).join("")}</div>`;
3853
+ body = chips + preview;
3854
+ }
3855
+ }
3856
+ return `
3857
+ <div class="export-overlay" id="export-overlay">
3858
+ <div class="export-modal">
3859
+ <div class="export-hd">${t("CONFIG.EXPORT_DIALOG_TITLE")}</div>
3860
+ ${exportTests.length ? `<div class="export-modes">
3861
+ ${modeBtn("all", "CONFIG.EXPORT_MODE_ALL")}
3862
+ ${modeBtn("manual", "CONFIG.EXPORT_MODE_MANUAL")}
3863
+ ${modeBtn("tags", "CONFIG.EXPORT_MODE_TAGS")}
3864
+ </div>` : ""}
3865
+ <div class="export-body">${body}</div>
3866
+ <div class="export-ft">
3867
+ <span class="export-count">${t("CONFIG.EXPORT_COUNT")} <b>${count}</b></span>
3868
+ <span class="export-ft-actions">
3869
+ <button id="btn-export-confirm" class="btn-export-confirm" ${count === 0 ? "disabled" : ""}>${t("CONFIG.EXPORT_CONFIRM")}</button>
3870
+ <button id="btn-export-cancel">${t("CONFIG.EXPORT_CANCEL")}</button>
3871
+ </span>
3872
+ </div>
3873
+ </div>
3874
+ </div>`;
3875
+ }
3401
3876
  function renderConfiguration(state, t) {
3402
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden } = state;
3877
+ const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
3403
3878
  const langOptions = LANGS.map(
3404
3879
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
3405
3880
  ).join("");
@@ -3451,6 +3926,26 @@ function renderConfiguration(state, t) {
3451
3926
  </label>
3452
3927
  </div>
3453
3928
 
3929
+ <!-- Widget position (draggable widget) -->
3930
+ <div class="card">
3931
+ <div class="card-hd">${t("CONFIG.WIDGET_POSITION_SECTION")}</div>
3932
+ <div class="check-sub" style="margin-bottom:10px">${t("CONFIG.WIDGET_POSITION_HINT")}</div>
3933
+ <div class="btn-row">
3934
+ <button id="btn-reset-position">${t("CONFIG.WIDGET_POSITION_RESET_BTN")}</button>
3935
+ </div>
3936
+ </div>
3937
+
3938
+ <!-- Recording continuity (cross-app resume TTL) -->
3939
+ <div class="card">
3940
+ <div class="card-hd">${t("CONFIG.RESUME_TTL_SECTION")}</div>
3941
+ <div class="field-row">
3942
+ <span class="field-label">${t("CONFIG.RESUME_TTL_LABEL")}</span>
3943
+ <input type="number" id="resume-ttl-input" min="1" step="1" value="${resumeTtlMinutes}"
3944
+ style="width:80px;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none" />
3945
+ </div>
3946
+ <div class="check-sub" style="margin-top:8px">${t("CONFIG.RESUME_TTL_HINT")}</div>
3947
+ </div>
3948
+
3454
3949
  <!-- Selector Strategy -->
3455
3950
  <div class="card card-wide">
3456
3951
  <div class="card-hd">${t("CONFIG.SELECTOR_SECTION")}</div>
@@ -3470,13 +3965,13 @@ function renderConfiguration(state, t) {
3470
3965
  <div class="card card-wide">
3471
3966
  <div class="card-hd">${t("CONFIG.FOLDER_SECTION")}</div>
3472
3967
  <div class="fs-layout">
3473
- <pre class="fs-tree">cypress/ <span style="color:#484f58">\u2190 selecciona</span>
3968
+ <pre class="fs-tree">cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
3474
3969
  \u2514\u2500\u2500 e2e/
3475
3970
  \u2514\u2500\u2500 *.cy.ts</pre>
3476
3971
  <div class="fs-right">
3477
3972
  <div class="fs-status">
3478
3973
  <span class="fs-dot ${filesystemGranted ? "on" : "off"}"></span>
3479
- ${filesystemGranted && cypressFolderName ? `<span>${t("CONFIG.FOLDER_CONNECTED")}</span>&nbsp;<span class="fs-folder">\u{1F4C1} ${cypressFolderName}</span>` : `<span>${t("CONFIG.FOLDER_NOT_SET")}</span>`}
3974
+ ${filesystemGranted && cypressFolderName ? `<span>${t("CONFIG.FOLDER_CONNECTED")}</span>&nbsp;<span class="fs-folder">\u{1F4C1} ${escHtml(cypressFolderName)}</span>` : `<span>${t("CONFIG.FOLDER_NOT_SET")}</span>`}
3480
3975
  </div>
3481
3976
  <div class="btn-row">
3482
3977
  <button id="btn-change-folder">
@@ -3501,7 +3996,7 @@ function renderConfiguration(state, t) {
3501
3996
  </div>
3502
3997
  </div>
3503
3998
 
3504
- </div>`;
3999
+ </div>${renderExportOverlay(state, t)}`;
3505
4000
  }
3506
4001
 
3507
4002
  // src/components/configuration/configuration.ts
@@ -3514,6 +4009,12 @@ var ConfigurationElement = class extends HTMLElement {
3514
4009
  selectorStrategy = "data-cy";
3515
4010
  smartSelectorEnabled = true;
3516
4011
  startHidden = false;
4012
+ resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
4013
+ isExporting = false;
4014
+ exportMode = "all";
4015
+ exportTests = [];
4016
+ exportSelectedIds = /* @__PURE__ */ new Set();
4017
+ exportSelectedTags = /* @__PURE__ */ new Set();
3517
4018
  filesystemGranted = false;
3518
4019
  cypressFolderName = null;
3519
4020
  constructor() {
@@ -3540,6 +4041,11 @@ var ConfigurationElement = class extends HTMLElement {
3540
4041
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
3541
4042
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
3542
4043
  this.startHidden = config?.["startHidden"] === "true";
4044
+ const ttlRaw = config?.[RESUME_TTL_CONFIG_KEY];
4045
+ if (ttlRaw !== void 0 && ttlRaw !== null) {
4046
+ const ttl = Number(ttlRaw);
4047
+ this.resumeTtlMinutes = Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_RESUME_TTL_MINUTES;
4048
+ }
3543
4049
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
3544
4050
  const handle = config?.["cypressDirectoryHandle"];
3545
4051
  this.cypressFolderName = handle?.name ?? null;
@@ -3563,6 +4069,16 @@ var ConfigurationElement = class extends HTMLElement {
3563
4069
  this.dispatchEvent(new CustomEvent("starthiddenchange", { detail: checked, bubbles: true, composed: true }));
3564
4070
  this.render();
3565
4071
  }
4072
+ async onResumeTtlChange(minutes) {
4073
+ const safe = Number.isFinite(minutes) && minutes > 0 ? Math.round(minutes) : DEFAULT_RESUME_TTL_MINUTES;
4074
+ this.resumeTtlMinutes = safe;
4075
+ await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
4076
+ this.render();
4077
+ }
4078
+ onResetWidgetPosition() {
4079
+ this.dispatchEvent(new CustomEvent("resetwidgetposition", { bubbles: true, composed: true }));
4080
+ showToast(this.t("CONFIG.WIDGET_POSITION_RESET_DONE"));
4081
+ }
3566
4082
  async onSmartSelectorChange(enabled) {
3567
4083
  this.smartSelectorEnabled = enabled;
3568
4084
  await this.persistence.setConfig({ smartSelectorEnabled: enabled ? "true" : "false" });
@@ -3592,8 +4108,7 @@ var ConfigurationElement = class extends HTMLElement {
3592
4108
  this.cypressFolderName = null;
3593
4109
  this.render();
3594
4110
  }
3595
- async exportAllData() {
3596
- const tests = await this.persistence.getAllTests();
4111
+ downloadTests(tests) {
3597
4112
  const blob = new Blob([JSON.stringify({ tests, interceptors: [] }, null, 2)], { type: "application/json" });
3598
4113
  const url = URL.createObjectURL(blob);
3599
4114
  const a = document.createElement("a");
@@ -3602,6 +4117,53 @@ var ConfigurationElement = class extends HTMLElement {
3602
4117
  a.click();
3603
4118
  URL.revokeObjectURL(url);
3604
4119
  }
4120
+ /** Downloads every saved test (used by the "Todo" mode and as a direct API). */
4121
+ async exportAllData() {
4122
+ const tests = await this.persistence.getAllTests();
4123
+ this.downloadTests(tests);
4124
+ }
4125
+ /** Opens the export selection dialog, loading the current tests. */
4126
+ async openExportDialog() {
4127
+ this.exportTests = await this.persistence.getAllTests();
4128
+ this.exportMode = "all";
4129
+ this.exportSelectedIds = /* @__PURE__ */ new Set();
4130
+ this.exportSelectedTags = /* @__PURE__ */ new Set();
4131
+ this.isExporting = true;
4132
+ this.render();
4133
+ }
4134
+ cancelExport() {
4135
+ this.isExporting = false;
4136
+ this.exportSelectedIds.clear();
4137
+ this.exportSelectedTags.clear();
4138
+ this.render();
4139
+ }
4140
+ setExportMode(mode) {
4141
+ this.exportMode = mode;
4142
+ this.render();
4143
+ }
4144
+ toggleExportTest(id) {
4145
+ if (this.exportSelectedIds.has(id)) this.exportSelectedIds.delete(id);
4146
+ else this.exportSelectedIds.add(id);
4147
+ this.render();
4148
+ }
4149
+ toggleExportTag(tag) {
4150
+ if (this.exportSelectedTags.has(tag)) this.exportSelectedTags.delete(tag);
4151
+ else this.exportSelectedTags.add(tag);
4152
+ this.render();
4153
+ }
4154
+ /** Downloads the tests resolved by the current mode + selection. No-op if empty. */
4155
+ confirmExport() {
4156
+ const subset = selectTestsForExport(this.exportTests, this.exportMode, {
4157
+ ids: this.exportSelectedIds,
4158
+ tags: this.exportSelectedTags
4159
+ });
4160
+ if (!subset.length) return;
4161
+ this.downloadTests(subset);
4162
+ this.isExporting = false;
4163
+ this.exportSelectedIds.clear();
4164
+ this.exportSelectedTags.clear();
4165
+ this.render();
4166
+ }
3605
4167
  async importAllData(file) {
3606
4168
  const text = await file.text();
3607
4169
  let data;
@@ -3613,7 +4175,6 @@ var ConfigurationElement = class extends HTMLElement {
3613
4175
  if (!data || !Array.isArray(data.tests) || !Array.isArray(data.interceptors)) {
3614
4176
  throw new Error(this.t("CONFIG.JSON_BAD_FORMAT"));
3615
4177
  }
3616
- await this.persistence.clearAllData();
3617
4178
  await this.persistence.ingestFileData(data.tests, data.interceptors);
3618
4179
  }
3619
4180
  render() {
@@ -3624,7 +4185,13 @@ var ConfigurationElement = class extends HTMLElement {
3624
4185
  filesystemGranted: this.filesystemGranted,
3625
4186
  cypressFolderName: this.cypressFolderName,
3626
4187
  smartSelectorEnabled: this.smartSelectorEnabled,
3627
- startHidden: this.startHidden
4188
+ startHidden: this.startHidden,
4189
+ resumeTtlMinutes: this.resumeTtlMinutes,
4190
+ isExporting: this.isExporting,
4191
+ exportMode: this.exportMode,
4192
+ exportTests: this.exportTests,
4193
+ exportSelectedIds: this.exportSelectedIds,
4194
+ exportSelectedTags: this.exportSelectedTags
3628
4195
  }, this.t.bind(this))}`;
3629
4196
  ;
3630
4197
  this.shadow.getElementById("lang-select").addEventListener(
@@ -3643,13 +4210,29 @@ var ConfigurationElement = class extends HTMLElement {
3643
4210
  "change",
3644
4211
  (e) => this.onStartHiddenChange(e.target.checked)
3645
4212
  );
4213
+ this.shadow.getElementById("resume-ttl-input").addEventListener(
4214
+ "change",
4215
+ (e) => this.onResumeTtlChange(Number(e.target.value))
4216
+ );
4217
+ this.shadow.getElementById("btn-reset-position")?.addEventListener("click", () => this.onResetWidgetPosition());
3646
4218
  this.shadow.getElementById("selector-strategy").addEventListener(
3647
4219
  "change",
3648
4220
  (e) => this.onSelectorStrategyChange(e.target.value)
3649
4221
  );
3650
4222
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
3651
4223
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
3652
- this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.exportAllData());
4224
+ this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.openExportDialog());
4225
+ this.shadow.getElementById("btn-export-confirm")?.addEventListener("click", () => this.confirmExport());
4226
+ this.shadow.getElementById("btn-export-cancel")?.addEventListener("click", () => this.cancelExport());
4227
+ this.shadow.querySelectorAll("[data-export-mode]").forEach(
4228
+ (el) => el.addEventListener("click", () => this.setExportMode(el.dataset["exportMode"]))
4229
+ );
4230
+ this.shadow.querySelectorAll("[data-export-test]").forEach(
4231
+ (el) => el.addEventListener("change", () => this.toggleExportTest(Number(el.dataset["exportTest"])))
4232
+ );
4233
+ this.shadow.querySelectorAll("[data-export-tag]").forEach(
4234
+ (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
4235
+ );
3653
4236
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
3654
4237
  const file = e.target.files?.[0];
3655
4238
  if (!file) return;
@@ -3759,18 +4342,21 @@ var ADVANCED_TEST_EDITOR_STYLES = `
3759
4342
  }
3760
4343
  .btn-copy:hover { background: #30363d; color: #e6edf3; }
3761
4344
  .sidebar-toolbar {
3762
- display: flex; gap: 4px; padding: 6px 6px 4px;
4345
+ display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 6px 4px;
3763
4346
  border-bottom: 1px solid #21262d; flex-shrink: 0; background: #0d1117;
3764
4347
  }
3765
4348
  .btn-toolbar {
3766
- flex: 1; padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
4349
+ flex: 1 1 calc(50% - 2px); padding: 4px 6px; border: 1px solid #30363d; border-radius: 5px; cursor: pointer;
3767
4350
  font-size: 10px; font-weight: 500; background: #161b22; color: #8b949e;
3768
4351
  transition: background 0.12s, color 0.12s; white-space: nowrap; overflow: hidden;
3769
4352
  text-overflow: ellipsis;
3770
4353
  }
4354
+ .btn-toolbar.btn-full { flex: 1 1 100%; }
3771
4355
  .btn-toolbar:hover { background: #30363d; color: #e6edf3; }
3772
4356
  .btn-toolbar.btn-new { color: #3fb950; border-color: #238636; }
3773
4357
  .btn-toolbar.btn-new:hover { background: #238636; color: #fff; }
4358
+ .btn-toolbar.btn-new-folder { color: #d29922; border-color: #9e6a03; }
4359
+ .btn-toolbar.btn-new-folder:hover { background: #9e6a03; color: #fff; }
3774
4360
  .new-file-form {
3775
4361
  padding: 6px; border-bottom: 1px solid #21262d; display: flex; flex-direction: column; gap: 4px;
3776
4362
  }
@@ -3804,7 +4390,7 @@ function renderNoPermission(needsReauth, t) {
3804
4390
  </div>`;
3805
4391
  }
3806
4392
  function renderAdvancedEditor(state, t) {
3807
- const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, collapsedDirs, sidebarWidth } = state;
4393
+ const { e2eTree, selectedFile, selectedFileContent, testItBlock, interceptorsBlock, testNotes, saveButtonEnabled, isCreatingFile, isCreatingFolder, collapsedDirs, sidebarWidth } = state;
3808
4394
  const treeHtml = e2eTree.length ? renderTree(e2eTree, selectedFile, collapsedDirs) : `<div class="tree-item" style="color:#6c7a99">${t("ADVANCED_EDITOR.NO_FILES")}</div>`;
3809
4395
  const contentHtml = selectedFileContent ? `<div class="file-name">\u{1F4C4} ${escHtml(selectedFile?.name ?? "")}</div>
3810
4396
  <pre class="pre-file">${syntaxHighlight(selectedFileContent.slice(0, 6e3))}${selectedFileContent.length > 6e3 ? "\n..." : ""}</pre>` : `<div class="placeholder">${t("ADVANCED_EDITOR.SELECT_FILE")}</div>`;
@@ -3831,16 +4417,25 @@ function renderAdvancedEditor(state, t) {
3831
4417
  <button id="btn-new-file-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
3832
4418
  </div>
3833
4419
  </div>` : "";
4420
+ const newFolderForm = isCreatingFolder ? `<div class="new-file-form">
4421
+ <input id="input-new-folder" type="text" placeholder="${escHtml(t("ADVANCED_EDITOR.NEW_FOLDER_PLACEHOLDER"))}" autocomplete="off" />
4422
+ <div class="new-file-actions">
4423
+ <button id="btn-new-folder-confirm" class="btn-confirm">${t("ADVANCED_EDITOR.NEW_FILE_CONFIRM")}</button>
4424
+ <button id="btn-new-folder-cancel" class="btn-cancel-form">${t("ADVANCED_EDITOR.NEW_FILE_CANCEL")}</button>
4425
+ </div>
4426
+ </div>` : "";
3834
4427
  const toolbar = `
3835
4428
  <div class="sidebar-toolbar">
3836
4429
  <button id="btn-new-file" class="btn-toolbar btn-new">${t("ADVANCED_EDITOR.NEW_FILE_BTN")}</button>
3837
- <button id="btn-refresh" class="btn-toolbar">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
4430
+ <button id="btn-new-folder" class="btn-toolbar btn-new-folder">${t("ADVANCED_EDITOR.NEW_FOLDER_BTN")}</button>
4431
+ <button id="btn-refresh" class="btn-toolbar btn-full">${t("ADVANCED_EDITOR.REFRESH_BTN")}</button>
3838
4432
  </div>`;
3839
4433
  return `
3840
4434
  <div class="layout">
3841
4435
  <div class="sidebar" style="width:${sidebarWidth}px">
3842
4436
  ${toolbar}
3843
4437
  ${newFileForm}
4438
+ ${newFolderForm}
3844
4439
  <div class="tree-scroll">${treeHtml}</div>
3845
4440
  </div>
3846
4441
  <div id="resize-handle" class="resize-handle"></div>
@@ -3911,6 +4506,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3911
4506
  previewFileName = null;
3912
4507
  previewFileContent = null;
3913
4508
  isCreatingFile = false;
4509
+ isCreatingFolder = false;
3914
4510
  collapsedDirs = /* @__PURE__ */ new Set();
3915
4511
  sidebarWidth = 220;
3916
4512
  hasPermission = false;
@@ -3973,7 +4569,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3973
4569
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
3974
4570
  if (!name) return;
3975
4571
  const fileName = `${name}.cy.ts`;
3976
- const template = `describe('${name}', () => {
4572
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
3977
4573
 
3978
4574
  it('should ', () => {
3979
4575
 
@@ -3991,6 +4587,17 @@ var AdvancedTestEditorElement = class extends HTMLElement {
3991
4587
  this.isCreatingFile = false;
3992
4588
  await this.getFoldersData();
3993
4589
  }
4590
+ async createNewFolder(rawName) {
4591
+ if (!this._e2eHandle) return;
4592
+ const name = rawName.trim().replace(/[/\\]/g, "");
4593
+ if (!name) return;
4594
+ try {
4595
+ await this._e2eHandle.getDirectoryHandle(name, { create: true });
4596
+ } catch {
4597
+ }
4598
+ this.isCreatingFolder = false;
4599
+ await this.getFoldersData();
4600
+ }
3994
4601
  async refreshTree() {
3995
4602
  await this.getFoldersData();
3996
4603
  }
@@ -4048,7 +4655,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4048
4655
  fileName: this.selectedFile?.name ?? "",
4049
4656
  testId: this.testId,
4050
4657
  itBlock: this.testItBlock,
4051
- interceptorsBlock: this.interceptorsBlock
4658
+ interceptorsBlock: this.interceptorsBlock,
4659
+ notes: this.testNotes
4052
4660
  },
4053
4661
  bubbles: true,
4054
4662
  composed: true
@@ -4125,6 +4733,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4125
4733
  testNotes: this.testNotes,
4126
4734
  saveButtonEnabled: this.saveButtonEnabled,
4127
4735
  isCreatingFile: this.isCreatingFile,
4736
+ isCreatingFolder: this.isCreatingFolder,
4128
4737
  collapsedDirs: this.collapsedDirs,
4129
4738
  sidebarWidth: this.sidebarWidth
4130
4739
  }, this.t.bind(this))}`;
@@ -4148,9 +4757,16 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4148
4757
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.closePreview());
4149
4758
  this.shadow.getElementById("btn-new-file")?.addEventListener("click", () => {
4150
4759
  this.isCreatingFile = !this.isCreatingFile;
4760
+ this.isCreatingFolder = false;
4151
4761
  this.render();
4152
4762
  if (this.isCreatingFile) this.shadow.getElementById("input-new-file")?.focus();
4153
4763
  });
4764
+ this.shadow.getElementById("btn-new-folder")?.addEventListener("click", () => {
4765
+ this.isCreatingFolder = !this.isCreatingFolder;
4766
+ this.isCreatingFile = false;
4767
+ this.render();
4768
+ if (this.isCreatingFolder) this.shadow.getElementById("input-new-folder")?.focus();
4769
+ });
4154
4770
  this.shadow.getElementById("btn-refresh")?.addEventListener("click", () => this.refreshTree());
4155
4771
  this.shadow.getElementById("btn-new-file-confirm")?.addEventListener("click", () => {
4156
4772
  const input2 = this.shadow.getElementById("input-new-file");
@@ -4169,6 +4785,23 @@ var AdvancedTestEditorElement = class extends HTMLElement {
4169
4785
  this.render();
4170
4786
  }
4171
4787
  });
4788
+ this.shadow.getElementById("btn-new-folder-confirm")?.addEventListener("click", () => {
4789
+ const folderInput2 = this.shadow.getElementById("input-new-folder");
4790
+ this.createNewFolder(folderInput2?.value ?? "");
4791
+ });
4792
+ this.shadow.getElementById("btn-new-folder-cancel")?.addEventListener("click", () => {
4793
+ this.isCreatingFolder = false;
4794
+ this.render();
4795
+ });
4796
+ const folderInput = this.shadow.getElementById("input-new-folder");
4797
+ folderInput?.addEventListener("keydown", (e) => {
4798
+ if (e.key === "Enter") {
4799
+ this.createNewFolder(folderInput.value);
4800
+ } else if (e.key === "Escape") {
4801
+ this.isCreatingFolder = false;
4802
+ this.render();
4803
+ }
4804
+ });
4172
4805
  }
4173
4806
  };
4174
4807
  if (!customElements.get("advanced-test-editor")) {
@@ -4254,7 +4887,30 @@ var FILE_PREVIEW_STYLES = `
4254
4887
  .btn-save:hover { background: #2ea043; border-color: #2ea043; }
4255
4888
  .btn-launch { background: transparent; border-color: #e3b341; color: #e3b341; }
4256
4889
  .btn-launch:hover { background: rgba(227,179,65,0.1); }
4890
+ .btn-launch:disabled { opacity: 0.45; cursor: not-allowed; }
4891
+ .btn-launch:disabled:hover { background: transparent; }
4892
+ .launch-hint { font-size: 10px; color: #8b6d3b; align-self: center; }
4893
+ .btn-insert { background: transparent; border-color: #a371f7; color: #a371f7; }
4894
+ .btn-insert:hover { background: rgba(163,113,247,0.12); color: #c8a8ff; }
4257
4895
  .btn-diff-active { background: rgba(47,129,247,0.15); border-color: rgba(47,129,247,0.4); color: #2f81f7; }
4896
+
4897
+ /* \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 */
4898
+ .run-result {
4899
+ border-top: 1px solid #21262d; padding: 8px 12px; background: #0d1117;
4900
+ max-height: 220px; overflow-y: auto; flex-shrink: 0;
4901
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
4902
+ }
4903
+ .run-status { font-size: 12px; font-weight: 600; }
4904
+ .run-passed .run-status { color: #3fb950; }
4905
+ .run-failed .run-status { color: #f85149; }
4906
+ .run-running .run-status { color: #e3b341; }
4907
+ .run-error .run-status { color: #f85149; }
4908
+ .run-output {
4909
+ margin: 6px 0 0; padding: 8px 10px; background: #161b22; border: 1px solid #21262d;
4910
+ border-radius: 5px; font-size: 10px; color: #c9d1d9; line-height: 1.5;
4911
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
4912
+ white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto;
4913
+ }
4258
4914
  `;
4259
4915
 
4260
4916
  // src/components/file-preview/file-preview.template.ts
@@ -4312,8 +4968,14 @@ function buildDiffHtml(original, current, t) {
4312
4968
  return `<div class="diff-line ${type}"><span class="diff-sign">${sign}</span><span>${escHtml(line)}</span></div>`;
4313
4969
  }).join("");
4314
4970
  }
4971
+ var RUN_STATUS_KEY = {
4972
+ running: "FILE_PREVIEW.LAUNCH_RUNNING",
4973
+ passed: "FILE_PREVIEW.LAUNCH_PASSED",
4974
+ failed: "FILE_PREVIEW.LAUNCH_FAILED",
4975
+ error: "FILE_PREVIEW.LAUNCH_NO_RUNNER"
4976
+ };
4315
4977
  function renderFilePreview(state, t) {
4316
- const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel } = state;
4978
+ const { fileName, showDiff, fileContent, originalContent, currentContent, itBlock, interceptorsBlock, closeLabel, isLocal, runState, runOutput } = state;
4317
4979
  const blocksPanelHtml = itBlock || interceptorsBlock ? `
4318
4980
  <div class="blocks-panel">
4319
4981
  ${itBlock ? `
@@ -4334,6 +4996,13 @@ function renderFilePreview(state, t) {
4334
4996
  </div>` : ""}
4335
4997
  </div>` : "";
4336
4998
  const hasChanges = originalContent !== null && originalContent !== (fileContent ?? "");
4999
+ const hasBlocks = !!(itBlock || interceptorsBlock);
5000
+ 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>
5001
+ <span class="launch-hint">${t("FILE_PREVIEW.LAUNCH_LOCAL_ONLY")}</span>`;
5002
+ const runResultHtml = runState !== "idle" ? `<div class="run-result run-${runState}">
5003
+ <span class="run-status">${t(RUN_STATUS_KEY[runState])}</span>
5004
+ ${runOutput ? `<pre class="run-output">${escHtml(runOutput.slice(0, 8e3))}</pre>` : ""}
5005
+ </div>` : "";
4337
5006
  return `
4338
5007
  <div class="container">
4339
5008
  <div class="header">
@@ -4344,9 +5013,11 @@ function renderFilePreview(state, t) {
4344
5013
  ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
4345
5014
  ${blocksPanelHtml}
4346
5015
  </div>
5016
+ ${runResultHtml}
4347
5017
  <div class="footer">
4348
- <button id="btn-launch" class="btn-launch">${t("FILE_PREVIEW.LAUNCH_BTN")}</button>
5018
+ ${launchBtnHtml}
4349
5019
  ${hasChanges ? `<button id="btn-diff" class="${showDiff ? "btn-diff-active" : ""}">${t("FILE_PREVIEW.DIFF_BTN")}</button>` : ""}
5020
+ ${hasBlocks && !showDiff ? `<button id="btn-insert" class="btn-insert" title="${t("FILE_PREVIEW.INSERT_TITLE")}">${t("FILE_PREVIEW.INSERT_BTN")}</button>` : ""}
4350
5021
  <button id="btn-save" class="btn-save">${t("FILE_PREVIEW.SAVE_BTN")}</button>
4351
5022
  <button id="btn-close">${escHtml(closeLabel || t("FILE_PREVIEW.CLOSE_BTN"))}</button>
4352
5023
  </div>
@@ -4362,11 +5033,18 @@ var FilePreviewElement = class extends HTMLElement {
4362
5033
  translation = translationService;
4363
5034
  itBlock = "";
4364
5035
  interceptorsBlock = "";
5036
+ notes = "";
4365
5037
  commands = [];
4366
5038
  interceptors = [];
5039
+ /** Endpoint of the local Cypress runner. Configurable; defaults to the bundled runner's port. */
5040
+ runnerUrl = "http://localhost:8123/run-test";
5041
+ /** Whether the app is served locally — gates the launch button. Overridable for tests. */
5042
+ isLocal = isLocalHost(window.location.hostname);
4367
5043
  _fileContent = null;
4368
5044
  _originalContent = null;
4369
5045
  _showDiff = false;
5046
+ _runState = "idle";
5047
+ _runOutput = "";
4370
5048
  constructor() {
4371
5049
  super();
4372
5050
  this.shadow = this.attachShadow({ mode: "open" });
@@ -4394,15 +5072,34 @@ var FilePreviewElement = class extends HTMLElement {
4394
5072
  onClose() {
4395
5073
  this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
4396
5074
  }
5075
+ /**
5076
+ * Runs the current spec headless via the local runner and shows the result.
5077
+ * No-op off localhost. Never throws — connection failures surface as an
5078
+ * "error" state + toast instead of an unhandled rejection.
5079
+ */
4397
5080
  async launchTest(specPath) {
4398
- const path = specPath ?? (this.fileName ? `cypress/e2e/${this.fileName}` : "");
5081
+ if (!this.isLocal) return;
5082
+ const path = specPath ?? this.fileName ?? "";
4399
5083
  if (!path) return;
4400
- const response = await fetch("http://localhost:8123/run-test", {
4401
- method: "POST",
4402
- headers: { "Content-Type": "application/json" },
4403
- body: JSON.stringify({ specPath: path })
4404
- });
4405
- await response.json();
5084
+ this._runState = "running";
5085
+ this._runOutput = "";
5086
+ this.render();
5087
+ try {
5088
+ const response = await fetch(this.runnerUrl, {
5089
+ method: "POST",
5090
+ headers: { "Content-Type": "application/json" },
5091
+ body: JSON.stringify({ specPath: path })
5092
+ });
5093
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
5094
+ const result = await response.json();
5095
+ this._runOutput = result.output ?? "";
5096
+ this._runState = result.success ? "passed" : "failed";
5097
+ } catch {
5098
+ this._runState = "error";
5099
+ this._runOutput = "";
5100
+ showToast(this.t("FILE_PREVIEW.LAUNCH_NO_RUNNER"), false);
5101
+ }
5102
+ this.render();
4406
5103
  }
4407
5104
  copyToClipboard(text) {
4408
5105
  navigator.clipboard?.writeText(text);
@@ -4411,6 +5108,32 @@ var FilePreviewElement = class extends HTMLElement {
4411
5108
  this._showDiff = !this._showDiff;
4412
5109
  this.render();
4413
5110
  }
5111
+ /**
5112
+ * Merges the it() and beforeEach() blocks into the current editor content
5113
+ * using the same logic as the automatic "Insert into file" action, so the
5114
+ * user does not have to copy/paste manually. Leaves the content untouched
5115
+ * (and warns) when the file has no valid describe() block to insert into.
5116
+ */
5117
+ insertBlocks() {
5118
+ const base = this.textarea?.value ?? this._fileContent ?? "";
5119
+ let content = base;
5120
+ if (this.interceptorsBlock) {
5121
+ const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
5122
+ if (merged) content = merged;
5123
+ }
5124
+ if (this.itBlock) {
5125
+ const comment = this.notes ? advancedTestTransformationService.buildBlockComment(this.notes) + "\n" : "";
5126
+ const merged = advancedTestTransformationService.insertItBlock(content, comment + this.itBlock);
5127
+ if (merged) content = merged;
5128
+ }
5129
+ if (content === base) {
5130
+ showToast(this.t("FILE_PREVIEW.INSERT_ERROR"), false);
5131
+ return;
5132
+ }
5133
+ this._fileContent = content;
5134
+ this.render();
5135
+ showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
5136
+ }
4414
5137
  render() {
4415
5138
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
4416
5139
  fileName: this.fileName,
@@ -4420,7 +5143,10 @@ var FilePreviewElement = class extends HTMLElement {
4420
5143
  currentContent: this.textarea?.value ?? this._fileContent ?? "",
4421
5144
  itBlock: this.itBlock,
4422
5145
  interceptorsBlock: this.interceptorsBlock,
4423
- closeLabel: this.closeLabel
5146
+ closeLabel: this.closeLabel,
5147
+ isLocal: this.isLocal,
5148
+ runState: this._runState,
5149
+ runOutput: this._runOutput
4424
5150
  }, this.t.bind(this))}`;
4425
5151
  this.textarea = this.shadow.getElementById("editor");
4426
5152
  if (this.textarea) {
@@ -4436,6 +5162,7 @@ var FilePreviewElement = class extends HTMLElement {
4436
5162
  this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
4437
5163
  });
4438
5164
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
5165
+ this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
4439
5166
  this.shadow.getElementById("btn-copy-it")?.addEventListener("click", () => {
4440
5167
  this.copyToClipboard(this.itBlock);
4441
5168
  });
@@ -4452,15 +5179,37 @@ if (!customElements.get("file-preview")) {
4452
5179
  var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
4453
5180
 
4454
5181
  // src/components/lib-e2e-recorder/lib-e2e-recorder.styles.ts
5182
+ var DIRECTIONS = ["up-left", "up-right", "down-left", "down-right"];
5183
+ function directionBlocks() {
5184
+ return DIRECTIONS.map((dir) => {
5185
+ const sx = dir.endsWith("left") ? -1 : 1;
5186
+ const sy = dir.startsWith("up") ? -1 : 1;
5187
+ const tv = dir.startsWith("up") ? "bottom" : "top";
5188
+ const th = dir.endsWith("left") ? "right" : "left";
5189
+ const ov = tv === "bottom" ? "top" : "bottom";
5190
+ const oh = th === "right" ? "left" : "right";
5191
+ const sel = `.widget[data-expand="${dir}"]`;
5192
+ const labelGeneral = `${sel} .btn-action::after { ${oh}: calc(100% + 9px); ${th}: auto; top: 50%; bottom: auto; transform: translateY(-50%); }`;
5193
+ const labelEnds = `${sel} .btn-action[data-n="1"]::after, ${sel} .btn-action[data-n="4"]::after { ${ov}: calc(100% + 9px); ${tv}: auto; left: 50%; right: auto; transform: translateX(-50%); }`;
5194
+ return `
5195
+ ${sel} { --sx: ${sx}; --sy: ${sy}; }
5196
+ ${sel} .btn-toggle { ${tv}: 24px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
5197
+ ${sel} .btn-pause { ${tv}: 78px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
5198
+ ${sel} .btn-action { ${tv}: 28px; ${th}: 28px; ${ov}: auto; ${oh}: auto; }
5199
+ ${labelGeneral}
5200
+ ${labelEnds}
5201
+ `;
5202
+ }).join("\n");
5203
+ }
4455
5204
  function getRecorderStyles(rec, paused) {
4456
5205
  return `
4457
5206
  :host { all: initial; }
4458
5207
  *, *::before, *::after { box-sizing: border-box; }
4459
5208
 
4460
5209
  /*
4461
- * Invisible 190\xD7190 hit area anchored at bottom-right.
4462
- * Keeps :hover alive while the cursor travels from the
4463
- * toggle to any of the radial action buttons.
5210
+ * Invisible 190\xD7190 hit area. Position (left/top) is set from JS so the
5211
+ * widget can be dragged; --sx/--sy + data-expand orient the radial menu so
5212
+ * it always expands toward the viewport interior. Defaults to bottom-right.
4464
5213
  */
4465
5214
  .widget {
4466
5215
  position: fixed;
@@ -4469,6 +5218,8 @@ function getRecorderStyles(rec, paused) {
4469
5218
  width: 190px;
4470
5219
  height: 190px;
4471
5220
  z-index: 2147483647;
5221
+ --sx: -1;
5222
+ --sy: -1;
4472
5223
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4473
5224
  }
4474
5225
 
@@ -4481,7 +5232,8 @@ function getRecorderStyles(rec, paused) {
4481
5232
  height: 44px;
4482
5233
  border-radius: 50%;
4483
5234
  border: none;
4484
- cursor: pointer;
5235
+ cursor: grab;
5236
+ touch-action: none;
4485
5237
  font-size: 19px;
4486
5238
  background: ${rec ? "linear-gradient(135deg,#f85149 0%,#da3633 100%)" : "linear-gradient(135deg,#2f81f7 0%,#1f6feb 100%)"};
4487
5239
  color: #fff;
@@ -4494,7 +5246,7 @@ function getRecorderStyles(rec, paused) {
4494
5246
  ${rec ? "animation: toggle-pulse 2s ease-in-out infinite;" : ""}
4495
5247
  }
4496
5248
  .btn-toggle:hover { transform: scale(1.1); }
4497
- .btn-toggle:active { transform: scale(0.93); }
5249
+ .btn-toggle:active { transform: scale(0.93); cursor: grabbing; }
4498
5250
 
4499
5251
  @keyframes toggle-pulse {
4500
5252
  0%,100% { box-shadow: 0 4px 20px rgba(248,81,73,.55),0 0 0 4px rgba(248,81,73,.13); }
@@ -4525,11 +5277,6 @@ function getRecorderStyles(rec, paused) {
4525
5277
  .btn-pause:active { transform: scale(0.93); }
4526
5278
 
4527
5279
  /* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4528
- /*
4529
- * All three start centered on the toggle button
4530
- * (toggle center = bottom:46px right:46px from widget edge;
4531
- * button half = 18px \u2192 bottom:28px right:28px).
4532
- */
4533
5280
  .btn-action {
4534
5281
  position: absolute;
4535
5282
  bottom: 28px;
@@ -4551,12 +5298,11 @@ function getRecorderStyles(rec, paused) {
4551
5298
  opacity: 0;
4552
5299
  transform: scale(0.35);
4553
5300
  pointer-events: none;
4554
- /* Collapse: fast, no spring */
4555
5301
  transition: opacity .15s, transform .18s ease-in,
4556
5302
  background .15s, color .12s, box-shadow .15s;
4557
5303
  }
4558
5304
 
4559
- /* Label to the left of each button */
5305
+ /* Label chip */
4560
5306
  .btn-action::after {
4561
5307
  content: attr(data-label);
4562
5308
  position: absolute;
@@ -4576,14 +5322,6 @@ function getRecorderStyles(rec, paused) {
4576
5322
  border: 1px solid rgba(48,54,61,.8);
4577
5323
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
4578
5324
  }
4579
- /* Button 1 (top) \u2014 label above instead of left to avoid overlap with btn 2 */
4580
- .btn-action[data-n="1"]::after {
4581
- right: auto;
4582
- left: 50%;
4583
- top: auto;
4584
- bottom: calc(100% + 9px);
4585
- transform: translateX(-50%);
4586
- }
4587
5325
  .btn-action:hover::after { opacity: 1; }
4588
5326
  .btn-action:hover { background: #21262d; color: #e6edf3; }
4589
5327
  .btn-action:active { background: #30363d !important; }
@@ -4596,31 +5334,26 @@ function getRecorderStyles(rec, paused) {
4596
5334
  background .15s, color .12s, box-shadow .15s;
4597
5335
  }
4598
5336
 
4599
- /* Arc positions \u2014 4 buttons, 30\xB0 spacing, radius 90px */
4600
- .widget:hover .btn-action[data-n="1"] { /* 0\xB0 \u2014 straight up */
4601
- transform: translateY(-90px) scale(1);
5337
+ /* Arc positions \u2014 signs come from --sx/--sy (data-expand) */
5338
+ .widget:hover .btn-action[data-n="1"] { /* vertical extreme */
5339
+ transform: translateY(calc(var(--sy) * 90px)) scale(1);
4602
5340
  transition-delay: .03s;
4603
5341
  }
4604
- .widget:hover .btn-action[data-n="2"] { /* 30\xB0 \u2014 upper-left */
4605
- transform: translate(-45px,-78px) scale(1);
5342
+ .widget:hover .btn-action[data-n="2"] { /* diagonal near-vertical */
5343
+ transform: translate(calc(var(--sx) * 45px), calc(var(--sy) * 78px)) scale(1);
4606
5344
  transition-delay: .07s;
4607
5345
  }
4608
- .widget:hover .btn-action[data-n="3"] { /* 60\xB0 \u2014 left-upper */
4609
- transform: translate(-78px,-45px) scale(1);
5346
+ .widget:hover .btn-action[data-n="3"] { /* diagonal near-horizontal */
5347
+ transform: translate(calc(var(--sx) * 78px), calc(var(--sy) * 45px)) scale(1);
4610
5348
  transition-delay: .11s;
4611
5349
  }
4612
- .widget:hover .btn-action[data-n="4"] { /* 90\xB0 \u2014 straight left */
4613
- transform: translateX(-90px) scale(1);
5350
+ .widget:hover .btn-action[data-n="4"] { /* horizontal extreme */
5351
+ transform: translateX(calc(var(--sx) * 90px)) scale(1);
4614
5352
  transition-delay: .15s;
4615
5353
  }
4616
- /* Button 4 (pure left) \u2014 label above to avoid going off-screen */
4617
- .btn-action[data-n="4"]::after {
4618
- right: auto;
4619
- left: 50%;
4620
- top: auto;
4621
- bottom: calc(100% + 9px);
4622
- transform: translateX(-50%);
4623
- }
5354
+
5355
+ /* Per-direction anchors + label sides */
5356
+ ${directionBlocks()}
4624
5357
 
4625
5358
  /* \u2500\u2500 REC / PAUSED badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4626
5359
  .rec-badge {
@@ -4683,7 +5416,18 @@ var LibE2eRecorderElement = class extends HTMLElement {
4683
5416
  interceptorsUnsub;
4684
5417
  pauseUnsub;
4685
5418
  selectorNotFoundUnsub;
5419
+ langUnsub;
5420
+ sessionUnsub;
5421
+ sessionSaveTimer;
4686
5422
  controlFirstTimeData = true;
5423
+ // ── draggable widget (spec 007) ──
5424
+ togglePos = null;
5425
+ expandDir = "up-left";
5426
+ dragState;
5427
+ suppressNextToggleClick = false;
5428
+ widgetPointerMove;
5429
+ widgetPointerUp;
5430
+ widgetResize;
4687
5431
  _previsualizerRef = null;
4688
5432
  httpMonitor;
4689
5433
  smartSelectorEnabled = true;
@@ -4726,17 +5470,31 @@ var LibE2eRecorderElement = class extends HTMLElement {
4726
5470
  this.initSubscriptions();
4727
5471
  this.render();
4728
5472
  this.initVisibility();
5473
+ this.initSessionContinuity();
5474
+ this.initWidgetPosition();
4729
5475
  this.keydownHandler = (e) => this.handleKeyboardEvent(e);
4730
5476
  window.addEventListener("keydown", this.keydownHandler);
5477
+ this.widgetPointerMove = (e) => this.onWidgetPointerMove(e);
5478
+ this.widgetPointerUp = () => this.onWidgetPointerUp();
5479
+ this.widgetResize = () => this.applyWidgetPosition();
5480
+ window.addEventListener("pointermove", this.widgetPointerMove);
5481
+ window.addEventListener("pointerup", this.widgetPointerUp);
5482
+ window.addEventListener("resize", this.widgetResize);
4731
5483
  }
4732
5484
  disconnectedCallback() {
4733
5485
  if (this._isDisabled) return;
4734
5486
  window.removeEventListener("keydown", this.keydownHandler);
5487
+ this.flushActiveSessionOnDisconnect();
4735
5488
  this.recordingUnsub?.();
4736
5489
  this.commandsUnsub?.();
4737
5490
  this.interceptorsUnsub?.();
4738
5491
  this.pauseUnsub?.();
4739
5492
  this.selectorNotFoundUnsub?.();
5493
+ this.langUnsub?.();
5494
+ this.sessionUnsub?.();
5495
+ if (this.widgetPointerMove) window.removeEventListener("pointermove", this.widgetPointerMove);
5496
+ if (this.widgetPointerUp) window.removeEventListener("pointerup", this.widgetPointerUp);
5497
+ if (this.widgetResize) window.removeEventListener("resize", this.widgetResize);
4740
5498
  this.httpMonitor?.uninstall();
4741
5499
  this.recording.destroy();
4742
5500
  }
@@ -4759,11 +5517,13 @@ var LibE2eRecorderElement = class extends HTMLElement {
4759
5517
  this.isRecording = val;
4760
5518
  if (!val && !this.controlFirstTimeData) {
4761
5519
  this.saveRecordingHistory();
5520
+ this.clearSessionPersistence();
4762
5521
  this.showSaveTestDialog();
4763
5522
  }
4764
5523
  this.controlFirstTimeData = false;
4765
5524
  this.render();
4766
5525
  });
5526
+ this.sessionUnsub = this.recording.onSessionChange((state) => this.persistActiveSession(state));
4767
5527
  this.commandsUnsub = this.recording.onCommandsChange((cmds) => {
4768
5528
  this.cypressCommands = cmds;
4769
5529
  if (this._previsualizerRef) this._previsualizerRef.commands = cmds;
@@ -4779,6 +5539,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
4779
5539
  this.selectorNotFoundUnsub = this.recording.onSelectorNotFound((target) => {
4780
5540
  if (this.smartSelectorEnabled) this.showSelectorPicker(target);
4781
5541
  });
5542
+ this.langUnsub = this.translation.onLangChange(() => this.render());
4782
5543
  }
4783
5544
  showSelectorPicker(target) {
4784
5545
  Promise.resolve().then(() => (init_selector_picker(), selector_picker_exports)).then(() => {
@@ -4807,6 +5568,176 @@ var LibE2eRecorderElement = class extends HTMLElement {
4807
5568
  if (strategy) this.recording.selectorStrategy = strategy;
4808
5569
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4809
5570
  }
5571
+ // ── cross-app session continuity (spec 006) ───────────────────────────────
5572
+ /**
5573
+ * On mount, detect a persisted live recording session and either resume it
5574
+ * silently (recent) or prompt continue/discard (stale). No-op when there is
5575
+ * no actively-recording session.
5576
+ */
5577
+ async initSessionContinuity() {
5578
+ if (this._isDisabled) return;
5579
+ const session = await this.persistence.getActiveSession();
5580
+ if (!session || !session.isRecording) return;
5581
+ const ttlMin = await this.getResumeTtlMinutes();
5582
+ const ageMs = Date.now() - session.updatedAt;
5583
+ if (ageMs <= ttlMin * 6e4) {
5584
+ this.resumeSessionState(session);
5585
+ } else {
5586
+ this.promptResumeOrDiscard(session);
5587
+ }
5588
+ }
5589
+ async getResumeTtlMinutes() {
5590
+ const config = await this.persistence.getConfig(RESUME_TTL_CONFIG_KEY);
5591
+ const raw = config?.[RESUME_TTL_CONFIG_KEY];
5592
+ const n = typeof raw === "number" ? raw : Number(raw);
5593
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_RESUME_TTL_MINUTES;
5594
+ }
5595
+ /** Rehydrates the recorder from a persisted session (no bootstrap re-emitted). */
5596
+ resumeSessionState(session) {
5597
+ this.recording.restoreSession(session);
5598
+ this.cypressCommands = session.commands;
5599
+ this.interceptors = session.interceptors;
5600
+ this.controlFirstTimeData = false;
5601
+ this.render();
5602
+ }
5603
+ async promptResumeOrDiscard(session) {
5604
+ const t = this.translation.translate.bind(this.translation);
5605
+ const result = await import_sweetalert2.default.fire({
5606
+ title: t("RECORDER.SESSION_RESUME_TITLE"),
5607
+ 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>`,
5608
+ showCancelButton: true,
5609
+ confirmButtonText: t("RECORDER.SESSION_CONTINUE_BTN"),
5610
+ cancelButtonText: t("RECORDER.SESSION_DISCARD_BTN"),
5611
+ color: "#e6edf3"
5612
+ });
5613
+ if (result && result.isConfirmed) {
5614
+ this.resumeSessionState(session);
5615
+ } else {
5616
+ this.discardSession();
5617
+ }
5618
+ }
5619
+ /** Debounced persistence of the live session; clears it the moment recording stops. */
5620
+ persistActiveSession(state) {
5621
+ if (this._isDisabled) return;
5622
+ if (!state.isRecording) {
5623
+ this.clearSessionPersistence();
5624
+ return;
5625
+ }
5626
+ this.writeSessionBreadcrumb(state);
5627
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5628
+ this.sessionSaveTimer = setTimeout(() => {
5629
+ const snap = this.recording.getSessionSnapshot();
5630
+ if (snap.isRecording) this.persistence.saveActiveSession(snap).catch(() => {
5631
+ });
5632
+ }, 300);
5633
+ }
5634
+ writeSessionBreadcrumb(state) {
5635
+ try {
5636
+ localStorage.setItem(ACTIVE_SESSION_BREADCRUMB_KEY, JSON.stringify({
5637
+ sessionId: state.sessionId,
5638
+ isRecording: state.isRecording,
5639
+ updatedAt: state.updatedAt
5640
+ }));
5641
+ } catch {
5642
+ }
5643
+ }
5644
+ flushActiveSessionOnDisconnect() {
5645
+ const snap = this.recording.getSessionSnapshot();
5646
+ if (!snap.isRecording) return;
5647
+ if (this.sessionSaveTimer) clearTimeout(this.sessionSaveTimer);
5648
+ this.writeSessionBreadcrumb(snap);
5649
+ this.persistence.saveActiveSession(snap).catch(() => {
5650
+ });
5651
+ }
5652
+ clearSessionPersistence() {
5653
+ if (this.sessionSaveTimer) {
5654
+ clearTimeout(this.sessionSaveTimer);
5655
+ this.sessionSaveTimer = void 0;
5656
+ }
5657
+ try {
5658
+ localStorage.removeItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5659
+ } catch {
5660
+ }
5661
+ this.persistence.clearActiveSession().catch(() => {
5662
+ });
5663
+ }
5664
+ /** True when a synchronous breadcrumb marks an active recording session. */
5665
+ hasActiveSession() {
5666
+ try {
5667
+ const raw = localStorage.getItem(ACTIVE_SESSION_BREADCRUMB_KEY);
5668
+ if (!raw) return false;
5669
+ return !!JSON.parse(raw).isRecording;
5670
+ } catch {
5671
+ return false;
5672
+ }
5673
+ }
5674
+ /** Programmatically resume the persisted session, if any. */
5675
+ resumeSession() {
5676
+ this.persistence.getActiveSession().then((s) => {
5677
+ if (s) this.resumeSessionState(s);
5678
+ });
5679
+ }
5680
+ /** Discard the persisted live session (breadcrumb + DB record). */
5681
+ discardSession() {
5682
+ this.clearSessionPersistence();
5683
+ }
5684
+ // ── draggable widget (spec 007) ────────────────────────────────────────────
5685
+ /** Loads a previously saved widget position and applies it. */
5686
+ async initWidgetPosition() {
5687
+ const config = await this.persistence.getGeneralConfig();
5688
+ const pos = config?.["widgetPosition"];
5689
+ if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
5690
+ this.togglePos = { x: pos.x, y: pos.y };
5691
+ }
5692
+ this.applyWidgetPosition();
5693
+ }
5694
+ /** Positions the `.widget` box from the (clamped) toggle centre and orients the arc. */
5695
+ applyWidgetPosition() {
5696
+ const widget = this.shadow.querySelector(".widget");
5697
+ if (!widget) return;
5698
+ const vw = window.innerWidth;
5699
+ const vh = window.innerHeight;
5700
+ const centre = this.togglePos ?? defaultTogglePosition(vw, vh);
5701
+ const clamped = clampTogglePosition(centre.x, centre.y, vw, vh);
5702
+ this.expandDir = resolveExpandDirection(clamped.x, clamped.y, vw, vh);
5703
+ const topLeft = boxTopLeftFor(clamped, this.expandDir);
5704
+ widget.style.left = `${topLeft.x}px`;
5705
+ widget.style.top = `${topLeft.y}px`;
5706
+ widget.style.right = "auto";
5707
+ widget.style.bottom = "auto";
5708
+ widget.setAttribute("data-expand", this.expandDir);
5709
+ }
5710
+ beginWidgetDrag(e) {
5711
+ const origin = this.togglePos ?? defaultTogglePosition(window.innerWidth, window.innerHeight);
5712
+ this.dragState = { startX: e.clientX, startY: e.clientY, origX: origin.x, origY: origin.y, moved: false };
5713
+ }
5714
+ onWidgetPointerMove(e) {
5715
+ if (!this.dragState) return;
5716
+ const dx = e.clientX - this.dragState.startX;
5717
+ const dy = e.clientY - this.dragState.startY;
5718
+ if (!this.dragState.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
5719
+ this.dragState.moved = true;
5720
+ this.togglePos = { x: this.dragState.origX + dx, y: this.dragState.origY + dy };
5721
+ this.applyWidgetPosition();
5722
+ }
5723
+ onWidgetPointerUp() {
5724
+ if (!this.dragState) return;
5725
+ const moved = this.dragState.moved;
5726
+ this.dragState = void 0;
5727
+ if (!moved || !this.togglePos) return;
5728
+ this.suppressNextToggleClick = true;
5729
+ const clamped = clampTogglePosition(this.togglePos.x, this.togglePos.y, window.innerWidth, window.innerHeight);
5730
+ this.togglePos = clamped;
5731
+ this.persistence.setConfig({ widgetPosition: clamped }).catch(() => {
5732
+ });
5733
+ }
5734
+ /** Resets the widget to its default corner and clears the saved position. */
5735
+ resetWidgetPosition() {
5736
+ this.togglePos = null;
5737
+ this.persistence.setConfig({ widgetPosition: null }).catch(() => {
5738
+ });
5739
+ this.applyWidgetPosition();
5740
+ }
4810
5741
  toggle() {
4811
5742
  this.recording.toggleRecording();
4812
5743
  }
@@ -5123,6 +6054,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5123
6054
  this.isVisible = !e.detail;
5124
6055
  this.style.display = this.isVisible ? "" : "none";
5125
6056
  });
6057
+ child.addEventListener("resetwidgetposition", () => this.resetWidgetPosition());
5126
6058
  },
5127
6059
  willClose: () => {
5128
6060
  this.isSettingsDialogOpen = false;
@@ -5177,7 +6109,8 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5177
6109
  e.detail.fileName,
5178
6110
  e.detail.testId,
5179
6111
  e.detail.itBlock,
5180
- e.detail.interceptorsBlock
6112
+ e.detail.interceptorsBlock,
6113
+ e.detail.notes
5181
6114
  ), 150);
5182
6115
  });
5183
6116
  },
@@ -5191,7 +6124,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5191
6124
  }, 0);
5192
6125
  });
5193
6126
  }
5194
- showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "") {
6127
+ showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
5195
6128
  import_sweetalert2.default.fire({
5196
6129
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
5197
6130
  html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
@@ -5212,6 +6145,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5212
6145
  child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
5213
6146
  child.itBlock = itBlock;
5214
6147
  child.interceptorsBlock = interceptorsBlock;
6148
+ child.notes = notes;
5215
6149
  container.appendChild(child);
5216
6150
  child.addEventListener("close", () => {
5217
6151
  import_sweetalert2.default.close();
@@ -5274,12 +6208,21 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5274
6208
  const paused = this.isPaused;
5275
6209
  this.style.display = this.isVisible ? "" : "none";
5276
6210
  this.shadow.innerHTML = `<style>${getRecorderStyles(rec, paused)}</style>${renderRecorderWidget(rec, paused, this.translation.translate.bind(this.translation))}`;
5277
- this.shadow.querySelector('[data-action="toggle"]')?.addEventListener("click", () => this.toggle());
6211
+ const toggleBtn = this.shadow.querySelector('[data-action="toggle"]');
6212
+ toggleBtn?.addEventListener("click", () => {
6213
+ if (this.suppressNextToggleClick) {
6214
+ this.suppressNextToggleClick = false;
6215
+ return;
6216
+ }
6217
+ this.toggle();
6218
+ });
6219
+ toggleBtn?.addEventListener("pointerdown", (e) => this.beginWidgetDrag(e));
5278
6220
  this.shadow.querySelector('[data-action="pause"]')?.addEventListener("click", () => this.togglePause());
5279
6221
  this.shadow.querySelector('[data-action="tests"]')?.addEventListener("click", () => this.showSavedTestsDialog());
5280
6222
  this.shadow.querySelector('[data-action="commands"]')?.addEventListener("click", () => this.showCommandsDialog());
5281
6223
  this.shadow.querySelector('[data-action="config"]')?.addEventListener("click", () => this.showSettingsDialog());
5282
6224
  this.shadow.querySelector('[data-action="browse"]')?.addEventListener("click", () => this.showAdvancedEditorDialog());
6225
+ this.applyWidgetPosition();
5283
6226
  }
5284
6227
  };
5285
6228
  if (!customElements.get("lib-e2e-recorder")) {
@@ -5287,38 +6230,53 @@ if (!customElements.get("lib-e2e-recorder")) {
5287
6230
  }
5288
6231
 
5289
6232
  // src/index.ts
5290
- var VERSION = "0.1.0";
6233
+ var VERSION = "0.6.0";
5291
6234
  // Annotate the CommonJS export names for ESM import in node:
5292
6235
  0 && (module.exports = {
6236
+ ACTIVE_SESSION_BREADCRUMB_KEY,
5293
6237
  AdvancedTestEditorElement,
5294
6238
  AdvancedTestTransformationService,
5295
6239
  ConfigurationElement,
5296
6240
  DB_SCHEMA,
5297
6241
  DB_STORE_NAMES,
6242
+ DEFAULT_RESUME_TTL_MINUTES,
6243
+ DRAG_THRESHOLD,
5298
6244
  FilePreviewElement,
5299
6245
  HttpMonitor,
5300
6246
  INPUT_TYPES,
5301
6247
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
6248
+ LOCALE_BY_LANG,
5302
6249
  LibE2eRecorderElement,
5303
6250
  PersistenceService,
6251
+ RESUME_TTL_CONFIG_KEY,
5304
6252
  RecordingService,
5305
6253
  SCROLLBAR_STYLES,
5306
6254
  SUPPORTED_LANGS,
5307
6255
  SaveTestElement,
6256
+ SelectorPickerElement,
5308
6257
  Subject,
6258
+ TOGGLE_MARGIN,
5309
6259
  TestEditorElement,
5310
6260
  TestPrevisualizerElement,
5311
6261
  TransformationService,
5312
6262
  TranslationService,
5313
6263
  VERSION,
5314
6264
  advancedTestTransformationService,
6265
+ boxOffsetForDirection,
6266
+ boxTopLeftFor,
6267
+ clampTogglePosition,
6268
+ defaultTogglePosition,
5315
6269
  generateAlias,
5316
6270
  injectStyles,
5317
6271
  isLang,
6272
+ isLocalHost,
6273
+ localeForLang,
5318
6274
  makeModalResizable,
5319
6275
  makeSwalDraggable,
5320
6276
  makeSwalDraggableByContentId,
5321
6277
  persistenceService,
6278
+ resolveExpandDirection,
6279
+ selectTestsForExport,
5322
6280
  setSwal2DataCyAttribute,
5323
6281
  showToast,
5324
6282
  transformationService,