lib-e2e-cypress-for-dummys-ts 0.8.0 → 0.10.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
@@ -30,6 +30,41 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/utils/code-format.utils.ts
34
+ function gcd(a, b) {
35
+ while (b) {
36
+ const t = b;
37
+ b = a % b;
38
+ a = t;
39
+ }
40
+ return a;
41
+ }
42
+ function escapeSingleQuotes(value) {
43
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
44
+ }
45
+ function escapeCssAttrValue(value) {
46
+ return value.replace(/"/g, '\\"');
47
+ }
48
+ function normalizeBlock(code, baseIndent) {
49
+ const lines = code.split("\n").map((l) => l.trimEnd());
50
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
51
+ if (!nonEmpty.length) return "";
52
+ const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
53
+ const minIndent = Math.min(...indents);
54
+ const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
55
+ const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
56
+ return lines.map((l) => {
57
+ if (!l.trim()) return "";
58
+ const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
59
+ return baseIndent + " ".repeat(level) + l.trimStart();
60
+ }).join("\n");
61
+ }
62
+ var init_code_format_utils = __esm({
63
+ "src/utils/code-format.utils.ts"() {
64
+ "use strict";
65
+ }
66
+ });
67
+
33
68
  // src/utils/selector-quality.utils.ts
34
69
  function getSelectorQuality(el) {
35
70
  if (el.hasAttribute("data-cy") || el.hasAttribute("data-testid") || el.hasAttribute("aria-label")) {
@@ -46,11 +81,11 @@ function getSelectorQuality(el) {
46
81
  }
47
82
  function buildPickerSelector(el) {
48
83
  const dataCy = el.getAttribute("data-cy");
49
- if (dataCy) return `[data-cy="${dataCy}"]`;
84
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
50
85
  const dataTestid = el.getAttribute("data-testid");
51
- if (dataTestid) return `[data-testid="${dataTestid}"]`;
86
+ if (dataTestid) return `[data-testid="${escapeCssAttrValue(dataTestid)}"]`;
52
87
  const ariaLabel = el.getAttribute("aria-label");
53
- if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
88
+ if (ariaLabel) return `[aria-label="${escapeCssAttrValue(ariaLabel)}"]`;
54
89
  const rawId = el.id;
55
90
  if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
56
91
  return `#${rawId}`;
@@ -84,6 +119,7 @@ var FORBIDDEN_ID_PREFIXES;
84
119
  var init_selector_quality_utils = __esm({
85
120
  "src/utils/selector-quality.utils.ts"() {
86
121
  "use strict";
122
+ init_code_format_utils();
87
123
  FORBIDDEN_ID_PREFIXES = [
88
124
  "cdk-",
89
125
  "mat-",
@@ -314,6 +350,7 @@ var init_selector_picker = __esm({
314
350
  "src/components/selector-picker/selector-picker.ts"() {
315
351
  "use strict";
316
352
  init_selector_quality_utils();
353
+ init_code_format_utils();
317
354
  init_selector_picker_styles();
318
355
  init_selector_picker_template();
319
356
  OWN_DATA_CY = "lib-e2e-cypress-for-dummys";
@@ -433,7 +470,7 @@ var init_selector_picker = __esm({
433
470
  confirm(index) {
434
471
  const row = this.rows[index];
435
472
  if (!row) return;
436
- const command = `cy.get('${row.selector}').click()`;
473
+ const command = `cy.get('${escapeSingleQuotes(row.selector)}').click()`;
437
474
  this.recording.appendCommand(command);
438
475
  this.dispatchEvent(new CustomEvent("selectorchosen", { detail: command, bubbles: true, composed: true }));
439
476
  this.closeSilently();
@@ -451,8 +488,8 @@ var init_selector_picker = __esm({
451
488
  this.remove();
452
489
  }
453
490
  };
454
- if (!customElements.get("selector-picker")) {
455
- customElements.define("selector-picker", SelectorPickerElement);
491
+ if (!customElements.get("lib-e2e-selector-picker")) {
492
+ customElements.define("lib-e2e-selector-picker", SelectorPickerElement);
456
493
  }
457
494
  }
458
495
  });
@@ -463,20 +500,15 @@ __export(index_exports, {
463
500
  ACTIVE_SESSION_BREADCRUMB_KEY: () => ACTIVE_SESSION_BREADCRUMB_KEY,
464
501
  AdvancedTestEditorElement: () => AdvancedTestEditorElement,
465
502
  AdvancedTestTransformationService: () => AdvancedTestTransformationService,
466
- CHAIN_METHODS: () => CHAIN_METHODS,
467
- CYPRESS_COMPLETIONS: () => CYPRESS_COMPLETIONS,
468
- CY_METHODS: () => CY_METHODS,
469
503
  ConfigurationElement: () => ConfigurationElement,
470
504
  DB_SCHEMA: () => DB_SCHEMA,
471
505
  DB_STORE_NAMES: () => DB_STORE_NAMES,
472
- DEFAULT_ISSUE_TRACKER_PROVIDER_ID: () => DEFAULT_ISSUE_TRACKER_PROVIDER_ID,
473
506
  DEFAULT_RESUME_TTL_MINUTES: () => DEFAULT_RESUME_TTL_MINUTES,
474
507
  DRAG_THRESHOLD: () => DRAG_THRESHOLD,
475
508
  FilePreviewElement: () => FilePreviewElement,
476
509
  HelpPanelElement: () => HelpPanelElement,
477
510
  HttpMonitor: () => HttpMonitor,
478
511
  INPUT_TYPES: () => INPUT_TYPES,
479
- ISSUE_TRACKER_PROVIDERS: () => ISSUE_TRACKER_PROVIDERS,
480
512
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES: () => LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
481
513
  LOCALE_BY_LANG: () => LOCALE_BY_LANG,
482
514
  LibE2eRecorderElement: () => LibE2eRecorderElement,
@@ -487,25 +519,19 @@ __export(index_exports, {
487
519
  SUPPORTED_LANGS: () => SUPPORTED_LANGS,
488
520
  SaveTestElement: () => SaveTestElement,
489
521
  SelectorPickerElement: () => SelectorPickerElement,
490
- Subject: () => Subject,
491
522
  TOGGLE_MARGIN: () => TOGGLE_MARGIN,
492
- TOP_LEVEL: () => TOP_LEVEL,
493
523
  TestEditorElement: () => TestEditorElement,
494
524
  TestPrevisualizerElement: () => TestPrevisualizerElement,
495
525
  TransformationService: () => TransformationService,
496
526
  TranslationService: () => TranslationService,
497
527
  VERSION: () => VERSION,
528
+ _resetHttpMonitorState: () => _resetHttpMonitorState,
498
529
  advancedTestTransformationService: () => advancedTestTransformationService,
499
530
  boxOffsetForDirection: () => boxOffsetForDirection,
500
531
  boxTopLeftFor: () => boxTopLeftFor,
501
- buildTicketComment: () => buildTicketComment,
502
- buildTicketUrl: () => buildTicketUrl,
503
532
  clampTogglePosition: () => clampTogglePosition,
504
- createCodeEditor: () => createCodeEditor,
505
533
  defaultTogglePosition: () => defaultTogglePosition,
506
- filterCompletions: () => filterCompletions,
507
- filterCypressCompletions: () => filterCypressCompletions,
508
- findIssueTrackerProvider: () => findIssueTrackerProvider,
534
+ ensurePopupDimensions: () => ensurePopupDimensions,
509
535
  generateAlias: () => generateAlias,
510
536
  inferAssertionCommand: () => inferAssertionCommand,
511
537
  injectStyles: () => injectStyles,
@@ -515,9 +541,7 @@ __export(index_exports, {
515
541
  makeModalResizable: () => makeModalResizable,
516
542
  makeSwalDraggable: () => makeSwalDraggable,
517
543
  makeSwalDraggableByContentId: () => makeSwalDraggableByContentId,
518
- persistenceService: () => persistenceService,
519
544
  resolveExpandDirection: () => resolveExpandDirection,
520
- resolveTicketUrl: () => resolveTicketUrl,
521
545
  selectTestsForExport: () => selectTestsForExport,
522
546
  setSwal2DataCyAttribute: () => setSwal2DataCyAttribute,
523
547
  showToast: () => showToast,
@@ -616,64 +640,6 @@ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
616
640
  var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
617
641
  var DEFAULT_RESUME_TTL_MINUTES = 30;
618
642
 
619
- // src/models/issue-tracker.model.ts
620
- var ISSUE_TRACKER_PROVIDERS = [
621
- {
622
- id: "jira",
623
- labelKey: "CONFIG.ISSUE_TRACKER_PROVIDER_JIRA",
624
- urlTemplate: "{baseUrl}/browse/{id}",
625
- fields: [
626
- { key: "baseUrl", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_BASEURL", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_JIRA_BASEURL" }
627
- ],
628
- idPattern: "^[A-Za-z][A-Za-z0-9_]+-\\d+$",
629
- idPlaceholderKey: "SAVE_TEST.TICKET_PH_JIRA"
630
- },
631
- {
632
- id: "azure-devops",
633
- labelKey: "CONFIG.ISSUE_TRACKER_PROVIDER_AZURE",
634
- urlTemplate: "https://dev.azure.com/{org}/{project}/_workitems/edit/{id}",
635
- fields: [
636
- { key: "org", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_ORG", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_AZURE_ORG" },
637
- { key: "project", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_PROJECT", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_AZURE_PROJECT" }
638
- ],
639
- idPattern: "^\\d+$",
640
- idPlaceholderKey: "SAVE_TEST.TICKET_PH_AZURE"
641
- },
642
- {
643
- id: "github",
644
- labelKey: "CONFIG.ISSUE_TRACKER_PROVIDER_GITHUB",
645
- urlTemplate: "https://github.com/{owner}/{repo}/issues/{id}",
646
- fields: [
647
- { key: "owner", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_OWNER", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_GITHUB_OWNER" },
648
- { key: "repo", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_REPO", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_GITHUB_REPO" }
649
- ],
650
- idPattern: "^\\d+$",
651
- idPlaceholderKey: "SAVE_TEST.TICKET_PH_GITHUB"
652
- },
653
- {
654
- id: "trello",
655
- labelKey: "CONFIG.ISSUE_TRACKER_PROVIDER_TRELLO",
656
- urlTemplate: "https://trello.com/c/{id}",
657
- fields: [],
658
- idPattern: "^[A-Za-z0-9]+$",
659
- idPlaceholderKey: "SAVE_TEST.TICKET_PH_TRELLO"
660
- },
661
- {
662
- id: "custom",
663
- labelKey: "CONFIG.ISSUE_TRACKER_PROVIDER_CUSTOM",
664
- urlTemplate: "{template}",
665
- fields: [
666
- { key: "template", labelKey: "CONFIG.ISSUE_TRACKER_FIELD_TEMPLATE", placeholderKey: "CONFIG.ISSUE_TRACKER_PH_CUSTOM_TEMPLATE" }
667
- ],
668
- idPattern: ".*",
669
- idPlaceholderKey: "SAVE_TEST.TICKET_PH_CUSTOM"
670
- }
671
- ];
672
- var DEFAULT_ISSUE_TRACKER_PROVIDER_ID = "custom";
673
- function findIssueTrackerProvider(id) {
674
- return ISSUE_TRACKER_PROVIDERS.find((p) => p.id === id);
675
- }
676
-
677
643
  // src/utils/subject.ts
678
644
  var Subject = class {
679
645
  _value;
@@ -719,13 +685,12 @@ var I18N_ES = {
719
685
  REMOVE_TAG_TITLE: "Quitar",
720
686
  NOTES_LABEL: "Descripci\xF3n / Notas (opcional):",
721
687
  NOTES_PLACEHOLDER: "Qu\xE9 valida este test, condiciones previas, etc.",
722
- TICKET_LABEL: "Ticket (opcional):",
723
- TICKET_PH_JIRA: "ej. PROJ-123",
724
- TICKET_PH_AZURE: "ej. 1234",
725
- TICKET_PH_GITHUB: "ej. 42",
726
- TICKET_PH_TRELLO: "ej. aBc123",
727
- TICKET_PH_CUSTOM: "id del ticket",
728
- TICKET_INVALID: "El id del ticket no coincide con el formato esperado del gestor seleccionado."
688
+ TICKET_LABEL: "Ticket / Issue (opcional):",
689
+ TICKET_PLACEHOLDER: "Ej: PROJ-123, #42",
690
+ TICKET_WARN: "ID no v\xE1lido para el proveedor configurado (se guardar\xE1 igualmente)",
691
+ CONFIRM_DISCARD: "\xBFSeguro que quieres descartar el test grabado? Se perder\xE1n todos los pasos registrados.",
692
+ CONFIRM_DISCARD_BTN: "S\xED, descartar",
693
+ BACK_BTN: "Volver"
729
694
  },
730
695
  TEST_EDITOR: {
731
696
  NO_TAGS: "Sin etiquetas",
@@ -741,7 +706,9 @@ var I18N_ES = {
741
706
  NO_TESTS: "No hay tests guardados",
742
707
  DELETE_TITLE: "Eliminar",
743
708
  GROUP_BY_TICKET: "\u{1F3AB} Agrupar por ticket",
744
- NO_TICKET_GROUP: "Sin ticket"
709
+ UNGROUP_TICKET: "\u{1F3AB} Desagrupar",
710
+ NO_TICKET: "Sin ticket",
711
+ TICKET_LINK_TITLE: "Abrir ticket"
745
712
  },
746
713
  CONFIG: {
747
714
  LANG_SECTION: "\u{1F310} Idioma",
@@ -800,26 +767,35 @@ var I18N_ES = {
800
767
  WIDGET_POSITION_HINT: "Arrastra el bot\xF3n de grabar para moverlo donde no estorbe. Se recuerda su posici\xF3n.",
801
768
  WIDGET_POSITION_RESET_BTN: "\u21BA Restablecer posici\xF3n",
802
769
  WIDGET_POSITION_RESET_DONE: "\u2713 Posici\xF3n restablecida",
803
- ISSUE_TRACKER_SECTION: "\u{1F3AB} Gestor de incidencias",
804
- ISSUE_TRACKER_LABEL: "Gestor (solo enlace, sin sincronizaci\xF3n)",
805
- ISSUE_TRACKER_DISCLAIMER: "Solo genera enlaces \u2014 nunca se conecta ni sincroniza con la plataforma.",
806
- ISSUE_TRACKER_PROVIDER_JIRA: "Jira",
807
- ISSUE_TRACKER_PROVIDER_AZURE: "Azure DevOps",
808
- ISSUE_TRACKER_PROVIDER_GITHUB: "GitHub",
809
- ISSUE_TRACKER_PROVIDER_TRELLO: "Trello",
810
- ISSUE_TRACKER_PROVIDER_CUSTOM: "Personalizado",
811
- ISSUE_TRACKER_FIELD_BASEURL: "URL base",
812
- ISSUE_TRACKER_FIELD_ORG: "Organizaci\xF3n",
813
- ISSUE_TRACKER_FIELD_PROJECT: "Proyecto",
814
- ISSUE_TRACKER_FIELD_OWNER: "Propietario",
815
- ISSUE_TRACKER_FIELD_REPO: "Repositorio",
816
- ISSUE_TRACKER_FIELD_TEMPLATE: "Plantilla de URL (debe contener {id})",
817
- ISSUE_TRACKER_PH_JIRA_BASEURL: "https://tu-org.atlassian.net",
818
- ISSUE_TRACKER_PH_AZURE_ORG: "organizaci\xF3n",
819
- ISSUE_TRACKER_PH_AZURE_PROJECT: "proyecto",
820
- ISSUE_TRACKER_PH_GITHUB_OWNER: "propietario",
821
- ISSUE_TRACKER_PH_GITHUB_REPO: "repositorio",
822
- ISSUE_TRACKER_PH_CUSTOM_TEMPLATE: "https://tracker.example.com/{id}"
770
+ ISSUE_TRACKER_SECTION: "\u{1F3AB} Issue tracker",
771
+ ISSUE_TRACKER_TITLE: "Vincular tests a tickets",
772
+ ISSUE_TRACKER_SUB: "A\xF1ade un ID de ticket al guardar y genera un enlace clickable en el listado.",
773
+ ISSUE_TRACKER_PROVIDER: "Proveedor",
774
+ ISSUE_TRACKER_BASE_URL: "URL base",
775
+ ISSUE_TRACKER_BASE_URL_PH: "Ej: https://miproyecto.atlassian.net",
776
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
777
+ LOGIN_SETUP_NOT_CONFIGURED: "No configurado",
778
+ LOGIN_SETUP_SET_UP_BTN: "Configurar",
779
+ LOGIN_SETUP_TITLE: "Configuraci\xF3n de login",
780
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Crear scaffold",
781
+ LOGIN_SETUP_SELECT_EXISTING: "Seleccionar fichero existente",
782
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nombre del fichero",
783
+ LOGIN_SETUP_FILE_NAME_PH: "Ej: cypress/common-services/login.service.ts",
784
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-escanear",
785
+ LOGIN_SETUP_NO_FUNCTIONS: "No se encontraron funciones exportadas. Rellena el scaffold y re-escanea.",
786
+ LOGIN_SETUP_BEFORE_LABEL: "Funci\xF3n before() (una vez por suite)",
787
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Funci\xF3n beforeEach() (antes de cada test)",
788
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Ninguna \u2014",
789
+ LOGIN_SETUP_SUMMARY: "Fichero: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
790
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Guardar",
791
+ LOGIN_SETUP_CLEAR_BTN: "Eliminar configuraci\xF3n",
792
+ LOGIN_SETUP_CANCEL_BTN: "Cancelar",
793
+ LOGIN_SETUP_SOURCE_LABEL: "Origen del fichero de login",
794
+ LOGIN_SETUP_CREATE_SUB: "Genera un scaffold con funciones de ejemplo",
795
+ LOGIN_SETUP_SELECT_SUB: "Apunta a un servicio de login existente",
796
+ LOGIN_SETUP_INJECT_TITLE: "\xBFA\xF1adir bloques de login?",
797
+ LOGIN_SETUP_INJECT_TEXT: "Este fichero no tiene los bloques de login configurados. \xBFQuieres a\xF1adir el import y los bloques before/beforeEach ahora?",
798
+ LOGIN_SETUP_INJECT_CONFIRM: "S\xED, a\xF1adir"
823
799
  },
824
800
  SELECTOR_PICKER: {
825
801
  TITLE: "Selecciona un elemento del DOM",
@@ -910,6 +886,9 @@ var I18N_ES = {
910
886
  FILE_SAVE_ERROR_TOAST: "Error al guardar el archivo",
911
887
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures escritos en cypress/fixtures",
912
888
  FIXTURES_NO_FOLDER_TOAST: "No se pudieron escribir los fixtures (configura la carpeta Cypress)",
889
+ FILE_UPLOAD_MANUAL_TOAST: "A\xF1ade {files} a cypress/fixtures/ antes de ejecutar el test.",
890
+ FILE_UPLOAD_COPIED_TOAST: "{files} copiado/s a cypress/fixtures/.",
891
+ FILE_UPLOAD_COPY_ERROR_TOAST: "No se pudo copiar {files} a cypress/fixtures/. A\xF1\xE1delo manualmente.",
913
892
  BTN_CONFIG: "Config",
914
893
  BTN_FILES: "Ficheros",
915
894
  BTN_COMMANDS: "Comandos",
@@ -957,7 +936,7 @@ var I18N_ES = {
957
936
  SEC_PANELS: "\u{1F5C2} Paneles",
958
937
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 guardados: filtrar por tag, copiar, combinar, exportar.",
959
938
  PANEL_COMMANDS: "\u2328\uFE0F Comandos \u2014 de la grabaci\xF3n actual: reordenar, borrar, a\xF1adir aserciones.",
960
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 idioma, selector, HTTP, carpeta Cypress, datos, visibilidad, posici\xF3n.",
939
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 idioma, selector, HTTP, carpeta Cypress, login setup, datos, visibilidad, posici\xF3n.",
961
940
  PANEL_FILES: "\u{1F4C1} Ficheros \u2014 inserta tests en tus .cy.ts y lanza el runner.",
962
941
  SEC_SELECTOR: "\u{1F50E} Selectores",
963
942
  SELECTOR_1: "Elige el atributo preferido (data-cy, data-testid, aria-label, id) en Config.",
@@ -970,6 +949,7 @@ var I18N_ES = {
970
949
  ADV_RUNNER: "Runner local: npx lib-e2e-cypress-runner para lanzar un spec headless desde el editor.",
971
950
  ADV_CROSSAPP: "Cross-app: la grabaci\xF3n sobrevive al cambiar de micro-frontend (mismo origen).",
972
951
  ADV_DRAG: "Arrastra el bot\xF3n \u23FA para mover el widget; recuerda su posici\xF3n.",
952
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
973
953
  CLOSE: "Cerrar",
974
954
  TAB_REFERENCE: "Referencia r\xE1pida",
975
955
  TAB_GUIDE: "Gu\xEDa de uso",
@@ -987,13 +967,13 @@ var I18N_ES = {
987
967
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
988
968
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
989
969
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
970
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
990
971
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
991
972
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
992
973
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
993
974
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
994
975
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
995
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
996
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
976
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
997
977
  }
998
978
  };
999
979
 
@@ -1022,13 +1002,12 @@ var I18N_EN = {
1022
1002
  REMOVE_TAG_TITLE: "Remove",
1023
1003
  NOTES_LABEL: "Description / Notes (optional):",
1024
1004
  NOTES_PLACEHOLDER: "What this test validates, preconditions, etc.",
1025
- TICKET_LABEL: "Ticket (optional):",
1026
- TICKET_PH_JIRA: "e.g. PROJ-123",
1027
- TICKET_PH_AZURE: "e.g. 1234",
1028
- TICKET_PH_GITHUB: "e.g. 42",
1029
- TICKET_PH_TRELLO: "e.g. aBc123",
1030
- TICKET_PH_CUSTOM: "ticket id",
1031
- TICKET_INVALID: "The ticket id does not match the expected format for the selected tracker."
1005
+ TICKET_LABEL: "Ticket / Issue (optional):",
1006
+ TICKET_PLACEHOLDER: "E.g.: PROJ-123, #42",
1007
+ TICKET_WARN: "Invalid ID for the configured provider (will be saved anyway)",
1008
+ CONFIRM_DISCARD: "Are you sure you want to discard this recording? All recorded steps will be lost.",
1009
+ CONFIRM_DISCARD_BTN: "Yes, discard",
1010
+ BACK_BTN: "Go back"
1032
1011
  },
1033
1012
  TEST_EDITOR: {
1034
1013
  NO_TAGS: "No tags",
@@ -1044,7 +1023,9 @@ var I18N_EN = {
1044
1023
  NO_TESTS: "No saved tests",
1045
1024
  DELETE_TITLE: "Delete",
1046
1025
  GROUP_BY_TICKET: "\u{1F3AB} Group by ticket",
1047
- NO_TICKET_GROUP: "No ticket"
1026
+ UNGROUP_TICKET: "\u{1F3AB} Ungroup",
1027
+ NO_TICKET: "No ticket",
1028
+ TICKET_LINK_TITLE: "Open ticket"
1048
1029
  },
1049
1030
  CONFIG: {
1050
1031
  LANG_SECTION: "\u{1F310} Language",
@@ -1104,25 +1085,34 @@ var I18N_EN = {
1104
1085
  WIDGET_POSITION_RESET_BTN: "\u21BA Reset position",
1105
1086
  WIDGET_POSITION_RESET_DONE: "\u2713 Position reset",
1106
1087
  ISSUE_TRACKER_SECTION: "\u{1F3AB} Issue tracker",
1107
- ISSUE_TRACKER_LABEL: "Tracker (link only, no sync)",
1108
- ISSUE_TRACKER_DISCLAIMER: "Builds links only \u2014 it never connects to or syncs with the platform.",
1109
- ISSUE_TRACKER_PROVIDER_JIRA: "Jira",
1110
- ISSUE_TRACKER_PROVIDER_AZURE: "Azure DevOps",
1111
- ISSUE_TRACKER_PROVIDER_GITHUB: "GitHub",
1112
- ISSUE_TRACKER_PROVIDER_TRELLO: "Trello",
1113
- ISSUE_TRACKER_PROVIDER_CUSTOM: "Custom",
1114
- ISSUE_TRACKER_FIELD_BASEURL: "Base URL",
1115
- ISSUE_TRACKER_FIELD_ORG: "Organization",
1116
- ISSUE_TRACKER_FIELD_PROJECT: "Project",
1117
- ISSUE_TRACKER_FIELD_OWNER: "Owner",
1118
- ISSUE_TRACKER_FIELD_REPO: "Repository",
1119
- ISSUE_TRACKER_FIELD_TEMPLATE: "URL template (must contain {id})",
1120
- ISSUE_TRACKER_PH_JIRA_BASEURL: "https://your-org.atlassian.net",
1121
- ISSUE_TRACKER_PH_AZURE_ORG: "organization",
1122
- ISSUE_TRACKER_PH_AZURE_PROJECT: "project",
1123
- ISSUE_TRACKER_PH_GITHUB_OWNER: "owner",
1124
- ISSUE_TRACKER_PH_GITHUB_REPO: "repository",
1125
- ISSUE_TRACKER_PH_CUSTOM_TEMPLATE: "https://tracker.example.com/{id}"
1088
+ ISSUE_TRACKER_TITLE: "Link tests to tickets",
1089
+ ISSUE_TRACKER_SUB: "Add a ticket ID when saving and generate a clickable link in the test list.",
1090
+ ISSUE_TRACKER_PROVIDER: "Provider",
1091
+ ISSUE_TRACKER_BASE_URL: "Base URL",
1092
+ ISSUE_TRACKER_BASE_URL_PH: "E.g.: https://myproject.atlassian.net",
1093
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1094
+ LOGIN_SETUP_NOT_CONFIGURED: "Not configured",
1095
+ LOGIN_SETUP_SET_UP_BTN: "Set up",
1096
+ LOGIN_SETUP_TITLE: "Login setup",
1097
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Create scaffold",
1098
+ LOGIN_SETUP_SELECT_EXISTING: "Select existing file",
1099
+ LOGIN_SETUP_FILE_NAME_LABEL: "File name",
1100
+ LOGIN_SETUP_FILE_NAME_PH: "E.g.: cypress/common-services/login.service.ts",
1101
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scan",
1102
+ LOGIN_SETUP_NO_FUNCTIONS: "No exported functions found. Fill in the scaffold and re-scan.",
1103
+ LOGIN_SETUP_BEFORE_LABEL: "before() function (once per suite)",
1104
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "beforeEach() function (before every test)",
1105
+ LOGIN_SETUP_NONE_OPTION: "\u2014 None \u2014",
1106
+ LOGIN_SETUP_SUMMARY: "File: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
1107
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Save",
1108
+ LOGIN_SETUP_CLEAR_BTN: "Remove configuration",
1109
+ LOGIN_SETUP_CANCEL_BTN: "Cancel",
1110
+ LOGIN_SETUP_SOURCE_LABEL: "Login file source",
1111
+ LOGIN_SETUP_CREATE_SUB: "Generate a scaffold with sample functions",
1112
+ LOGIN_SETUP_SELECT_SUB: "Point to an existing login service file",
1113
+ LOGIN_SETUP_INJECT_TITLE: "Add login blocks?",
1114
+ LOGIN_SETUP_INJECT_TEXT: "This file does not have the configured login blocks. Do you want to add the import and before/beforeEach blocks now?",
1115
+ LOGIN_SETUP_INJECT_CONFIRM: "Yes, add them"
1126
1116
  },
1127
1117
  SELECTOR_PICKER: {
1128
1118
  TITLE: "Select a DOM element",
@@ -1213,6 +1203,9 @@ var I18N_EN = {
1213
1203
  FILE_SAVE_ERROR_TOAST: "Error saving the file",
1214
1204
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures written to cypress/fixtures",
1215
1205
  FIXTURES_NO_FOLDER_TOAST: "Could not write fixtures (configure the Cypress folder)",
1206
+ FILE_UPLOAD_MANUAL_TOAST: "Add {files} to cypress/fixtures/ before running the test.",
1207
+ FILE_UPLOAD_COPIED_TOAST: "{files} copied to cypress/fixtures/.",
1208
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Could not copy {files} to cypress/fixtures/. Add it manually.",
1216
1209
  BTN_CONFIG: "Config",
1217
1210
  BTN_FILES: "Files",
1218
1211
  BTN_COMMANDS: "Commands",
@@ -1260,7 +1253,7 @@ var I18N_EN = {
1260
1253
  SEC_PANELS: "\u{1F5C2} Panels",
1261
1254
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 saved tests: filter by tag, copy, combine, export.",
1262
1255
  PANEL_COMMANDS: "\u2328\uFE0F Commands \u2014 current recording: reorder, delete, add assertions.",
1263
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 language, selector, HTTP, Cypress folder, data, visibility, position.",
1256
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 language, selector, HTTP, Cypress folder, login setup, data, visibility, position.",
1264
1257
  PANEL_FILES: "\u{1F4C1} Files \u2014 insert tests into your .cy.ts and launch the runner.",
1265
1258
  SEC_SELECTOR: "\u{1F50E} Selectors",
1266
1259
  SELECTOR_1: "Choose the preferred attribute (data-cy, data-testid, aria-label, id) in Config.",
@@ -1273,6 +1266,7 @@ var I18N_EN = {
1273
1266
  ADV_RUNNER: "Local runner: npx lib-e2e-cypress-runner to run a spec headless from the editor.",
1274
1267
  ADV_CROSSAPP: "Cross-app: the recording survives switching micro-frontend (same origin).",
1275
1268
  ADV_DRAG: "Drag the \u23FA button to move the widget; its position is remembered.",
1269
+ ADV_LOGIN: "Login Setup (Ctrl+3): point to your login service; the recorder automatically injects before()/beforeEach() blocks into each new test.",
1276
1270
  CLOSE: "Close",
1277
1271
  TAB_REFERENCE: "Quick reference",
1278
1272
  TAB_GUIDE: "Usage guide",
@@ -1290,13 +1284,13 @@ var I18N_EN = {
1290
1284
  G_COV_4: "Reliable selector (data-cy/testid/aria/id) and a picker when none is obvious.",
1291
1285
  G_COV_5: "Automatic assertions (Alt+click) and the manual builder.",
1292
1286
  G_COV_6: "Continuity when switching micro-frontend (same origin), invisible mode and i18n.",
1287
+ G_COV_7: "Login Setup: configure your login function under Ctrl+3 \u2192 Login Setup; the recorder adds before()/beforeEach() blocks to every new test.",
1293
1288
  G_SEC_NOTCOVERED: "\u{1F6AB} What it does NOT cover (yet)",
1294
1289
  G_NC_1: "Different origins / subdomains (same origin only).",
1295
1290
  G_NC_2: "File upload (selectFile), drag & drop, and hover.",
1296
1291
  G_NC_3: "The Tab key (Cypress can't .type() it).",
1297
1292
  G_NC_4: "DELETE requests (intentionally ignored).",
1298
- G_NC_5: "Third-party iframes and Shadow DOM may not be captured.",
1299
- G_NC_6: "Programmatic login generation (on the roadmap)."
1293
+ G_NC_5: "Third-party iframes and Shadow DOM may not be captured."
1300
1294
  }
1301
1295
  };
1302
1296
 
@@ -1325,13 +1319,13 @@ var I18N_FR = {
1325
1319
  REMOVE_TAG_TITLE: "Retirer",
1326
1320
  NOTES_LABEL: "Description / Notes (optionnel) :",
1327
1321
  NOTES_PLACEHOLDER: "Ce que ce test valide, conditions pr\xE9alables, etc.",
1328
- TICKET_LABEL: "Ticket (optionnel) :",
1329
- TICKET_PH_JIRA: "p. ex. PROJ-123",
1330
- TICKET_PH_AZURE: "p. ex. 1234",
1331
- TICKET_PH_GITHUB: "p. ex. 42",
1332
- TICKET_PH_TRELLO: "p. ex. aBc123",
1333
- TICKET_PH_CUSTOM: "id du ticket",
1334
- TICKET_INVALID: "L'id du ticket ne correspond pas au format attendu par le gestionnaire s\xE9lectionn\xE9."
1322
+ TICKET_LABEL: "Ticket / Issue (optionnel) :",
1323
+ TICKET_PLACEHOLDER: "Ex : PROJ-123, #42",
1324
+ TICKET_WARN: "ID non valide pour le fournisseur configur\xE9 (sera enregistr\xE9 quand m\xEAme)",
1325
+ // TODO: translate
1326
+ CONFIRM_DISCARD: "\xCAtes-vous s\xFBr de vouloir abandonner cet enregistrement ? Toutes les \xE9tapes enregistr\xE9es seront perdues.",
1327
+ CONFIRM_DISCARD_BTN: "Oui, abandonner",
1328
+ BACK_BTN: "Retour"
1335
1329
  },
1336
1330
  TEST_EDITOR: {
1337
1331
  NO_TAGS: "Sans \xE9tiquettes",
@@ -1347,7 +1341,9 @@ var I18N_FR = {
1347
1341
  NO_TESTS: "Aucun test enregistr\xE9",
1348
1342
  DELETE_TITLE: "Supprimer",
1349
1343
  GROUP_BY_TICKET: "\u{1F3AB} Grouper par ticket",
1350
- NO_TICKET_GROUP: "Sans ticket"
1344
+ UNGROUP_TICKET: "\u{1F3AB} D\xE9grouper",
1345
+ NO_TICKET: "Sans ticket",
1346
+ TICKET_LINK_TITLE: "Ouvrir le ticket"
1351
1347
  },
1352
1348
  CONFIG: {
1353
1349
  LANG_SECTION: "\u{1F310} Langue",
@@ -1407,25 +1403,34 @@ var I18N_FR = {
1407
1403
  WIDGET_POSITION_RESET_BTN: "\u21BA R\xE9initialiser la position",
1408
1404
  WIDGET_POSITION_RESET_DONE: "\u2713 Position r\xE9initialis\xE9e",
1409
1405
  ISSUE_TRACKER_SECTION: "\u{1F3AB} Gestionnaire de tickets",
1410
- ISSUE_TRACKER_LABEL: "Gestionnaire (lien uniquement, sans synchronisation)",
1411
- ISSUE_TRACKER_DISCLAIMER: "G\xE9n\xE8re uniquement des liens \u2014 ne se connecte ni ne se synchronise jamais avec la plateforme.",
1412
- ISSUE_TRACKER_PROVIDER_JIRA: "Jira",
1413
- ISSUE_TRACKER_PROVIDER_AZURE: "Azure DevOps",
1414
- ISSUE_TRACKER_PROVIDER_GITHUB: "GitHub",
1415
- ISSUE_TRACKER_PROVIDER_TRELLO: "Trello",
1416
- ISSUE_TRACKER_PROVIDER_CUSTOM: "Personnalis\xE9",
1417
- ISSUE_TRACKER_FIELD_BASEURL: "URL de base",
1418
- ISSUE_TRACKER_FIELD_ORG: "Organisation",
1419
- ISSUE_TRACKER_FIELD_PROJECT: "Projet",
1420
- ISSUE_TRACKER_FIELD_OWNER: "Propri\xE9taire",
1421
- ISSUE_TRACKER_FIELD_REPO: "D\xE9p\xF4t",
1422
- ISSUE_TRACKER_FIELD_TEMPLATE: "Mod\xE8le d'URL (doit contenir {id})",
1423
- ISSUE_TRACKER_PH_JIRA_BASEURL: "https://votre-org.atlassian.net",
1424
- ISSUE_TRACKER_PH_AZURE_ORG: "organisation",
1425
- ISSUE_TRACKER_PH_AZURE_PROJECT: "projet",
1426
- ISSUE_TRACKER_PH_GITHUB_OWNER: "propri\xE9taire",
1427
- ISSUE_TRACKER_PH_GITHUB_REPO: "d\xE9p\xF4t",
1428
- ISSUE_TRACKER_PH_CUSTOM_TEMPLATE: "https://tracker.example.com/{id}"
1406
+ ISSUE_TRACKER_TITLE: "Lier les tests aux tickets",
1407
+ ISSUE_TRACKER_SUB: "Ajoutez un ID de ticket lors de l'enregistrement et g\xE9n\xE9rez un lien cliquable.",
1408
+ ISSUE_TRACKER_PROVIDER: "Fournisseur",
1409
+ ISSUE_TRACKER_BASE_URL: "URL de base",
1410
+ ISSUE_TRACKER_BASE_URL_PH: "Ex : https://monprojet.atlassian.net",
1411
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1412
+ LOGIN_SETUP_NOT_CONFIGURED: "Non configur\xE9",
1413
+ LOGIN_SETUP_SET_UP_BTN: "Configurer",
1414
+ LOGIN_SETUP_TITLE: "Configuration du login",
1415
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Cr\xE9er un scaffold",
1416
+ LOGIN_SETUP_SELECT_EXISTING: "S\xE9lectionner un fichier existant",
1417
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nom du fichier",
1418
+ LOGIN_SETUP_FILE_NAME_PH: "Ex : cypress/common-services/login.service.ts",
1419
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scanner",
1420
+ LOGIN_SETUP_NO_FUNCTIONS: "Aucune fonction export\xE9e trouv\xE9e. Remplissez le scaffold et re-scannez.",
1421
+ LOGIN_SETUP_BEFORE_LABEL: "Fonction before() (une fois par suite)",
1422
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Fonction beforeEach() (avant chaque test)",
1423
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Aucune \u2014",
1424
+ LOGIN_SETUP_SUMMARY: "Fichier : {file} \xB7 before : {before} \xB7 beforeEach : {beforeEach}",
1425
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Enregistrer",
1426
+ LOGIN_SETUP_CLEAR_BTN: "Supprimer la configuration",
1427
+ LOGIN_SETUP_CANCEL_BTN: "Annuler",
1428
+ LOGIN_SETUP_SOURCE_LABEL: "Source du fichier de login",
1429
+ LOGIN_SETUP_CREATE_SUB: "G\xE9n\xE9rer un scaffold avec des fonctions exemples",
1430
+ LOGIN_SETUP_SELECT_SUB: "Pointer vers un fichier de service de login existant",
1431
+ LOGIN_SETUP_INJECT_TITLE: "Ajouter les blocs de login ?",
1432
+ LOGIN_SETUP_INJECT_TEXT: "Ce fichier ne contient pas les blocs de login configur\xE9s. Voulez-vous ajouter l'import et les blocs before/beforeEach maintenant ?",
1433
+ LOGIN_SETUP_INJECT_CONFIRM: "Oui, ajouter"
1429
1434
  },
1430
1435
  SELECTOR_PICKER: {
1431
1436
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -1516,6 +1521,12 @@ var I18N_FR = {
1516
1521
  FILE_SAVE_ERROR_TOAST: "Erreur lors de l'enregistrement",
1517
1522
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures \xE9crites dans cypress/fixtures",
1518
1523
  FIXTURES_NO_FOLDER_TOAST: "Impossible d'\xE9crire les fixtures (configure le dossier Cypress)",
1524
+ FILE_UPLOAD_MANUAL_TOAST: "Ajoutez {files} \xE0 cypress/fixtures/ avant d'ex\xE9cuter le test.",
1525
+ // TODO: translate
1526
+ FILE_UPLOAD_COPIED_TOAST: "{files} copi\xE9(s) dans cypress/fixtures/.",
1527
+ // TODO: translate
1528
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Impossible de copier {files} vers cypress/fixtures/. Ajoutez-le manuellement.",
1529
+ // TODO: translate
1519
1530
  BTN_CONFIG: "Config",
1520
1531
  BTN_FILES: "Fichiers",
1521
1532
  BTN_COMMANDS: "Commandes",
@@ -1563,7 +1574,7 @@ var I18N_FR = {
1563
1574
  SEC_PANELS: "\u{1F5C2} Panneaux",
1564
1575
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 enregistr\xE9s : filtrer par tag, copier, combiner, exporter.",
1565
1576
  PANEL_COMMANDS: "\u2328\uFE0F Commandes \u2014 enregistrement en cours : r\xE9ordonner, supprimer, ajouter des assertions.",
1566
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 langue, s\xE9lecteur, HTTP, dossier Cypress, donn\xE9es, visibilit\xE9, position.",
1577
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 langue, s\xE9lecteur, HTTP, dossier Cypress, login setup, donn\xE9es, visibilit\xE9, position.",
1567
1578
  PANEL_FILES: "\u{1F4C1} Fichiers \u2014 ins\xE8re des tests dans tes .cy.ts et lance le runner.",
1568
1579
  SEC_SELECTOR: "\u{1F50E} S\xE9lecteurs",
1569
1580
  SELECTOR_1: "Choisis l'attribut pr\xE9f\xE9r\xE9 (data-cy, data-testid, aria-label, id) dans Config.",
@@ -1576,6 +1587,8 @@ var I18N_FR = {
1576
1587
  ADV_RUNNER: "Runner local : npx lib-e2e-cypress-runner pour lancer un spec headless depuis l'\xE9diteur.",
1577
1588
  ADV_CROSSAPP: "Cross-app : l'enregistrement survit au changement de micro-frontend (m\xEAme origine).",
1578
1589
  ADV_DRAG: "Fais glisser le bouton \u23FA pour d\xE9placer le widget ; sa position est m\xE9moris\xE9e.",
1590
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
1591
+ // TODO: translate (fr)
1579
1592
  CLOSE: "Fermer",
1580
1593
  TAB_REFERENCE: "R\xE9f\xE9rence rapide",
1581
1594
  TAB_GUIDE: "Guide d'utilisation",
@@ -1594,13 +1607,14 @@ var I18N_FR = {
1594
1607
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1595
1608
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1596
1609
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1610
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
1611
+ // TODO: translate (fr)
1597
1612
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1598
1613
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1599
1614
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1600
1615
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1601
1616
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
1602
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
1603
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
1617
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
1604
1618
  }
1605
1619
  };
1606
1620
 
@@ -1629,13 +1643,13 @@ var I18N_IT = {
1629
1643
  REMOVE_TAG_TITLE: "Rimuovi",
1630
1644
  NOTES_LABEL: "Descrizione / Note (opzionale):",
1631
1645
  NOTES_PLACEHOLDER: "Cosa valida questo test, condizioni preliminari, ecc.",
1632
- TICKET_LABEL: "Ticket (opzionale):",
1633
- TICKET_PH_JIRA: "es. PROJ-123",
1634
- TICKET_PH_AZURE: "es. 1234",
1635
- TICKET_PH_GITHUB: "es. 42",
1636
- TICKET_PH_TRELLO: "es. aBc123",
1637
- TICKET_PH_CUSTOM: "id del ticket",
1638
- TICKET_INVALID: "L'id del ticket non corrisponde al formato previsto dal gestore selezionato."
1646
+ TICKET_LABEL: "Ticket / Issue (opzionale):",
1647
+ TICKET_PLACEHOLDER: "Es: PROJ-123, #42",
1648
+ TICKET_WARN: "ID non valido per il provider configurato (verr\xE0 salvato comunque)",
1649
+ // TODO: translate
1650
+ CONFIRM_DISCARD: "Sei sicuro di voler scartare questa registrazione? Tutti i passi registrati andranno persi.",
1651
+ CONFIRM_DISCARD_BTN: "S\xEC, scarta",
1652
+ BACK_BTN: "Indietro"
1639
1653
  },
1640
1654
  TEST_EDITOR: {
1641
1655
  NO_TAGS: "Nessuna etichetta",
@@ -1651,7 +1665,9 @@ var I18N_IT = {
1651
1665
  NO_TESTS: "Nessun test salvato",
1652
1666
  DELETE_TITLE: "Elimina",
1653
1667
  GROUP_BY_TICKET: "\u{1F3AB} Raggruppa per ticket",
1654
- NO_TICKET_GROUP: "Senza ticket"
1668
+ UNGROUP_TICKET: "\u{1F3AB} Separa",
1669
+ NO_TICKET: "Senza ticket",
1670
+ TICKET_LINK_TITLE: "Apri ticket"
1655
1671
  },
1656
1672
  CONFIG: {
1657
1673
  LANG_SECTION: "\u{1F310} Lingua",
@@ -1710,26 +1726,35 @@ var I18N_IT = {
1710
1726
  WIDGET_POSITION_HINT: "Trascina il pulsante di registrazione per spostarlo. La posizione viene ricordata.",
1711
1727
  WIDGET_POSITION_RESET_BTN: "\u21BA Reimposta posizione",
1712
1728
  WIDGET_POSITION_RESET_DONE: "\u2713 Posizione reimpostata",
1713
- ISSUE_TRACKER_SECTION: "\u{1F3AB} Gestore ticket",
1714
- ISSUE_TRACKER_LABEL: "Gestore (solo link, senza sincronizzazione)",
1715
- ISSUE_TRACKER_DISCLAIMER: "Genera solo link \u2014 non si connette n\xE9 si sincronizza mai con la piattaforma.",
1716
- ISSUE_TRACKER_PROVIDER_JIRA: "Jira",
1717
- ISSUE_TRACKER_PROVIDER_AZURE: "Azure DevOps",
1718
- ISSUE_TRACKER_PROVIDER_GITHUB: "GitHub",
1719
- ISSUE_TRACKER_PROVIDER_TRELLO: "Trello",
1720
- ISSUE_TRACKER_PROVIDER_CUSTOM: "Personalizzato",
1721
- ISSUE_TRACKER_FIELD_BASEURL: "URL di base",
1722
- ISSUE_TRACKER_FIELD_ORG: "Organizzazione",
1723
- ISSUE_TRACKER_FIELD_PROJECT: "Progetto",
1724
- ISSUE_TRACKER_FIELD_OWNER: "Proprietario",
1725
- ISSUE_TRACKER_FIELD_REPO: "Repository",
1726
- ISSUE_TRACKER_FIELD_TEMPLATE: "Modello di URL (deve contenere {id})",
1727
- ISSUE_TRACKER_PH_JIRA_BASEURL: "https://tua-org.atlassian.net",
1728
- ISSUE_TRACKER_PH_AZURE_ORG: "organizzazione",
1729
- ISSUE_TRACKER_PH_AZURE_PROJECT: "progetto",
1730
- ISSUE_TRACKER_PH_GITHUB_OWNER: "proprietario",
1731
- ISSUE_TRACKER_PH_GITHUB_REPO: "repository",
1732
- ISSUE_TRACKER_PH_CUSTOM_TEMPLATE: "https://tracker.example.com/{id}"
1729
+ ISSUE_TRACKER_SECTION: "\u{1F3AB} Issue tracker",
1730
+ ISSUE_TRACKER_TITLE: "Collega i test ai ticket",
1731
+ ISSUE_TRACKER_SUB: "Aggiungi un ID ticket al salvataggio e genera un link cliccabile nell'elenco.",
1732
+ ISSUE_TRACKER_PROVIDER: "Provider",
1733
+ ISSUE_TRACKER_BASE_URL: "URL base",
1734
+ ISSUE_TRACKER_BASE_URL_PH: "Es: https://mioprogetto.atlassian.net",
1735
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1736
+ LOGIN_SETUP_NOT_CONFIGURED: "Non configurato",
1737
+ LOGIN_SETUP_SET_UP_BTN: "Configura",
1738
+ LOGIN_SETUP_TITLE: "Configurazione del login",
1739
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Crea scaffold",
1740
+ LOGIN_SETUP_SELECT_EXISTING: "Seleziona file esistente",
1741
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nome del file",
1742
+ LOGIN_SETUP_FILE_NAME_PH: "Es: cypress/common-services/login.service.ts",
1743
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scansiona",
1744
+ LOGIN_SETUP_NO_FUNCTIONS: "Nessuna funzione esportata trovata. Compila lo scaffold e ri-scansiona.",
1745
+ LOGIN_SETUP_BEFORE_LABEL: "Funzione before() (una volta per suite)",
1746
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Funzione beforeEach() (prima di ogni test)",
1747
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Nessuna \u2014",
1748
+ LOGIN_SETUP_SUMMARY: "File: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
1749
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Salva",
1750
+ LOGIN_SETUP_CLEAR_BTN: "Rimuovi configurazione",
1751
+ LOGIN_SETUP_CANCEL_BTN: "Annulla",
1752
+ LOGIN_SETUP_SOURCE_LABEL: "Sorgente del file di login",
1753
+ LOGIN_SETUP_CREATE_SUB: "Genera uno scaffold con funzioni di esempio",
1754
+ LOGIN_SETUP_SELECT_SUB: "Punta a un file di servizio di login esistente",
1755
+ LOGIN_SETUP_INJECT_TITLE: "Aggiungere i blocchi di login?",
1756
+ LOGIN_SETUP_INJECT_TEXT: "Questo file non contiene i blocchi di login configurati. Vuoi aggiungere l'import e i blocchi before/beforeEach ora?",
1757
+ LOGIN_SETUP_INJECT_CONFIRM: "S\xEC, aggiungi"
1733
1758
  },
1734
1759
  SELECTOR_PICKER: {
1735
1760
  TITLE: "Seleziona un elemento DOM",
@@ -1820,6 +1845,12 @@ var I18N_IT = {
1820
1845
  FILE_SAVE_ERROR_TOAST: "Errore durante il salvataggio",
1821
1846
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixture scritte in cypress/fixtures",
1822
1847
  FIXTURES_NO_FOLDER_TOAST: "Impossibile scrivere le fixture (configura la cartella Cypress)",
1848
+ FILE_UPLOAD_MANUAL_TOAST: "Aggiungi {files} a cypress/fixtures/ prima di eseguire il test.",
1849
+ // TODO: translate
1850
+ FILE_UPLOAD_COPIED_TOAST: "{files} copiato/i in cypress/fixtures/.",
1851
+ // TODO: translate
1852
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Impossibile copiare {files} in cypress/fixtures/. Aggiungilo manualmente.",
1853
+ // TODO: translate
1823
1854
  BTN_CONFIG: "Config",
1824
1855
  BTN_FILES: "File",
1825
1856
  BTN_COMMANDS: "Comandi",
@@ -1867,7 +1898,7 @@ var I18N_IT = {
1867
1898
  SEC_PANELS: "\u{1F5C2} Pannelli",
1868
1899
  PANEL_TESTS: "\u{1F4CB} Test \u2014 salvati: filtra per tag, copia, combina, esporta.",
1869
1900
  PANEL_COMMANDS: "\u2328\uFE0F Comandi \u2014 registrazione corrente: riordina, elimina, aggiungi asserzioni.",
1870
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 lingua, selettore, HTTP, cartella Cypress, dati, visibilit\xE0, posizione.",
1901
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 lingua, selettore, HTTP, cartella Cypress, login setup, dati, visibilit\xE0, posizione.",
1871
1902
  PANEL_FILES: "\u{1F4C1} File \u2014 inserisci test nei tuoi .cy.ts e avvia il runner.",
1872
1903
  SEC_SELECTOR: "\u{1F50E} Selettori",
1873
1904
  SELECTOR_1: "Scegli l'attributo preferito (data-cy, data-testid, aria-label, id) in Config.",
@@ -1880,6 +1911,8 @@ var I18N_IT = {
1880
1911
  ADV_RUNNER: "Runner locale: npx lib-e2e-cypress-runner per eseguire uno spec headless dall'editor.",
1881
1912
  ADV_CROSSAPP: "Cross-app: la registrazione sopravvive al cambio di micro-frontend (stessa origine).",
1882
1913
  ADV_DRAG: "Trascina il pulsante \u23FA per spostare il widget; la posizione viene ricordata.",
1914
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
1915
+ // TODO: translate (it)
1883
1916
  CLOSE: "Chiudi",
1884
1917
  TAB_REFERENCE: "Riferimento rapido",
1885
1918
  TAB_GUIDE: "Guida all'uso",
@@ -1898,13 +1931,14 @@ var I18N_IT = {
1898
1931
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1899
1932
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1900
1933
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1934
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
1935
+ // TODO: translate (it)
1901
1936
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1902
1937
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1903
1938
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1904
1939
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1905
1940
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
1906
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
1907
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
1941
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
1908
1942
  }
1909
1943
  };
1910
1944
 
@@ -1933,13 +1967,13 @@ var I18N_DE = {
1933
1967
  REMOVE_TAG_TITLE: "Entfernen",
1934
1968
  NOTES_LABEL: "Beschreibung / Notizen (optional):",
1935
1969
  NOTES_PLACEHOLDER: "Was dieser Test validiert, Voraussetzungen usw.",
1936
- TICKET_LABEL: "Ticket (optional):",
1937
- TICKET_PH_JIRA: "z. B. PROJ-123",
1938
- TICKET_PH_AZURE: "z. B. 1234",
1939
- TICKET_PH_GITHUB: "z. B. 42",
1940
- TICKET_PH_TRELLO: "z. B. aBc123",
1941
- TICKET_PH_CUSTOM: "Ticket-ID",
1942
- TICKET_INVALID: "Die Ticket-ID entspricht nicht dem erwarteten Format des ausgew\xE4hlten Trackers."
1970
+ TICKET_LABEL: "Ticket / Issue (optional):",
1971
+ TICKET_PLACEHOLDER: "Z. B.: PROJ-123, #42",
1972
+ TICKET_WARN: "Ung\xFCltige ID f\xFCr den konfigurierten Anbieter (wird trotzdem gespeichert)",
1973
+ // TODO: translate
1974
+ CONFIRM_DISCARD: "Sind Sie sicher, dass Sie diese Aufzeichnung verwerfen m\xF6chten? Alle aufgezeichneten Schritte gehen verloren.",
1975
+ CONFIRM_DISCARD_BTN: "Ja, verwerfen",
1976
+ BACK_BTN: "Zur\xFCck"
1943
1977
  },
1944
1978
  TEST_EDITOR: {
1945
1979
  NO_TAGS: "Keine Tags",
@@ -1955,7 +1989,9 @@ var I18N_DE = {
1955
1989
  NO_TESTS: "Keine gespeicherten Tests",
1956
1990
  DELETE_TITLE: "L\xF6schen",
1957
1991
  GROUP_BY_TICKET: "\u{1F3AB} Nach Ticket gruppieren",
1958
- NO_TICKET_GROUP: "Kein Ticket"
1992
+ UNGROUP_TICKET: "\u{1F3AB} Gruppierung aufheben",
1993
+ NO_TICKET: "Kein Ticket",
1994
+ TICKET_LINK_TITLE: "Ticket \xF6ffnen"
1959
1995
  },
1960
1996
  CONFIG: {
1961
1997
  LANG_SECTION: "\u{1F310} Sprache",
@@ -2015,25 +2051,34 @@ var I18N_DE = {
2015
2051
  WIDGET_POSITION_RESET_BTN: "\u21BA Position zur\xFCcksetzen",
2016
2052
  WIDGET_POSITION_RESET_DONE: "\u2713 Position zur\xFCckgesetzt",
2017
2053
  ISSUE_TRACKER_SECTION: "\u{1F3AB} Issue-Tracker",
2018
- ISSUE_TRACKER_LABEL: "Tracker (nur Link, keine Synchronisierung)",
2019
- ISSUE_TRACKER_DISCLAIMER: "Erstellt nur Links \u2014 verbindet oder synchronisiert sich nie mit der Plattform.",
2020
- ISSUE_TRACKER_PROVIDER_JIRA: "Jira",
2021
- ISSUE_TRACKER_PROVIDER_AZURE: "Azure DevOps",
2022
- ISSUE_TRACKER_PROVIDER_GITHUB: "GitHub",
2023
- ISSUE_TRACKER_PROVIDER_TRELLO: "Trello",
2024
- ISSUE_TRACKER_PROVIDER_CUSTOM: "Benutzerdefiniert",
2025
- ISSUE_TRACKER_FIELD_BASEURL: "Basis-URL",
2026
- ISSUE_TRACKER_FIELD_ORG: "Organisation",
2027
- ISSUE_TRACKER_FIELD_PROJECT: "Projekt",
2028
- ISSUE_TRACKER_FIELD_OWNER: "Eigent\xFCmer",
2029
- ISSUE_TRACKER_FIELD_REPO: "Repository",
2030
- ISSUE_TRACKER_FIELD_TEMPLATE: "URL-Vorlage (muss {id} enthalten)",
2031
- ISSUE_TRACKER_PH_JIRA_BASEURL: "https://ihre-org.atlassian.net",
2032
- ISSUE_TRACKER_PH_AZURE_ORG: "Organisation",
2033
- ISSUE_TRACKER_PH_AZURE_PROJECT: "Projekt",
2034
- ISSUE_TRACKER_PH_GITHUB_OWNER: "Eigent\xFCmer",
2035
- ISSUE_TRACKER_PH_GITHUB_REPO: "Repository",
2036
- ISSUE_TRACKER_PH_CUSTOM_TEMPLATE: "https://tracker.example.com/{id}"
2054
+ ISSUE_TRACKER_TITLE: "Tests mit Tickets verkn\xFCpfen",
2055
+ ISSUE_TRACKER_SUB: "F\xFCge beim Speichern eine Ticket-ID hinzu und erstelle einen klickbaren Link.",
2056
+ ISSUE_TRACKER_PROVIDER: "Anbieter",
2057
+ ISSUE_TRACKER_BASE_URL: "Basis-URL",
2058
+ ISSUE_TRACKER_BASE_URL_PH: "Z. B.: https://meinprojekt.atlassian.net",
2059
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
2060
+ LOGIN_SETUP_NOT_CONFIGURED: "Nicht konfiguriert",
2061
+ LOGIN_SETUP_SET_UP_BTN: "Einrichten",
2062
+ LOGIN_SETUP_TITLE: "Login-Konfiguration",
2063
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Scaffold erstellen",
2064
+ LOGIN_SETUP_SELECT_EXISTING: "Vorhandene Datei ausw\xE4hlen",
2065
+ LOGIN_SETUP_FILE_NAME_LABEL: "Dateiname",
2066
+ LOGIN_SETUP_FILE_NAME_PH: "Z. B.: cypress/common-services/login.service.ts",
2067
+ LOGIN_SETUP_RESCAN: "\u{1F504} Erneut scannen",
2068
+ LOGIN_SETUP_NO_FUNCTIONS: "Keine exportierten Funktionen gefunden. Scaffold ausf\xFCllen und erneut scannen.",
2069
+ LOGIN_SETUP_BEFORE_LABEL: "before()-Funktion (einmal pro Suite)",
2070
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "beforeEach()-Funktion (vor jedem Test)",
2071
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Keine \u2014",
2072
+ LOGIN_SETUP_SUMMARY: "Datei: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
2073
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Speichern",
2074
+ LOGIN_SETUP_CLEAR_BTN: "Konfiguration entfernen",
2075
+ LOGIN_SETUP_CANCEL_BTN: "Abbrechen",
2076
+ LOGIN_SETUP_SOURCE_LABEL: "Quelle der Login-Datei",
2077
+ LOGIN_SETUP_CREATE_SUB: "Scaffold mit Beispielfunktionen erstellen",
2078
+ LOGIN_SETUP_SELECT_SUB: "Auf eine vorhandene Login-Service-Datei verweisen",
2079
+ LOGIN_SETUP_INJECT_TITLE: "Login-Bl\xF6cke hinzuf\xFCgen?",
2080
+ LOGIN_SETUP_INJECT_TEXT: "Diese Datei enth\xE4lt die konfigurierten Login-Bl\xF6cke nicht. M\xF6chten Sie jetzt den Import und die before/beforeEach-Bl\xF6cke hinzuf\xFCgen?",
2081
+ LOGIN_SETUP_INJECT_CONFIRM: "Ja, hinzuf\xFCgen"
2037
2082
  },
2038
2083
  SELECTOR_PICKER: {
2039
2084
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -2124,6 +2169,12 @@ var I18N_DE = {
2124
2169
  FILE_SAVE_ERROR_TOAST: "Fehler beim Speichern",
2125
2170
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures in cypress/fixtures geschrieben",
2126
2171
  FIXTURES_NO_FOLDER_TOAST: "Fixtures konnten nicht geschrieben werden (Cypress-Ordner konfigurieren)",
2172
+ FILE_UPLOAD_MANUAL_TOAST: "F\xFCge {files} zu cypress/fixtures/ hinzu, bevor du den Test ausf\xFChrst.",
2173
+ // TODO: translate
2174
+ FILE_UPLOAD_COPIED_TOAST: "{files} nach cypress/fixtures/ kopiert.",
2175
+ // TODO: translate
2176
+ FILE_UPLOAD_COPY_ERROR_TOAST: "{files} konnte nicht nach cypress/fixtures/ kopiert werden. F\xFCge es manuell hinzu.",
2177
+ // TODO: translate
2127
2178
  BTN_CONFIG: "Config",
2128
2179
  BTN_FILES: "Dateien",
2129
2180
  BTN_COMMANDS: "Befehle",
@@ -2171,7 +2222,7 @@ var I18N_DE = {
2171
2222
  SEC_PANELS: "\u{1F5C2} Panels",
2172
2223
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 gespeichert: nach Tag filtern, kopieren, kombinieren, exportieren.",
2173
2224
  PANEL_COMMANDS: "\u2328\uFE0F Befehle \u2014 aktuelle Aufzeichnung: umsortieren, l\xF6schen, Assertions hinzuf\xFCgen.",
2174
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 Sprache, Selektor, HTTP, Cypress-Ordner, Daten, Sichtbarkeit, Position.",
2225
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 Sprache, Selektor, HTTP, Cypress-Ordner, Login-Setup, Daten, Sichtbarkeit, Position.",
2175
2226
  PANEL_FILES: "\u{1F4C1} Dateien \u2014 Tests in deine .cy.ts einf\xFCgen und den Runner starten.",
2176
2227
  SEC_SELECTOR: "\u{1F50E} Selektoren",
2177
2228
  SELECTOR_1: "W\xE4hle das bevorzugte Attribut (data-cy, data-testid, aria-label, id) unter Config.",
@@ -2184,6 +2235,8 @@ var I18N_DE = {
2184
2235
  ADV_RUNNER: "Lokaler Runner: npx lib-e2e-cypress-runner, um einen Spec headless aus dem Editor zu starten.",
2185
2236
  ADV_CROSSAPP: "Cross-App: die Aufzeichnung \xFCbersteht den Wechsel des Micro-Frontends (gleicher Origin).",
2186
2237
  ADV_DRAG: "Ziehe den \u23FA-Button, um das Widget zu verschieben; die Position wird gemerkt.",
2238
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
2239
+ // TODO: translate (de)
2187
2240
  CLOSE: "Schlie\xDFen",
2188
2241
  TAB_REFERENCE: "Kurzreferenz",
2189
2242
  TAB_GUIDE: "Nutzungsleitfaden",
@@ -2202,13 +2255,14 @@ var I18N_DE = {
2202
2255
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
2203
2256
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
2204
2257
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
2258
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
2259
+ // TODO: translate (de)
2205
2260
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
2206
2261
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
2207
2262
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
2208
2263
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
2209
2264
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
2210
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
2211
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
2265
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
2212
2266
  }
2213
2267
  };
2214
2268
 
@@ -2251,34 +2305,8 @@ var TranslationService = class {
2251
2305
  };
2252
2306
  var translationService = new TranslationService();
2253
2307
 
2254
- // src/utils/code-format.utils.ts
2255
- function gcd(a, b) {
2256
- while (b) {
2257
- const t = b;
2258
- b = a % b;
2259
- a = t;
2260
- }
2261
- return a;
2262
- }
2263
- function escapeSingleQuotes(value) {
2264
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2265
- }
2266
- function normalizeBlock(code, baseIndent) {
2267
- const lines = code.split("\n").map((l) => l.trimEnd());
2268
- const nonEmpty = lines.filter((l) => l.trim().length > 0);
2269
- if (!nonEmpty.length) return "";
2270
- const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
2271
- const minIndent = Math.min(...indents);
2272
- const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
2273
- const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
2274
- return lines.map((l) => {
2275
- if (!l.trim()) return "";
2276
- const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
2277
- return baseIndent + " ".repeat(level) + l.trimStart();
2278
- }).join("\n");
2279
- }
2280
-
2281
2308
  // src/services/transformation.service.ts
2309
+ init_code_format_utils();
2282
2310
  var TransformationService = class {
2283
2311
  toLang(lang) {
2284
2312
  return isLang(lang) ? lang : "en";
@@ -2343,6 +2371,7 @@ var advancedTestTransformationService = new AdvancedTestTransformationService();
2343
2371
  init_selector_quality_utils();
2344
2372
 
2345
2373
  // src/utils/assertion.utils.ts
2374
+ init_code_format_utils();
2346
2375
  var MAX_TEXT_LENGTH = 60;
2347
2376
  function inferAssertionCommand(el, selector) {
2348
2377
  const should = (body) => `cy.get('${selector}').should(${body})`;
@@ -2367,6 +2396,7 @@ function inferAssertionCommand(el, selector) {
2367
2396
  }
2368
2397
 
2369
2398
  // src/services/recording.service.ts
2399
+ init_code_format_utils();
2370
2400
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
2371
2401
  function createSessionId() {
2372
2402
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -2382,6 +2412,7 @@ var RecordingService = class {
2382
2412
  selectorNotFound$ = new Subject(null);
2383
2413
  inputDebounceTimers = /* @__PURE__ */ new Map();
2384
2414
  fixtures = /* @__PURE__ */ new Map();
2415
+ uploadedFiles = /* @__PURE__ */ new Map();
2385
2416
  abort = new AbortController();
2386
2417
  selectorStrategy = "data-cy";
2387
2418
  /** Stable id of the current live session (null when none has started). */
@@ -2402,12 +2433,13 @@ var RecordingService = class {
2402
2433
  }
2403
2434
  // ── Public API ────────────────────────────────────────────────────────────
2404
2435
  startRecording() {
2436
+ this.clearCommands();
2405
2437
  this.sessionId = createSessionId();
2406
2438
  this.startedAt = Date.now();
2407
2439
  this.isPaused$.next(false);
2408
2440
  this.isRecording$.next(true);
2409
2441
  this.addCommand(`cy.viewport(1900, 1200)`);
2410
- this.addCommand(`cy.visit('${window.location.pathname}')`);
2442
+ this.addCommand(`cy.visit('${escapeSingleQuotes(window.location.pathname)}')`);
2411
2443
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
2412
2444
  }
2413
2445
  /**
@@ -2505,10 +2537,20 @@ var RecordingService = class {
2505
2537
  getFixturesSnapshot() {
2506
2538
  return [...this.fixtures.entries()].map(([name, content]) => ({ name, content }));
2507
2539
  }
2540
+ addUploadedFile(filename, bytes) {
2541
+ this.uploadedFiles.set(filename, bytes);
2542
+ }
2543
+ getUploadedFilesSnapshot() {
2544
+ return [...this.uploadedFiles.entries()].map(([filename, bytes]) => ({ filename, bytes }));
2545
+ }
2546
+ clearUploadedFiles() {
2547
+ this.uploadedFiles.clear();
2548
+ }
2508
2549
  clearCommands() {
2509
2550
  this.commands$.next([]);
2510
2551
  this.interceptors$.next([]);
2511
2552
  this.fixtures.clear();
2553
+ this.uploadedFiles.clear();
2512
2554
  }
2513
2555
  clearInterceptors() {
2514
2556
  this.interceptors$.next([]);
@@ -2628,7 +2670,8 @@ var RecordingService = class {
2628
2670
  if (e.altKey) return;
2629
2671
  const selector = this.resolveSelectorFor(e.target);
2630
2672
  if (!selector) return;
2631
- const clickCmd = `cy.get('${selector}').click()`;
2673
+ const safeSelector = escapeSingleQuotes(selector);
2674
+ const clickCmd = `cy.get('${safeSelector}').click()`;
2632
2675
  let cmds = this.commands$.getValue();
2633
2676
  let removed = 0;
2634
2677
  while (removed < 2 && cmds.length > 0 && cmds[cmds.length - 1] === clickCmd) {
@@ -2636,7 +2679,7 @@ var RecordingService = class {
2636
2679
  removed++;
2637
2680
  }
2638
2681
  if (removed > 0) this.commands$.next(cmds);
2639
- this.addCommand(`cy.get('${selector}').dblclick()`);
2682
+ this.addCommand(`cy.get('${safeSelector}').dblclick()`);
2640
2683
  },
2641
2684
  { signal: this.abort.signal }
2642
2685
  );
@@ -2648,7 +2691,7 @@ var RecordingService = class {
2648
2691
  if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2649
2692
  const selector = this.resolveSelectorFor(e.target);
2650
2693
  if (!selector) return;
2651
- this.addCommand(`cy.get('${selector}').rightclick()`);
2694
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').rightclick()`);
2652
2695
  },
2653
2696
  { signal: this.abort.signal }
2654
2697
  );
@@ -2668,7 +2711,7 @@ var RecordingService = class {
2668
2711
  if (!selector) return;
2669
2712
  this.flushInputDebounce(target);
2670
2713
  const token = key === "Enter" ? "{enter}" : "{esc}";
2671
- this.addCommand(`cy.get('${selector}').type('${token}')`);
2714
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').type('${token}')`);
2672
2715
  },
2673
2716
  { signal: this.abort.signal }
2674
2717
  );
@@ -2692,19 +2735,52 @@ var RecordingService = class {
2692
2735
  if (!this.isRecording$.getValue()) return;
2693
2736
  const target = e.target;
2694
2737
  if (!target || this.isOwnElement(target)) return;
2695
- if (target.tagName.toLowerCase() === "select") {
2738
+ const tag = target.tagName.toLowerCase();
2739
+ if (tag === "select") {
2696
2740
  this.handleSelectEvent(target);
2741
+ } else if (tag === "input") {
2742
+ const type = (target.getAttribute("type") ?? "").toLowerCase();
2743
+ if (type === "file") {
2744
+ void this.handleFileInputChange(target);
2745
+ }
2697
2746
  }
2698
2747
  },
2699
2748
  { signal: this.abort.signal }
2700
2749
  );
2701
2750
  }
2751
+ async handleFileInputChange(target) {
2752
+ if (this.isPaused$.getValue()) return;
2753
+ const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2754
+ if (!container) return;
2755
+ const selector = this.getReliableSelector(container);
2756
+ if (!selector || this.isOwnSelector(selector)) return;
2757
+ const files = Array.from(target.files ?? []);
2758
+ if (!files.length) return;
2759
+ const paths = files.map((f) => `'cypress/fixtures/${escapeSingleQuotes(f.name)}'`);
2760
+ const pathArg = paths.length === 1 ? paths[0] : `[${paths.join(", ")}]`;
2761
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').selectFile(${pathArg})`);
2762
+ for (const file of files) {
2763
+ const bytes = await this.readFileAsArrayBuffer(file);
2764
+ this.addUploadedFile(file.name, bytes);
2765
+ }
2766
+ }
2767
+ readFileAsArrayBuffer(file) {
2768
+ if (typeof file.arrayBuffer === "function") {
2769
+ return file.arrayBuffer();
2770
+ }
2771
+ return new Promise((resolve, reject) => {
2772
+ const reader = new FileReader();
2773
+ reader.onload = () => resolve(reader.result);
2774
+ reader.onerror = () => reject(reader.error);
2775
+ reader.readAsArrayBuffer(file);
2776
+ });
2777
+ }
2702
2778
  listenToRouteChanges() {
2703
2779
  let lastUrl = window.location.pathname + window.location.search + window.location.hash;
2704
2780
  const addUrlCommand = (newUrl) => {
2705
2781
  if (!this.isRecording$.getValue()) return;
2706
2782
  if (newUrl === lastUrl) return;
2707
- this.addCommand(`cy.url().should('include', '${newUrl}')`);
2783
+ this.addCommand(`cy.url().should('include', '${escapeSingleQuotes(newUrl)}')`);
2708
2784
  lastUrl = newUrl;
2709
2785
  };
2710
2786
  const wrapMethod = (type) => {
@@ -2751,6 +2827,13 @@ var RecordingService = class {
2751
2827
  }
2752
2828
  return;
2753
2829
  }
2830
+ if (!this.isInteractiveElement(target)) {
2831
+ const interactive = target.closest(
2832
+ 'button,a,input,select,textarea,label,summary,mat-option,[role="button"],[role="link"],[role="menuitem"],[role="option"],[role="tab"]'
2833
+ );
2834
+ if (!interactive) return;
2835
+ target = interactive;
2836
+ }
2754
2837
  }
2755
2838
  const tag = target.tagName.toLowerCase();
2756
2839
  if (tag === "input" || tag === "textarea" || tag === "select") {
@@ -2783,6 +2866,10 @@ var RecordingService = class {
2783
2866
  this.handleMatOptionClick(target, selector);
2784
2867
  return;
2785
2868
  }
2869
+ if (!selector) {
2870
+ this.selectorNotFound$.next({ target, action: "click" });
2871
+ return;
2872
+ }
2786
2873
  this.addGenericCommand({
2787
2874
  selector,
2788
2875
  action: (s) => `cy.get('${s}').click()`
@@ -2794,7 +2881,8 @@ var RecordingService = class {
2794
2881
  if (matSelect) dataCy = matSelect.closest("[data-cy]")?.getAttribute("data-cy") ?? null;
2795
2882
  if (!dataCy) dataCy = target.closest("[data-cy]")?.getAttribute("data-cy") ?? null;
2796
2883
  if (dataCy) {
2797
- this.addCommand(`cy.get('[data-cy="${dataCy}"]').click()`);
2884
+ const safeSel = escapeSingleQuotes(`[data-cy="${escapeCssAttrValue(dataCy)}"]`);
2885
+ this.addCommand(`cy.get('${safeSel}').click()`);
2798
2886
  return;
2799
2887
  }
2800
2888
  if (selector) {
@@ -2826,7 +2914,7 @@ var RecordingService = class {
2826
2914
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2827
2915
  if (!container) return;
2828
2916
  const selector = this.getReliableSelector(container);
2829
- const value = target.value.replace(/'/g, "\\'");
2917
+ const value = escapeSingleQuotes(target.value);
2830
2918
  this.addGenericCommand({
2831
2919
  selector,
2832
2920
  action: (s) => `cy.get('${s}').clear().type('${value}')`
@@ -2845,7 +2933,7 @@ var RecordingService = class {
2845
2933
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2846
2934
  if (!container) return;
2847
2935
  const selector = this.getReliableSelector(container);
2848
- const value = target.value.replace(/'/g, "\\'");
2936
+ const value = escapeSingleQuotes(target.value);
2849
2937
  this.addGenericCommand({
2850
2938
  selector,
2851
2939
  action: (s) => `cy.get('${s}').select('${value}')`
@@ -2854,7 +2942,7 @@ var RecordingService = class {
2854
2942
  // ── Shared helpers ────────────────────────────────────────────────────────
2855
2943
  addGenericCommand(opts) {
2856
2944
  if (!opts.selector || this.isOwnSelector(opts.selector)) return;
2857
- this.addCommand(opts.action(opts.selector));
2945
+ this.addCommand(opts.action(escapeSingleQuotes(opts.selector)));
2858
2946
  }
2859
2947
  getReliableSelector(el) {
2860
2948
  const strategy = this.selectorStrategy;
@@ -2866,20 +2954,20 @@ var RecordingService = class {
2866
2954
  const validId = rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId) ? rawId : null;
2867
2955
  switch (strategy) {
2868
2956
  case "data-testid":
2869
- if (dataTestid) return `[data-testid="${dataTestid}"]`;
2870
- if (dataCy) return `[data-cy="${dataCy}"]`;
2957
+ if (dataTestid) return `[data-testid="${escapeCssAttrValue(dataTestid)}"]`;
2958
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2871
2959
  return validId ? `#${validId}` : null;
2872
2960
  case "aria-label":
2873
- if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
2874
- if (dataCy) return `[data-cy="${dataCy}"]`;
2961
+ if (ariaLabel) return `[aria-label="${escapeCssAttrValue(ariaLabel)}"]`;
2962
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2875
2963
  return validId ? `#${validId}` : null;
2876
2964
  case "id":
2877
2965
  if (validId) return `#${validId}`;
2878
- if (dataCy) return `[data-cy="${dataCy}"]`;
2879
- return dataTestid ? `[data-testid="${dataTestid}"]` : null;
2966
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2967
+ return dataTestid ? `[data-testid="${escapeCssAttrValue(dataTestid)}"]` : null;
2880
2968
  default:
2881
- if (dataCy) return `[data-cy="${dataCy}"]`;
2882
- if (dataDotCy) return `[data.cy="${dataDotCy}"]`;
2969
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2970
+ if (dataDotCy) return `[data.cy="${escapeCssAttrValue(dataDotCy)}"]`;
2883
2971
  return validId ? `#${validId}` : null;
2884
2972
  }
2885
2973
  }
@@ -2905,44 +2993,41 @@ var RecordingService = class {
2905
2993
 
2906
2994
  // src/services/persistence.service.ts
2907
2995
  var import_idb = require("idb");
2996
+ init_code_format_utils();
2997
+
2998
+ // src/models/issue-tracker.model.ts
2999
+ var ISSUE_TRACKER_PRESETS = {
3000
+ jira: { id: "jira", label: "Jira", urlPattern: "{baseUrl}/browse/{id}", idPattern: /^[A-Z][A-Z0-9]+-\d+$/ },
3001
+ github: { id: "github", label: "GitHub Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
3002
+ gitlab: { id: "gitlab", label: "GitLab Issues", urlPattern: "{baseUrl}/-/issues/{id}", idPattern: /^\d+$/ },
3003
+ azure: { id: "azure", label: "Azure DevOps", urlPattern: "{baseUrl}/_workitems/edit/{id}", idPattern: /^\d+$/ },
3004
+ bitbucket: { id: "bitbucket", label: "Bitbucket Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
3005
+ custom: { id: "custom", label: "Custom", urlPattern: "{baseUrl}/{id}", idPattern: /.+/ }
3006
+ };
3007
+ var DEFAULT_ISSUE_TRACKER_CONFIG = {
3008
+ enabled: false,
3009
+ provider: "jira",
3010
+ baseUrl: ""
3011
+ };
2908
3012
 
2909
3013
  // src/utils/ticket.utils.ts
2910
- function buildTicketUrl(provider, params, id) {
2911
- const ticketId = (id ?? "").trim();
2912
- if (!provider || !ticketId) return null;
2913
- let url = provider.urlTemplate;
2914
- for (const field of provider.fields) {
2915
- const value = (params?.[field.key] ?? "").trim().replace(/\/+$/, "");
2916
- if (!value) return null;
2917
- url = url.split(`{${field.key}}`).join(value);
2918
- }
2919
- url = url.split("{id}").join(encodeURIComponent(ticketId));
2920
- if (url.includes("{") || url.includes("}")) return null;
2921
- return toSafeHttpUrl(url);
3014
+ function buildTicketUrl(ticketId, config) {
3015
+ if (!config.enabled || !ticketId.trim() || !config.baseUrl.trim()) return null;
3016
+ const preset = ISSUE_TRACKER_PRESETS[config.provider];
3017
+ const base = config.baseUrl.replace(/\/+$/, "");
3018
+ return preset.urlPattern.replace("{baseUrl}", base).replace("{id}", encodeURIComponent(ticketId.trim()));
2922
3019
  }
2923
- function resolveTicketUrl(providerId, allParams, ticketId) {
2924
- const id = (ticketId ?? "").trim();
2925
- if (!id) return null;
2926
- const provider = findIssueTrackerProvider(providerId);
2927
- if (!provider) return null;
2928
- return buildTicketUrl(provider, allParams?.[providerId] ?? {}, id);
2929
- }
2930
- function toSafeHttpUrl(candidate) {
2931
- try {
2932
- const parsed = new URL(candidate);
2933
- return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
2934
- } catch {
2935
- return null;
2936
- }
3020
+ function buildTicketComment(ticketId) {
3021
+ return `// Ticket: ${ticketId.trim()}`;
2937
3022
  }
2938
- function buildTicketComment(id, url) {
2939
- const ticketId = (id ?? "").trim();
2940
- return url ? `// ${ticketId} \u2014 ${url}` : `// ${ticketId}`;
3023
+ function isValidTicketId(ticketId, provider) {
3024
+ if (!ticketId.trim()) return false;
3025
+ return ISSUE_TRACKER_PRESETS[provider].idPattern.test(ticketId.trim());
2941
3026
  }
2942
3027
 
2943
3028
  // src/services/persistence.service.ts
2944
3029
  var ACTIVE_SESSION_ID = 1;
2945
- var PersistenceService = class {
3030
+ var PersistenceService = class _PersistenceService {
2946
3031
  // dbName is overridable so tests can use unique names for isolation.
2947
3032
  constructor(dbName = DB_SCHEMA.name) {
2948
3033
  this.dbName = dbName;
@@ -2995,28 +3080,13 @@ var PersistenceService = class {
2995
3080
  if (!record) return null;
2996
3081
  const commands = await this.getCommandStrings(testId);
2997
3082
  const interceptors = await this.getInterceptorStrings(testId);
2998
- const ticketComment = await this.ticketCommentFor(record);
2999
- const itBody = `it('${escapeSingleQuotes(record.name)}', () => {
3083
+ const ticketComment = record.ticketId ? buildTicketComment(record.ticketId) + "\n" : "";
3084
+ const itBlock = `${ticketComment}it('${escapeSingleQuotes(record.name)}', () => {
3000
3085
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
3001
3086
  });`;
3002
- const itBlock = ticketComment ? `${ticketComment}
3003
- ${itBody}` : itBody;
3004
3087
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
3005
3088
  return { ...record, commands, interceptors, cypressCommands: commands, itBlock, interceptorsBlock };
3006
3089
  }
3007
- /**
3008
- * Builds the traceability comment for a test record (spec 014), reading the
3009
- * configured issue-tracker provider/params so the link — when resolvable — is
3010
- * embedded. Returns `null` when the record has no ticket id.
3011
- */
3012
- async ticketCommentFor(record) {
3013
- if (!record.ticketId) return null;
3014
- const config = await this.getGeneralConfig();
3015
- const providerId = config?.["issueTrackerProviderId"] ?? DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
3016
- const params = config?.["issueTrackerParams"] ?? {};
3017
- const url = resolveTicketUrl(providerId, params, record.ticketId);
3018
- return buildTicketComment(record.ticketId, url);
3019
- }
3020
3090
  async deleteTest(id) {
3021
3091
  const db = await this.getDB();
3022
3092
  await db.delete("tests", id);
@@ -3092,6 +3162,30 @@ ${itBody}` : itBody;
3092
3162
  const records = await db.getAll("configuration");
3093
3163
  return records[0] ?? null;
3094
3164
  }
3165
+ // ── Login setup config ───────────────────────────────────────────────────
3166
+ static LOGIN_SETUP_KEY = "loginSetupConfig";
3167
+ async saveLoginSetup(config) {
3168
+ await this.setConfigKey(_PersistenceService.LOGIN_SETUP_KEY, JSON.stringify(config));
3169
+ }
3170
+ async getLoginSetup() {
3171
+ const record = await this.getConfig(_PersistenceService.LOGIN_SETUP_KEY);
3172
+ if (!record) return null;
3173
+ const raw = record[_PersistenceService.LOGIN_SETUP_KEY];
3174
+ if (typeof raw !== "string") return null;
3175
+ try {
3176
+ return JSON.parse(raw);
3177
+ } catch {
3178
+ return null;
3179
+ }
3180
+ }
3181
+ async clearLoginSetup() {
3182
+ const db = await this.getDB();
3183
+ const records = await db.getAll("configuration");
3184
+ if (!records.length) return;
3185
+ const current = { ...records[0] };
3186
+ delete current[_PersistenceService.LOGIN_SETUP_KEY];
3187
+ await db.put("configuration", current);
3188
+ }
3095
3189
  // ── Active recording session ──────────────────────────────────────────────
3096
3190
  /** Upserts the live recording session (single record, fixed key). */
3097
3191
  async saveActiveSession(state) {
@@ -3134,6 +3228,24 @@ ${itBody}` : itBody;
3134
3228
  }
3135
3229
  await this.bulkInsertWithoutId("interceptors", interceptors);
3136
3230
  }
3231
+ async hasDirectoryAccess() {
3232
+ const config = await this.getGeneralConfig();
3233
+ return !!config?.["cypressDirectoryHandle"];
3234
+ }
3235
+ async writeUploadedFile(filename, bytes) {
3236
+ const config = await this.getGeneralConfig();
3237
+ const dirHandle = config?.["cypressDirectoryHandle"];
3238
+ if (!dirHandle) throw new Error("No Cypress folder configured");
3239
+ const perm = dirHandle;
3240
+ let state = await perm.queryPermission({ mode: "readwrite" });
3241
+ if (state !== "granted") state = await perm.requestPermission({ mode: "readwrite" });
3242
+ if (state !== "granted") throw new Error("Write permission denied");
3243
+ const fixturesDir = await dirHandle.getDirectoryHandle("fixtures", { create: true });
3244
+ const fileHandle = await fixturesDir.getFileHandle(filename, { create: true });
3245
+ const writable = await fileHandle.createWritable();
3246
+ await writable.write(bytes);
3247
+ await writable.close();
3248
+ }
3137
3249
  async requestDirectoryPermissions() {
3138
3250
  if (!("showDirectoryPicker" in window)) {
3139
3251
  throw new Error("File System Access API not supported");
@@ -3176,7 +3288,36 @@ ${itBody}` : itBody;
3176
3288
  }
3177
3289
  }
3178
3290
  };
3179
- var persistenceService = new PersistenceService();
3291
+
3292
+ // src/services/http-monitor.ts
3293
+ init_code_format_utils();
3294
+
3295
+ // src/utils/redact.utils.ts
3296
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
3297
+ "password",
3298
+ "token",
3299
+ "secret",
3300
+ "authorization",
3301
+ "cookie",
3302
+ "access_token",
3303
+ "refresh_token"
3304
+ ]);
3305
+ function isSensitiveKey(key) {
3306
+ return SENSITIVE_KEYS.has(key.toLowerCase());
3307
+ }
3308
+ function redactSensitiveFields(obj) {
3309
+ if (Array.isArray(obj)) {
3310
+ return obj.map(redactSensitiveFields);
3311
+ }
3312
+ if (obj !== null && typeof obj === "object") {
3313
+ const result = {};
3314
+ for (const [key, value] of Object.entries(obj)) {
3315
+ result[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveFields(value);
3316
+ }
3317
+ return result;
3318
+ }
3319
+ return obj;
3320
+ }
3180
3321
 
3181
3322
  // src/services/http-monitor.ts
3182
3323
  var INTERCEPTED_METHODS = ["GET", "POST", "PUT"];
@@ -3189,8 +3330,12 @@ function generateAlias(method, url) {
3189
3330
  return `${method.toLowerCase()}-intercepted-request`;
3190
3331
  }
3191
3332
  }
3333
+ var SAFE_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3192
3334
  function buildValidations(base, obj) {
3193
- return Object.keys(obj).filter((key) => key !== "id" && key !== "uid").map((key) => `expect(${base}.${key}).to.equal(${JSON.stringify(obj[key])});`).join("\n");
3335
+ return Object.keys(obj).filter((key) => key !== "id" && key !== "uid").map((key) => {
3336
+ const accessor = SAFE_IDENTIFIER.test(key) ? `.${key}` : `['${escapeSingleQuotes(key)}']`;
3337
+ return `expect(${base}${accessor}).to.equal(${JSON.stringify(obj[key])});`;
3338
+ }).join("\n");
3194
3339
  }
3195
3340
  function buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody) {
3196
3341
  if (extendedHttp && method === "GET" && responseBody) {
@@ -3242,20 +3387,149 @@ function resolveMethod(input, init) {
3242
3387
  if (input instanceof Request) return input.method.toUpperCase();
3243
3388
  return "GET";
3244
3389
  }
3390
+ async function _handleFetchInterception(recording, input, init, responseClone) {
3391
+ const method = resolveMethod(input, init);
3392
+ if (!INTERCEPTED_METHODS.includes(method)) return;
3393
+ const url = resolveUrl(input);
3394
+ const alias = generateAlias(method, url);
3395
+ recording.registerInterceptor(method, url, alias);
3396
+ const extendedHttp = localStorage.getItem("extendedHttpCommands") === "true";
3397
+ const fixtureMode = localStorage.getItem("fixtureMode") === "true";
3398
+ const requestBody = extendedHttp ? parseRequestBody(init) : null;
3399
+ let responseText = null;
3400
+ let responseBody = null;
3401
+ if (extendedHttp || fixtureMode && method === "GET") {
3402
+ try {
3403
+ responseText = await responseClone.text();
3404
+ if (extendedHttp) responseBody = parseJsonObject(responseText);
3405
+ } catch {
3406
+ }
3407
+ }
3408
+ if (fixtureMode && method === "GET" && responseText !== null) {
3409
+ const pretty = prettyJsonOrNull(responseText);
3410
+ if (pretty !== null) {
3411
+ const redacted = redactSensitiveFields(JSON.parse(pretty));
3412
+ recording.registerFixture(`${alias}.json`, JSON.stringify(redacted, null, 2));
3413
+ }
3414
+ }
3415
+ const cmd = buildCyWaitCommand(
3416
+ method,
3417
+ alias,
3418
+ extendedHttp,
3419
+ requestBody ? redactSensitiveFields(requestBody) : null,
3420
+ responseBody ? redactSensitiveFields(responseBody) : null
3421
+ );
3422
+ recording.addCommand(cmd);
3423
+ }
3424
+ function _handleXhrInterception(recording, method, url, requestBody, responseText) {
3425
+ if (!INTERCEPTED_METHODS.includes(method)) return;
3426
+ const alias = generateAlias(method, url);
3427
+ recording.registerInterceptor(method, url, alias);
3428
+ if (localStorage.getItem("fixtureMode") === "true" && method === "GET") {
3429
+ const pretty = prettyJsonOrNull(responseText);
3430
+ if (pretty !== null) {
3431
+ const redacted = redactSensitiveFields(JSON.parse(pretty));
3432
+ recording.registerFixture(`${alias}.json`, JSON.stringify(redacted, null, 2));
3433
+ }
3434
+ }
3435
+ const extendedHttp = localStorage.getItem("extendedHttpCommands") === "true";
3436
+ const responseBody = extendedHttp ? parseJsonObject(responseText) : null;
3437
+ const cmd = buildCyWaitCommand(
3438
+ method,
3439
+ alias,
3440
+ extendedHttp,
3441
+ requestBody ? redactSensitiveFields(requestBody) : null,
3442
+ responseBody ? redactSensitiveFields(responseBody) : null
3443
+ );
3444
+ recording.addCommand(cmd);
3445
+ }
3446
+ var _refCount = 0;
3447
+ var _originalFetch = null;
3448
+ var _originalXHR = null;
3449
+ var _recordings = /* @__PURE__ */ new Set();
3450
+ function _patchFetch() {
3451
+ const orig = window.fetch;
3452
+ _originalFetch = orig;
3453
+ window.fetch = async (input, init) => {
3454
+ const response = await orig(input, init);
3455
+ try {
3456
+ const active = [..._recordings];
3457
+ if (active.length > 0) {
3458
+ await Promise.all(
3459
+ active.map((rec) => _handleFetchInterception(rec, input, init, response.clone()).catch(() => {
3460
+ }))
3461
+ );
3462
+ }
3463
+ } catch {
3464
+ }
3465
+ return response;
3466
+ };
3467
+ }
3468
+ function _restoreFetch() {
3469
+ if (!_originalFetch) return;
3470
+ window.fetch = _originalFetch;
3471
+ _originalFetch = null;
3472
+ }
3473
+ function _patchXhr() {
3474
+ const OrigXHR = window.XMLHttpRequest;
3475
+ _originalXHR = OrigXHR;
3476
+ window.XMLHttpRequest = class extends OrigXHR {
3477
+ _xhrMethod = "GET";
3478
+ _xhrUrl = "";
3479
+ _xhrRequestBody = null;
3480
+ open(method, url, async = true, user, password) {
3481
+ this._xhrMethod = method.toUpperCase();
3482
+ this._xhrUrl = url instanceof URL ? url.href : url;
3483
+ super.open(method, url, async, user, password);
3484
+ }
3485
+ send(body) {
3486
+ if (typeof body === "string") {
3487
+ this._xhrRequestBody = parseJsonObject(body);
3488
+ }
3489
+ this.addEventListener("load", () => {
3490
+ try {
3491
+ for (const rec of _recordings) {
3492
+ _handleXhrInterception(rec, this._xhrMethod, this._xhrUrl, this._xhrRequestBody, this.responseText);
3493
+ }
3494
+ } catch {
3495
+ }
3496
+ });
3497
+ super.send(body);
3498
+ }
3499
+ };
3500
+ }
3501
+ function _restoreXhr() {
3502
+ if (!_originalXHR) return;
3503
+ window.XMLHttpRequest = _originalXHR;
3504
+ _originalXHR = null;
3505
+ }
3506
+ function _resetHttpMonitorState() {
3507
+ _refCount = 0;
3508
+ _originalFetch = null;
3509
+ _originalXHR = null;
3510
+ _recordings.clear();
3511
+ }
3245
3512
  var HttpMonitor = class {
3246
3513
  constructor(recording) {
3247
3514
  this.recording = recording;
3248
3515
  }
3249
3516
  recording;
3250
- originalFetch = null;
3251
- originalXHR = null;
3252
3517
  install() {
3253
- this.installFetch();
3254
- this.installXhr();
3518
+ _recordings.add(this.recording);
3519
+ _refCount++;
3520
+ if (_refCount === 1) {
3521
+ _patchFetch();
3522
+ _patchXhr();
3523
+ }
3255
3524
  }
3256
3525
  uninstall() {
3257
- this.uninstallFetch();
3258
- this.uninstallXhr();
3526
+ _recordings.delete(this.recording);
3527
+ if (_refCount <= 0) return;
3528
+ _refCount--;
3529
+ if (_refCount === 0) {
3530
+ _restoreFetch();
3531
+ _restoreXhr();
3532
+ }
3259
3533
  }
3260
3534
  isExtendedHttpEnabled() {
3261
3535
  return localStorage.getItem("extendedHttpCommands") === "true";
@@ -3263,105 +3537,6 @@ var HttpMonitor = class {
3263
3537
  isFixtureModeEnabled() {
3264
3538
  return localStorage.getItem("fixtureMode") === "true";
3265
3539
  }
3266
- installFetch() {
3267
- if (this.originalFetch) return;
3268
- const originalFetch = window.fetch;
3269
- this.originalFetch = originalFetch;
3270
- window.fetch = async (input, init) => {
3271
- const response = await originalFetch(input, init);
3272
- try {
3273
- await this.handleFetchInterception(input, init, response.clone());
3274
- } catch {
3275
- }
3276
- return response;
3277
- };
3278
- }
3279
- uninstallFetch() {
3280
- if (!this.originalFetch) return;
3281
- window.fetch = this.originalFetch;
3282
- this.originalFetch = null;
3283
- }
3284
- installXhr() {
3285
- if (this.originalXHR) return;
3286
- this.originalXHR = window.XMLHttpRequest;
3287
- const OrigXHR = this.originalXHR;
3288
- const handleXhrInterception = this.handleXhrInterception.bind(this);
3289
- window.XMLHttpRequest = class extends OrigXHR {
3290
- _xhrMethod = "GET";
3291
- _xhrUrl = "";
3292
- _xhrRequestBody = null;
3293
- open(method, url, async = true, user, password) {
3294
- this._xhrMethod = method.toUpperCase();
3295
- this._xhrUrl = url instanceof URL ? url.href : url;
3296
- super.open(method, url, async, user, password);
3297
- }
3298
- send(body) {
3299
- if (typeof body === "string") {
3300
- this._xhrRequestBody = parseJsonObject(body);
3301
- }
3302
- this.addEventListener("load", () => {
3303
- try {
3304
- handleXhrInterception(
3305
- this._xhrMethod,
3306
- this._xhrUrl,
3307
- this._xhrRequestBody,
3308
- this.responseText
3309
- );
3310
- } catch {
3311
- }
3312
- });
3313
- super.send(body);
3314
- }
3315
- };
3316
- }
3317
- uninstallXhr() {
3318
- if (!this.originalXHR) return;
3319
- window.XMLHttpRequest = this.originalXHR;
3320
- this.originalXHR = null;
3321
- }
3322
- async handleFetchInterception(input, init, responseClone) {
3323
- const method = resolveMethod(input, init);
3324
- if (!INTERCEPTED_METHODS.includes(method)) return;
3325
- const url = resolveUrl(input);
3326
- const alias = generateAlias(method, url);
3327
- if (this.isFixtureModeEnabled() && method === "GET") {
3328
- const pretty = prettyJsonOrNull(await responseClone.text());
3329
- const fixtureFile = `${alias}.json`;
3330
- if (pretty !== null) this.recording.registerFixture(fixtureFile, pretty);
3331
- this.recording.registerInterceptor(method, url, alias, pretty !== null ? fixtureFile : void 0);
3332
- this.recording.addCommand(`cy.wait('@${alias}').then((interception) => { })`);
3333
- return;
3334
- }
3335
- this.recording.registerInterceptor(method, url, alias);
3336
- const extendedHttp = this.isExtendedHttpEnabled();
3337
- const requestBody = extendedHttp ? parseRequestBody(init) : null;
3338
- let responseBody = null;
3339
- if (extendedHttp) {
3340
- try {
3341
- responseBody = parseJsonObject(await responseClone.text());
3342
- } catch {
3343
- }
3344
- }
3345
- const cmd = buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody);
3346
- this.recording.addCommand(cmd);
3347
- }
3348
- handleXhrInterception(method, url, requestBody, responseText) {
3349
- if (!INTERCEPTED_METHODS.includes(method)) return;
3350
- const alias = generateAlias(method, url);
3351
- if (this.isFixtureModeEnabled() && method === "GET") {
3352
- const pretty = prettyJsonOrNull(responseText);
3353
- const fixtureFile = `${alias}.json`;
3354
- if (pretty !== null) this.recording.registerFixture(fixtureFile, pretty);
3355
- this.recording.registerInterceptor(method, url, alias, pretty !== null ? fixtureFile : void 0);
3356
- this.recording.addCommand(`cy.wait('@${alias}').then((interception) => { })`);
3357
- return;
3358
- }
3359
- this.recording.registerInterceptor(method, url, alias);
3360
- const extendedHttp = this.isExtendedHttpEnabled();
3361
- const responseBody = extendedHttp ? parseJsonObject(responseText) : null;
3362
- const cmd = buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody);
3363
- this.recording.addCommand(cmd);
3364
- }
3365
3540
  };
3366
3541
 
3367
3542
  // src/utils/styles.utils.ts
@@ -3610,6 +3785,19 @@ function setSwal2DataCyAttribute(dataCy = "lib-e2e-cypress-for-dummys") {
3610
3785
  document.querySelector(".swal2-html-container")?.setAttribute("data-cy", dataCy);
3611
3786
  document.querySelector(".swal2-title")?.setAttribute("data-cy", dataCy);
3612
3787
  }
3788
+ function ensurePopupDimensions(popup, heightPx) {
3789
+ if (!popup) return;
3790
+ popup.style.height = `${heightPx}px`;
3791
+ const htmlContainer = popup.querySelector(".swal2-html-container");
3792
+ if (!htmlContainer) return;
3793
+ htmlContainer.style.flex = "1";
3794
+ htmlContainer.style.minHeight = "0";
3795
+ htmlContainer.style.overflow = "hidden";
3796
+ htmlContainer.style.padding = "0";
3797
+ htmlContainer.style.margin = "0";
3798
+ htmlContainer.style.display = "flex";
3799
+ htmlContainer.style.flexDirection = "column";
3800
+ }
3613
3801
 
3614
3802
  // src/utils/toast.utils.ts
3615
3803
  function showToast(message, isSuccess = true) {
@@ -3681,177 +3869,6 @@ function boxTopLeftFor(toggleCentre, dir) {
3681
3869
  return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
3682
3870
  }
3683
3871
 
3684
- // src/utils/cypress-completions.ts
3685
- var CY_METHODS = [
3686
- "visit",
3687
- "get",
3688
- "contains",
3689
- "intercept",
3690
- "wait",
3691
- "request",
3692
- "url",
3693
- "location",
3694
- "viewport",
3695
- "fixture",
3696
- "wrap",
3697
- "reload",
3698
- "go",
3699
- "window",
3700
- "document",
3701
- "session",
3702
- "clearCookies",
3703
- "clearLocalStorage",
3704
- "screenshot",
3705
- "log"
3706
- ];
3707
- var CHAIN_METHODS = [
3708
- "click",
3709
- "dblclick",
3710
- "rightclick",
3711
- "type",
3712
- "clear",
3713
- "select",
3714
- "check",
3715
- "uncheck",
3716
- "should",
3717
- "and",
3718
- "find",
3719
- "first",
3720
- "last",
3721
- "eq",
3722
- "then",
3723
- "its",
3724
- "invoke",
3725
- "as",
3726
- "trigger",
3727
- "focus",
3728
- "blur",
3729
- "scrollIntoView",
3730
- "within",
3731
- "parent",
3732
- "children",
3733
- "siblings",
3734
- "contains",
3735
- "each",
3736
- "filter",
3737
- "wait"
3738
- ];
3739
- var TOP_LEVEL = [
3740
- "cy",
3741
- "describe",
3742
- "it",
3743
- "context",
3744
- "beforeEach",
3745
- "afterEach",
3746
- "before",
3747
- "after",
3748
- "Cypress"
3749
- ];
3750
- var CYPRESS_COMPLETIONS = [
3751
- ...TOP_LEVEL,
3752
- ...CY_METHODS.map((m) => `cy.${m}`),
3753
- ...CHAIN_METHODS
3754
- ];
3755
- function filterCompletions(list, prefix) {
3756
- const p = prefix.toLowerCase();
3757
- if (!p) return [...list];
3758
- return list.filter((label) => label.toLowerCase().includes(p));
3759
- }
3760
- function filterCypressCompletions(prefix) {
3761
- return filterCompletions(CYPRESS_COMPLETIONS, prefix);
3762
- }
3763
-
3764
- // src/utils/code-editor.ts
3765
- function toOptions(labels, type) {
3766
- return labels.map((label) => ({ label, type }));
3767
- }
3768
- function cypressCompletionSource(context) {
3769
- const cyMatch = context.matchBefore(/cy\.\w*$/);
3770
- if (cyMatch) {
3771
- return { from: cyMatch.from + 3, options: toOptions(filterCompletions(CY_METHODS, cyMatch.text.slice(3)), "method") };
3772
- }
3773
- const chainMatch = context.matchBefore(/\.\w*$/);
3774
- if (chainMatch) {
3775
- return { from: chainMatch.from + 1, options: toOptions(filterCompletions(CHAIN_METHODS, chainMatch.text.slice(1)), "method") };
3776
- }
3777
- const word = context.matchBefore(/\w*/);
3778
- if (!word || word.from === word.to && !context.explicit) return null;
3779
- return { from: word.from, options: toOptions(filterCompletions(TOP_LEVEL, word.text), "keyword") };
3780
- }
3781
- async function createCodeEditor(opts) {
3782
- const [view, state, language, langJs, autocomplete, commands, highlight] = await Promise.all([
3783
- import("@codemirror/view"),
3784
- import("@codemirror/state"),
3785
- import("@codemirror/language"),
3786
- import("@codemirror/lang-javascript"),
3787
- import("@codemirror/autocomplete"),
3788
- import("@codemirror/commands"),
3789
- import("@lezer/highlight")
3790
- ]);
3791
- const { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } = view;
3792
- const { EditorState } = state;
3793
- const { syntaxHighlighting, HighlightStyle, indentOnInput, bracketMatching } = language;
3794
- const { javascript } = langJs;
3795
- const { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } = autocomplete;
3796
- const { history: history2, defaultKeymap, historyKeymap, indentWithTab } = commands;
3797
- const { tags } = highlight;
3798
- const darkHighlight = HighlightStyle.define([
3799
- { tag: tags.keyword, color: "#ff7b72" },
3800
- { tag: tags.string, color: "#a5d6ff" },
3801
- { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: "#d2a8ff" },
3802
- { tag: tags.number, color: "#79c0ff" },
3803
- { tag: [tags.bool, tags.null], color: "#79c0ff" },
3804
- { tag: tags.comment, color: "#8b949e", fontStyle: "italic" },
3805
- { tag: [tags.propertyName, tags.attributeName], color: "#7ee787" },
3806
- { tag: [tags.typeName, tags.className], color: "#ffa657" },
3807
- { tag: tags.operator, color: "#ff7b72" },
3808
- { tag: [tags.brace, tags.bracket, tags.punctuation], color: "#c9d1d9" }
3809
- ]);
3810
- const theme = EditorView.theme(
3811
- {
3812
- "&": { color: "#c9d1d9", backgroundColor: "#0d1117", height: "100%", fontSize: "13px" },
3813
- ".cm-scroller": { fontFamily: "'Cascadia Code','Fira Code',Consolas,monospace", overflow: "auto" },
3814
- ".cm-gutters": { backgroundColor: "#0d1117", color: "#484f58", border: "none" },
3815
- ".cm-activeLine": { backgroundColor: "rgba(110,118,129,0.10)" },
3816
- ".cm-activeLineGutter": { backgroundColor: "transparent" },
3817
- "&.cm-focused": { outline: "none" },
3818
- ".cm-tooltip": { background: "#161b22", border: "1px solid #30363d", color: "#c9d1d9", borderRadius: "6px" },
3819
- ".cm-tooltip-autocomplete ul li[aria-selected]": { background: "#1f6feb", color: "#fff" }
3820
- },
3821
- { dark: true }
3822
- );
3823
- const editorView = new EditorView({
3824
- parent: opts.parent,
3825
- ...opts.root ? { root: opts.root } : {},
3826
- state: EditorState.create({
3827
- doc: opts.doc,
3828
- extensions: [
3829
- lineNumbers(),
3830
- highlightActiveLine(),
3831
- drawSelection(),
3832
- history2(),
3833
- indentOnInput(),
3834
- bracketMatching(),
3835
- closeBrackets(),
3836
- javascript({ typescript: true }),
3837
- syntaxHighlighting(darkHighlight),
3838
- autocompletion({ override: [cypressCompletionSource] }),
3839
- keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...completionKeymap, indentWithTab]),
3840
- theme,
3841
- EditorView.updateListener.of((update) => {
3842
- if (update.docChanged && opts.onChange) opts.onChange(update.state.doc.toString());
3843
- })
3844
- ]
3845
- })
3846
- });
3847
- return {
3848
- getValue: () => editorView.state.doc.toString(),
3849
- setValue: (value) => editorView.dispatch({ changes: { from: 0, to: editorView.state.doc.length, insert: value } }),
3850
- focus: () => editorView.focus(),
3851
- destroy: () => editorView.destroy()
3852
- };
3853
- }
3854
-
3855
3872
  // src/index.ts
3856
3873
  init_selector_picker();
3857
3874
 
@@ -4053,8 +4070,8 @@ var TestPrevisualizerElement = class extends HTMLElement {
4053
4070
  }
4054
4071
  }
4055
4072
  };
4056
- if (!customElements.get("test-previsualizer")) {
4057
- customElements.define("test-previsualizer", TestPrevisualizerElement);
4073
+ if (!customElements.get("lib-e2e-test-previsualizer")) {
4074
+ customElements.define("lib-e2e-test-previsualizer", TestPrevisualizerElement);
4058
4075
  }
4059
4076
 
4060
4077
  // src/components/save-test/save-test.styles.ts
@@ -4118,10 +4135,21 @@ var SAVE_TEST_STYLES = `
4118
4135
  transition: color 0.12s; padding: 0; background: none; border: none;
4119
4136
  }
4120
4137
  .chip-del:hover { color: #f85149; filter: none; }
4138
+ .discard-warn { color: #e6edf3; font-size: 13px; margin-bottom: 20px; line-height: 1.5; }
4121
4139
  `;
4122
4140
 
4123
4141
  // src/components/save-test/save-test.template.ts
4124
4142
  init_html_utils();
4143
+ function renderSaveTestConfirmDiscard(t) {
4144
+ return `
4145
+ <div class="container">
4146
+ <p class="discard-warn">\u26A0 ${t("SAVE_TEST.CONFIRM_DISCARD")}</p>
4147
+ <div class="btn-row">
4148
+ <button class="btn-danger" id="btn-confirm-discard">${t("SAVE_TEST.CONFIRM_DISCARD_BTN")}</button>
4149
+ <button id="btn-back-discard">${t("SAVE_TEST.BACK_BTN")}</button>
4150
+ </div>
4151
+ </div>`;
4152
+ }
4125
4153
  function renderSaveTestAsk(t) {
4126
4154
  return `
4127
4155
  <div class="container">
@@ -4132,11 +4160,15 @@ function renderSaveTestAsk(t) {
4132
4160
  </div>
4133
4161
  </div>`;
4134
4162
  }
4135
- function renderSaveTestDesc(description, notes, tags, ticket, t) {
4163
+ function renderSaveTestDesc(description, notes, tags, ticketId, issueTrackerEnabled, ticketWarn, t) {
4136
4164
  const chipsHtml = tags.map(
4137
4165
  (tag) => `<span class="chip">${escHtml(tag)}<button class="chip-del" data-tag="${escAttr(tag)}" title="${t("SAVE_TEST.REMOVE_TAG_TITLE")}">\u2715</button></span>`
4138
4166
  ).join("");
4139
- const ticketWarning = ticket.invalid ? `<div class="ticket-warning" style="color:#d29922;font-size:11px;margin-top:2px">${t("SAVE_TEST.TICKET_INVALID")}</div>` : "";
4167
+ const ticketHtml = issueTrackerEnabled ? `
4168
+ <span class="tag-label">${t("SAVE_TEST.TICKET_LABEL")}</span>
4169
+ <input id="ticket-input" type="text" placeholder="${escAttr(t("SAVE_TEST.TICKET_PLACEHOLDER"))}"
4170
+ value="${escAttr(ticketId)}" autocomplete="off" />
4171
+ ${ticketWarn ? `<span class="ticket-warn">\u26A0 ${t("SAVE_TEST.TICKET_WARN")}</span>` : ""}` : "";
4140
4172
  return `
4141
4173
  <div class="container">
4142
4174
  <p>${t("SAVE_TEST.DESC_LABEL")} (<code>it()</code>):</p>
@@ -4144,16 +4176,13 @@ function renderSaveTestDesc(description, notes, tags, ticket, t) {
4144
4176
  value="${escAttr(description)}" autocomplete="off" />
4145
4177
  <span class="tag-label">${t("SAVE_TEST.NOTES_LABEL")}</span>
4146
4178
  <textarea id="notes-input" rows="3" placeholder="${escAttr(t("SAVE_TEST.NOTES_PLACEHOLDER"))}">${escHtml(notes)}</textarea>
4147
- <span class="tag-label">${t("SAVE_TEST.TICKET_LABEL")}</span>
4148
- <input id="ticket-input" type="text" placeholder="${escAttr(t(ticket.placeholderKey))}"
4149
- value="${escAttr(ticket.id)}" autocomplete="off" />
4150
- ${ticketWarning}
4151
4179
  <span class="tag-label">${t("SAVE_TEST.TAGS_LABEL")}</span>
4152
4180
  <div class="tag-input-row">
4153
4181
  <input id="tag-input" type="text" placeholder="${t("SAVE_TEST.TAGS_PLACEHOLDER")}" autocomplete="off" />
4154
4182
  <button class="btn-tag-add" id="btn-add-tag">${t("SAVE_TEST.ADD_TAG")}</button>
4155
4183
  </div>
4156
4184
  <div class="chips" id="chips-container">${chipsHtml || `<span style="color:#484f58;font-size:11px">${t("SAVE_TEST.NO_TAGS")}</span>`}</div>
4185
+ ${ticketHtml}
4157
4186
  <div class="btn-row">
4158
4187
  <button class="btn-primary" id="btn-confirm">${t("SAVE_TEST.SAVE_BTN")}</button>
4159
4188
  <button class="btn-success" id="btn-export">${t("SAVE_TEST.SAVE_AND_EDIT")}</button>
@@ -4166,12 +4195,12 @@ function renderSaveTestDesc(description, notes, tags, ticket, t) {
4166
4195
  var SaveTestElement = class extends HTMLElement {
4167
4196
  shadow;
4168
4197
  _step = "ask";
4198
+ _stepBeforeDiscard = "ask";
4169
4199
  description = "";
4170
4200
  notes = "";
4171
4201
  tags = [];
4172
4202
  ticketId = "";
4173
- /** Set by the host from config so the ticket placeholder/validation is provider-aware. */
4174
- issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
4203
+ issueTrackerConfig = DEFAULT_ISSUE_TRACKER_CONFIG;
4175
4204
  translation = translationService;
4176
4205
  constructor() {
4177
4206
  super();
@@ -4188,13 +4217,22 @@ var SaveTestElement = class extends HTMLElement {
4188
4217
  this.render();
4189
4218
  }
4190
4219
  confirmSave() {
4191
- this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() || null });
4220
+ this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() });
4192
4221
  }
4193
4222
  confirmSaveAndExport() {
4194
- this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() || null });
4223
+ this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() });
4195
4224
  }
4196
4225
  cancel() {
4197
- this.dispatch("savetest", { description: null, notes: "", tags: [], ticketId: null });
4226
+ this._stepBeforeDiscard = this._step === "confirm-discard" ? "ask" : this._step;
4227
+ this._step = "confirm-discard";
4228
+ this.render();
4229
+ }
4230
+ confirmDiscard() {
4231
+ this.dispatch("savetest", { description: null, notes: "", tags: [], ticketId: "" });
4232
+ }
4233
+ backFromDiscard() {
4234
+ this._step = this._stepBeforeDiscard;
4235
+ this.render();
4198
4236
  }
4199
4237
  restartComponent() {
4200
4238
  this._step = "ask";
@@ -4218,44 +4256,37 @@ var SaveTestElement = class extends HTMLElement {
4218
4256
  t(key) {
4219
4257
  return this.translation.translate(key);
4220
4258
  }
4221
- /** Builds the ticket field view: provider-aware placeholder + soft (non-blocking) format check. */
4222
- ticketView() {
4223
- const provider = findIssueTrackerProvider(this.issueTrackerProviderId) ?? ISSUE_TRACKER_PROVIDERS[0];
4224
- const trimmed = this.ticketId.trim();
4225
- let invalid = false;
4226
- if (trimmed) {
4227
- try {
4228
- invalid = !new RegExp(provider.idPattern).test(trimmed);
4229
- } catch {
4230
- invalid = false;
4231
- }
4232
- }
4233
- return { id: this.ticketId, placeholderKey: provider.idPlaceholderKey, invalid };
4259
+ get ticketIdWarning() {
4260
+ if (!this.ticketId.trim() || !this.issueTrackerConfig.enabled) return false;
4261
+ return !isValidTicketId(this.ticketId, this.issueTrackerConfig.provider);
4234
4262
  }
4235
4263
  dispatch(type, detail) {
4236
4264
  this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
4237
4265
  }
4238
4266
  render() {
4267
+ if (this._step === "confirm-discard") {
4268
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestConfirmDiscard(this.t.bind(this))}`;
4269
+ this.shadow.getElementById("btn-confirm-discard")?.addEventListener("click", () => this.confirmDiscard());
4270
+ this.shadow.getElementById("btn-back-discard")?.addEventListener("click", () => this.backFromDiscard());
4271
+ return;
4272
+ }
4239
4273
  if (this._step === "ask") {
4240
4274
  this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestAsk(this.t.bind(this))}`;
4241
4275
  this.shadow.getElementById("btn-yes")?.addEventListener("click", () => this.askSave());
4242
4276
  this.shadow.getElementById("btn-no")?.addEventListener("click", () => this.cancel());
4243
4277
  } else {
4244
- this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.ticketView(), this.t.bind(this))}`;
4278
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.ticketId, this.issueTrackerConfig.enabled, this.ticketIdWarning, this.t.bind(this))}`;
4245
4279
  const descInput = this.shadow.getElementById("desc-input");
4246
4280
  const notesInput = this.shadow.getElementById("notes-input");
4247
- const ticketInput = this.shadow.getElementById("ticket-input");
4248
4281
  const tagInput = this.shadow.getElementById("tag-input");
4282
+ const ticketInput = this.shadow.getElementById("ticket-input");
4249
4283
  descInput.addEventListener("input", () => {
4250
4284
  this.description = descInput.value;
4251
4285
  });
4252
4286
  notesInput.addEventListener("input", () => {
4253
4287
  this.notes = notesInput.value;
4254
4288
  });
4255
- ticketInput.addEventListener("input", () => {
4256
- this.ticketId = ticketInput.value;
4257
- });
4258
- ticketInput.addEventListener("change", () => {
4289
+ ticketInput?.addEventListener("input", () => {
4259
4290
  this.ticketId = ticketInput.value;
4260
4291
  this.render();
4261
4292
  });
@@ -4280,10 +4311,13 @@ var SaveTestElement = class extends HTMLElement {
4280
4311
  }
4281
4312
  }
4282
4313
  };
4283
- if (!customElements.get("save-test")) {
4284
- customElements.define("save-test", SaveTestElement);
4314
+ if (!customElements.get("lib-e2e-save-test")) {
4315
+ customElements.define("lib-e2e-save-test", SaveTestElement);
4285
4316
  }
4286
4317
 
4318
+ // src/components/test-editor/test-editor.ts
4319
+ init_code_format_utils();
4320
+
4287
4321
  // src/components/test-editor/test-editor.styles.ts
4288
4322
  var TEST_EDITOR_STYLES = `
4289
4323
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -4350,6 +4384,18 @@ var TEST_EDITOR_STYLES = `
4350
4384
  padding: 1px 7px; border-radius: 20px; font-size: 10px;
4351
4385
  background: rgba(47,129,247,0.1); color: #8b949e; border: 1px solid rgba(47,129,247,0.2);
4352
4386
  }
4387
+ .ticket-badge, .ticket-link {
4388
+ padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 500;
4389
+ background: rgba(210,153,34,0.12); color: #d29922; border: 1px solid rgba(210,153,34,0.3);
4390
+ white-space: nowrap; flex-shrink: 0;
4391
+ }
4392
+ .ticket-link {
4393
+ text-decoration: none; cursor: pointer;
4394
+ transition: background 0.12s, border-color 0.12s;
4395
+ }
4396
+ .ticket-link::before { content: '\u{1F3AB} '; font-size: 9px; }
4397
+ .ticket-link:hover { background: rgba(210,153,34,0.22); border-color: rgba(210,153,34,0.55); color: #e3b341; }
4398
+ .ticket-badge::before { content: '\u{1F3AB} '; font-size: 9px; }
4353
4399
  .btn-icon {
4354
4400
  padding: 3px 8px; border: none; border-radius: 6px; cursor: pointer;
4355
4401
  font-size: 11px; background: #21262d; color: #8b949e;
@@ -4381,6 +4427,13 @@ var TEST_EDITOR_STYLES = `
4381
4427
  .code-preview::-webkit-scrollbar { height: 4px; }
4382
4428
  .code-preview::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
4383
4429
  .code-preview-icp { margin-top: 8px; }
4430
+ .ticket-group { margin-bottom: 8px; }
4431
+ .ticket-group-header {
4432
+ padding: 4px 10px; margin-bottom: 4px;
4433
+ font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.7px;
4434
+ color: #d29922; border-left: 3px solid rgba(210,153,34,0.5);
4435
+ background: rgba(210,153,34,0.06); border-radius: 0 4px 4px 0;
4436
+ }
4384
4437
  .sh-kw { color: #ff7b72; }
4385
4438
  .sh-bi { color: #d2a8ff; }
4386
4439
  .sh-str { color: #a5d6ff; }
@@ -4487,8 +4540,50 @@ function highlightLine(line) {
4487
4540
  }
4488
4541
 
4489
4542
  // src/components/test-editor/test-editor.template.ts
4543
+ init_code_format_utils();
4544
+ function renderTestRow(test, i, state, t) {
4545
+ const { selectMode, selectedIds, expandedIndex, interceptorsByTest, locale, issueTrackerConfig } = state;
4546
+ const expanded = expandedIndex === i;
4547
+ const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
4548
+ const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
4549
+ const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
4550
+ const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
4551
+ const hasIcps = (test.interceptors ?? []).length > 0;
4552
+ const ticketId = test.ticketId;
4553
+ const ticketComment = ticketId ? buildTicketComment(ticketId) + "\n" : "";
4554
+ const itBlockCode = `${ticketComment}it('${escapeSingleQuotes(test.name)}', () => {
4555
+ ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
4556
+ });`;
4557
+ const icpBlockCode = icps.length ? `beforeEach(() => {
4558
+ // Auto-generated Cypress interceptors
4559
+ ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
4560
+ });` : "";
4561
+ const notesHtml = expanded && test.notes ? `<p class="test-notes">${escHtml(test.notes)}</p>` : "";
4562
+ const ticketUrl = ticketId ? buildTicketUrl(ticketId, issueTrackerConfig) : null;
4563
+ const ticketHtml = ticketId ? ticketUrl ? `<a class="ticket-link" href="${escAttr(ticketUrl)}" target="_blank" rel="noopener noreferrer" title="${t("TEST_EDITOR.TICKET_LINK_TITLE")}">${escHtml(ticketId)}</a>` : `<span class="ticket-badge">${escHtml(ticketId)}</span>` : "";
4564
+ const body = expanded ? `
4565
+ <div class="row-body">
4566
+ ${notesHtml}
4567
+ <pre class="code-preview">${syntaxHighlight(itBlockCode)}</pre>
4568
+ ${icpBlockCode ? `<pre class="code-preview code-preview-icp">${syntaxHighlight(icpBlockCode)}</pre>` : ""}
4569
+ </div>` : "";
4570
+ return `
4571
+ <div class="row${selectedIds.has(test.id) ? " selected-row" : ""}">
4572
+ <div class="row-header" data-action="expand" data-idx="${i}">
4573
+ ${checkbox}
4574
+ <span class="test-name">${escHtml(test.name)}</span>
4575
+ ${tagsHtml}
4576
+ ${ticketHtml}
4577
+ <span class="test-date">${date}</span>
4578
+ <button class="btn-icon" data-action="copy-cmds" data-idx="${i}" title="${t("TEST_EDITOR.COPY_CMDS_BTN")}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
4579
+ ${hasIcps ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}" title="${t("TEST_EDITOR.COPY_ICPS_BTN")}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
4580
+ <button class="btn-icon btn-del" data-action="delete" data-id="${test.id}" title="${t("TEST_EDITOR.DELETE_TITLE")}">\u{1F5D1}</button>
4581
+ </div>
4582
+ ${body}
4583
+ </div>`;
4584
+ }
4490
4585
  function renderTestEditor(state, t) {
4491
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, groupByTicket, ticketUrlByTest, locale } = state;
4586
+ const { tags, visible, selectedVisible, activeTag, selectMode, describeName, groupByTicket } = state;
4492
4587
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
4493
4588
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
4494
4589
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -4497,80 +4592,43 @@ function renderTestEditor(state, t) {
4497
4592
  <input id="describe-name" type="text" placeholder="${t("TEST_EDITOR.DESCRIBE_PLACEHOLDER")}" value="${escAttr(describeName)}" />
4498
4593
  <button class="btn-gen-describe" id="btn-gen-describe">${t("TEST_EDITOR.COPY_DESCRIBE")}</button>
4499
4594
  </div>` : "";
4500
- const renderRow = (test, i) => {
4501
- const expanded = expandedIndex === i;
4502
- const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
4503
- const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
4504
- const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
4505
- const ticketId = (test.ticketId ?? "").trim();
4506
- const ticketUrl = ticketUrlByTest[test.id] ?? null;
4507
- const ticketHtml = !ticketId ? "" : ticketUrl ? `<a class="test-ticket" data-ticket-link href="${escAttr(ticketUrl)}" target="_blank" rel="noopener noreferrer" style="color:#58a6ff;font-size:11px;text-decoration:none">\u{1F3AB} ${escHtml(ticketId)}</a>` : `<span class="test-ticket" style="color:#8b949e;font-size:11px">\u{1F3AB} ${escHtml(ticketId)}</span>`;
4508
- const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
4509
- const hasIcps = (test.interceptors ?? []).length > 0;
4510
- const ticketComment = ticketId ? `${buildTicketComment(ticketId, ticketUrl)}
4511
- ` : "";
4512
- const itBlockCode = `${ticketComment}it('${escapeSingleQuotes(test.name)}', () => {
4513
- ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
4514
- });`;
4515
- const icpBlockCode = icps.length ? `beforeEach(() => {
4516
- // Auto-generated Cypress interceptors
4517
- ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
4518
- });` : "";
4519
- const notesHtml = expanded && test.notes ? `<p class="test-notes">${escHtml(test.notes)}</p>` : "";
4520
- const body = expanded ? `
4521
- <div class="row-body">
4522
- ${notesHtml}
4523
- <pre class="code-preview">${syntaxHighlight(itBlockCode)}</pre>
4524
- ${icpBlockCode ? `<pre class="code-preview code-preview-icp">${syntaxHighlight(icpBlockCode)}</pre>` : ""}
4525
- </div>` : "";
4526
- return `
4527
- <div class="row${selectedIds.has(test.id) ? " selected-row" : ""}">
4528
- <div class="row-header" data-action="expand" data-idx="${i}">
4529
- ${checkbox}
4530
- <span class="test-name">${escHtml(test.name)}</span>
4531
- ${tagsHtml}
4532
- ${ticketHtml}
4533
- <span class="test-date">${date}</span>
4534
- <button class="btn-icon" data-action="copy-cmds" data-idx="${i}" title="${t("TEST_EDITOR.COPY_CMDS_BTN")}">${t("TEST_EDITOR.COPY_CMDS_BTN")}</button>
4535
- ${hasIcps ? `<button class="btn-icon" data-action="copy-icps" data-idx="${i}" title="${t("TEST_EDITOR.COPY_ICPS_BTN")}">${t("TEST_EDITOR.COPY_ICPS_BTN")}</button>` : ""}
4536
- <button class="btn-icon btn-del" data-action="delete" data-id="${test.id}" title="${t("TEST_EDITOR.DELETE_TITLE")}">\u{1F5D1}</button>
4537
- </div>
4538
- ${body}
4539
- </div>`;
4540
- };
4541
- const indexed = visible.map((test, i) => ({ test, i }));
4542
- const groupedRows = () => {
4595
+ let rows;
4596
+ if (groupByTicket) {
4543
4597
  const groups = /* @__PURE__ */ new Map();
4544
- for (const item of indexed) {
4545
- const key = (item.test.ticketId ?? "").trim();
4546
- const bucket = groups.get(key);
4547
- if (bucket) bucket.push(item);
4548
- else groups.set(key, [item]);
4598
+ for (const test of visible) {
4599
+ const key = test.ticketId?.trim() || "";
4600
+ const group = groups.get(key) ?? [];
4601
+ group.push(test);
4602
+ groups.set(key, group);
4549
4603
  }
4550
- const keys = [...groups.keys()].sort((a, b) => a === "" ? 1 : b === "" ? -1 : a.localeCompare(b));
4551
- return keys.map((key) => {
4552
- const items = groups.get(key) ?? [];
4553
- const label = key || t("TEST_EDITOR.NO_TICKET_GROUP");
4554
- const url = key ? ticketUrlByTest[items[0].test.id] ?? null : null;
4555
- const header = url ? `<a data-ticket-link href="${escAttr(url)}" target="_blank" rel="noopener noreferrer" style="color:#58a6ff;text-decoration:none">\u{1F3AB} ${escHtml(label)}</a>` : `\u{1F3AB} ${escHtml(label)}`;
4556
- return `<div class="ticket-group">
4557
- <div class="ticket-group-hd" style="padding:6px 10px;color:#8b949e;font-size:12px;font-weight:600;border-top:1px solid #21262d">${header}</div>
4558
- ${items.map(({ test, i }) => renderRow(test, i)).join("")}
4559
- </div>`;
4604
+ const sortedKeys = [...groups.keys()].sort(
4605
+ (a, b) => a === "" ? 1 : b === "" ? -1 : a.localeCompare(b)
4606
+ );
4607
+ rows = sortedKeys.map((key) => {
4608
+ const group = groups.get(key);
4609
+ const header = `<div class="ticket-group-header">${key ? escHtml(key) : t("TEST_EDITOR.NO_TICKET")}</div>`;
4610
+ const groupRows = group.map((test) => {
4611
+ const i = visible.indexOf(test);
4612
+ return renderTestRow(test, i, state, t);
4613
+ }).join("");
4614
+ return `<div class="ticket-group">${header}${groupRows}</div>`;
4560
4615
  }).join("");
4561
- };
4562
- const listBody = !visible.length ? `<div class="empty">${t("TEST_EDITOR.NO_TESTS")}</div>` : groupByTicket ? groupedRows() : indexed.map(({ test, i }) => renderRow(test, i)).join("");
4616
+ } else {
4617
+ rows = visible.map((test, i) => renderTestRow(test, i, state, t)).join("");
4618
+ }
4563
4619
  return `
4564
4620
  <div class="toolbar">
4565
4621
  ${tagFilterHtml}
4566
- <button class="btn-select${groupByTicket ? " active" : ""}" id="btn-group-ticket">${t("TEST_EDITOR.GROUP_BY_TICKET")}</button>
4622
+ <button class="btn-select${groupByTicket ? " active" : ""}" id="btn-group-ticket">
4623
+ ${groupByTicket ? t("TEST_EDITOR.UNGROUP_TICKET") : t("TEST_EDITOR.GROUP_BY_TICKET")}
4624
+ </button>
4567
4625
  <button class="btn-select${selectMode ? " active" : ""}" id="btn-select-mode">
4568
4626
  ${selectMode ? t("TEST_EDITOR.CANCEL_SELECT") : t("TEST_EDITOR.MULTI_SELECT")}
4569
4627
  </button>
4570
4628
  </div>
4571
4629
  ${describeBarHtml}
4572
4630
  <div class="list">
4573
- ${listBody}
4631
+ ${visible.length ? rows : `<div class="empty">${t("TEST_EDITOR.NO_TESTS")}</div>`}
4574
4632
  </div>`;
4575
4633
  }
4576
4634
 
@@ -4587,8 +4645,7 @@ var TestEditorElement = class extends HTMLElement {
4587
4645
  selectedIds = /* @__PURE__ */ new Set();
4588
4646
  describeName = "";
4589
4647
  groupByTicket = false;
4590
- issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
4591
- issueTrackerParams = {};
4648
+ issueTrackerConfig = { ...DEFAULT_ISSUE_TRACKER_CONFIG };
4592
4649
  constructor() {
4593
4650
  super();
4594
4651
  this.shadow = this.attachShadow({ mode: "open" });
@@ -4598,17 +4655,7 @@ var TestEditorElement = class extends HTMLElement {
4598
4655
  this.loadTests();
4599
4656
  }
4600
4657
  async loadTests() {
4601
- const [tests, config] = await Promise.all([
4602
- this.persistence.getAllTests(),
4603
- this.persistence.getGeneralConfig()
4604
- ]);
4605
- this.tests = tests;
4606
- this.issueTrackerProviderId = config?.["issueTrackerProviderId"] ?? DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
4607
- this.issueTrackerParams = config?.["issueTrackerParams"] ?? {};
4608
- this.render();
4609
- }
4610
- toggleGroupByTicket() {
4611
- this.groupByTicket = !this.groupByTicket;
4658
+ this.tests = await this.persistence.getAllTests();
4612
4659
  this.render();
4613
4660
  }
4614
4661
  async deleteTest(id) {
@@ -4629,6 +4676,10 @@ var TestEditorElement = class extends HTMLElement {
4629
4676
  hasInterceptors(testId) {
4630
4677
  return Array.isArray(this.interceptorsByTest[testId]) && this.interceptorsByTest[testId].length > 0;
4631
4678
  }
4679
+ toggleGroupByTicket() {
4680
+ this.groupByTicket = !this.groupByTicket;
4681
+ this.render();
4682
+ }
4632
4683
  toggleSelectMode() {
4633
4684
  this.selectMode = !this.selectMode;
4634
4685
  if (!this.selectMode) this.selectedIds.clear();
@@ -4683,12 +4734,6 @@ ${beforeEach}${itBlocks}
4683
4734
  const tags = this.allTags;
4684
4735
  const visible = this.visibleTests;
4685
4736
  const selectedVisible = visible.filter((t) => this.selectedIds.has(t.id));
4686
- const ticketUrlByTest = {};
4687
- for (const test of visible) {
4688
- if (test.ticketId) {
4689
- ticketUrlByTest[test.id] = resolveTicketUrl(this.issueTrackerProviderId, this.issueTrackerParams, test.ticketId);
4690
- }
4691
- }
4692
4737
  this.shadow.innerHTML = `<style>${TEST_EDITOR_STYLES}</style>${renderTestEditor({
4693
4738
  tags,
4694
4739
  visible,
@@ -4699,15 +4744,12 @@ ${beforeEach}${itBlocks}
4699
4744
  describeName: this.describeName,
4700
4745
  expandedIndex: this.expandedIndex,
4701
4746
  interceptorsByTest: this.interceptorsByTest,
4747
+ locale: localeForLang(this.translation.getLang()),
4702
4748
  groupByTicket: this.groupByTicket,
4703
- ticketUrlByTest,
4704
- locale: localeForLang(this.translation.getLang())
4749
+ issueTrackerConfig: this.issueTrackerConfig
4705
4750
  }, this.t.bind(this))}`;
4706
- this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
4707
4751
  this.shadow.getElementById("btn-group-ticket")?.addEventListener("click", () => this.toggleGroupByTicket());
4708
- this.shadow.querySelectorAll("[data-ticket-link]").forEach(
4709
- (el) => el.addEventListener("click", (ev) => ev.stopPropagation())
4710
- );
4752
+ this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
4711
4753
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
4712
4754
  el.addEventListener("click", () => {
4713
4755
  const tag = el.dataset["filterTag"] ?? "";
@@ -4765,32 +4807,99 @@ ${beforeEach}${itBlocks}
4765
4807
  });
4766
4808
  }
4767
4809
  };
4768
- if (!customElements.get("test-editor")) {
4769
- customElements.define("test-editor", TestEditorElement);
4810
+ if (!customElements.get("lib-e2e-test-editor")) {
4811
+ customElements.define("lib-e2e-test-editor", TestEditorElement);
4812
+ }
4813
+
4814
+ // src/utils/login-setup.utils.ts
4815
+ var EXPORT_FN_RE = /^(?!\/\/).*\bexport\s+(?:async\s+)?function\s+(\w+)|^(?!\/\/).*\bexport\s+const\s+(\w+)\s*=\s*(?:async\s*)?\(/gm;
4816
+ function extractExportedFunctions(fileContent) {
4817
+ const names = [];
4818
+ let match;
4819
+ const re = new RegExp(EXPORT_FN_RE.source, EXPORT_FN_RE.flags);
4820
+ while ((match = re.exec(fileContent)) !== null) {
4821
+ const name = match[1] ?? match[2];
4822
+ if (name) names.push(name);
4823
+ }
4824
+ return names;
4825
+ }
4826
+ function buildLoginScaffold() {
4827
+ return [
4828
+ "/**",
4829
+ " * Login service scaffold \u2014 fill in your own auth logic.",
4830
+ " * This file is not committed to source control.",
4831
+ " */",
4832
+ "",
4833
+ "export function fetchAuthToken() {",
4834
+ " // TODO: implement \u2014 retrieve and return your auth token",
4835
+ "}",
4836
+ "",
4837
+ "export function setupRequestInterceptors() {",
4838
+ " // TODO: implement \u2014 configure cy.intercept() or set localStorage/cookies",
4839
+ "}",
4840
+ ""
4841
+ ].join("\n");
4842
+ }
4843
+ function buildLoginImportPath(fromFilePath, toServiceFilePath) {
4844
+ const fromParts = fromFilePath.replace(/\\/g, "/").split("/");
4845
+ const toParts = toServiceFilePath.replace(/\\/g, "/").split("/");
4846
+ fromParts.pop();
4847
+ let common = 0;
4848
+ while (common < fromParts.length && common < toParts.length - 1 && fromParts[common] === toParts[common]) {
4849
+ common++;
4850
+ }
4851
+ const ups = fromParts.length - common;
4852
+ const downs = toParts.slice(common);
4853
+ const serviceFile = downs[downs.length - 1].replace(/\.ts$/, "");
4854
+ const prefix = ups === 0 ? "." : Array(ups).fill("..").join("/");
4855
+ const middle = downs.slice(0, -1);
4856
+ return [prefix, ...middle, serviceFile].join("/");
4857
+ }
4858
+ function hasLoginBlocks(content, fnNames) {
4859
+ return fnNames.some((fn) => new RegExp(`\\b${fn}\\s*\\(`).test(content));
4860
+ }
4861
+ function injectLoginBlocksIntoExisting(content, importLine, beforeBlock, beforeEachBlock) {
4862
+ let result = content;
4863
+ if (importLine && !content.includes(importLine)) {
4864
+ result = `${importLine}
4865
+
4866
+ ${result}`;
4867
+ }
4868
+ const blocks = [beforeBlock, beforeEachBlock].filter(Boolean).join("");
4869
+ if (blocks) {
4870
+ const describeMatch = result.match(/(describe\s*\(.*?{)/s);
4871
+ if (describeMatch) {
4872
+ const insertPos = (describeMatch.index ?? 0) + describeMatch[0].length;
4873
+ result = result.slice(0, insertPos) + "\n" + blocks + result.slice(insertPos);
4874
+ }
4875
+ }
4876
+ return result;
4877
+ }
4878
+ function buildLoginBlocks(importPath, beforeFn, beforeEachFn) {
4879
+ if (!beforeFn && !beforeEachFn) {
4880
+ return { importLine: "", beforeBlock: "", beforeEachBlock: "" };
4881
+ }
4882
+ const fns = [...new Set([beforeFn, beforeEachFn].filter(Boolean))];
4883
+ const importLine = `import { ${fns.join(", ")} } from '${importPath}';`;
4884
+ const beforeBlock = beforeFn ? ` before(() => { ${beforeFn}(); });
4885
+ ` : "";
4886
+ const beforeEachBlock = beforeEachFn ? ` beforeEach(() => { ${beforeEachFn}(); });
4887
+ ` : "";
4888
+ return { importLine, beforeBlock, beforeEachBlock };
4770
4889
  }
4771
4890
 
4772
4891
  // src/components/configuration/configuration.styles.ts
4773
4892
  var CONFIGURATION_STYLES = `
4774
- :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; }
4893
+ :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; max-height: 70vh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #30363d transparent; }
4775
4894
  * { box-sizing: border-box; }
4776
4895
 
4777
4896
  /* \u2500\u2500 Grid container \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\u2500\u2500 */
4778
- /* The SweetAlert2 host clips overflow (overflow:hidden), so the grid owns its
4779
- own scroll \u2014 otherwise the last cards fall below the fold with no scrollbar. */
4780
4897
  .cfg-grid {
4781
4898
  display: grid;
4782
4899
  grid-template-columns: 1fr 1fr;
4783
4900
  gap: 10px;
4784
4901
  padding: 14px;
4785
- max-height: 72vh;
4786
- overflow-y: auto;
4787
- scrollbar-width: thin;
4788
- scrollbar-color: #30363d transparent;
4789
4902
  }
4790
- .cfg-grid::-webkit-scrollbar { width: 8px; }
4791
- .cfg-grid::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
4792
- .cfg-grid::-webkit-scrollbar-thumb:hover { background: #484f58; }
4793
- .cfg-grid::-webkit-scrollbar-track { background: transparent; }
4794
4903
 
4795
4904
  /* \u2500\u2500 Cards \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4796
4905
  .card {
@@ -4937,6 +5046,99 @@ var CONFIGURATION_STYLES = `
4937
5046
  .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
4938
5047
  .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
4939
5048
  .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
5049
+
5050
+ /* \u2500\u2500 Form-element font reset \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 */
5051
+ input, select, button, textarea { font-family: inherit; }
5052
+
5053
+ /* \u2500\u2500 Primary action button \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 */
5054
+ .btn-primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
5055
+ .btn-primary:hover { background: #388bfd; border-color: #388bfd; color: #fff; }
5056
+ .btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
5057
+
5058
+ /* \u2500\u2500 Login Setup overlay \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\u2500 */
5059
+ .ls-modal { max-width: 500px; }
5060
+ .ls-hd { display: flex; align-items: center; gap: 8px; }
5061
+ .ls-hd-icon { font-size: 15px; }
5062
+
5063
+ .ls-body {
5064
+ padding: 20px; overflow-y: auto; flex: 1;
5065
+ display: flex; flex-direction: column; gap: 20px;
5066
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
5067
+ }
5068
+
5069
+ .ls-section { display: flex; flex-direction: column; gap: 8px; }
5070
+ .ls-section-label {
5071
+ font-size: 10px; font-weight: 700; text-transform: uppercase;
5072
+ letter-spacing: 0.8px; color: #484f58;
5073
+ }
5074
+
5075
+ .ls-actions { display: flex; gap: 10px; }
5076
+ .ls-action-btn {
5077
+ flex: 1; padding: 14px 12px; border-radius: 8px;
5078
+ background: #0d1117; border: 1px solid #21262d; color: #8b949e;
5079
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
5080
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
5081
+ }
5082
+ .ls-action-btn:hover { background: #161b22; color: #e6edf3; border-color: #388bfd; }
5083
+ .ls-action-icon { font-size: 22px; line-height: 1; }
5084
+ .ls-action-label { font-size: 12px; font-weight: 600; color: #c9d1d9; }
5085
+ .ls-action-btn:hover .ls-action-label { color: #e6edf3; }
5086
+ .ls-action-sub { font-size: 10px; color: #484f58; line-height: 1.4; text-align: center; }
5087
+
5088
+ .ls-file-row {
5089
+ display: flex; align-items: center; gap: 10px;
5090
+ padding: 10px 12px; background: #0d1117;
5091
+ border: 1px solid #21262d; border-radius: 8px;
5092
+ }
5093
+ .ls-file-icon { font-size: 14px; flex-shrink: 0; }
5094
+ .ls-file-name {
5095
+ flex: 1; font-size: 11px; color: #c9d1d9;
5096
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
5097
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
5098
+ min-width: 0;
5099
+ }
5100
+ .ls-btn-rescan {
5101
+ flex-shrink: 0; padding: 4px 10px; font-size: 11px;
5102
+ background: #161b22; border-color: #30363d; color: #8b949e;
5103
+ }
5104
+ .ls-btn-rescan:hover { background: #21262d; color: #e6edf3; }
5105
+
5106
+ .ls-input {
5107
+ width: 100%; padding: 8px 10px; background: #0d1117;
5108
+ border: 1px solid #30363d; border-radius: 6px;
5109
+ color: #c9d1d9; font-size: 12px; outline: none; font-family: inherit;
5110
+ }
5111
+ .ls-input:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.12); }
5112
+
5113
+ .ls-fns {
5114
+ display: flex; flex-wrap: wrap; gap: 6px;
5115
+ padding: 10px; background: #0d1117;
5116
+ border: 1px solid #21262d; border-radius: 6px; min-height: 38px;
5117
+ }
5118
+ .ls-fn-chip {
5119
+ padding: 3px 10px; font-size: 11px; color: #79c0ff;
5120
+ background: rgba(47,129,247,0.1); border: 1px solid rgba(47,129,247,0.2);
5121
+ border-radius: 12px; font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
5122
+ }
5123
+
5124
+ .ls-field-block { display: flex; flex-direction: column; gap: 6px; }
5125
+ .ls-field-block + .ls-field-block { margin-top: 12px; }
5126
+ .ls-label { font-size: 11px; color: #8b949e; }
5127
+ .ls-select {
5128
+ width: 100%; padding: 8px 10px; background: #0d1117;
5129
+ border: 1px solid #30363d; border-radius: 6px;
5130
+ color: #c9d1d9; font-size: 12px; outline: none; cursor: pointer;
5131
+ }
5132
+ .ls-select:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.12); }
5133
+
5134
+ .ls-empty-state {
5135
+ display: flex; align-items: center; gap: 10px;
5136
+ padding: 14px; background: #0d1117; border: 1px solid #21262d;
5137
+ border-radius: 8px; font-size: 12px; color: #484f58;
5138
+ }
5139
+ .ls-empty-icon { font-size: 16px; }
5140
+
5141
+ .ls-ft { padding: 14px 20px; }
4940
5142
  `;
4941
5143
 
4942
5144
  // src/components/configuration/configuration.template.ts
@@ -5004,33 +5206,8 @@ function renderExportOverlay(state, t) {
5004
5206
  </div>
5005
5207
  </div>`;
5006
5208
  }
5007
- var TRACKER_INPUT_STYLE = "flex:1;min-width:180px;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none";
5008
- function renderIssueTrackerCard(state, t) {
5009
- const { issueTrackerProviderId, issueTrackerParams } = state;
5010
- const provider = findIssueTrackerProvider(issueTrackerProviderId) ?? ISSUE_TRACKER_PROVIDERS[0];
5011
- const values = issueTrackerParams[provider.id] ?? {};
5012
- const options = ISSUE_TRACKER_PROVIDERS.map(
5013
- (p) => `<option value="${escAttr(p.id)}" ${provider.id === p.id ? "selected" : ""}>${escHtml(t(p.labelKey))}</option>`
5014
- ).join("");
5015
- const fields = provider.fields.map((f) => `
5016
- <div class="field-row">
5017
- <span class="field-label">${escHtml(t(f.labelKey))}</span>
5018
- <input type="text" data-tracker-field="${escAttr(f.key)}" value="${escAttr(values[f.key] ?? "")}"
5019
- placeholder="${escAttr(t(f.placeholderKey))}" style="${TRACKER_INPUT_STYLE}" />
5020
- </div>`).join("");
5021
- return `
5022
- <div class="card card-wide">
5023
- <div class="card-hd">${t("CONFIG.ISSUE_TRACKER_SECTION")}</div>
5024
- <div class="field-row">
5025
- <span class="field-label">${t("CONFIG.ISSUE_TRACKER_LABEL")}</span>
5026
- <select id="issue-tracker-provider">${options}</select>
5027
- </div>
5028
- ${fields}
5029
- <div class="check-sub issue-tracker-disclaimer" style="margin-top:8px">${t("CONFIG.ISSUE_TRACKER_DISCLAIMER")}</div>
5030
- </div>`;
5031
- }
5032
5209
  function renderConfiguration(state, t) {
5033
- const { selectedLanguage, advancedHttpConfig, fixtureMode, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
5210
+ const { selectedLanguage, advancedHttpConfig, fixtureMode, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes, issueTrackerConfig } = state;
5034
5211
  const langOptions = LANGS.map(
5035
5212
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
5036
5213
  ).join("");
@@ -5151,8 +5328,40 @@ function renderConfiguration(state, t) {
5151
5328
  </div>
5152
5329
  </div>
5153
5330
 
5154
- <!-- Issue tracker (spec 014) -->
5155
- ${renderIssueTrackerCard(state, t)}
5331
+ <!-- Issue Tracker -->
5332
+ <div class="card card-wide">
5333
+ <div class="card-hd">${t("CONFIG.ISSUE_TRACKER_SECTION")}</div>
5334
+ <label class="check-row">
5335
+ <input type="checkbox" id="issue-tracker-toggle" ${issueTrackerConfig.enabled ? "checked" : ""} />
5336
+ <div>
5337
+ <div class="check-title">${t("CONFIG.ISSUE_TRACKER_TITLE")}</div>
5338
+ <div class="check-sub">${t("CONFIG.ISSUE_TRACKER_SUB")}</div>
5339
+ </div>
5340
+ </label>
5341
+ ${issueTrackerConfig.enabled ? `
5342
+ <div class="field-row" style="margin-top:10px">
5343
+ <span class="field-label">${t("CONFIG.ISSUE_TRACKER_PROVIDER")}</span>
5344
+ <select id="issue-tracker-provider">
5345
+ ${Object.values(ISSUE_TRACKER_PRESETS).map(
5346
+ (p) => `<option value="${p.id}" ${issueTrackerConfig.provider === p.id ? "selected" : ""}>${escHtml(p.label)}</option>`
5347
+ ).join("")}
5348
+ </select>
5349
+ </div>
5350
+ <div class="field-row" style="margin-top:8px">
5351
+ <span class="field-label">${t("CONFIG.ISSUE_TRACKER_BASE_URL")}</span>
5352
+ <input id="issue-tracker-base-url" type="text"
5353
+ placeholder="${escAttr(t("CONFIG.ISSUE_TRACKER_BASE_URL_PH"))}"
5354
+ value="${escAttr(issueTrackerConfig.baseUrl)}"
5355
+ style="flex:1;padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;color:#c9d1d9;font-size:12px;outline:none" />
5356
+ </div>` : ""}
5357
+ </div>
5358
+
5359
+ <!-- Login Setup -->
5360
+ <div class="card card-wide">
5361
+ <div class="card-hd">${t("CONFIG.LOGIN_SETUP_SECTION")}</div>
5362
+ ${state.loginSetupConfig ? `<p style="font-size:12px;color:#8b949e;margin:0 0 8px">${escHtml(state.loginSetupConfig.filePath)}</p>` : `<p style="font-size:12px;color:#8b949e;margin:0 0 8px">${t("CONFIG.LOGIN_SETUP_NOT_CONFIGURED")}</p>`}
5363
+ <button id="btn-open-login-setup" class="btn-sm">${t("CONFIG.LOGIN_SETUP_SET_UP_BTN")}</button>
5364
+ </div>
5156
5365
 
5157
5366
  <!-- Data -->
5158
5367
  <div class="card card-wide">
@@ -5167,7 +5376,93 @@ function renderConfiguration(state, t) {
5167
5376
  </div>
5168
5377
  </div>
5169
5378
 
5170
- </div>${renderExportOverlay(state, t)}`;
5379
+ </div>${renderExportOverlay(state, t)}${renderLoginSetupOverlay(state, t)}`;
5380
+ }
5381
+ function renderLoginSetupOverlay(state, t) {
5382
+ if (!state.isLoginSetupOpen) return "";
5383
+ const cfg = state.loginSetupConfig;
5384
+ const path = state.loginSetupDraftPath ?? cfg?.filePath ?? "";
5385
+ const fns = state.loginSetupDraftFunctions ?? cfg?.detectedFunctions ?? [];
5386
+ const beforeFn = state.loginSetupDraftBeforeFn !== void 0 ? state.loginSetupDraftBeforeFn : cfg?.beforeFn ?? null;
5387
+ const beforeEachFn = state.loginSetupDraftBeforeEachFn !== void 0 ? state.loginSetupDraftBeforeEachFn : cfg?.beforeEachFn ?? null;
5388
+ const hasContent = state.loginSetupDraftHasContent ?? !!cfg?.fileContent;
5389
+ const fnOption = (name, selected) => `<option value="${escAttr(name ?? "")}" ${selected === name ? "selected" : ""}>${name ? escHtml(name) : t("CONFIG.LOGIN_SETUP_NONE_OPTION")}</option>`;
5390
+ const fnDropdown = (id, label, selected) => `
5391
+ <div class="ls-field-block">
5392
+ <label class="ls-label" for="${id}">${escHtml(label)}</label>
5393
+ <select id="${id}" class="ls-select">
5394
+ ${fnOption(null, selected)}
5395
+ ${fns.map((fn) => fnOption(fn, selected)).join("")}
5396
+ </select>
5397
+ </div>`;
5398
+ const functionsSection = hasContent ? fns.length > 0 ? `<div class="ls-section">
5399
+ <span class="ls-section-label">Funciones detectadas</span>
5400
+ <div class="ls-fns">${fns.map((fn) => `<span class="ls-fn-chip">${escHtml(fn)}</span>`).join("")}</div>
5401
+ </div>
5402
+ <div class="ls-section">
5403
+ <span class="ls-section-label">Asignaci\xF3n de bloques</span>
5404
+ ${fnDropdown("login-setup-before-fn", t("CONFIG.LOGIN_SETUP_BEFORE_LABEL"), beforeFn)}
5405
+ ${fnDropdown("login-setup-before-each-fn", t("CONFIG.LOGIN_SETUP_BEFORE_EACH_LABEL"), beforeEachFn)}
5406
+ </div>` : `<div class="ls-empty-state">
5407
+ <span class="ls-empty-icon">\u{1F50D}</span>
5408
+ <span>${t("CONFIG.LOGIN_SETUP_NO_FUNCTIONS")}</span>
5409
+ </div>` : "";
5410
+ const fileSection = hasContent ? `<div class="ls-section">
5411
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_FILE_NAME_LABEL")}</span>
5412
+ <div class="ls-file-row">
5413
+ <span class="ls-file-icon">\u{1F4C4}</span>
5414
+ <span class="ls-file-name">${escHtml(path)}</span>
5415
+ <button id="btn-login-setup-rescan" class="ls-btn-rescan">${t("CONFIG.LOGIN_SETUP_RESCAN")}</button>
5416
+ </div>
5417
+ </div>` : `<div class="ls-section">
5418
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_FILE_NAME_LABEL")}</span>
5419
+ <input id="login-setup-filepath" type="text" class="ls-input"
5420
+ placeholder="${escAttr(t("CONFIG.LOGIN_SETUP_FILE_NAME_PH"))}"
5421
+ value="${escAttr(path)}" />
5422
+ </div>`;
5423
+ return `
5424
+ <div class="export-overlay" id="login-setup-overlay">
5425
+ <div class="export-modal ls-modal">
5426
+
5427
+ <div class="export-hd ls-hd">
5428
+ <span class="ls-hd-icon">\u{1F510}</span>
5429
+ ${t("CONFIG.LOGIN_SETUP_TITLE")}
5430
+ </div>
5431
+
5432
+ <div class="ls-body">
5433
+
5434
+ <div class="ls-section">
5435
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_SOURCE_LABEL")}</span>
5436
+ <div class="ls-actions">
5437
+ <button id="btn-login-setup-create-scaffold" class="ls-action-btn">
5438
+ <span class="ls-action-icon">\u2728</span>
5439
+ <span class="ls-action-label">${t("CONFIG.LOGIN_SETUP_CREATE_SCAFFOLD")}</span>
5440
+ <span class="ls-action-sub">${t("CONFIG.LOGIN_SETUP_CREATE_SUB")}</span>
5441
+ </button>
5442
+ <button id="btn-login-setup-pick-file" class="ls-action-btn">
5443
+ <span class="ls-action-icon">\u{1F4C2}</span>
5444
+ <span class="ls-action-label">${t("CONFIG.LOGIN_SETUP_SELECT_EXISTING")}</span>
5445
+ <span class="ls-action-sub">${t("CONFIG.LOGIN_SETUP_SELECT_SUB")}</span>
5446
+ </button>
5447
+ </div>
5448
+ </div>
5449
+
5450
+ ${fileSection}
5451
+
5452
+ ${functionsSection}
5453
+
5454
+ </div>
5455
+
5456
+ <div class="export-ft ls-ft">
5457
+ <button id="btn-login-setup-clear" class="btn-danger">${t("CONFIG.LOGIN_SETUP_CLEAR_BTN")}</button>
5458
+ <span class="export-ft-actions">
5459
+ <button id="btn-login-setup-cancel">${t("CONFIG.LOGIN_SETUP_CANCEL_BTN")}</button>
5460
+ <button id="btn-login-setup-save" class="btn-primary">${t("CONFIG.LOGIN_SETUP_SAVE_BTN")}</button>
5461
+ </span>
5462
+ </div>
5463
+
5464
+ </div>
5465
+ </div>`;
5171
5466
  }
5172
5467
 
5173
5468
  // src/components/configuration/configuration.ts
@@ -5182,13 +5477,21 @@ var ConfigurationElement = class extends HTMLElement {
5182
5477
  smartSelectorEnabled = true;
5183
5478
  startHidden = false;
5184
5479
  resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
5185
- issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
5186
- issueTrackerParams = {};
5187
5480
  isExporting = false;
5188
5481
  exportMode = "all";
5189
5482
  exportTests = [];
5190
5483
  exportSelectedIds = /* @__PURE__ */ new Set();
5191
5484
  exportSelectedTags = /* @__PURE__ */ new Set();
5485
+ issueTrackerConfig = { ...DEFAULT_ISSUE_TRACKER_CONFIG };
5486
+ isLoginSetupOpen = false;
5487
+ loginSetupConfig = null;
5488
+ loginSetupDraftPath = "";
5489
+ loginSetupDraftFunctions = [];
5490
+ loginSetupDraftBeforeFn = null;
5491
+ loginSetupDraftBeforeEachFn = null;
5492
+ loginSetupDraftHasContent = false;
5493
+ loginSetupDraftHandle = null;
5494
+ loginSetupDraftContent = "";
5192
5495
  filesystemGranted = false;
5193
5496
  cypressFolderName = null;
5194
5497
  constructor() {
@@ -5225,12 +5528,12 @@ var ConfigurationElement = class extends HTMLElement {
5225
5528
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
5226
5529
  const handle = config?.["cypressDirectoryHandle"];
5227
5530
  this.cypressFolderName = handle?.name ?? null;
5228
- if (config?.["issueTrackerProviderId"] !== void 0) {
5229
- this.issueTrackerProviderId = config["issueTrackerProviderId"];
5230
- }
5231
- if (config?.["issueTrackerParams"] !== void 0) {
5232
- this.issueTrackerParams = config["issueTrackerParams"];
5233
- }
5531
+ this.issueTrackerConfig = {
5532
+ enabled: config?.["issueTrackerEnabled"] === "true",
5533
+ provider: config?.["issueTrackerProvider"] ?? DEFAULT_ISSUE_TRACKER_CONFIG.provider,
5534
+ baseUrl: config?.["issueTrackerBaseUrl"] ?? ""
5535
+ };
5536
+ this.loginSetupConfig = await this.persistence.getLoginSetup();
5234
5537
  this.render();
5235
5538
  }
5236
5539
  async onLanguageChange(lang) {
@@ -5263,20 +5566,6 @@ var ConfigurationElement = class extends HTMLElement {
5263
5566
  await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
5264
5567
  this.render();
5265
5568
  }
5266
- /** Selects the issue-tracker provider (link-only, no sync — spec 014). */
5267
- async onIssueTrackerProviderChange(id) {
5268
- this.issueTrackerProviderId = id;
5269
- await this.persistence.setConfig({ issueTrackerProviderId: id });
5270
- this.render();
5271
- }
5272
- /** Stores a provider field value, scoped to the active provider so switching keeps values. */
5273
- async onIssueTrackerParamChange(fieldKey, value) {
5274
- const providerId = this.issueTrackerProviderId;
5275
- const current = { ...this.issueTrackerParams[providerId] ?? {} };
5276
- current[fieldKey] = value;
5277
- this.issueTrackerParams = { ...this.issueTrackerParams, [providerId]: current };
5278
- await this.persistence.setConfig({ issueTrackerParams: this.issueTrackerParams });
5279
- }
5280
5569
  onResetWidgetPosition() {
5281
5570
  this.dispatchEvent(new CustomEvent("resetwidgetposition", { bubbles: true, composed: true }));
5282
5571
  showToast(this.t("CONFIG.WIDGET_POSITION_RESET_DONE"));
@@ -5293,6 +5582,102 @@ var ConfigurationElement = class extends HTMLElement {
5293
5582
  this.dispatchEvent(new CustomEvent("selectorstrategychange", { detail: strategy, bubbles: true, composed: true }));
5294
5583
  this.render();
5295
5584
  }
5585
+ async onIssueTrackerEnabledChange(enabled) {
5586
+ this.issueTrackerConfig = { ...this.issueTrackerConfig, enabled };
5587
+ await this.persistence.setConfig({ issueTrackerEnabled: enabled ? "true" : "false" });
5588
+ this.dispatchEvent(new CustomEvent("issuetrackerchange", { detail: { ...this.issueTrackerConfig }, bubbles: true, composed: true }));
5589
+ this.render();
5590
+ }
5591
+ async onIssueTrackerProviderChange(provider) {
5592
+ this.issueTrackerConfig = { ...this.issueTrackerConfig, provider };
5593
+ await this.persistence.setConfig({ issueTrackerProvider: provider });
5594
+ this.dispatchEvent(new CustomEvent("issuetrackerchange", { detail: { ...this.issueTrackerConfig }, bubbles: true, composed: true }));
5595
+ this.render();
5596
+ }
5597
+ async onIssueTrackerBaseUrlChange(baseUrl) {
5598
+ this.issueTrackerConfig = { ...this.issueTrackerConfig, baseUrl };
5599
+ await this.persistence.setConfig({ issueTrackerBaseUrl: baseUrl });
5600
+ this.dispatchEvent(new CustomEvent("issuetrackerchange", { detail: { ...this.issueTrackerConfig }, bubbles: true, composed: true }));
5601
+ }
5602
+ openLoginSetupPanel() {
5603
+ this.isLoginSetupOpen = true;
5604
+ const cfg = this.loginSetupConfig;
5605
+ this.loginSetupDraftHandle = null;
5606
+ this.loginSetupDraftContent = cfg?.fileContent ?? "";
5607
+ this.loginSetupDraftPath = cfg?.filePath ?? "";
5608
+ this.loginSetupDraftFunctions = cfg?.detectedFunctions ?? [];
5609
+ this.loginSetupDraftBeforeFn = cfg?.beforeFn ?? null;
5610
+ this.loginSetupDraftBeforeEachFn = cfg?.beforeEachFn ?? null;
5611
+ this.loginSetupDraftHasContent = !!cfg?.fileContent;
5612
+ this.render();
5613
+ }
5614
+ closeLoginSetupPanel() {
5615
+ this.isLoginSetupOpen = false;
5616
+ this.render();
5617
+ }
5618
+ async saveLoginSetupConfig(config) {
5619
+ await this.persistence.saveLoginSetup(config);
5620
+ this.loginSetupConfig = config;
5621
+ this.isLoginSetupOpen = false;
5622
+ this.render();
5623
+ }
5624
+ async clearLoginSetupConfig() {
5625
+ await this.persistence.clearLoginSetup();
5626
+ this.loginSetupConfig = null;
5627
+ this.render();
5628
+ }
5629
+ async loginSetupPickFile() {
5630
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim() ?? "";
5631
+ try {
5632
+ const [handle] = await window.showOpenFilePicker({
5633
+ types: [{ description: "TypeScript files", accept: { "text/plain": [".ts"] } }],
5634
+ multiple: false
5635
+ });
5636
+ this.loginSetupDraftHandle = handle;
5637
+ const file = await handle.getFile();
5638
+ this.loginSetupDraftContent = await file.text();
5639
+ this.loginSetupDraftHasContent = true;
5640
+ this.loginSetupDraftPath = currentPath || `cypress/common-services/${handle.name}`;
5641
+ this.loginSetupDraftFunctions = extractExportedFunctions(this.loginSetupDraftContent);
5642
+ this.render();
5643
+ } catch (e) {
5644
+ if (e?.name !== "AbortError") showToast(this.t("CONFIG.FOLDER_ERROR_TOAST"), false);
5645
+ }
5646
+ }
5647
+ async loginSetupCreateScaffold() {
5648
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim() ?? "";
5649
+ try {
5650
+ const handle = await window.showSaveFilePicker({
5651
+ suggestedName: "login.service.ts",
5652
+ types: [{ description: "TypeScript files", accept: { "text/plain": [".ts"] } }]
5653
+ });
5654
+ const content = buildLoginScaffold();
5655
+ const writable = await handle.createWritable();
5656
+ await writable.write(content);
5657
+ await writable.close();
5658
+ this.loginSetupDraftHandle = handle;
5659
+ this.loginSetupDraftContent = content;
5660
+ this.loginSetupDraftHasContent = true;
5661
+ this.loginSetupDraftPath = currentPath || `cypress/common-services/${handle.name}`;
5662
+ this.loginSetupDraftFunctions = extractExportedFunctions(content);
5663
+ this.render();
5664
+ } catch (e) {
5665
+ if (e?.name !== "AbortError") showToast(this.t("CONFIG.FOLDER_ERROR_TOAST"), false);
5666
+ }
5667
+ }
5668
+ async loginSetupRescan() {
5669
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim();
5670
+ if (currentPath !== void 0) this.loginSetupDraftPath = currentPath;
5671
+ try {
5672
+ if (this.loginSetupDraftHandle) {
5673
+ const file = await this.loginSetupDraftHandle.getFile();
5674
+ this.loginSetupDraftContent = await file.text();
5675
+ }
5676
+ this.loginSetupDraftFunctions = extractExportedFunctions(this.loginSetupDraftContent);
5677
+ this.render();
5678
+ } catch {
5679
+ }
5680
+ }
5296
5681
  async changeFolder() {
5297
5682
  try {
5298
5683
  await this.persistence.requestDirectoryPermissions();
@@ -5390,13 +5775,19 @@ var ConfigurationElement = class extends HTMLElement {
5390
5775
  smartSelectorEnabled: this.smartSelectorEnabled,
5391
5776
  startHidden: this.startHidden,
5392
5777
  resumeTtlMinutes: this.resumeTtlMinutes,
5393
- issueTrackerProviderId: this.issueTrackerProviderId,
5394
- issueTrackerParams: this.issueTrackerParams,
5395
5778
  isExporting: this.isExporting,
5396
5779
  exportMode: this.exportMode,
5397
5780
  exportTests: this.exportTests,
5398
5781
  exportSelectedIds: this.exportSelectedIds,
5399
- exportSelectedTags: this.exportSelectedTags
5782
+ exportSelectedTags: this.exportSelectedTags,
5783
+ issueTrackerConfig: this.issueTrackerConfig,
5784
+ isLoginSetupOpen: this.isLoginSetupOpen,
5785
+ loginSetupConfig: this.loginSetupConfig,
5786
+ loginSetupDraftPath: this.loginSetupDraftPath,
5787
+ loginSetupDraftFunctions: this.loginSetupDraftFunctions,
5788
+ loginSetupDraftBeforeFn: this.loginSetupDraftBeforeFn,
5789
+ loginSetupDraftBeforeEachFn: this.loginSetupDraftBeforeEachFn,
5790
+ loginSetupDraftHasContent: this.loginSetupDraftHasContent
5400
5791
  }, this.t.bind(this))}`;
5401
5792
  ;
5402
5793
  this.shadow.getElementById("lang-select").addEventListener(
@@ -5428,15 +5819,18 @@ var ConfigurationElement = class extends HTMLElement {
5428
5819
  "change",
5429
5820
  (e) => this.onSelectorStrategyChange(e.target.value)
5430
5821
  );
5822
+ ;
5823
+ this.shadow.getElementById("issue-tracker-toggle")?.addEventListener(
5824
+ "change",
5825
+ (e) => this.onIssueTrackerEnabledChange(e.target.checked)
5826
+ );
5431
5827
  this.shadow.getElementById("issue-tracker-provider")?.addEventListener(
5432
5828
  "change",
5433
5829
  (e) => this.onIssueTrackerProviderChange(e.target.value)
5434
5830
  );
5435
- this.shadow.querySelectorAll("[data-tracker-field]").forEach(
5436
- (el) => el.addEventListener(
5437
- "change",
5438
- () => this.onIssueTrackerParamChange(el.dataset["trackerField"] ?? "", el.value)
5439
- )
5831
+ this.shadow.getElementById("issue-tracker-base-url")?.addEventListener(
5832
+ "change",
5833
+ (e) => this.onIssueTrackerBaseUrlChange(e.target.value)
5440
5834
  );
5441
5835
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
5442
5836
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
@@ -5452,24 +5846,48 @@ var ConfigurationElement = class extends HTMLElement {
5452
5846
  this.shadow.querySelectorAll("[data-export-tag]").forEach(
5453
5847
  (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
5454
5848
  );
5849
+ this.shadow.getElementById("btn-open-login-setup")?.addEventListener("click", () => this.openLoginSetupPanel());
5850
+ this.shadow.getElementById("btn-login-setup-cancel")?.addEventListener("click", () => this.closeLoginSetupPanel());
5851
+ this.shadow.getElementById("btn-login-setup-pick-file")?.addEventListener("click", () => this.loginSetupPickFile());
5852
+ this.shadow.getElementById("btn-login-setup-create-scaffold")?.addEventListener("click", () => this.loginSetupCreateScaffold());
5853
+ this.shadow.getElementById("btn-login-setup-rescan")?.addEventListener("click", () => this.loginSetupRescan());
5854
+ this.shadow.getElementById("btn-login-setup-clear")?.addEventListener("click", () => this.clearLoginSetupConfig());
5855
+ this.shadow.getElementById("btn-login-setup-save")?.addEventListener("click", async () => {
5856
+ const pathEl = this.shadow.getElementById("login-setup-filepath");
5857
+ const beforeEl = this.shadow.getElementById("login-setup-before-fn");
5858
+ const beforeEachEl = this.shadow.getElementById("login-setup-before-each-fn");
5859
+ await this.saveLoginSetupConfig({
5860
+ enabled: true,
5861
+ filePath: pathEl?.value.trim() ?? this.loginSetupDraftPath,
5862
+ fileContent: this.loginSetupDraftContent,
5863
+ detectedFunctions: this.loginSetupDraftFunctions,
5864
+ beforeFn: beforeEl?.value || null,
5865
+ beforeEachFn: beforeEachEl?.value || null
5866
+ });
5867
+ });
5868
+ ;
5455
5869
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
5456
5870
  const file = e.target.files?.[0];
5457
5871
  if (!file) return;
5458
5872
  try {
5459
5873
  await this.importAllData(file);
5460
- alert(this.t("CONFIG.IMPORT_SUCCESS"));
5874
+ showToast(this.t("CONFIG.IMPORT_SUCCESS"));
5461
5875
  } catch (err) {
5462
- alert(err.message ?? this.t("CONFIG.IMPORT_ERROR"));
5876
+ showToast(err.message ?? this.t("CONFIG.IMPORT_ERROR"), false);
5463
5877
  }
5464
5878
  const target = e.target;
5465
5879
  if (target) target.value = "";
5466
5880
  });
5467
5881
  }
5468
5882
  };
5469
- if (!customElements.get("e2e-configuration")) {
5470
- customElements.define("e2e-configuration", ConfigurationElement);
5883
+ if (!customElements.get("lib-e2e-configuration")) {
5884
+ customElements.define("lib-e2e-configuration", ConfigurationElement);
5471
5885
  }
5472
5886
 
5887
+ // src/components/advanced-test-editor/advanced-test-editor.ts
5888
+ var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
5889
+ init_code_format_utils();
5890
+
5473
5891
  // src/components/advanced-test-editor/advanced-test-editor.styles.ts
5474
5892
  var ADVANCED_TEST_EDITOR_STYLES = `
5475
5893
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -5728,6 +6146,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5728
6146
  isCreatingFolder = false;
5729
6147
  collapsedDirs = /* @__PURE__ */ new Set();
5730
6148
  sidebarWidth = 220;
6149
+ loginSetupConfig = null;
5731
6150
  hasPermission = false;
5732
6151
  needsReauth = false;
5733
6152
  _dirHandle = null;
@@ -5788,20 +6207,51 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5788
6207
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
5789
6208
  if (!name) return;
5790
6209
  const fileName = `${name}.cy.ts`;
5791
- const template = `describe('${escapeSingleQuotes(name)}', () => {
6210
+ const cfg = this.loginSetupConfig;
6211
+ if (cfg?.enabled && (cfg.beforeFn || cfg.beforeEachFn)) {
6212
+ const e2eName = this._e2eHandle.name ?? "e2e";
6213
+ const fromPath = `cypress/${e2eName}/${fileName}`;
6214
+ const importPath = buildLoginImportPath(fromPath, cfg.filePath);
6215
+ const { importLine, beforeBlock, beforeEachBlock } = buildLoginBlocks(
6216
+ importPath,
6217
+ cfg.beforeFn,
6218
+ cfg.beforeEachFn
6219
+ );
6220
+ const header = importLine ? `${importLine}
5792
6221
 
6222
+ ` : "";
6223
+ const inner = [beforeBlock, beforeEachBlock].filter(Boolean).join("");
6224
+ const template = `${header}describe('${escapeSingleQuotes(name)}', () => {
6225
+ ${inner}
5793
6226
  it('should ', () => {
5794
6227
 
5795
6228
  });
5796
6229
 
5797
6230
  });
5798
6231
  `;
5799
- try {
5800
- const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
5801
- const writable = await fileHandle.createWritable();
5802
- await writable.write(template);
5803
- await writable.close();
5804
- } catch {
6232
+ try {
6233
+ const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
6234
+ const writable = await fileHandle.createWritable();
6235
+ await writable.write(template);
6236
+ await writable.close();
6237
+ } catch {
6238
+ }
6239
+ } else {
6240
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
6241
+
6242
+ it('should ', () => {
6243
+
6244
+ });
6245
+
6246
+ });
6247
+ `;
6248
+ try {
6249
+ const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
6250
+ const writable = await fileHandle.createWritable();
6251
+ await writable.write(template);
6252
+ await writable.close();
6253
+ } catch {
6254
+ }
5805
6255
  }
5806
6256
  this.isCreatingFile = false;
5807
6257
  await this.getFoldersData();
@@ -5829,6 +6279,30 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5829
6279
  const comment = this.testNotes ? this.transformationService.buildBlockComment(this.testNotes) + "\n" : "";
5830
6280
  content = this.transformationService.insertItBlock(content, comment + this.testItBlock);
5831
6281
  if (!content) return;
6282
+ const cfg = this.loginSetupConfig;
6283
+ if (cfg?.enabled && (cfg.beforeFn || cfg.beforeEachFn)) {
6284
+ const fns = [cfg.beforeFn, cfg.beforeEachFn].filter(Boolean);
6285
+ if (!hasLoginBlocks(content, fns)) {
6286
+ const e2eName = this._e2eHandle?.name ?? "e2e";
6287
+ const fileName = this.selectedFile?.name ?? "";
6288
+ const fromPath = `cypress/${e2eName}/${fileName}`;
6289
+ const importPath = buildLoginImportPath(fromPath, cfg.filePath);
6290
+ const { importLine, beforeBlock, beforeEachBlock } = buildLoginBlocks(importPath, cfg.beforeFn, cfg.beforeEachFn);
6291
+ const result = await import_sweetalert2.default.fire({
6292
+ title: this.t("CONFIG.LOGIN_SETUP_INJECT_TITLE"),
6293
+ html: `<div style="padding:10px 18px 14px;color:#8b949e;font-size:13px;line-height:1.6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">${this.t("CONFIG.LOGIN_SETUP_INJECT_TEXT")}</div>`,
6294
+ icon: "question",
6295
+ showCancelButton: true,
6296
+ confirmButtonText: this.t("CONFIG.LOGIN_SETUP_INJECT_CONFIRM"),
6297
+ cancelButtonText: this.t("CONFIG.LOGIN_SETUP_CANCEL_BTN"),
6298
+ confirmButtonColor: "#1f6feb",
6299
+ width: 460
6300
+ });
6301
+ if (result.isConfirmed) {
6302
+ content = injectLoginBlocksIntoExisting(content, importLine, beforeBlock, beforeEachBlock);
6303
+ }
6304
+ }
6305
+ }
5832
6306
  const writable = await this.selectedFileHandle.createWritable();
5833
6307
  await writable.write(content);
5834
6308
  await writable.close();
@@ -6023,8 +6497,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
6023
6497
  });
6024
6498
  }
6025
6499
  };
6026
- if (!customElements.get("advanced-test-editor")) {
6027
- customElements.define("advanced-test-editor", AdvancedTestEditorElement);
6500
+ if (!customElements.get("lib-e2e-advanced-test-editor")) {
6501
+ customElements.define("lib-e2e-advanced-test-editor", AdvancedTestEditorElement);
6028
6502
  }
6029
6503
 
6030
6504
  // src/components/file-preview/file-preview.styles.ts
@@ -6040,9 +6514,6 @@ var FILE_PREVIEW_STYLES = `
6040
6514
  .file-name { flex: 1; font-size: 12px; color: #8b949e;
6041
6515
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; }
6042
6516
  .body { display: flex; flex: 1; overflow: hidden; min-height: 0; }
6043
- /* Host for the CodeMirror editor (or the textarea fallback). */
6044
- .editor-host { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
6045
- .editor-host .cm-editor { flex: 1; min-height: 0; }
6046
6517
  .editor {
6047
6518
  flex: 1; min-width: 0; padding: 14px;
6048
6519
  background: #0d1117; color: #c9d1d9; border: none; outline: none;
@@ -6232,7 +6703,7 @@ function renderFilePreview(state, t) {
6232
6703
  <button id="btn-copy" title="${t("FILE_PREVIEW.COPY_TITLE")}">\u{1F4CB}</button>
6233
6704
  </div>
6234
6705
  <div class="body">
6235
- ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<div class="editor-host" id="editor-host"><textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea></div>`}
6706
+ ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
6236
6707
  ${blocksPanelHtml}
6237
6708
  </div>
6238
6709
  ${runResultHtml}
@@ -6250,7 +6721,6 @@ function renderFilePreview(state, t) {
6250
6721
  var FilePreviewElement = class extends HTMLElement {
6251
6722
  shadow;
6252
6723
  textarea = null;
6253
- editor = null;
6254
6724
  fileName = null;
6255
6725
  closeLabel = "";
6256
6726
  translation = translationService;
@@ -6276,29 +6746,20 @@ var FilePreviewElement = class extends HTMLElement {
6276
6746
  if (!this.closeLabel) this.closeLabel = this.translation.translate("FILE_PREVIEW.CLOSE_BTN");
6277
6747
  this.render();
6278
6748
  }
6279
- disconnectedCallback() {
6280
- this.editor?.destroy();
6281
- this.editor = null;
6282
- }
6283
6749
  t(key) {
6284
6750
  return this.translation.translate(key);
6285
6751
  }
6286
- /** Current editor content (CodeMirror if upgraded, else the textarea/fallback). */
6287
- currentValue() {
6288
- return this.editor?.getValue() ?? this.textarea?.value ?? this._fileContent ?? "";
6289
- }
6290
6752
  get fileContent() {
6291
6753
  return this._fileContent;
6292
6754
  }
6293
6755
  set fileContent(v) {
6294
6756
  if (this._originalContent === null) this._originalContent = v;
6295
6757
  this._fileContent = v;
6296
- if (this.editor) this.editor.setValue(v ?? "");
6297
- else if (this.textarea) this.textarea.value = v ?? "";
6758
+ if (this.textarea) this.textarea.value = v ?? "";
6298
6759
  else this.render();
6299
6760
  }
6300
6761
  saveFile() {
6301
- const content = this.currentValue();
6762
+ const content = this.textarea?.value ?? this._fileContent ?? "";
6302
6763
  this.dispatchEvent(new CustomEvent("save", { detail: content, bubbles: true, composed: true }));
6303
6764
  }
6304
6765
  onClose() {
@@ -6347,7 +6808,7 @@ var FilePreviewElement = class extends HTMLElement {
6347
6808
  * (and warns) when the file has no valid describe() block to insert into.
6348
6809
  */
6349
6810
  insertBlocks() {
6350
- const base = this.currentValue();
6811
+ const base = this.textarea?.value ?? this._fileContent ?? "";
6351
6812
  let content = base;
6352
6813
  if (this.interceptorsBlock) {
6353
6814
  const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
@@ -6367,14 +6828,12 @@ var FilePreviewElement = class extends HTMLElement {
6367
6828
  showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
6368
6829
  }
6369
6830
  render() {
6370
- this.editor?.destroy();
6371
- this.editor = null;
6372
6831
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
6373
6832
  fileName: this.fileName,
6374
6833
  showDiff: this._showDiff,
6375
6834
  fileContent: this._fileContent,
6376
6835
  originalContent: this._originalContent,
6377
- currentContent: this._fileContent ?? "",
6836
+ currentContent: this.textarea?.value ?? this._fileContent ?? "",
6378
6837
  itBlock: this.itBlock,
6379
6838
  interceptorsBlock: this.interceptorsBlock,
6380
6839
  closeLabel: this.closeLabel,
@@ -6388,13 +6847,12 @@ var FilePreviewElement = class extends HTMLElement {
6388
6847
  this.textarea.addEventListener("input", () => {
6389
6848
  this._fileContent = ta.value;
6390
6849
  });
6391
- void this.upgradeEditor();
6392
6850
  }
6393
6851
  this.shadow.getElementById("btn-save")?.addEventListener("click", () => this.saveFile());
6394
6852
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.onClose());
6395
6853
  this.shadow.getElementById("btn-launch")?.addEventListener("click", () => this.launchTest());
6396
6854
  this.shadow.getElementById("btn-copy")?.addEventListener("click", () => {
6397
- this.copyToClipboard(this.currentValue());
6855
+ this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
6398
6856
  });
6399
6857
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
6400
6858
  this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
@@ -6405,37 +6863,9 @@ var FilePreviewElement = class extends HTMLElement {
6405
6863
  this.copyToClipboard(this.interceptorsBlock);
6406
6864
  });
6407
6865
  }
6408
- /**
6409
- * Lazily upgrades the textarea to a CodeMirror editor (syntax highlight +
6410
- * Cypress autocomplete). On any failure the textarea is left in place, so the
6411
- * editor degrades gracefully when CodeMirror can't load.
6412
- */
6413
- async upgradeEditor() {
6414
- const host = this.shadow.getElementById("editor-host");
6415
- const ta = this.textarea;
6416
- if (!host || !ta) return;
6417
- try {
6418
- const handle = await createCodeEditor({
6419
- parent: host,
6420
- root: this.shadow,
6421
- doc: this._fileContent ?? "",
6422
- onChange: (value) => {
6423
- this._fileContent = value;
6424
- }
6425
- });
6426
- if (!host.isConnected) {
6427
- handle.destroy();
6428
- return;
6429
- }
6430
- ta.remove();
6431
- this.textarea = null;
6432
- this.editor = handle;
6433
- } catch {
6434
- }
6435
- }
6436
6866
  };
6437
- if (!customElements.get("file-preview")) {
6438
- customElements.define("file-preview", FilePreviewElement);
6867
+ if (!customElements.get("lib-e2e-file-preview")) {
6868
+ customElements.define("lib-e2e-file-preview", FilePreviewElement);
6439
6869
  }
6440
6870
 
6441
6871
  // src/components/help-panel/help-panel.styles.ts
@@ -6531,12 +6961,12 @@ var REFERENCE_SECTIONS = [
6531
6961
  { title: "HELP.SEC_PANELS", items: ["HELP.PANEL_TESTS", "HELP.PANEL_COMMANDS", "HELP.PANEL_CONFIG", "HELP.PANEL_FILES"] },
6532
6962
  { title: "HELP.SEC_SELECTOR", items: ["HELP.SELECTOR_1", "HELP.SELECTOR_2"] },
6533
6963
  { title: "HELP.SEC_DATA", items: ["HELP.DATA_1", "HELP.DATA_2"] },
6534
- { title: "HELP.SEC_ADVANCED", items: ["HELP.ADV_INVISIBLE", "HELP.ADV_RUNNER", "HELP.ADV_CROSSAPP", "HELP.ADV_DRAG"] }
6964
+ { title: "HELP.SEC_ADVANCED", items: ["HELP.ADV_INVISIBLE", "HELP.ADV_RUNNER", "HELP.ADV_CROSSAPP", "HELP.ADV_DRAG", "HELP.ADV_LOGIN"] }
6535
6965
  ];
6536
6966
  var GUIDE_SECTIONS = [
6537
6967
  { title: "HELP.G_SEC_WORKFLOW", items: ["HELP.G_WF_1", "HELP.G_WF_2", "HELP.G_WF_3", "HELP.G_WF_4", "HELP.G_WF_5"] },
6538
- { title: "HELP.G_SEC_COVERED", items: ["HELP.G_COV_1", "HELP.G_COV_2", "HELP.G_COV_3", "HELP.G_COV_4", "HELP.G_COV_5", "HELP.G_COV_6"] },
6539
- { title: "HELP.G_SEC_NOTCOVERED", items: ["HELP.G_NC_1", "HELP.G_NC_2", "HELP.G_NC_3", "HELP.G_NC_4", "HELP.G_NC_5", "HELP.G_NC_6"] }
6968
+ { title: "HELP.G_SEC_COVERED", items: ["HELP.G_COV_1", "HELP.G_COV_2", "HELP.G_COV_3", "HELP.G_COV_4", "HELP.G_COV_5", "HELP.G_COV_6", "HELP.G_COV_7"] },
6969
+ { title: "HELP.G_SEC_NOTCOVERED", items: ["HELP.G_NC_1", "HELP.G_NC_2", "HELP.G_NC_3", "HELP.G_NC_4", "HELP.G_NC_5"] }
6540
6970
  ];
6541
6971
  function sectionsHtml(sections, t) {
6542
6972
  return sections.map(
@@ -6583,12 +7013,12 @@ var HelpPanelElement = class extends HTMLElement {
6583
7013
  });
6584
7014
  }
6585
7015
  };
6586
- if (!customElements.get("help-panel")) {
6587
- customElements.define("help-panel", HelpPanelElement);
7016
+ if (!customElements.get("lib-e2e-help-panel")) {
7017
+ customElements.define("lib-e2e-help-panel", HelpPanelElement);
6588
7018
  }
6589
7019
 
6590
7020
  // src/components/lib-e2e-recorder/lib-e2e-recorder.ts
6591
- var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
7021
+ var import_sweetalert22 = __toESM(require("sweetalert2"), 1);
6592
7022
 
6593
7023
  // src/components/lib-e2e-recorder/lib-e2e-recorder.styles.ts
6594
7024
  var DIRECTIONS = ["up-left", "up-right", "down-left", "down-right"];
@@ -6789,6 +7219,129 @@ function renderRecorderWidget(rec, paused, t) {
6789
7219
  <div class="rec-badge">${paused ? t("RECORDER.BADGE_PAUSED") : t("RECORDER.BADGE_REC")}</div>`;
6790
7220
  }
6791
7221
 
7222
+ // src/components/lib-e2e-recorder/assertion-builder.ts
7223
+ init_code_format_utils();
7224
+ var NO_VALUE_ASSERTIONS = /* @__PURE__ */ new Set(["be.visible", "not.exist", "be.disabled", "be.checked"]);
7225
+ function injectAssertionBuilder(container, t, onAddCommand) {
7226
+ container.insertAdjacentHTML("beforeend", `
7227
+ <div id="assertion-builder"
7228
+ style="padding:10px 12px;border-top:1px solid #21262d;background:#0d1117">
7229
+ <div style="font-size:10px;color:#484f58;text-transform:uppercase;letter-spacing:.8px;font-weight:600;margin-bottom:8px">
7230
+ ${t("RECORDER.ASSERTION_SECTION")}
7231
+ </div>
7232
+ <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end">
7233
+ <input id="assert-selector" type="text"
7234
+ placeholder="${t("RECORDER.ASSERT_SELECTOR_PH").replace(/"/g, "&quot;")}"
7235
+ style="flex:2;min-width:140px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7236
+ border-radius:5px;color:#c9d1d9;font-size:11px;outline:none;
7237
+ font-family:'Cascadia Code','Fira Code',monospace"/>
7238
+ <select id="assert-type"
7239
+ style="padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;
7240
+ color:#c9d1d9;font-size:11px;outline:none;cursor:pointer;flex-shrink:0">
7241
+ <option value="be.visible">be.visible</option>
7242
+ <option value="not.exist">not.exist</option>
7243
+ <option value="be.disabled">be.disabled</option>
7244
+ <option value="be.checked">be.checked</option>
7245
+ <option value="contain.text">contain.text</option>
7246
+ <option value="have.value">have.value</option>
7247
+ <option value="have.length">have.length</option>
7248
+ <option value="have.class">have.class</option>
7249
+ <option value="have.attr">have.attr</option>
7250
+ </select>
7251
+ <input id="assert-value" type="text" placeholder="${t("RECORDER.ASSERT_VALUE_PH")}"
7252
+ style="flex:2;min-width:110px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7253
+ border-radius:5px;color:#c9d1d9;font-size:11px;outline:none"/>
7254
+ <button id="btn-add-assertion"
7255
+ style="padding:5px 12px;border:none;border-radius:5px;cursor:pointer;
7256
+ font-size:11px;font-weight:500;background:#2f81f7;color:#fff;
7257
+ white-space:nowrap;flex-shrink:0">
7258
+ ${t("RECORDER.ASSERT_ADD_BTN")}
7259
+ </button>
7260
+ </div>
7261
+ </div>`);
7262
+ document.getElementById("btn-add-assertion")?.addEventListener("click", () => {
7263
+ const sel = document.getElementById("assert-selector").value.trim();
7264
+ const type = document.getElementById("assert-type").value;
7265
+ const val = document.getElementById("assert-value").value.trim();
7266
+ if (!sel) return;
7267
+ const s = escapeSingleQuotes(sel);
7268
+ const v = escapeSingleQuotes(val);
7269
+ const cmd = NO_VALUE_ASSERTIONS.has(type) || !val ? `cy.get('${s}').should('${type}')` : `cy.get('${s}').should('${type}', '${v}')`;
7270
+ onAddCommand(cmd);
7271
+ document.getElementById("assert-selector").value = "";
7272
+ document.getElementById("assert-value").value = "";
7273
+ });
7274
+ }
7275
+
7276
+ // src/components/lib-e2e-recorder/filesystem-setup.ts
7277
+ function mountFilesystemSetupContent(container, t, onSkip, onSelect) {
7278
+ container.innerHTML = `
7279
+ <div style="padding:16px 20px 20px;color:#8b949e;font-size:13px;line-height:1.7">
7280
+ <p>${t("RECORDER.FS_INTRO_HTML")}</p>
7281
+ <p style="margin-top:10px;margin-bottom:6px;font-size:11px;color:#8b949e">
7282
+ ${t("RECORDER.FS_STRUCTURE_HINT_HTML")}
7283
+ </p>
7284
+ <pre style="margin:0;padding:10px 14px;background:#0d1117;border:1px solid #21262d;
7285
+ border-radius:8px;font-size:11px;color:#c9d1d9;line-height:1.8;
7286
+ font-family:'Cascadia Code','Fira Code','Consolas',monospace">
7287
+ cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
7288
+ \u2514\u2500\u2500 e2e/ <span style="color:#484f58">${t("RECORDER.FS_TREE_READ_HINT")}</span>
7289
+ \u2514\u2500\u2500 *.cy.ts</pre>
7290
+ <p style="margin-top:8px;font-size:11px;color:#484f58">
7291
+ ${t("RECORDER.FS_PERMISSION_NOTE")}
7292
+ </p>
7293
+ <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:20px">
7294
+ <button id="fs-skip"
7295
+ style="padding:7px 16px;border:1px solid #30363d;border-radius:6px;cursor:pointer;
7296
+ font-size:12px;font-weight:500;background:transparent;color:#8b949e">
7297
+ ${t("RECORDER.FS_LATER_BTN")}
7298
+ </button>
7299
+ <button id="fs-select"
7300
+ style="padding:7px 16px;border:none;border-radius:6px;cursor:pointer;
7301
+ font-size:12px;font-weight:500;background:#2f81f7;color:#fff">
7302
+ ${t("RECORDER.FS_SELECT_BTN")}
7303
+ </button>
7304
+ </div>
7305
+ </div>`;
7306
+ document.getElementById("fs-skip")?.addEventListener("click", () => {
7307
+ void onSkip();
7308
+ });
7309
+ document.getElementById("fs-select")?.addEventListener("click", () => {
7310
+ void onSelect();
7311
+ });
7312
+ }
7313
+
7314
+ // src/utils/swal-mount.utils.ts
7315
+ function mountComponentInSwal(tagName, container, props, events) {
7316
+ const el = document.createElement(tagName);
7317
+ Object.assign(el, props);
7318
+ container.appendChild(el);
7319
+ for (const { name, handler } of events ?? []) {
7320
+ el.addEventListener(name, handler);
7321
+ }
7322
+ return el;
7323
+ }
7324
+
7325
+ // src/utils/fixture-convert.utils.ts
7326
+ function toFixtureInterceptors(interceptors, fixtures) {
7327
+ const fixtureNames = new Set(fixtures.map((f) => f.name));
7328
+ return interceptors.map((icp) => {
7329
+ const match = icp.match(/\.as\('([^']+)'\)$/);
7330
+ if (!match) return icp;
7331
+ const fixtureFile = `${match[1]}.json`;
7332
+ if (!fixtureNames.has(fixtureFile)) return icp;
7333
+ return icp.replace(/\)\.as\(/, `, { fixture: '${fixtureFile}' }).as(`);
7334
+ });
7335
+ }
7336
+ function simplifyFixtureWaits(commands, fixtures) {
7337
+ const fixtureAliases = new Set(fixtures.map((f) => f.name.replace(/\.json$/, "")));
7338
+ return commands.map((cmd) => {
7339
+ const match = cmd.match(/^cy\.wait\('@([^']+)'\)/);
7340
+ if (!match || !fixtureAliases.has(match[1])) return cmd;
7341
+ return `cy.wait('@${match[1]}').then((interception) => { })`;
7342
+ });
7343
+ }
7344
+
6792
7345
  // src/components/lib-e2e-recorder/lib-e2e-recorder.ts
6793
7346
  var LibE2eRecorderElement = class extends HTMLElement {
6794
7347
  shadow;
@@ -6815,6 +7368,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
6815
7368
  httpMonitor;
6816
7369
  smartSelectorEnabled = true;
6817
7370
  _needsRecordingRebuild = false;
7371
+ issueTrackerConfig = { ...DEFAULT_ISSUE_TRACKER_CONFIG };
6818
7372
  recording;
6819
7373
  persistence;
6820
7374
  translation;
@@ -6887,6 +7441,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
6887
7441
  this.recording.destroy();
6888
7442
  this._needsRecordingRebuild = true;
6889
7443
  }
7444
+ // HttpMonitor reads extendedHttpCommands and fixtureMode synchronously inside
7445
+ // fetch/XHR interception handlers, so they must live in localStorage (sync).
7446
+ // IndexedDB is the persistent source of truth; this method mirrors it into
7447
+ // localStorage on every mount so the sync cache is always fresh.
6890
7448
  async initHttpConfig() {
6891
7449
  const config = await this.persistence.getConfig("extendedHttpCommands");
6892
7450
  if (config === null) {
@@ -6934,9 +7492,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
6934
7492
  }
6935
7493
  showSelectorPicker(target) {
6936
7494
  Promise.resolve().then(() => (init_selector_picker(), selector_picker_exports)).then(() => {
6937
- const existing = document.querySelector("selector-picker");
7495
+ const existing = document.querySelector("lib-e2e-selector-picker");
6938
7496
  if (existing) existing.remove();
6939
- const picker = document.createElement("selector-picker");
7497
+ const picker = document.createElement("lib-e2e-selector-picker");
6940
7498
  picker.targetElement = target;
6941
7499
  picker.recording = this.recording;
6942
7500
  picker.translation = this.translation;
@@ -6958,6 +7516,11 @@ var LibE2eRecorderElement = class extends HTMLElement {
6958
7516
  const strategy = config?.["selectorStrategy"];
6959
7517
  if (strategy) this.recording.selectorStrategy = strategy;
6960
7518
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
7519
+ this.issueTrackerConfig = {
7520
+ enabled: config?.["issueTrackerEnabled"] === "true",
7521
+ provider: config?.["issueTrackerProvider"] ?? DEFAULT_ISSUE_TRACKER_CONFIG.provider,
7522
+ baseUrl: config?.["issueTrackerBaseUrl"] ?? ""
7523
+ };
6961
7524
  }
6962
7525
  // ── cross-app session continuity (spec 006) ───────────────────────────────
6963
7526
  /**
@@ -6993,7 +7556,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
6993
7556
  }
6994
7557
  async promptResumeOrDiscard(session) {
6995
7558
  const t = this.translation.translate.bind(this.translation);
6996
- const result = await import_sweetalert2.default.fire({
7559
+ const result = await import_sweetalert22.default.fire({
6997
7560
  title: t("RECORDER.SESSION_RESUME_TITLE"),
6998
7561
  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>`,
6999
7562
  showCancelButton: true,
@@ -7212,8 +7775,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
7212
7775
  this.showFilesystemSetupDialog();
7213
7776
  }
7214
7777
  showFilesystemSetupDialog() {
7215
- import_sweetalert2.default.fire({
7216
- title: this.translation.translate("RECORDER.FS_TITLE"),
7778
+ const t = this.translation.translate.bind(this.translation);
7779
+ import_sweetalert22.default.fire({
7780
+ title: t("RECORDER.FS_TITLE"),
7217
7781
  html: '<div id="fs-setup-content"></div>',
7218
7782
  showCloseButton: true,
7219
7783
  showConfirmButton: false,
@@ -7222,276 +7786,171 @@ var LibE2eRecorderElement = class extends HTMLElement {
7222
7786
  setSwal2DataCyAttribute();
7223
7787
  const container = document.getElementById("fs-setup-content");
7224
7788
  if (!container) return;
7225
- container.innerHTML = `
7226
- <div style="padding:16px 20px 20px;color:#8b949e;font-size:13px;line-height:1.7">
7227
- <p>${this.translation.translate("RECORDER.FS_INTRO_HTML")}</p>
7228
- <p style="margin-top:10px;margin-bottom:6px;font-size:11px;color:#8b949e">
7229
- ${this.translation.translate("RECORDER.FS_STRUCTURE_HINT_HTML")}
7230
- </p>
7231
- <pre style="margin:0;padding:10px 14px;background:#0d1117;border:1px solid #21262d;
7232
- border-radius:8px;font-size:11px;color:#c9d1d9;line-height:1.8;
7233
- font-family:'Cascadia Code','Fira Code','Consolas',monospace">
7234
- cypress/ <span style="color:#484f58">${this.translation.translate("RECORDER.FS_TREE_PICK_HINT")}</span>
7235
- \u2514\u2500\u2500 e2e/ <span style="color:#484f58">${this.translation.translate("RECORDER.FS_TREE_READ_HINT")}</span>
7236
- \u2514\u2500\u2500 *.cy.ts</pre>
7237
- <p style="margin-top:8px;font-size:11px;color:#484f58">
7238
- ${this.translation.translate("RECORDER.FS_PERMISSION_NOTE")}
7239
- </p>
7240
- <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:20px">
7241
- <button id="fs-skip"
7242
- style="padding:7px 16px;border:1px solid #30363d;border-radius:6px;cursor:pointer;
7243
- font-size:12px;font-weight:500;background:transparent;color:#8b949e">
7244
- ${this.translation.translate("RECORDER.FS_LATER_BTN")}
7245
- </button>
7246
- <button id="fs-select"
7247
- style="padding:7px 16px;border:none;border-radius:6px;cursor:pointer;
7248
- font-size:12px;font-weight:500;background:#2f81f7;color:#fff">
7249
- ${this.translation.translate("RECORDER.FS_SELECT_BTN")}
7250
- </button>
7251
- </div>
7252
- </div>`;
7253
- document.getElementById("fs-skip")?.addEventListener("click", async () => {
7254
- await this.persistence.setConfigKey("allowReadWriteFiles", "false");
7255
- import_sweetalert2.default.close();
7256
- });
7257
- document.getElementById("fs-select")?.addEventListener("click", async () => {
7258
- try {
7259
- await this.persistence.requestDirectoryPermissions();
7260
- import_sweetalert2.default.close();
7261
- showToast(this.translation.translate("RECORDER.FS_SUCCESS_TOAST"));
7262
- } catch (e) {
7263
- if (e?.name !== "AbortError") {
7264
- showToast(this.translation.translate("RECORDER.FS_ERROR_TOAST"), false);
7789
+ mountFilesystemSetupContent(
7790
+ container,
7791
+ t,
7792
+ async () => {
7793
+ await this.persistence.setConfigKey("allowReadWriteFiles", "false");
7794
+ import_sweetalert22.default.close();
7795
+ },
7796
+ async () => {
7797
+ try {
7798
+ await this.persistence.requestDirectoryPermissions();
7799
+ import_sweetalert22.default.close();
7800
+ showToast(t("RECORDER.FS_SUCCESS_TOAST"));
7801
+ } catch (e) {
7802
+ if (e?.name !== "AbortError") {
7803
+ showToast(t("RECORDER.FS_ERROR_TOAST"), false);
7804
+ }
7265
7805
  }
7266
7806
  }
7267
- });
7807
+ );
7268
7808
  }
7269
7809
  });
7270
7810
  }
7271
7811
  // ── dialogs ──────────────────────────────────────────────────────────────
7272
7812
  showCommandsDialog() {
7273
- this.toggleModal("isCommandsDialogOpen", () => {
7274
- import_sweetalert2.default.fire({
7813
+ this.openSwalDialog(
7814
+ "isCommandsDialogOpen",
7815
+ "commands-modal-content",
7816
+ {
7275
7817
  title: this.translation.translate("MAIN_FRAME.DIALOG_COMMANDS"),
7276
7818
  html: '<div id="commands-modal-content" style="min-height:250px;padding:0"></div>',
7277
7819
  showCloseButton: true,
7278
- showConfirmButton: false,
7279
7820
  allowOutsideClick: false,
7280
7821
  backdrop: false,
7281
- width: 640,
7282
- color: "#e6edf3",
7283
- didOpen: () => {
7284
- makeSwalDraggable();
7285
- setSwal2DataCyAttribute();
7822
+ width: 640
7823
+ },
7824
+ (container) => {
7825
+ const child = mountComponentInSwal("lib-e2e-test-previsualizer", container, {
7826
+ translation: this.translation,
7827
+ commands: this.cypressCommands,
7828
+ interceptors: this.interceptors,
7829
+ editable: true
7830
+ }, [
7831
+ { name: "deletecommand", handler: (e) => this.recording.removeCommand(e.detail) },
7832
+ { name: "movecommand", handler: (e) => this.recording.moveCommand(e.detail.from, e.detail.to) },
7833
+ { name: "deleteinterceptor", handler: (e) => this.recording.removeInterceptor(e.detail) }
7834
+ ]);
7835
+ this._previsualizerRef = child;
7836
+ injectAssertionBuilder(container, this.translation.translate.bind(this.translation), (cmd) => this.recording.appendCommand(cmd));
7837
+ },
7838
+ {
7839
+ // Make the container click-through so the page underneath remains interactive
7840
+ onDidOpenPreamble: () => {
7286
7841
  const swalContainer = document.querySelector(".swal2-container");
7287
7842
  if (swalContainer) {
7288
7843
  swalContainer.style.pointerEvents = "none";
7289
7844
  const popup = swalContainer.querySelector(".swal2-popup");
7290
7845
  if (popup) popup.style.pointerEvents = "all";
7291
7846
  }
7292
- const container = document.getElementById("commands-modal-content");
7293
- if (!container) return;
7294
- const child = document.createElement("test-previsualizer");
7295
- child.translation = this.translation;
7296
- child.commands = this.cypressCommands;
7297
- child.interceptors = this.interceptors;
7298
- child.editable = true;
7299
- container.appendChild(child);
7300
- this._previsualizerRef = child;
7301
- child.addEventListener("deletecommand", (e) => {
7302
- this.recording.removeCommand(e.detail);
7303
- });
7304
- child.addEventListener("movecommand", (e) => {
7305
- this.recording.moveCommand(e.detail.from, e.detail.to);
7306
- });
7307
- child.addEventListener("deleteinterceptor", (e) => {
7308
- this.recording.removeInterceptor(e.detail);
7309
- });
7310
- const assertionHtml = `
7311
- <div id="assertion-builder"
7312
- style="padding:10px 12px;border-top:1px solid #21262d;background:#0d1117">
7313
- <div style="font-size:10px;color:#484f58;text-transform:uppercase;letter-spacing:.8px;font-weight:600;margin-bottom:8px">
7314
- ${this.translation.translate("RECORDER.ASSERTION_SECTION")}
7315
- </div>
7316
- <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end">
7317
- <input id="assert-selector" type="text"
7318
- placeholder="${this.translation.translate("RECORDER.ASSERT_SELECTOR_PH").replace(/"/g, "&quot;")}"
7319
- style="flex:2;min-width:140px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7320
- border-radius:5px;color:#c9d1d9;font-size:11px;outline:none;
7321
- font-family:'Cascadia Code','Fira Code',monospace"/>
7322
- <select id="assert-type"
7323
- style="padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;
7324
- color:#c9d1d9;font-size:11px;outline:none;cursor:pointer;flex-shrink:0">
7325
- <option value="be.visible">be.visible</option>
7326
- <option value="not.exist">not.exist</option>
7327
- <option value="be.disabled">be.disabled</option>
7328
- <option value="be.checked">be.checked</option>
7329
- <option value="contain.text">contain.text</option>
7330
- <option value="have.value">have.value</option>
7331
- <option value="have.length">have.length</option>
7332
- <option value="have.class">have.class</option>
7333
- <option value="have.attr">have.attr</option>
7334
- </select>
7335
- <input id="assert-value" type="text" placeholder="${this.translation.translate("RECORDER.ASSERT_VALUE_PH")}"
7336
- style="flex:2;min-width:110px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7337
- border-radius:5px;color:#c9d1d9;font-size:11px;outline:none"/>
7338
- <button id="btn-add-assertion"
7339
- style="padding:5px 12px;border:none;border-radius:5px;cursor:pointer;
7340
- font-size:11px;font-weight:500;background:#2f81f7;color:#fff;
7341
- white-space:nowrap;flex-shrink:0">
7342
- ${this.translation.translate("RECORDER.ASSERT_ADD_BTN")}
7343
- </button>
7344
- </div>
7345
- </div>`;
7346
- container.insertAdjacentHTML("beforeend", assertionHtml);
7347
- const NO_VALUE_ASSERTIONS = /* @__PURE__ */ new Set(["be.visible", "not.exist", "be.disabled", "be.checked"]);
7348
- document.getElementById("btn-add-assertion")?.addEventListener("click", () => {
7349
- const sel = document.getElementById("assert-selector").value.trim();
7350
- const type = document.getElementById("assert-type").value;
7351
- const val = document.getElementById("assert-value").value.trim();
7352
- if (!sel) return;
7353
- const s = escapeSingleQuotes(sel);
7354
- const v = escapeSingleQuotes(val);
7355
- const cmd = NO_VALUE_ASSERTIONS.has(type) || !val ? `cy.get('${s}').should('${type}')` : `cy.get('${s}').should('${type}', '${v}')`;
7356
- this.recording.appendCommand(cmd);
7357
- document.getElementById("assert-selector").value = "";
7358
- document.getElementById("assert-value").value = "";
7359
- });
7360
7847
  },
7361
- willClose: () => {
7362
- this.isCommandsDialogOpen = false;
7848
+ onWillClose: () => {
7363
7849
  this._previsualizerRef = null;
7364
7850
  }
7365
- });
7366
- this.resizePopup();
7367
- });
7851
+ }
7852
+ );
7368
7853
  }
7369
7854
  showSavedTestsDialog() {
7370
- this.toggleModal("isSavedTestsDialogOpen", () => {
7371
- import_sweetalert2.default.fire({
7855
+ this.openSwalDialog(
7856
+ "isSavedTestsDialogOpen",
7857
+ "saved-tests-modal-content",
7858
+ {
7372
7859
  title: this.translation.translate("MAIN_FRAME.DIALOG_SAVED_TESTS"),
7373
7860
  html: '<div id="saved-tests-modal-content" style="min-height:250px;padding:0"></div>',
7374
7861
  showCloseButton: true,
7375
- showConfirmButton: false,
7376
- width: 680,
7377
- color: "#e6edf3",
7378
- didOpen: () => {
7379
- makeSwalDraggable();
7380
- setSwal2DataCyAttribute();
7381
- const container = document.getElementById("saved-tests-modal-content");
7382
- if (!container) return;
7383
- const child = document.createElement("test-editor");
7384
- child.persistence = this.persistence;
7385
- child.translation = this.translation;
7386
- container.appendChild(child);
7387
- },
7388
- willClose: () => {
7389
- this.isSavedTestsDialogOpen = false;
7390
- }
7391
- });
7392
- this.resizePopup();
7393
- });
7862
+ width: 680
7863
+ },
7864
+ (container) => {
7865
+ mountComponentInSwal("lib-e2e-test-editor", container, {
7866
+ persistence: this.persistence,
7867
+ translation: this.translation,
7868
+ issueTrackerConfig: this.issueTrackerConfig
7869
+ });
7870
+ }
7871
+ );
7394
7872
  }
7395
7873
  showSaveTestDialog() {
7396
- this.toggleModal("isSaveTestDialogOpen", () => {
7397
- import_sweetalert2.default.fire({
7874
+ this.openSwalDialog(
7875
+ "isSaveTestDialogOpen",
7876
+ "save-test-modal-content",
7877
+ {
7398
7878
  title: this.translation.translate("MAIN_FRAME.DIALOG_SAVE"),
7399
7879
  html: '<div id="save-test-modal-content" style="padding:0"></div>',
7400
- showCloseButton: true,
7401
- showConfirmButton: false,
7880
+ showCloseButton: false,
7402
7881
  allowOutsideClick: false,
7403
- color: "#e6edf3",
7404
- didOpen: () => {
7405
- makeSwalDraggable();
7406
- setSwal2DataCyAttribute();
7407
- const container = document.getElementById("save-test-modal-content");
7408
- if (!container) return;
7409
- const child = document.createElement("save-test");
7410
- child.translation = this.translation;
7411
- this.persistence.getGeneralConfig().then((config) => {
7412
- const providerId = config?.["issueTrackerProviderId"];
7413
- if (providerId) child.issueTrackerProviderId = providerId;
7414
- });
7415
- container.appendChild(child);
7416
- child.addEventListener("savetest", (e) => {
7882
+ allowEscapeKey: false
7883
+ },
7884
+ (container) => {
7885
+ mountComponentInSwal("lib-e2e-save-test", container, {
7886
+ translation: this.translation,
7887
+ issueTrackerConfig: this.issueTrackerConfig
7888
+ }, [
7889
+ { name: "savetest", handler: (e) => {
7417
7890
  const { description, notes, tags, ticketId } = e.detail ?? {};
7418
- this.onSaveTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? null);
7419
- import_sweetalert2.default.close();
7420
- });
7421
- child.addEventListener("saveandexport", (e) => {
7891
+ this.onSaveTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? "");
7892
+ import_sweetalert22.default.close();
7893
+ } },
7894
+ { name: "saveandexport", handler: (e) => {
7422
7895
  const { description, notes, tags, ticketId } = e.detail ?? {};
7423
- this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? null);
7424
- import_sweetalert2.default.close();
7425
- });
7426
- },
7427
- willClose: () => {
7428
- this.isSaveTestDialogOpen = false;
7429
- }
7430
- });
7431
- });
7896
+ this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? "");
7897
+ import_sweetalert22.default.close();
7898
+ } }
7899
+ ]);
7900
+ },
7901
+ { skipResize: true }
7902
+ );
7432
7903
  }
7433
7904
  showSettingsDialog() {
7434
- this.toggleModal("isSettingsDialogOpen", () => {
7435
- import_sweetalert2.default.fire({
7905
+ this.openSwalDialog(
7906
+ "isSettingsDialogOpen",
7907
+ "settings-modal-content",
7908
+ {
7436
7909
  title: this.translation.translate("MAIN_FRAME.SETTINGS"),
7437
7910
  html: '<div id="settings-modal-content" style="padding:0"></div>',
7438
7911
  showCloseButton: true,
7439
- showConfirmButton: false,
7440
- width: 520,
7441
- color: "#e6edf3",
7442
- didOpen: () => {
7443
- makeSwalDraggable();
7444
- setSwal2DataCyAttribute();
7445
- const container = document.getElementById("settings-modal-content");
7446
- if (!container) return;
7447
- const child = document.createElement("e2e-configuration");
7448
- child.persistence = this.persistence;
7449
- child.translation = this.translation;
7450
- container.appendChild(child);
7451
- child.addEventListener("smartselectorchange", (e) => {
7912
+ width: 520
7913
+ },
7914
+ (container) => {
7915
+ mountComponentInSwal("lib-e2e-configuration", container, {
7916
+ persistence: this.persistence,
7917
+ translation: this.translation
7918
+ }, [
7919
+ { name: "smartselectorchange", handler: (e) => {
7452
7920
  this.smartSelectorEnabled = e.detail;
7453
- });
7454
- child.addEventListener("starthiddenchange", (e) => {
7921
+ } },
7922
+ { name: "starthiddenchange", handler: (e) => {
7455
7923
  this.isVisible = !e.detail;
7456
7924
  this.style.display = this.isVisible ? "" : "none";
7457
- });
7458
- child.addEventListener("resetwidgetposition", () => this.resetWidgetPosition());
7459
- },
7460
- willClose: () => {
7461
- this.isSettingsDialogOpen = false;
7462
- }
7463
- });
7464
- this.resizePopup();
7465
- });
7925
+ } },
7926
+ { name: "resetwidgetposition", handler: () => this.resetWidgetPosition() },
7927
+ { name: "issuetrackerchange", handler: (e) => {
7928
+ this.issueTrackerConfig = e.detail;
7929
+ } }
7930
+ ]);
7931
+ }
7932
+ );
7466
7933
  }
7467
7934
  showHelpDialog() {
7468
- this.toggleModal("isHelpDialogOpen", () => {
7469
- import_sweetalert2.default.fire({
7935
+ this.openSwalDialog(
7936
+ "isHelpDialogOpen",
7937
+ "help-modal-content",
7938
+ {
7470
7939
  title: this.translation.translate("HELP.TITLE"),
7471
7940
  html: '<div id="help-modal-content" style="padding:0"></div>',
7472
7941
  showCloseButton: true,
7473
- showConfirmButton: false,
7474
- width: 560,
7475
- color: "#e6edf3",
7476
- didOpen: () => {
7477
- makeSwalDraggable();
7478
- setSwal2DataCyAttribute();
7479
- const container = document.getElementById("help-modal-content");
7480
- if (!container) return;
7481
- const child = document.createElement("help-panel");
7482
- child.translation = this.translation;
7483
- container.appendChild(child);
7484
- },
7485
- willClose: () => {
7486
- this.isHelpDialogOpen = false;
7487
- }
7488
- });
7489
- this.resizePopup();
7490
- });
7942
+ width: 560
7943
+ },
7944
+ (container) => {
7945
+ mountComponentInSwal("lib-e2e-help-panel", container, {
7946
+ translation: this.translation
7947
+ });
7948
+ }
7949
+ );
7491
7950
  }
7492
7951
  showAdvancedEditorDialog(testId) {
7493
7952
  this.toggleModal("isAdvancedEditorDialogOpen", () => {
7494
- import_sweetalert2.default.fire({
7953
+ import_sweetalert22.default.fire({
7495
7954
  title: this.translation.translate("MAIN_FRAME.SHOW_ADVANCED_EDITOR"),
7496
7955
  html: '<div id="advanced-editor-modal-content" style="flex:1;min-height:0;display:flex;flex-direction:column;padding:0"></div>',
7497
7956
  showCloseButton: true,
@@ -7502,42 +7961,33 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7502
7961
  didOpen: () => {
7503
7962
  makeSwalDraggable();
7504
7963
  setSwal2DataCyAttribute();
7505
- const popup = import_sweetalert2.default.getPopup();
7506
- if (popup) {
7507
- popup.style.height = "600px";
7508
- const htmlContainer = popup.querySelector(".swal2-html-container");
7509
- if (htmlContainer) {
7510
- htmlContainer.style.flex = "1";
7511
- htmlContainer.style.minHeight = "0";
7512
- htmlContainer.style.overflow = "hidden";
7513
- htmlContainer.style.padding = "0";
7514
- htmlContainer.style.margin = "0";
7515
- htmlContainer.style.display = "flex";
7516
- htmlContainer.style.flexDirection = "column";
7517
- }
7518
- }
7964
+ ensurePopupDimensions(import_sweetalert22.default.getPopup(), 600);
7519
7965
  const container = document.getElementById("advanced-editor-modal-content");
7520
7966
  if (!container) return;
7521
- const child = document.createElement("advanced-test-editor");
7522
- child.persistence = this.persistence;
7523
- child.translation = this.translation;
7524
- if (testId !== void 0) child.testId = testId;
7525
- container.appendChild(child);
7526
- child.addEventListener("selectorstrategychange", (e) => {
7527
- this.recording.selectorStrategy = e.detail;
7528
- });
7529
- child.addEventListener("closemodal", () => import_sweetalert2.default.close());
7530
- child.addEventListener("openfileeditor", (e) => {
7531
- import_sweetalert2.default.close();
7532
- setTimeout(() => this.showFileEditorDialog(
7533
- e.detail.handle,
7534
- e.detail.content,
7535
- e.detail.fileName,
7536
- e.detail.testId,
7537
- e.detail.itBlock,
7538
- e.detail.interceptorsBlock,
7539
- e.detail.notes
7540
- ), 150);
7967
+ const child = mountComponentInSwal("lib-e2e-advanced-test-editor", container, {
7968
+ persistence: this.persistence,
7969
+ translation: this.translation,
7970
+ ...testId !== void 0 ? { testId } : {}
7971
+ }, [
7972
+ { name: "selectorstrategychange", handler: (e) => {
7973
+ this.recording.selectorStrategy = e.detail;
7974
+ } },
7975
+ { name: "closemodal", handler: () => import_sweetalert22.default.close() },
7976
+ { name: "openfileeditor", handler: (e) => {
7977
+ import_sweetalert22.default.close();
7978
+ setTimeout(() => this.showFileEditorDialog(
7979
+ e.detail.handle,
7980
+ e.detail.content,
7981
+ e.detail.fileName,
7982
+ e.detail.testId,
7983
+ e.detail.itBlock,
7984
+ e.detail.interceptorsBlock,
7985
+ e.detail.notes
7986
+ ), 150);
7987
+ } }
7988
+ ]);
7989
+ void this.persistence.getLoginSetup().then((cfg) => {
7990
+ child.loginSetupConfig = cfg;
7541
7991
  });
7542
7992
  },
7543
7993
  willClose: () => {
@@ -7545,13 +7995,13 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7545
7995
  }
7546
7996
  });
7547
7997
  setTimeout(() => {
7548
- const popup = import_sweetalert2.default.getPopup();
7998
+ const popup = import_sweetalert22.default.getPopup();
7549
7999
  if (popup) makeModalResizable(popup, { minWidth: 400, minHeight: 0 });
7550
8000
  }, 0);
7551
8001
  });
7552
8002
  }
7553
8003
  showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
7554
- import_sweetalert2.default.fire({
8004
+ import_sweetalert22.default.fire({
7555
8005
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
7556
8006
  html: '<div id="file-editor-modal-content" style="flex:1;min-height:0;display:flex;flex-direction:column;padding:0"></div>',
7557
8007
  showCloseButton: false,
@@ -7562,89 +8012,106 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7562
8012
  didOpen: () => {
7563
8013
  makeSwalDraggable();
7564
8014
  setSwal2DataCyAttribute();
7565
- const popup = import_sweetalert2.default.getPopup();
7566
- if (popup) {
7567
- popup.style.height = "640px";
7568
- const htmlContainer = popup.querySelector(".swal2-html-container");
7569
- if (htmlContainer) {
7570
- htmlContainer.style.flex = "1";
7571
- htmlContainer.style.minHeight = "0";
7572
- htmlContainer.style.overflow = "hidden";
7573
- htmlContainer.style.padding = "0";
7574
- htmlContainer.style.margin = "0";
7575
- htmlContainer.style.display = "flex";
7576
- htmlContainer.style.flexDirection = "column";
7577
- }
7578
- }
8015
+ ensurePopupDimensions(import_sweetalert22.default.getPopup(), 640);
7579
8016
  const container = document.getElementById("file-editor-modal-content");
7580
8017
  if (!container) return;
7581
- const child = document.createElement("file-preview");
7582
- child.translation = this.translation;
7583
- child.fileContent = content;
7584
- child.fileName = fileName;
7585
- child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
7586
- child.itBlock = itBlock;
7587
- child.interceptorsBlock = interceptorsBlock;
7588
- child.notes = notes;
7589
- container.appendChild(child);
7590
- child.addEventListener("close", () => {
7591
- import_sweetalert2.default.close();
7592
- setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
7593
- });
7594
- child.addEventListener("save", async (e) => {
7595
- try {
7596
- const writable = await handle.createWritable();
7597
- await writable.write(e.detail);
7598
- await writable.close();
7599
- showToast(this.translation.translate("RECORDER.FILE_SAVED_TOAST"));
7600
- } catch {
7601
- showToast(this.translation.translate("RECORDER.FILE_SAVE_ERROR_TOAST"), false);
7602
- }
7603
- import_sweetalert2.default.close();
7604
- setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
7605
- });
8018
+ mountComponentInSwal("lib-e2e-file-preview", container, {
8019
+ translation: this.translation,
8020
+ fileContent: content,
8021
+ fileName,
8022
+ closeLabel: this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR"),
8023
+ itBlock,
8024
+ interceptorsBlock,
8025
+ notes
8026
+ }, [
8027
+ { name: "close", handler: () => {
8028
+ import_sweetalert22.default.close();
8029
+ setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
8030
+ } },
8031
+ { name: "save", handler: async (e) => {
8032
+ try {
8033
+ const writable = await handle.createWritable();
8034
+ await writable.write(e.detail);
8035
+ await writable.close();
8036
+ showToast(this.translation.translate("RECORDER.FILE_SAVED_TOAST"));
8037
+ } catch {
8038
+ showToast(this.translation.translate("RECORDER.FILE_SAVE_ERROR_TOAST"), false);
8039
+ }
8040
+ import_sweetalert22.default.close();
8041
+ setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
8042
+ } }
8043
+ ]);
7606
8044
  }
7607
8045
  });
7608
8046
  this.resizePopup();
7609
8047
  }
7610
8048
  // ── save handlers ─────────────────────────────────────────────────────────
7611
- async onSaveTest(description, tags = [], notes = "", ticketId = null) {
7612
- if (description) {
7613
- const fixtures = this.recording.getFixturesSnapshot();
7614
- await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes, ticketId ?? void 0);
7615
- await this.writeFixturesIfAny(fixtures);
7616
- this.recording.clearCommands();
7617
- this.clearRecordingHistory();
7618
- this.cypressCommands = [];
7619
- this.interceptors = [];
8049
+ async onSaveTest(description, tags = [], notes = "", ticketId = "") {
8050
+ if (!description) return;
8051
+ await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes, ticketId || void 0);
8052
+ const uploadedFiles = this.recording.getUploadedFilesSnapshot();
8053
+ if (uploadedFiles.length > 0) {
8054
+ const filenames = uploadedFiles.map((f) => f.filename).join(", ");
8055
+ showToast(
8056
+ this.translation.translate("RECORDER.FILE_UPLOAD_MANUAL_TOAST").replace("{files}", filenames),
8057
+ false
8058
+ );
7620
8059
  }
7621
- }
7622
- async onSaveAndExportTest(description, tags = [], notes = "", ticketId = null) {
7623
- if (description) {
7624
- const fixtures = this.recording.getFixturesSnapshot();
7625
- const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes, ticketId ?? void 0);
7626
- await this.writeFixturesIfAny(fixtures);
7627
- this.recording.clearCommands();
7628
- this.clearRecordingHistory();
7629
- this.cypressCommands = [];
7630
- this.interceptors = [];
7631
- setTimeout(() => this.showAdvancedEditorDialog(id), 300);
8060
+ this.recording.clearCommands();
8061
+ this.clearRecordingHistory();
8062
+ this.cypressCommands = [];
8063
+ this.interceptors = [];
8064
+ }
8065
+ async onSaveAndExportTest(description, tags = [], notes = "", ticketId = "") {
8066
+ if (!description) return;
8067
+ const fixtures = this.recording.getFixturesSnapshot();
8068
+ const isFixtureMode = this.httpMonitor?.isFixtureModeEnabled() ?? false;
8069
+ let finalInterceptors = this.interceptors;
8070
+ let finalCommands = this.cypressCommands;
8071
+ if (isFixtureMode && fixtures.length > 0) {
8072
+ try {
8073
+ const n = await this.persistence.writeFixtures(fixtures);
8074
+ finalInterceptors = toFixtureInterceptors(this.interceptors, fixtures);
8075
+ finalCommands = simplifyFixtureWaits(this.cypressCommands, fixtures);
8076
+ showToast(`${this.translation.translate("RECORDER.FIXTURES_WRITTEN_TOAST")} (${n})`);
8077
+ } catch {
8078
+ showToast(this.translation.translate("RECORDER.FIXTURES_NO_FOLDER_TOAST"), false);
8079
+ }
7632
8080
  }
7633
- }
7634
- /** Writes captured fixtures to cypress/fixtures and toasts the outcome (spec 012). */
7635
- async writeFixturesIfAny(fixtures) {
7636
- if (!fixtures.length) return;
7637
- try {
7638
- const n = await this.persistence.writeFixtures(fixtures);
7639
- showToast(`${this.translation.translate("RECORDER.FIXTURES_WRITTEN_TOAST")} (${n})`);
7640
- } catch {
7641
- showToast(this.translation.translate("RECORDER.FIXTURES_NO_FOLDER_TOAST"), false);
8081
+ const uploadedFiles = this.recording.getUploadedFilesSnapshot();
8082
+ if (uploadedFiles.length > 0) {
8083
+ const filenames = uploadedFiles.map((f) => f.filename).join(", ");
8084
+ const hasAccess = await this.persistence.hasDirectoryAccess();
8085
+ if (hasAccess) {
8086
+ try {
8087
+ for (const uf of uploadedFiles) {
8088
+ await this.persistence.writeUploadedFile(uf.filename, uf.bytes);
8089
+ }
8090
+ showToast(this.translation.translate("RECORDER.FILE_UPLOAD_COPIED_TOAST").replace("{files}", filenames));
8091
+ } catch {
8092
+ showToast(
8093
+ this.translation.translate("RECORDER.FILE_UPLOAD_COPY_ERROR_TOAST").replace("{files}", filenames),
8094
+ false
8095
+ );
8096
+ }
8097
+ } else {
8098
+ showToast(
8099
+ this.translation.translate("RECORDER.FILE_UPLOAD_MANUAL_TOAST").replace("{files}", filenames),
8100
+ false
8101
+ );
8102
+ }
7642
8103
  }
8104
+ const id = await this.persistence.insertTest(description, finalCommands, finalInterceptors, tags, notes, ticketId || void 0);
8105
+ this.recording.clearCommands();
8106
+ this.clearRecordingHistory();
8107
+ this.cypressCommands = [];
8108
+ this.interceptors = [];
8109
+ setTimeout(() => this.showAdvancedEditorDialog(id), 300);
7643
8110
  }
7644
8111
  // ── helpers ───────────────────────────────────────────────────────────────
7645
8112
  toggleModal(flag, openFn) {
7646
8113
  if (this[flag]) {
7647
- import_sweetalert2.default.close();
8114
+ import_sweetalert22.default.close();
7648
8115
  this[flag] = false;
7649
8116
  return;
7650
8117
  }
@@ -7653,10 +8120,32 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7653
8120
  }
7654
8121
  resizePopup() {
7655
8122
  setTimeout(() => {
7656
- const popup = import_sweetalert2.default.getPopup();
8123
+ const popup = import_sweetalert22.default.getPopup();
7657
8124
  if (popup) makeModalResizable(popup, { minWidth: 400, minHeight: 200 });
7658
8125
  }, 0);
7659
8126
  }
8127
+ openSwalDialog(flag, contentId, swalOptions, onMount, extra) {
8128
+ this.toggleModal(flag, () => {
8129
+ import_sweetalert22.default.fire({
8130
+ showConfirmButton: false,
8131
+ color: "#e6edf3",
8132
+ ...swalOptions,
8133
+ didOpen: () => {
8134
+ makeSwalDraggable();
8135
+ setSwal2DataCyAttribute();
8136
+ extra?.onDidOpenPreamble?.();
8137
+ const container = document.getElementById(contentId);
8138
+ if (!container) return;
8139
+ onMount(container);
8140
+ },
8141
+ willClose: () => {
8142
+ this[flag] = false;
8143
+ extra?.onWillClose?.();
8144
+ }
8145
+ });
8146
+ if (!extra?.skipResize) this.resizePopup();
8147
+ });
8148
+ }
7660
8149
  render() {
7661
8150
  const rec = this.isRecording;
7662
8151
  const paused = this.isPaused;
@@ -7685,26 +8174,21 @@ if (!customElements.get("lib-e2e-recorder")) {
7685
8174
  }
7686
8175
 
7687
8176
  // src/index.ts
7688
- var VERSION = "0.8.0";
8177
+ var VERSION = "0.10.0";
7689
8178
  // Annotate the CommonJS export names for ESM import in node:
7690
8179
  0 && (module.exports = {
7691
8180
  ACTIVE_SESSION_BREADCRUMB_KEY,
7692
8181
  AdvancedTestEditorElement,
7693
8182
  AdvancedTestTransformationService,
7694
- CHAIN_METHODS,
7695
- CYPRESS_COMPLETIONS,
7696
- CY_METHODS,
7697
8183
  ConfigurationElement,
7698
8184
  DB_SCHEMA,
7699
8185
  DB_STORE_NAMES,
7700
- DEFAULT_ISSUE_TRACKER_PROVIDER_ID,
7701
8186
  DEFAULT_RESUME_TTL_MINUTES,
7702
8187
  DRAG_THRESHOLD,
7703
8188
  FilePreviewElement,
7704
8189
  HelpPanelElement,
7705
8190
  HttpMonitor,
7706
8191
  INPUT_TYPES,
7707
- ISSUE_TRACKER_PROVIDERS,
7708
8192
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
7709
8193
  LOCALE_BY_LANG,
7710
8194
  LibE2eRecorderElement,
@@ -7715,25 +8199,19 @@ var VERSION = "0.8.0";
7715
8199
  SUPPORTED_LANGS,
7716
8200
  SaveTestElement,
7717
8201
  SelectorPickerElement,
7718
- Subject,
7719
8202
  TOGGLE_MARGIN,
7720
- TOP_LEVEL,
7721
8203
  TestEditorElement,
7722
8204
  TestPrevisualizerElement,
7723
8205
  TransformationService,
7724
8206
  TranslationService,
7725
8207
  VERSION,
8208
+ _resetHttpMonitorState,
7726
8209
  advancedTestTransformationService,
7727
8210
  boxOffsetForDirection,
7728
8211
  boxTopLeftFor,
7729
- buildTicketComment,
7730
- buildTicketUrl,
7731
8212
  clampTogglePosition,
7732
- createCodeEditor,
7733
8213
  defaultTogglePosition,
7734
- filterCompletions,
7735
- filterCypressCompletions,
7736
- findIssueTrackerProvider,
8214
+ ensurePopupDimensions,
7737
8215
  generateAlias,
7738
8216
  inferAssertionCommand,
7739
8217
  injectStyles,
@@ -7743,9 +8221,7 @@ var VERSION = "0.8.0";
7743
8221
  makeModalResizable,
7744
8222
  makeSwalDraggable,
7745
8223
  makeSwalDraggableByContentId,
7746
- persistenceService,
7747
8224
  resolveExpandDirection,
7748
- resolveTicketUrl,
7749
8225
  selectTestsForExport,
7750
8226
  setSwal2DataCyAttribute,
7751
8227
  showToast,