lib-e2e-cypress-for-dummys-ts 0.6.0 → 0.8.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
@@ -463,14 +463,20 @@ __export(index_exports, {
463
463
  ACTIVE_SESSION_BREADCRUMB_KEY: () => ACTIVE_SESSION_BREADCRUMB_KEY,
464
464
  AdvancedTestEditorElement: () => AdvancedTestEditorElement,
465
465
  AdvancedTestTransformationService: () => AdvancedTestTransformationService,
466
+ CHAIN_METHODS: () => CHAIN_METHODS,
467
+ CYPRESS_COMPLETIONS: () => CYPRESS_COMPLETIONS,
468
+ CY_METHODS: () => CY_METHODS,
466
469
  ConfigurationElement: () => ConfigurationElement,
467
470
  DB_SCHEMA: () => DB_SCHEMA,
468
471
  DB_STORE_NAMES: () => DB_STORE_NAMES,
472
+ DEFAULT_ISSUE_TRACKER_PROVIDER_ID: () => DEFAULT_ISSUE_TRACKER_PROVIDER_ID,
469
473
  DEFAULT_RESUME_TTL_MINUTES: () => DEFAULT_RESUME_TTL_MINUTES,
470
474
  DRAG_THRESHOLD: () => DRAG_THRESHOLD,
471
475
  FilePreviewElement: () => FilePreviewElement,
476
+ HelpPanelElement: () => HelpPanelElement,
472
477
  HttpMonitor: () => HttpMonitor,
473
478
  INPUT_TYPES: () => INPUT_TYPES,
479
+ ISSUE_TRACKER_PROVIDERS: () => ISSUE_TRACKER_PROVIDERS,
474
480
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES: () => LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
475
481
  LOCALE_BY_LANG: () => LOCALE_BY_LANG,
476
482
  LibE2eRecorderElement: () => LibE2eRecorderElement,
@@ -483,6 +489,7 @@ __export(index_exports, {
483
489
  SelectorPickerElement: () => SelectorPickerElement,
484
490
  Subject: () => Subject,
485
491
  TOGGLE_MARGIN: () => TOGGLE_MARGIN,
492
+ TOP_LEVEL: () => TOP_LEVEL,
486
493
  TestEditorElement: () => TestEditorElement,
487
494
  TestPrevisualizerElement: () => TestPrevisualizerElement,
488
495
  TransformationService: () => TransformationService,
@@ -491,9 +498,16 @@ __export(index_exports, {
491
498
  advancedTestTransformationService: () => advancedTestTransformationService,
492
499
  boxOffsetForDirection: () => boxOffsetForDirection,
493
500
  boxTopLeftFor: () => boxTopLeftFor,
501
+ buildTicketComment: () => buildTicketComment,
502
+ buildTicketUrl: () => buildTicketUrl,
494
503
  clampTogglePosition: () => clampTogglePosition,
504
+ createCodeEditor: () => createCodeEditor,
495
505
  defaultTogglePosition: () => defaultTogglePosition,
506
+ filterCompletions: () => filterCompletions,
507
+ filterCypressCompletions: () => filterCypressCompletions,
508
+ findIssueTrackerProvider: () => findIssueTrackerProvider,
496
509
  generateAlias: () => generateAlias,
510
+ inferAssertionCommand: () => inferAssertionCommand,
497
511
  injectStyles: () => injectStyles,
498
512
  isLang: () => isLang,
499
513
  isLocalHost: () => isLocalHost,
@@ -503,6 +517,7 @@ __export(index_exports, {
503
517
  makeSwalDraggableByContentId: () => makeSwalDraggableByContentId,
504
518
  persistenceService: () => persistenceService,
505
519
  resolveExpandDirection: () => resolveExpandDirection,
520
+ resolveTicketUrl: () => resolveTicketUrl,
506
521
  selectTestsForExport: () => selectTestsForExport,
507
522
  setSwal2DataCyAttribute: () => setSwal2DataCyAttribute,
508
523
  showToast: () => showToast,
@@ -601,6 +616,64 @@ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
601
616
  var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
602
617
  var DEFAULT_RESUME_TTL_MINUTES = 30;
603
618
 
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
+
604
677
  // src/utils/subject.ts
605
678
  var Subject = class {
606
679
  _value;
@@ -645,7 +718,14 @@ var I18N_ES = {
645
718
  SAVE_AND_EDIT: "\u{1F4DD} Guardar y editar",
646
719
  REMOVE_TAG_TITLE: "Quitar",
647
720
  NOTES_LABEL: "Descripci\xF3n / Notas (opcional):",
648
- NOTES_PLACEHOLDER: "Qu\xE9 valida este test, condiciones previas, etc."
721
+ 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."
649
729
  },
650
730
  TEST_EDITOR: {
651
731
  NO_TAGS: "Sin etiquetas",
@@ -659,7 +739,9 @@ var I18N_ES = {
659
739
  COPY_ICPS_BTN: "\u{1F4CB} Copiar interceptores",
660
740
  DEFAULT_DESCRIBE: "Suite de tests",
661
741
  NO_TESTS: "No hay tests guardados",
662
- DELETE_TITLE: "Eliminar"
742
+ DELETE_TITLE: "Eliminar",
743
+ GROUP_BY_TICKET: "\u{1F3AB} Agrupar por ticket",
744
+ NO_TICKET_GROUP: "Sin ticket"
663
745
  },
664
746
  CONFIG: {
665
747
  LANG_SECTION: "\u{1F310} Idioma",
@@ -667,6 +749,9 @@ var I18N_ES = {
667
749
  HTTP_SECTION: "\u26A1 HTTP Avanzado",
668
750
  HTTP_TITLE: "Validaciones de body",
669
751
  HTTP_SUB: "GET \u2192 response \xB7 POST/PUT \u2192 request",
752
+ FIXTURE_SECTION: "\u{1F9EA} Fixtures HTTP",
753
+ FIXTURE_TITLE: "Grabar respuestas como fixtures",
754
+ FIXTURE_SUB: "Las GET se guardan en cypress/fixtures y se stubean con cy.intercept({ fixture }).",
670
755
  SELECTOR_SECTION: "\u{1F3AF} Estrategia de selector",
671
756
  SELECTOR_LABEL: "Atributo preferido para cy.get()",
672
757
  SELECTOR_OPT_DATACY: "data-cy (por defecto)",
@@ -714,7 +799,27 @@ var I18N_ES = {
714
799
  WIDGET_POSITION_SECTION: "\u{1F9F2} Posici\xF3n del widget",
715
800
  WIDGET_POSITION_HINT: "Arrastra el bot\xF3n de grabar para moverlo donde no estorbe. Se recuerda su posici\xF3n.",
716
801
  WIDGET_POSITION_RESET_BTN: "\u21BA Restablecer posici\xF3n",
717
- WIDGET_POSITION_RESET_DONE: "\u2713 Posici\xF3n restablecida"
802
+ 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}"
718
823
  },
719
824
  SELECTOR_PICKER: {
720
825
  TITLE: "Selecciona un elemento del DOM",
@@ -803,6 +908,8 @@ var I18N_ES = {
803
908
  FILE_EDITOR_TITLE: "\u270F\uFE0F Editor de archivo",
804
909
  FILE_SAVED_TOAST: "\u2713 Archivo guardado",
805
910
  FILE_SAVE_ERROR_TOAST: "Error al guardar el archivo",
911
+ FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures escritos en cypress/fixtures",
912
+ FIXTURES_NO_FOLDER_TOAST: "No se pudieron escribir los fixtures (configura la carpeta Cypress)",
806
913
  BTN_CONFIG: "Config",
807
914
  BTN_FILES: "Ficheros",
808
915
  BTN_COMMANDS: "Comandos",
@@ -817,7 +924,76 @@ var I18N_ES = {
817
924
  SESSION_RESUME_TEXT: "Hay una grabaci\xF3n activa sin terminar.",
818
925
  SESSION_RESUME_COUNT: "comandos capturados",
819
926
  SESSION_CONTINUE_BTN: "Continuar grabando",
820
- SESSION_DISCARD_BTN: "Descartar"
927
+ SESSION_DISCARD_BTN: "Descartar",
928
+ BTN_HELP: "Ayuda"
929
+ },
930
+ HELP: {
931
+ TITLE: "\u2753 Ayuda \u2014 Gu\xEDa r\xE1pida",
932
+ INTRO: "Todo lo que puede hacer el grabador.",
933
+ SEC_RECORDING: "\u23FA Grabar",
934
+ RECORDING_1: "Pulsa \u23FA (o Ctrl+R) para empezar; \u23F9 para parar y guardar.",
935
+ RECORDING_2: "Pausa / reanuda con \u23F8 (o Ctrl+P).",
936
+ SEC_SHORTCUTS: "\u2328\uFE0F Atajos de teclado",
937
+ SC_RECORD: "Ctrl+R \u2014 Grabar / detener",
938
+ SC_PAUSE: "Ctrl+P \u2014 Pausar / reanudar",
939
+ SC_TESTS: "Ctrl+1 \u2014 Panel de tests guardados",
940
+ SC_COMMANDS: "Ctrl+2 \u2014 Panel de comandos",
941
+ SC_CONFIG: "Ctrl+3 \u2014 Configuraci\xF3n",
942
+ SC_VISIBILITY: "Ctrl+Shift+E \u2014 Mostrar / ocultar el widget",
943
+ SC_HELP: "Ctrl+Shift+H \u2014 Esta ayuda",
944
+ SEC_CAPTURED: "\u{1F3AF} Qu\xE9 se graba",
945
+ CAP_CLICK: "Click \u2192 .click()",
946
+ CAP_DBLCLICK: "Doble-click \u2192 .dblclick()",
947
+ CAP_RIGHTCLICK: "Click derecho \u2192 .rightclick()",
948
+ CAP_TYPE: "Escribir en un campo \u2192 .clear().type(...)",
949
+ CAP_CHECK: "Checkbox / radio \u2192 .check() / .uncheck()",
950
+ CAP_SELECT: "<select> \u2192 .select(...)",
951
+ CAP_KEYS: 'Enter / Escape \u2192 .type("{enter}") / ("{esc}")',
952
+ CAP_ROUTE: "Cambio de ruta (SPA) \u2192 cy.url().should(...)",
953
+ CAP_HTTP: "fetch / XHR \u2192 cy.intercept + cy.wait",
954
+ SEC_ASSERT: "\u2705 Aserciones",
955
+ ASSERT_1: "Alt+click sobre un elemento para verificarlo sin ejecutar su acci\xF3n (texto, valor, checked o visible).",
956
+ ASSERT_2: "O usa el constructor de aserciones en el panel de Comandos.",
957
+ SEC_PANELS: "\u{1F5C2} Paneles",
958
+ PANEL_TESTS: "\u{1F4CB} Tests \u2014 guardados: filtrar por tag, copiar, combinar, exportar.",
959
+ 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.",
961
+ PANEL_FILES: "\u{1F4C1} Ficheros \u2014 inserta tests en tus .cy.ts y lanza el runner.",
962
+ SEC_SELECTOR: "\u{1F50E} Selectores",
963
+ SELECTOR_1: "Elige el atributo preferido (data-cy, data-testid, aria-label, id) en Config.",
964
+ SELECTOR_2: "Si un click no tiene selector fiable, aparece un picker para elegir el mejor ancestro.",
965
+ SEC_DATA: "\u{1F4BE} Datos",
966
+ DATA_1: "Exporta tests a JSON (todo / selecci\xF3n / por tags).",
967
+ DATA_2: "Importa un JSON: se fusiona con lo existente (no se borra nada).",
968
+ SEC_ADVANCED: "\u{1F680} Avanzado",
969
+ ADV_INVISIBLE: "Modo invisible: start-hidden + Ctrl+Shift+E para grabar en producci\xF3n sin molestar.",
970
+ ADV_RUNNER: "Runner local: npx lib-e2e-cypress-runner para lanzar un spec headless desde el editor.",
971
+ ADV_CROSSAPP: "Cross-app: la grabaci\xF3n sobrevive al cambiar de micro-frontend (mismo origen).",
972
+ ADV_DRAG: "Arrastra el bot\xF3n \u23FA para mover el widget; recuerda su posici\xF3n.",
973
+ CLOSE: "Cerrar",
974
+ TAB_REFERENCE: "Referencia r\xE1pida",
975
+ TAB_GUIDE: "Gu\xEDa de uso",
976
+ GUIDE_INTRO: "C\xF3mo sacarle partido al grabador, qu\xE9 cubre y qu\xE9 no.",
977
+ G_SEC_WORKFLOW: "\u{1F9ED} Flujo de trabajo",
978
+ G_WF_1: "1. Pulsa \u23FA (o Ctrl+R) para empezar a grabar.",
979
+ G_WF_2: "2. Usa tu app como un usuario: clicks, formularios, navegaci\xF3n. Cada acci\xF3n se convierte en un comando Cypress.",
980
+ G_WF_3: "3. A\xF1ade verificaciones con Alt+click sobre lo que quieras comprobar.",
981
+ G_WF_4: "4. Pulsa \u23F9 para parar; ponle nombre y tags, y gu\xE1rdalo.",
982
+ G_WF_5: "5. En \u{1F4C1} Ficheros, inserta el test en un .cy.ts o l\xE1nzalo con el runner local.",
983
+ G_SEC_COVERED: "\u2705 Qu\xE9 cubre",
984
+ G_COV_1: "Clicks, doble-click, click derecho, inputs de texto, checkbox/radio, select y teclas Enter/Escape.",
985
+ G_COV_2: "Navegaci\xF3n SPA (pushState/replaceState/popstate) \u2192 cy.url().should(...).",
986
+ G_COV_3: "Llamadas fetch/XHR (GET/POST/PUT) \u2192 cy.intercept + cy.wait, con validaci\xF3n de body opcional.",
987
+ G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
988
+ G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
989
+ G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
990
+ G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
991
+ G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
992
+ G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
993
+ G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
994
+ 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)."
821
997
  }
822
998
  };
823
999
 
@@ -845,7 +1021,14 @@ var I18N_EN = {
845
1021
  SAVE_AND_EDIT: "\u{1F4DD} Save and edit",
846
1022
  REMOVE_TAG_TITLE: "Remove",
847
1023
  NOTES_LABEL: "Description / Notes (optional):",
848
- NOTES_PLACEHOLDER: "What this test validates, preconditions, etc."
1024
+ 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."
849
1032
  },
850
1033
  TEST_EDITOR: {
851
1034
  NO_TAGS: "No tags",
@@ -859,7 +1042,9 @@ var I18N_EN = {
859
1042
  COPY_ICPS_BTN: "\u{1F4CB} Copy interceptors",
860
1043
  DEFAULT_DESCRIBE: "Test suite",
861
1044
  NO_TESTS: "No saved tests",
862
- DELETE_TITLE: "Delete"
1045
+ DELETE_TITLE: "Delete",
1046
+ GROUP_BY_TICKET: "\u{1F3AB} Group by ticket",
1047
+ NO_TICKET_GROUP: "No ticket"
863
1048
  },
864
1049
  CONFIG: {
865
1050
  LANG_SECTION: "\u{1F310} Language",
@@ -867,6 +1052,9 @@ var I18N_EN = {
867
1052
  HTTP_SECTION: "\u26A1 Advanced HTTP",
868
1053
  HTTP_TITLE: "Body validations",
869
1054
  HTTP_SUB: "GET \u2192 response \xB7 POST/PUT \u2192 request",
1055
+ FIXTURE_SECTION: "\u{1F9EA} HTTP Fixtures",
1056
+ FIXTURE_TITLE: "Record responses as fixtures",
1057
+ FIXTURE_SUB: "GET responses are saved to cypress/fixtures and stubbed via cy.intercept({ fixture }).",
870
1058
  SELECTOR_SECTION: "\u{1F3AF} Selector strategy",
871
1059
  SELECTOR_LABEL: "Preferred attribute for cy.get()",
872
1060
  SELECTOR_OPT_DATACY: "data-cy (default)",
@@ -914,7 +1102,27 @@ var I18N_EN = {
914
1102
  WIDGET_POSITION_SECTION: "\u{1F9F2} Widget position",
915
1103
  WIDGET_POSITION_HINT: "Drag the record button to move it out of the way. Its position is remembered.",
916
1104
  WIDGET_POSITION_RESET_BTN: "\u21BA Reset position",
917
- WIDGET_POSITION_RESET_DONE: "\u2713 Position reset"
1105
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position reset",
1106
+ 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}"
918
1126
  },
919
1127
  SELECTOR_PICKER: {
920
1128
  TITLE: "Select a DOM element",
@@ -1003,6 +1211,8 @@ var I18N_EN = {
1003
1211
  FILE_EDITOR_TITLE: "\u270F\uFE0F File editor",
1004
1212
  FILE_SAVED_TOAST: "\u2713 File saved",
1005
1213
  FILE_SAVE_ERROR_TOAST: "Error saving the file",
1214
+ FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures written to cypress/fixtures",
1215
+ FIXTURES_NO_FOLDER_TOAST: "Could not write fixtures (configure the Cypress folder)",
1006
1216
  BTN_CONFIG: "Config",
1007
1217
  BTN_FILES: "Files",
1008
1218
  BTN_COMMANDS: "Commands",
@@ -1017,7 +1227,76 @@ var I18N_EN = {
1017
1227
  SESSION_RESUME_TEXT: "There is an unfinished active recording.",
1018
1228
  SESSION_RESUME_COUNT: "commands captured",
1019
1229
  SESSION_CONTINUE_BTN: "Keep recording",
1020
- SESSION_DISCARD_BTN: "Discard"
1230
+ SESSION_DISCARD_BTN: "Discard",
1231
+ BTN_HELP: "Help"
1232
+ },
1233
+ HELP: {
1234
+ TITLE: "\u2753 Help \u2014 Quick guide",
1235
+ INTRO: "Everything the recorder can do.",
1236
+ SEC_RECORDING: "\u23FA Recording",
1237
+ RECORDING_1: "Press \u23FA (or Ctrl+R) to start; \u23F9 to stop and save.",
1238
+ RECORDING_2: "Pause / resume with \u23F8 (or Ctrl+P).",
1239
+ SEC_SHORTCUTS: "\u2328\uFE0F Keyboard shortcuts",
1240
+ SC_RECORD: "Ctrl+R \u2014 Record / stop",
1241
+ SC_PAUSE: "Ctrl+P \u2014 Pause / resume",
1242
+ SC_TESTS: "Ctrl+1 \u2014 Saved tests panel",
1243
+ SC_COMMANDS: "Ctrl+2 \u2014 Commands panel",
1244
+ SC_CONFIG: "Ctrl+3 \u2014 Settings",
1245
+ SC_VISIBILITY: "Ctrl+Shift+E \u2014 Show / hide the widget",
1246
+ SC_HELP: "Ctrl+Shift+H \u2014 This help",
1247
+ SEC_CAPTURED: "\u{1F3AF} What gets recorded",
1248
+ CAP_CLICK: "Click \u2192 .click()",
1249
+ CAP_DBLCLICK: "Double-click \u2192 .dblclick()",
1250
+ CAP_RIGHTCLICK: "Right-click \u2192 .rightclick()",
1251
+ CAP_TYPE: "Type into a field \u2192 .clear().type(...)",
1252
+ CAP_CHECK: "Checkbox / radio \u2192 .check() / .uncheck()",
1253
+ CAP_SELECT: "<select> \u2192 .select(...)",
1254
+ CAP_KEYS: 'Enter / Escape \u2192 .type("{enter}") / ("{esc}")',
1255
+ CAP_ROUTE: "Route change (SPA) \u2192 cy.url().should(...)",
1256
+ CAP_HTTP: "fetch / XHR \u2192 cy.intercept + cy.wait",
1257
+ SEC_ASSERT: "\u2705 Assertions",
1258
+ ASSERT_1: "Alt+click an element to verify it without running its action (text, value, checked or visible).",
1259
+ ASSERT_2: "Or use the assertion builder in the Commands panel.",
1260
+ SEC_PANELS: "\u{1F5C2} Panels",
1261
+ PANEL_TESTS: "\u{1F4CB} Tests \u2014 saved tests: filter by tag, copy, combine, export.",
1262
+ 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.",
1264
+ PANEL_FILES: "\u{1F4C1} Files \u2014 insert tests into your .cy.ts and launch the runner.",
1265
+ SEC_SELECTOR: "\u{1F50E} Selectors",
1266
+ SELECTOR_1: "Choose the preferred attribute (data-cy, data-testid, aria-label, id) in Config.",
1267
+ SELECTOR_2: "If a click has no reliable selector, a picker appears to choose the best ancestor.",
1268
+ SEC_DATA: "\u{1F4BE} Data",
1269
+ DATA_1: "Export tests to JSON (all / selection / by tags).",
1270
+ DATA_2: "Import a JSON: it merges into your data (nothing is wiped).",
1271
+ SEC_ADVANCED: "\u{1F680} Advanced",
1272
+ ADV_INVISIBLE: "Invisible mode: start-hidden + Ctrl+Shift+E to record in production unobtrusively.",
1273
+ ADV_RUNNER: "Local runner: npx lib-e2e-cypress-runner to run a spec headless from the editor.",
1274
+ ADV_CROSSAPP: "Cross-app: the recording survives switching micro-frontend (same origin).",
1275
+ ADV_DRAG: "Drag the \u23FA button to move the widget; its position is remembered.",
1276
+ CLOSE: "Close",
1277
+ TAB_REFERENCE: "Quick reference",
1278
+ TAB_GUIDE: "Usage guide",
1279
+ GUIDE_INTRO: "How to get the most out of the recorder, and what it does / doesn't cover.",
1280
+ G_SEC_WORKFLOW: "\u{1F9ED} Workflow",
1281
+ G_WF_1: "1. Press \u23FA (or Ctrl+R) to start recording.",
1282
+ G_WF_2: "2. Use your app like a user: clicks, forms, navigation. Each action becomes a Cypress command.",
1283
+ G_WF_3: "3. Add checks with Alt+click on whatever you want to verify.",
1284
+ G_WF_4: "4. Press \u23F9 to stop; give it a name and tags, and save it.",
1285
+ G_WF_5: "5. In \u{1F4C1} Files, insert the test into a .cy.ts or run it with the local runner.",
1286
+ G_SEC_COVERED: "\u2705 What it covers",
1287
+ G_COV_1: "Clicks, double-click, right-click, text inputs, checkbox/radio, select and Enter/Escape keys.",
1288
+ G_COV_2: "SPA navigation (pushState/replaceState/popstate) \u2192 cy.url().should(...).",
1289
+ G_COV_3: "fetch/XHR calls (GET/POST/PUT) \u2192 cy.intercept + cy.wait, with optional body validation.",
1290
+ G_COV_4: "Reliable selector (data-cy/testid/aria/id) and a picker when none is obvious.",
1291
+ G_COV_5: "Automatic assertions (Alt+click) and the manual builder.",
1292
+ G_COV_6: "Continuity when switching micro-frontend (same origin), invisible mode and i18n.",
1293
+ G_SEC_NOTCOVERED: "\u{1F6AB} What it does NOT cover (yet)",
1294
+ G_NC_1: "Different origins / subdomains (same origin only).",
1295
+ G_NC_2: "File upload (selectFile), drag & drop, and hover.",
1296
+ G_NC_3: "The Tab key (Cypress can't .type() it).",
1297
+ 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)."
1021
1300
  }
1022
1301
  };
1023
1302
 
@@ -1045,7 +1324,14 @@ var I18N_FR = {
1045
1324
  SAVE_AND_EDIT: "\u{1F4DD} Enregistrer et \xE9diter",
1046
1325
  REMOVE_TAG_TITLE: "Retirer",
1047
1326
  NOTES_LABEL: "Description / Notes (optionnel) :",
1048
- NOTES_PLACEHOLDER: "Ce que ce test valide, conditions pr\xE9alables, etc."
1327
+ 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."
1049
1335
  },
1050
1336
  TEST_EDITOR: {
1051
1337
  NO_TAGS: "Sans \xE9tiquettes",
@@ -1059,7 +1345,9 @@ var I18N_FR = {
1059
1345
  COPY_ICPS_BTN: "\u{1F4CB} Copier les intercepteurs",
1060
1346
  DEFAULT_DESCRIBE: "Suite de tests",
1061
1347
  NO_TESTS: "Aucun test enregistr\xE9",
1062
- DELETE_TITLE: "Supprimer"
1348
+ DELETE_TITLE: "Supprimer",
1349
+ GROUP_BY_TICKET: "\u{1F3AB} Grouper par ticket",
1350
+ NO_TICKET_GROUP: "Sans ticket"
1063
1351
  },
1064
1352
  CONFIG: {
1065
1353
  LANG_SECTION: "\u{1F310} Langue",
@@ -1067,6 +1355,9 @@ var I18N_FR = {
1067
1355
  HTTP_SECTION: "\u26A1 HTTP Avanc\xE9",
1068
1356
  HTTP_TITLE: "Validations du corps",
1069
1357
  HTTP_SUB: "GET \u2192 r\xE9ponse \xB7 POST/PUT \u2192 requ\xEAte",
1358
+ FIXTURE_SECTION: "\u{1F9EA} Fixtures HTTP",
1359
+ FIXTURE_TITLE: "Enregistrer les r\xE9ponses comme fixtures",
1360
+ FIXTURE_SUB: "Les GET sont enregistr\xE9es dans cypress/fixtures et stub\xE9es via cy.intercept({ fixture }).",
1070
1361
  SELECTOR_SECTION: "\u{1F3AF} Strat\xE9gie de s\xE9lecteur",
1071
1362
  SELECTOR_LABEL: "Attribut pr\xE9f\xE9r\xE9 pour cy.get()",
1072
1363
  SELECTOR_OPT_DATACY: "data-cy (par d\xE9faut)",
@@ -1114,7 +1405,27 @@ var I18N_FR = {
1114
1405
  WIDGET_POSITION_SECTION: "\u{1F9F2} Position du widget",
1115
1406
  WIDGET_POSITION_HINT: "Fais glisser le bouton d'enregistrement pour le d\xE9placer. Sa position est m\xE9moris\xE9e.",
1116
1407
  WIDGET_POSITION_RESET_BTN: "\u21BA R\xE9initialiser la position",
1117
- WIDGET_POSITION_RESET_DONE: "\u2713 Position r\xE9initialis\xE9e"
1408
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position r\xE9initialis\xE9e",
1409
+ 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}"
1118
1429
  },
1119
1430
  SELECTOR_PICKER: {
1120
1431
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -1203,6 +1514,8 @@ var I18N_FR = {
1203
1514
  FILE_EDITOR_TITLE: "\u270F\uFE0F \xC9diteur de fichier",
1204
1515
  FILE_SAVED_TOAST: "\u2713 Fichier enregistr\xE9",
1205
1516
  FILE_SAVE_ERROR_TOAST: "Erreur lors de l'enregistrement",
1517
+ FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures \xE9crites dans cypress/fixtures",
1518
+ FIXTURES_NO_FOLDER_TOAST: "Impossible d'\xE9crire les fixtures (configure le dossier Cypress)",
1206
1519
  BTN_CONFIG: "Config",
1207
1520
  BTN_FILES: "Fichiers",
1208
1521
  BTN_COMMANDS: "Commandes",
@@ -1217,7 +1530,77 @@ var I18N_FR = {
1217
1530
  SESSION_RESUME_TEXT: "Un enregistrement actif est inachev\xE9.",
1218
1531
  SESSION_RESUME_COUNT: "commandes captur\xE9es",
1219
1532
  SESSION_CONTINUE_BTN: "Continuer l'enregistrement",
1220
- SESSION_DISCARD_BTN: "Ignorer"
1533
+ SESSION_DISCARD_BTN: "Ignorer",
1534
+ BTN_HELP: "Aide"
1535
+ },
1536
+ HELP: {
1537
+ TITLE: "\u2753 Aide \u2014 Guide rapide",
1538
+ INTRO: "Tout ce que l'enregistreur sait faire.",
1539
+ SEC_RECORDING: "\u23FA Enregistrer",
1540
+ RECORDING_1: "Appuie sur \u23FA (ou Ctrl+R) pour commencer ; \u23F9 pour arr\xEAter et enregistrer.",
1541
+ RECORDING_2: "Pause / reprise avec \u23F8 (ou Ctrl+P).",
1542
+ SEC_SHORTCUTS: "\u2328\uFE0F Raccourcis clavier",
1543
+ SC_RECORD: "Ctrl+R \u2014 Enregistrer / arr\xEAter",
1544
+ SC_PAUSE: "Ctrl+P \u2014 Pause / reprise",
1545
+ SC_TESTS: "Ctrl+1 \u2014 Panneau des tests enregistr\xE9s",
1546
+ SC_COMMANDS: "Ctrl+2 \u2014 Panneau des commandes",
1547
+ SC_CONFIG: "Ctrl+3 \u2014 Param\xE8tres",
1548
+ SC_VISIBILITY: "Ctrl+Shift+E \u2014 Afficher / masquer le widget",
1549
+ SC_HELP: "Ctrl+Shift+H \u2014 Cette aide",
1550
+ SEC_CAPTURED: "\u{1F3AF} Ce qui est enregistr\xE9",
1551
+ CAP_CLICK: "Clic \u2192 .click()",
1552
+ CAP_DBLCLICK: "Double-clic \u2192 .dblclick()",
1553
+ CAP_RIGHTCLICK: "Clic droit \u2192 .rightclick()",
1554
+ CAP_TYPE: "Saisie dans un champ \u2192 .clear().type(...)",
1555
+ CAP_CHECK: "Case \xE0 cocher / radio \u2192 .check() / .uncheck()",
1556
+ CAP_SELECT: "<select> \u2192 .select(...)",
1557
+ CAP_KEYS: 'Entr\xE9e / \xC9chap \u2192 .type("{enter}") / ("{esc}")',
1558
+ CAP_ROUTE: "Changement de route (SPA) \u2192 cy.url().should(...)",
1559
+ CAP_HTTP: "fetch / XHR \u2192 cy.intercept + cy.wait",
1560
+ SEC_ASSERT: "\u2705 Assertions",
1561
+ ASSERT_1: "Alt+clic sur un \xE9l\xE9ment pour le v\xE9rifier sans ex\xE9cuter son action (texte, valeur, coch\xE9 ou visible).",
1562
+ ASSERT_2: "Ou utilise le constructeur d'assertions dans le panneau Commandes.",
1563
+ SEC_PANELS: "\u{1F5C2} Panneaux",
1564
+ PANEL_TESTS: "\u{1F4CB} Tests \u2014 enregistr\xE9s : filtrer par tag, copier, combiner, exporter.",
1565
+ 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.",
1567
+ PANEL_FILES: "\u{1F4C1} Fichiers \u2014 ins\xE8re des tests dans tes .cy.ts et lance le runner.",
1568
+ SEC_SELECTOR: "\u{1F50E} S\xE9lecteurs",
1569
+ SELECTOR_1: "Choisis l'attribut pr\xE9f\xE9r\xE9 (data-cy, data-testid, aria-label, id) dans Config.",
1570
+ SELECTOR_2: "Si un clic n'a pas de s\xE9lecteur fiable, un s\xE9lecteur appara\xEEt pour choisir le meilleur anc\xEAtre.",
1571
+ SEC_DATA: "\u{1F4BE} Donn\xE9es",
1572
+ DATA_1: "Exporte les tests en JSON (tout / s\xE9lection / par tags).",
1573
+ DATA_2: "Importe un JSON : il fusionne avec tes donn\xE9es (rien n'est effac\xE9).",
1574
+ SEC_ADVANCED: "\u{1F680} Avanc\xE9",
1575
+ ADV_INVISIBLE: "Mode invisible : start-hidden + Ctrl+Shift+E pour enregistrer en production discr\xE8tement.",
1576
+ ADV_RUNNER: "Runner local : npx lib-e2e-cypress-runner pour lancer un spec headless depuis l'\xE9diteur.",
1577
+ ADV_CROSSAPP: "Cross-app : l'enregistrement survit au changement de micro-frontend (m\xEAme origine).",
1578
+ ADV_DRAG: "Fais glisser le bouton \u23FA pour d\xE9placer le widget ; sa position est m\xE9moris\xE9e.",
1579
+ CLOSE: "Fermer",
1580
+ TAB_REFERENCE: "R\xE9f\xE9rence rapide",
1581
+ TAB_GUIDE: "Guide d'utilisation",
1582
+ // TODO: translate (fr) — usage guide body kept in Spanish for now.
1583
+ GUIDE_INTRO: "C\xF3mo sacarle partido al grabador, qu\xE9 cubre y qu\xE9 no.",
1584
+ G_SEC_WORKFLOW: "\u{1F9ED} Flujo de trabajo",
1585
+ G_WF_1: "1. Pulsa \u23FA (o Ctrl+R) para empezar a grabar.",
1586
+ G_WF_2: "2. Usa tu app como un usuario: clicks, formularios, navegaci\xF3n. Cada acci\xF3n se convierte en un comando Cypress.",
1587
+ G_WF_3: "3. A\xF1ade verificaciones con Alt+click sobre lo que quieras comprobar.",
1588
+ G_WF_4: "4. Pulsa \u23F9 para parar; ponle nombre y tags, y gu\xE1rdalo.",
1589
+ G_WF_5: "5. En \u{1F4C1} Ficheros, inserta el test en un .cy.ts o l\xE1nzalo con el runner local.",
1590
+ G_SEC_COVERED: "\u2705 Qu\xE9 cubre",
1591
+ G_COV_1: "Clicks, doble-click, click derecho, inputs de texto, checkbox/radio, select y teclas Enter/Escape.",
1592
+ G_COV_2: "Navegaci\xF3n SPA (pushState/replaceState/popstate) \u2192 cy.url().should(...).",
1593
+ G_COV_3: "Llamadas fetch/XHR (GET/POST/PUT) \u2192 cy.intercept + cy.wait, con validaci\xF3n de body opcional.",
1594
+ G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1595
+ G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1596
+ G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1597
+ G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1598
+ G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1599
+ G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1600
+ G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1601
+ 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)."
1221
1604
  }
1222
1605
  };
1223
1606
 
@@ -1245,7 +1628,14 @@ var I18N_IT = {
1245
1628
  SAVE_AND_EDIT: "\u{1F4DD} Salva e modifica",
1246
1629
  REMOVE_TAG_TITLE: "Rimuovi",
1247
1630
  NOTES_LABEL: "Descrizione / Note (opzionale):",
1248
- NOTES_PLACEHOLDER: "Cosa valida questo test, condizioni preliminari, ecc."
1631
+ 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."
1249
1639
  },
1250
1640
  TEST_EDITOR: {
1251
1641
  NO_TAGS: "Nessuna etichetta",
@@ -1259,7 +1649,9 @@ var I18N_IT = {
1259
1649
  COPY_ICPS_BTN: "\u{1F4CB} Copia interceptor",
1260
1650
  DEFAULT_DESCRIBE: "Suite di test",
1261
1651
  NO_TESTS: "Nessun test salvato",
1262
- DELETE_TITLE: "Elimina"
1652
+ DELETE_TITLE: "Elimina",
1653
+ GROUP_BY_TICKET: "\u{1F3AB} Raggruppa per ticket",
1654
+ NO_TICKET_GROUP: "Senza ticket"
1263
1655
  },
1264
1656
  CONFIG: {
1265
1657
  LANG_SECTION: "\u{1F310} Lingua",
@@ -1267,6 +1659,9 @@ var I18N_IT = {
1267
1659
  HTTP_SECTION: "\u26A1 HTTP Avanzato",
1268
1660
  HTTP_TITLE: "Validazioni del body",
1269
1661
  HTTP_SUB: "GET \u2192 risposta \xB7 POST/PUT \u2192 richiesta",
1662
+ FIXTURE_SECTION: "\u{1F9EA} Fixture HTTP",
1663
+ FIXTURE_TITLE: "Registra le risposte come fixture",
1664
+ FIXTURE_SUB: "Le GET vengono salvate in cypress/fixtures e stubbate con cy.intercept({ fixture }).",
1270
1665
  SELECTOR_SECTION: "\u{1F3AF} Strategia selettore",
1271
1666
  SELECTOR_LABEL: "Attributo preferito per cy.get()",
1272
1667
  SELECTOR_OPT_DATACY: "data-cy (predefinito)",
@@ -1314,7 +1709,27 @@ var I18N_IT = {
1314
1709
  WIDGET_POSITION_SECTION: "\u{1F9F2} Posizione del widget",
1315
1710
  WIDGET_POSITION_HINT: "Trascina il pulsante di registrazione per spostarlo. La posizione viene ricordata.",
1316
1711
  WIDGET_POSITION_RESET_BTN: "\u21BA Reimposta posizione",
1317
- WIDGET_POSITION_RESET_DONE: "\u2713 Posizione reimpostata"
1712
+ 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}"
1318
1733
  },
1319
1734
  SELECTOR_PICKER: {
1320
1735
  TITLE: "Seleziona un elemento DOM",
@@ -1403,6 +1818,8 @@ var I18N_IT = {
1403
1818
  FILE_EDITOR_TITLE: "\u270F\uFE0F Editor di file",
1404
1819
  FILE_SAVED_TOAST: "\u2713 File salvato",
1405
1820
  FILE_SAVE_ERROR_TOAST: "Errore durante il salvataggio",
1821
+ FIXTURES_WRITTEN_TOAST: "\u2713 Fixture scritte in cypress/fixtures",
1822
+ FIXTURES_NO_FOLDER_TOAST: "Impossibile scrivere le fixture (configura la cartella Cypress)",
1406
1823
  BTN_CONFIG: "Config",
1407
1824
  BTN_FILES: "File",
1408
1825
  BTN_COMMANDS: "Comandi",
@@ -1417,7 +1834,77 @@ var I18N_IT = {
1417
1834
  SESSION_RESUME_TEXT: "C'\xE8 una registrazione attiva non terminata.",
1418
1835
  SESSION_RESUME_COUNT: "comandi catturati",
1419
1836
  SESSION_CONTINUE_BTN: "Continua a registrare",
1420
- SESSION_DISCARD_BTN: "Annulla"
1837
+ SESSION_DISCARD_BTN: "Annulla",
1838
+ BTN_HELP: "Aiuto"
1839
+ },
1840
+ HELP: {
1841
+ TITLE: "\u2753 Aiuto \u2014 Guida rapida",
1842
+ INTRO: "Tutto ci\xF2 che il registratore sa fare.",
1843
+ SEC_RECORDING: "\u23FA Registrare",
1844
+ RECORDING_1: "Premi \u23FA (o Ctrl+R) per iniziare; \u23F9 per fermare e salvare.",
1845
+ RECORDING_2: "Pausa / ripresa con \u23F8 (o Ctrl+P).",
1846
+ SEC_SHORTCUTS: "\u2328\uFE0F Scorciatoie da tastiera",
1847
+ SC_RECORD: "Ctrl+R \u2014 Registra / ferma",
1848
+ SC_PAUSE: "Ctrl+P \u2014 Pausa / ripresa",
1849
+ SC_TESTS: "Ctrl+1 \u2014 Pannello test salvati",
1850
+ SC_COMMANDS: "Ctrl+2 \u2014 Pannello comandi",
1851
+ SC_CONFIG: "Ctrl+3 \u2014 Impostazioni",
1852
+ SC_VISIBILITY: "Ctrl+Shift+E \u2014 Mostra / nascondi il widget",
1853
+ SC_HELP: "Ctrl+Shift+H \u2014 Questo aiuto",
1854
+ SEC_CAPTURED: "\u{1F3AF} Cosa viene registrato",
1855
+ CAP_CLICK: "Click \u2192 .click()",
1856
+ CAP_DBLCLICK: "Doppio click \u2192 .dblclick()",
1857
+ CAP_RIGHTCLICK: "Click destro \u2192 .rightclick()",
1858
+ CAP_TYPE: "Digitare in un campo \u2192 .clear().type(...)",
1859
+ CAP_CHECK: "Checkbox / radio \u2192 .check() / .uncheck()",
1860
+ CAP_SELECT: "<select> \u2192 .select(...)",
1861
+ CAP_KEYS: 'Invio / Esc \u2192 .type("{enter}") / ("{esc}")',
1862
+ CAP_ROUTE: "Cambio di rotta (SPA) \u2192 cy.url().should(...)",
1863
+ CAP_HTTP: "fetch / XHR \u2192 cy.intercept + cy.wait",
1864
+ SEC_ASSERT: "\u2705 Asserzioni",
1865
+ ASSERT_1: "Alt+click su un elemento per verificarlo senza eseguire la sua azione (testo, valore, checked o visibile).",
1866
+ ASSERT_2: "Oppure usa il costruttore di asserzioni nel pannello Comandi.",
1867
+ SEC_PANELS: "\u{1F5C2} Pannelli",
1868
+ PANEL_TESTS: "\u{1F4CB} Test \u2014 salvati: filtra per tag, copia, combina, esporta.",
1869
+ 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.",
1871
+ PANEL_FILES: "\u{1F4C1} File \u2014 inserisci test nei tuoi .cy.ts e avvia il runner.",
1872
+ SEC_SELECTOR: "\u{1F50E} Selettori",
1873
+ SELECTOR_1: "Scegli l'attributo preferito (data-cy, data-testid, aria-label, id) in Config.",
1874
+ SELECTOR_2: "Se un click non ha un selettore affidabile, appare un picker per scegliere l'antenato migliore.",
1875
+ SEC_DATA: "\u{1F4BE} Dati",
1876
+ DATA_1: "Esporta i test in JSON (tutto / selezione / per tag).",
1877
+ DATA_2: "Importa un JSON: si fonde con i tuoi dati (non si cancella nulla).",
1878
+ SEC_ADVANCED: "\u{1F680} Avanzato",
1879
+ ADV_INVISIBLE: "Modalit\xE0 invisibile: start-hidden + Ctrl+Shift+E per registrare in produzione senza disturbare.",
1880
+ ADV_RUNNER: "Runner locale: npx lib-e2e-cypress-runner per eseguire uno spec headless dall'editor.",
1881
+ ADV_CROSSAPP: "Cross-app: la registrazione sopravvive al cambio di micro-frontend (stessa origine).",
1882
+ ADV_DRAG: "Trascina il pulsante \u23FA per spostare il widget; la posizione viene ricordata.",
1883
+ CLOSE: "Chiudi",
1884
+ TAB_REFERENCE: "Riferimento rapido",
1885
+ TAB_GUIDE: "Guida all'uso",
1886
+ // TODO: translate (it) — usage guide body kept in Spanish for now.
1887
+ GUIDE_INTRO: "C\xF3mo sacarle partido al grabador, qu\xE9 cubre y qu\xE9 no.",
1888
+ G_SEC_WORKFLOW: "\u{1F9ED} Flujo de trabajo",
1889
+ G_WF_1: "1. Pulsa \u23FA (o Ctrl+R) para empezar a grabar.",
1890
+ G_WF_2: "2. Usa tu app como un usuario: clicks, formularios, navegaci\xF3n. Cada acci\xF3n se convierte en un comando Cypress.",
1891
+ G_WF_3: "3. A\xF1ade verificaciones con Alt+click sobre lo que quieras comprobar.",
1892
+ G_WF_4: "4. Pulsa \u23F9 para parar; ponle nombre y tags, y gu\xE1rdalo.",
1893
+ G_WF_5: "5. En \u{1F4C1} Ficheros, inserta el test en un .cy.ts o l\xE1nzalo con el runner local.",
1894
+ G_SEC_COVERED: "\u2705 Qu\xE9 cubre",
1895
+ G_COV_1: "Clicks, doble-click, click derecho, inputs de texto, checkbox/radio, select y teclas Enter/Escape.",
1896
+ G_COV_2: "Navegaci\xF3n SPA (pushState/replaceState/popstate) \u2192 cy.url().should(...).",
1897
+ G_COV_3: "Llamadas fetch/XHR (GET/POST/PUT) \u2192 cy.intercept + cy.wait, con validaci\xF3n de body opcional.",
1898
+ G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1899
+ G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1900
+ G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1901
+ G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1902
+ G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1903
+ G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1904
+ G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1905
+ 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)."
1421
1908
  }
1422
1909
  };
1423
1910
 
@@ -1445,7 +1932,14 @@ var I18N_DE = {
1445
1932
  SAVE_AND_EDIT: "\u{1F4DD} Speichern und bearbeiten",
1446
1933
  REMOVE_TAG_TITLE: "Entfernen",
1447
1934
  NOTES_LABEL: "Beschreibung / Notizen (optional):",
1448
- NOTES_PLACEHOLDER: "Was dieser Test validiert, Voraussetzungen usw."
1935
+ 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."
1449
1943
  },
1450
1944
  TEST_EDITOR: {
1451
1945
  NO_TAGS: "Keine Tags",
@@ -1459,7 +1953,9 @@ var I18N_DE = {
1459
1953
  COPY_ICPS_BTN: "\u{1F4CB} Interceptors kopieren",
1460
1954
  DEFAULT_DESCRIBE: "Test-Suite",
1461
1955
  NO_TESTS: "Keine gespeicherten Tests",
1462
- DELETE_TITLE: "L\xF6schen"
1956
+ DELETE_TITLE: "L\xF6schen",
1957
+ GROUP_BY_TICKET: "\u{1F3AB} Nach Ticket gruppieren",
1958
+ NO_TICKET_GROUP: "Kein Ticket"
1463
1959
  },
1464
1960
  CONFIG: {
1465
1961
  LANG_SECTION: "\u{1F310} Sprache",
@@ -1467,6 +1963,9 @@ var I18N_DE = {
1467
1963
  HTTP_SECTION: "\u26A1 Erweitertes HTTP",
1468
1964
  HTTP_TITLE: "Body-Validierungen",
1469
1965
  HTTP_SUB: "GET \u2192 Antwort \xB7 POST/PUT \u2192 Anfrage",
1966
+ FIXTURE_SECTION: "\u{1F9EA} HTTP-Fixtures",
1967
+ FIXTURE_TITLE: "Antworten als Fixtures aufzeichnen",
1968
+ FIXTURE_SUB: "GET-Antworten werden in cypress/fixtures gespeichert und mit cy.intercept({ fixture }) gestubbt.",
1470
1969
  SELECTOR_SECTION: "\u{1F3AF} Selector-Strategie",
1471
1970
  SELECTOR_LABEL: "Bevorzugtes Attribut f\xFCr cy.get()",
1472
1971
  SELECTOR_OPT_DATACY: "data-cy (Standard)",
@@ -1514,7 +2013,27 @@ var I18N_DE = {
1514
2013
  WIDGET_POSITION_SECTION: "\u{1F9F2} Widget-Position",
1515
2014
  WIDGET_POSITION_HINT: "Ziehe den Aufnahme-Button, um ihn aus dem Weg zu schieben. Die Position wird gespeichert.",
1516
2015
  WIDGET_POSITION_RESET_BTN: "\u21BA Position zur\xFCcksetzen",
1517
- WIDGET_POSITION_RESET_DONE: "\u2713 Position zur\xFCckgesetzt"
2016
+ WIDGET_POSITION_RESET_DONE: "\u2713 Position zur\xFCckgesetzt",
2017
+ 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}"
1518
2037
  },
1519
2038
  SELECTOR_PICKER: {
1520
2039
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -1603,6 +2122,8 @@ var I18N_DE = {
1603
2122
  FILE_EDITOR_TITLE: "\u270F\uFE0F Datei-Editor",
1604
2123
  FILE_SAVED_TOAST: "\u2713 Datei gespeichert",
1605
2124
  FILE_SAVE_ERROR_TOAST: "Fehler beim Speichern",
2125
+ FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures in cypress/fixtures geschrieben",
2126
+ FIXTURES_NO_FOLDER_TOAST: "Fixtures konnten nicht geschrieben werden (Cypress-Ordner konfigurieren)",
1606
2127
  BTN_CONFIG: "Config",
1607
2128
  BTN_FILES: "Dateien",
1608
2129
  BTN_COMMANDS: "Befehle",
@@ -1617,7 +2138,77 @@ var I18N_DE = {
1617
2138
  SESSION_RESUME_TEXT: "Es gibt eine nicht abgeschlossene aktive Aufzeichnung.",
1618
2139
  SESSION_RESUME_COUNT: "erfasste Befehle",
1619
2140
  SESSION_CONTINUE_BTN: "Weiter aufzeichnen",
1620
- SESSION_DISCARD_BTN: "Verwerfen"
2141
+ SESSION_DISCARD_BTN: "Verwerfen",
2142
+ BTN_HELP: "Hilfe"
2143
+ },
2144
+ HELP: {
2145
+ TITLE: "\u2753 Hilfe \u2014 Kurzanleitung",
2146
+ INTRO: "Alles, was der Recorder kann.",
2147
+ SEC_RECORDING: "\u23FA Aufzeichnen",
2148
+ RECORDING_1: "Dr\xFCcke \u23FA (oder Ctrl+R) zum Starten; \u23F9 zum Stoppen und Speichern.",
2149
+ RECORDING_2: "Pause / Fortsetzen mit \u23F8 (oder Ctrl+P).",
2150
+ SEC_SHORTCUTS: "\u2328\uFE0F Tastenk\xFCrzel",
2151
+ SC_RECORD: "Ctrl+R \u2014 Aufzeichnen / stoppen",
2152
+ SC_PAUSE: "Ctrl+P \u2014 Pausieren / fortsetzen",
2153
+ SC_TESTS: "Ctrl+1 \u2014 Panel gespeicherte Tests",
2154
+ SC_COMMANDS: "Ctrl+2 \u2014 Befehle-Panel",
2155
+ SC_CONFIG: "Ctrl+3 \u2014 Einstellungen",
2156
+ SC_VISIBILITY: "Ctrl+Shift+E \u2014 Widget ein-/ausblenden",
2157
+ SC_HELP: "Ctrl+Shift+H \u2014 Diese Hilfe",
2158
+ SEC_CAPTURED: "\u{1F3AF} Was aufgezeichnet wird",
2159
+ CAP_CLICK: "Klick \u2192 .click()",
2160
+ CAP_DBLCLICK: "Doppelklick \u2192 .dblclick()",
2161
+ CAP_RIGHTCLICK: "Rechtsklick \u2192 .rightclick()",
2162
+ CAP_TYPE: "In ein Feld tippen \u2192 .clear().type(...)",
2163
+ CAP_CHECK: "Checkbox / Radio \u2192 .check() / .uncheck()",
2164
+ CAP_SELECT: "<select> \u2192 .select(...)",
2165
+ CAP_KEYS: 'Enter / Escape \u2192 .type("{enter}") / ("{esc}")',
2166
+ CAP_ROUTE: "Routenwechsel (SPA) \u2192 cy.url().should(...)",
2167
+ CAP_HTTP: "fetch / XHR \u2192 cy.intercept + cy.wait",
2168
+ SEC_ASSERT: "\u2705 Assertions",
2169
+ ASSERT_1: "Alt+Klick auf ein Element, um es zu pr\xFCfen, ohne seine Aktion auszul\xF6sen (Text, Wert, checked oder sichtbar).",
2170
+ ASSERT_2: "Oder nutze den Assertion-Builder im Befehle-Panel.",
2171
+ SEC_PANELS: "\u{1F5C2} Panels",
2172
+ PANEL_TESTS: "\u{1F4CB} Tests \u2014 gespeichert: nach Tag filtern, kopieren, kombinieren, exportieren.",
2173
+ 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.",
2175
+ PANEL_FILES: "\u{1F4C1} Dateien \u2014 Tests in deine .cy.ts einf\xFCgen und den Runner starten.",
2176
+ SEC_SELECTOR: "\u{1F50E} Selektoren",
2177
+ SELECTOR_1: "W\xE4hle das bevorzugte Attribut (data-cy, data-testid, aria-label, id) unter Config.",
2178
+ SELECTOR_2: "Hat ein Klick keinen zuverl\xE4ssigen Selektor, erscheint ein Picker zur Wahl des besten Vorfahren.",
2179
+ SEC_DATA: "\u{1F4BE} Daten",
2180
+ DATA_1: "Tests als JSON exportieren (alle / Auswahl / nach Tags).",
2181
+ DATA_2: "JSON importieren: wird zu deinen Daten hinzugef\xFCgt (nichts wird gel\xF6scht).",
2182
+ SEC_ADVANCED: "\u{1F680} Erweitert",
2183
+ ADV_INVISIBLE: "Unsichtbarer Modus: start-hidden + Ctrl+Shift+E, um in Produktion unauff\xE4llig aufzuzeichnen.",
2184
+ ADV_RUNNER: "Lokaler Runner: npx lib-e2e-cypress-runner, um einen Spec headless aus dem Editor zu starten.",
2185
+ ADV_CROSSAPP: "Cross-App: die Aufzeichnung \xFCbersteht den Wechsel des Micro-Frontends (gleicher Origin).",
2186
+ ADV_DRAG: "Ziehe den \u23FA-Button, um das Widget zu verschieben; die Position wird gemerkt.",
2187
+ CLOSE: "Schlie\xDFen",
2188
+ TAB_REFERENCE: "Kurzreferenz",
2189
+ TAB_GUIDE: "Nutzungsleitfaden",
2190
+ // TODO: translate (de) — usage guide body kept in Spanish for now.
2191
+ GUIDE_INTRO: "C\xF3mo sacarle partido al grabador, qu\xE9 cubre y qu\xE9 no.",
2192
+ G_SEC_WORKFLOW: "\u{1F9ED} Flujo de trabajo",
2193
+ G_WF_1: "1. Pulsa \u23FA (o Ctrl+R) para empezar a grabar.",
2194
+ G_WF_2: "2. Usa tu app como un usuario: clicks, formularios, navegaci\xF3n. Cada acci\xF3n se convierte en un comando Cypress.",
2195
+ G_WF_3: "3. A\xF1ade verificaciones con Alt+click sobre lo que quieras comprobar.",
2196
+ G_WF_4: "4. Pulsa \u23F9 para parar; ponle nombre y tags, y gu\xE1rdalo.",
2197
+ G_WF_5: "5. En \u{1F4C1} Ficheros, inserta el test en un .cy.ts o l\xE1nzalo con el runner local.",
2198
+ G_SEC_COVERED: "\u2705 Qu\xE9 cubre",
2199
+ G_COV_1: "Clicks, doble-click, click derecho, inputs de texto, checkbox/radio, select y teclas Enter/Escape.",
2200
+ G_COV_2: "Navegaci\xF3n SPA (pushState/replaceState/popstate) \u2192 cy.url().should(...).",
2201
+ G_COV_3: "Llamadas fetch/XHR (GET/POST/PUT) \u2192 cy.intercept + cy.wait, con validaci\xF3n de body opcional.",
2202
+ G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
2203
+ G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
2204
+ G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
2205
+ G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
2206
+ G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
2207
+ G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
2208
+ G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
2209
+ 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)."
1621
2212
  }
1622
2213
  };
1623
2214
 
@@ -1750,6 +2341,32 @@ var advancedTestTransformationService = new AdvancedTestTransformationService();
1750
2341
 
1751
2342
  // src/services/recording.service.ts
1752
2343
  init_selector_quality_utils();
2344
+
2345
+ // src/utils/assertion.utils.ts
2346
+ var MAX_TEXT_LENGTH = 60;
2347
+ function inferAssertionCommand(el, selector) {
2348
+ const should = (body) => `cy.get('${selector}').should(${body})`;
2349
+ const tag = el.tagName.toLowerCase();
2350
+ if (tag === "input") {
2351
+ const input = el;
2352
+ const type = (input.getAttribute("type") ?? "text").toLowerCase();
2353
+ if (type === "checkbox" || type === "radio") {
2354
+ return should(`'${input.checked ? "be.checked" : "not.be.checked"}'`);
2355
+ }
2356
+ return input.value ? should(`'have.value', '${escapeSingleQuotes(input.value)}'`) : should(`'be.visible'`);
2357
+ }
2358
+ if (tag === "textarea" || tag === "select") {
2359
+ const value = el.value;
2360
+ return value ? should(`'have.value', '${escapeSingleQuotes(value)}'`) : should(`'be.visible'`);
2361
+ }
2362
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2363
+ if (text && text.length <= MAX_TEXT_LENGTH) {
2364
+ return should(`'contain.text', '${escapeSingleQuotes(text)}'`);
2365
+ }
2366
+ return should(`'be.visible'`);
2367
+ }
2368
+
2369
+ // src/services/recording.service.ts
1753
2370
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
1754
2371
  function createSessionId() {
1755
2372
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -1764,6 +2381,7 @@ var RecordingService = class {
1764
2381
  isPaused$ = new Subject(false);
1765
2382
  selectorNotFound$ = new Subject(null);
1766
2383
  inputDebounceTimers = /* @__PURE__ */ new Map();
2384
+ fixtures = /* @__PURE__ */ new Map();
1767
2385
  abort = new AbortController();
1768
2386
  selectorStrategy = "data-cy";
1769
2387
  /** Stable id of the current live session (null when none has started). */
@@ -1773,9 +2391,13 @@ var RecordingService = class {
1773
2391
  origPushState = history.pushState.bind(history);
1774
2392
  origReplaceState = history.replaceState.bind(history);
1775
2393
  constructor() {
2394
+ this.listenToAssertClicks();
1776
2395
  this.listenToClicks();
2396
+ this.listenToDoubleClicks();
2397
+ this.listenToContextMenu();
1777
2398
  this.listenToInput();
1778
2399
  this.listenToSelect();
2400
+ this.listenToKeys();
1779
2401
  this.listenToRouteChanges();
1780
2402
  }
1781
2403
  // ── Public API ────────────────────────────────────────────────────────────
@@ -1866,17 +2488,27 @@ var RecordingService = class {
1866
2488
  if (index < 0 || index >= ints.length) return;
1867
2489
  this.interceptors$.next([...ints.slice(0, index), ...ints.slice(index + 1)]);
1868
2490
  }
1869
- registerInterceptor(method, url, alias) {
1870
- if (this.isPaused$.getValue()) return;
1871
- const command = `cy.intercept('${method}', '${this.urlToWildcard(url, method)}').as('${alias}')`;
2491
+ registerInterceptor(method, url, alias, fixtureFile) {
2492
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2493
+ const wildcard = this.urlToWildcard(url, method);
2494
+ const command = fixtureFile ? `cy.intercept('${method}', '${wildcard}', { fixture: '${fixtureFile}' }).as('${alias}')` : `cy.intercept('${method}', '${wildcard}').as('${alias}')`;
1872
2495
  const current = this.interceptors$.getValue();
1873
2496
  if (!current.includes(command)) {
1874
2497
  this.interceptors$.next([...current, command]);
1875
2498
  }
1876
2499
  }
2500
+ /** Registers a captured response as a Cypress fixture (name → JSON content). */
2501
+ registerFixture(name, content) {
2502
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2503
+ this.fixtures.set(name, content);
2504
+ }
2505
+ getFixturesSnapshot() {
2506
+ return [...this.fixtures.entries()].map(([name, content]) => ({ name, content }));
2507
+ }
1877
2508
  clearCommands() {
1878
2509
  this.commands$.next([]);
1879
2510
  this.interceptors$.next([]);
2511
+ this.fixtures.clear();
1880
2512
  }
1881
2513
  clearInterceptors() {
1882
2514
  this.interceptors$.next([]);
@@ -1930,6 +2562,39 @@ var RecordingService = class {
1930
2562
  this.inputDebounceTimers.clear();
1931
2563
  }
1932
2564
  // ── DOM listeners ─────────────────────────────────────────────────────────
2565
+ /**
2566
+ * Alt+click captures an ASSERTION for the element instead of a click, and
2567
+ * suppresses the element's real action. Runs in the capture phase so it fires
2568
+ * before the app's handlers and before the bubble-phase click listener below
2569
+ * (stopImmediatePropagation prevents a duplicate `.click()`). See spec 009.
2570
+ */
2571
+ listenToAssertClicks() {
2572
+ document.addEventListener(
2573
+ "click",
2574
+ (e) => {
2575
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2576
+ if (!e.altKey) return;
2577
+ const target = e.target;
2578
+ if (!target || this.isOwnElement(target)) return;
2579
+ const tag = target.tagName?.toLowerCase();
2580
+ if (tag === "body" || tag === "html") return;
2581
+ const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2582
+ if (!container) return;
2583
+ const selector = this.getReliableSelector(container);
2584
+ if (!selector || this.isOwnSelector(selector)) return;
2585
+ e.preventDefault();
2586
+ e.stopImmediatePropagation();
2587
+ const emit = () => this.addCommand(inferAssertionCommand(container, selector));
2588
+ const type = (container.getAttribute("type") ?? "").toLowerCase();
2589
+ if (container.tagName.toLowerCase() === "input" && (type === "checkbox" || type === "radio")) {
2590
+ queueMicrotask(emit);
2591
+ } else {
2592
+ emit();
2593
+ }
2594
+ },
2595
+ { capture: true, signal: this.abort.signal }
2596
+ );
2597
+ }
1933
2598
  listenToClicks() {
1934
2599
  document.addEventListener(
1935
2600
  "click",
@@ -1944,6 +2609,70 @@ var RecordingService = class {
1944
2609
  { signal: this.abort.signal }
1945
2610
  );
1946
2611
  }
2612
+ /** Centralised selector resolution for the pointer/key listeners (spec 010). */
2613
+ resolveSelectorFor(target) {
2614
+ if (!target || this.isOwnElement(target)) return null;
2615
+ const tag = target.tagName?.toLowerCase();
2616
+ if (tag === "body" || tag === "html") return null;
2617
+ const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2618
+ if (!container) return null;
2619
+ const selector = this.getReliableSelector(container);
2620
+ if (!selector || this.isOwnSelector(selector)) return null;
2621
+ return selector;
2622
+ }
2623
+ listenToDoubleClicks() {
2624
+ document.addEventListener(
2625
+ "dblclick",
2626
+ (e) => {
2627
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2628
+ if (e.altKey) return;
2629
+ const selector = this.resolveSelectorFor(e.target);
2630
+ if (!selector) return;
2631
+ const clickCmd = `cy.get('${selector}').click()`;
2632
+ let cmds = this.commands$.getValue();
2633
+ let removed = 0;
2634
+ while (removed < 2 && cmds.length > 0 && cmds[cmds.length - 1] === clickCmd) {
2635
+ cmds = cmds.slice(0, -1);
2636
+ removed++;
2637
+ }
2638
+ if (removed > 0) this.commands$.next(cmds);
2639
+ this.addCommand(`cy.get('${selector}').dblclick()`);
2640
+ },
2641
+ { signal: this.abort.signal }
2642
+ );
2643
+ }
2644
+ listenToContextMenu() {
2645
+ document.addEventListener(
2646
+ "contextmenu",
2647
+ (e) => {
2648
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2649
+ const selector = this.resolveSelectorFor(e.target);
2650
+ if (!selector) return;
2651
+ this.addCommand(`cy.get('${selector}').rightclick()`);
2652
+ },
2653
+ { signal: this.abort.signal }
2654
+ );
2655
+ }
2656
+ /** Enter → type('{enter}'), Escape → type('{esc}'), only inside a field. */
2657
+ listenToKeys() {
2658
+ document.addEventListener(
2659
+ "keydown",
2660
+ (e) => {
2661
+ if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2662
+ const key = e.key;
2663
+ if (key !== "Enter" && key !== "Escape") return;
2664
+ const target = e.target;
2665
+ const tag = target?.tagName?.toLowerCase();
2666
+ if (tag !== "input" && tag !== "textarea" && tag !== "select") return;
2667
+ const selector = this.resolveSelectorFor(target);
2668
+ if (!selector) return;
2669
+ this.flushInputDebounce(target);
2670
+ const token = key === "Enter" ? "{enter}" : "{esc}";
2671
+ this.addCommand(`cy.get('${selector}').type('${token}')`);
2672
+ },
2673
+ { signal: this.abort.signal }
2674
+ );
2675
+ }
1947
2676
  listenToInput() {
1948
2677
  document.addEventListener(
1949
2678
  "input",
@@ -2026,7 +2755,21 @@ var RecordingService = class {
2026
2755
  const tag = target.tagName.toLowerCase();
2027
2756
  if (tag === "input" || tag === "textarea" || tag === "select") {
2028
2757
  const container2 = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2029
- if (!container2) this.selectorNotFound$.next({ target, action: "click" });
2758
+ if (!container2) {
2759
+ this.selectorNotFound$.next({ target, action: "click" });
2760
+ return;
2761
+ }
2762
+ if (tag === "input") {
2763
+ const input = target;
2764
+ const type = (input.getAttribute("type") ?? "").toLowerCase();
2765
+ if (type === "checkbox" || type === "radio") {
2766
+ const selector2 = this.getReliableSelector(container2);
2767
+ if (selector2 && !this.isOwnSelector(selector2)) {
2768
+ const action = type === "radio" || input.checked ? "check" : "uncheck";
2769
+ this.addCommand(`cy.get('${selector2}').${action}()`);
2770
+ }
2771
+ }
2772
+ }
2030
2773
  return;
2031
2774
  }
2032
2775
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
@@ -2073,16 +2816,30 @@ var RecordingService = class {
2073
2816
  this.inputDebounceTimers.set(
2074
2817
  target,
2075
2818
  setTimeout(() => {
2076
- const selector = this.getReliableSelector(container);
2077
- const value = target.value.replace(/'/g, "\\'");
2078
- this.addGenericCommand({
2079
- selector,
2080
- action: (s) => `cy.get('${s}').clear().type('${value}')`
2081
- });
2082
2819
  this.inputDebounceTimers.delete(target);
2820
+ this.recordInputValue(target);
2083
2821
  }, 1e3)
2084
2822
  );
2085
2823
  }
2824
+ /** Records a text field's current value as a `clear().type()` command. */
2825
+ recordInputValue(target) {
2826
+ const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2827
+ if (!container) return;
2828
+ const selector = this.getReliableSelector(container);
2829
+ const value = target.value.replace(/'/g, "\\'");
2830
+ this.addGenericCommand({
2831
+ selector,
2832
+ action: (s) => `cy.get('${s}').clear().type('${value}')`
2833
+ });
2834
+ }
2835
+ /** Flushes any pending debounced value for an element, recording it now. */
2836
+ flushInputDebounce(target) {
2837
+ const timer = this.inputDebounceTimers.get(target);
2838
+ if (timer === void 0) return;
2839
+ clearTimeout(timer);
2840
+ this.inputDebounceTimers.delete(target);
2841
+ this.recordInputValue(target);
2842
+ }
2086
2843
  // ── Select helpers ────────────────────────────────────────────────────────
2087
2844
  handleSelectEvent(target) {
2088
2845
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
@@ -2148,6 +2905,42 @@ var RecordingService = class {
2148
2905
 
2149
2906
  // src/services/persistence.service.ts
2150
2907
  var import_idb = require("idb");
2908
+
2909
+ // 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);
2922
+ }
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
+ }
2937
+ }
2938
+ function buildTicketComment(id, url) {
2939
+ const ticketId = (id ?? "").trim();
2940
+ return url ? `// ${ticketId} \u2014 ${url}` : `// ${ticketId}`;
2941
+ }
2942
+
2943
+ // src/services/persistence.service.ts
2151
2944
  var ACTIVE_SESSION_ID = 1;
2152
2945
  var PersistenceService = class {
2153
2946
  // dbName is overridable so tests can use unique names for isolation.
@@ -2177,9 +2970,9 @@ var PersistenceService = class {
2177
2970
  return this._db;
2178
2971
  }
2179
2972
  // ── Tests ─────────────────────────────────────────────────────────────────
2180
- async insertTest(name, commands = [], interceptors = [], tags = [], notes) {
2973
+ async insertTest(name, commands = [], interceptors = [], tags = [], notes, ticketId) {
2181
2974
  const db = await this.getDB();
2182
- const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {}, ...notes ? { notes } : {} };
2975
+ const record = { name, createdAt: Date.now(), ...tags.length ? { tags } : {}, ...notes ? { notes } : {}, ...ticketId ? { ticketId } : {} };
2183
2976
  const id = await db.add("tests", record);
2184
2977
  if (commands.length) await this.insertCommands(commands, id);
2185
2978
  if (interceptors.length) await this.insertInterceptors(interceptors, id);
@@ -2202,12 +2995,28 @@ var PersistenceService = class {
2202
2995
  if (!record) return null;
2203
2996
  const commands = await this.getCommandStrings(testId);
2204
2997
  const interceptors = await this.getInterceptorStrings(testId);
2205
- const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
2998
+ const ticketComment = await this.ticketCommentFor(record);
2999
+ const itBody = `it('${escapeSingleQuotes(record.name)}', () => {
2206
3000
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2207
3001
  });`;
3002
+ const itBlock = ticketComment ? `${ticketComment}
3003
+ ${itBody}` : itBody;
2208
3004
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
2209
3005
  return { ...record, commands, interceptors, cypressCommands: commands, itBlock, interceptorsBlock };
2210
3006
  }
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
+ }
2211
3020
  async deleteTest(id) {
2212
3021
  const db = await this.getDB();
2213
3022
  await db.delete("tests", id);
@@ -2333,6 +3142,31 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2333
3142
  await this.setConfigKey("cypressDirectoryHandle", dirHandle);
2334
3143
  await this.setConfigKey("allowReadWriteFiles", "true");
2335
3144
  }
3145
+ /**
3146
+ * Writes captured fixtures into `cypress/fixtures/` using the configured Cypress
3147
+ * folder handle (spec 012). Returns the number written. Throws if no folder is
3148
+ * configured or write permission is denied.
3149
+ */
3150
+ async writeFixtures(fixtures) {
3151
+ if (!fixtures.length) return 0;
3152
+ const config = await this.getGeneralConfig();
3153
+ const dirHandle = config?.["cypressDirectoryHandle"];
3154
+ if (!dirHandle) throw new Error("No Cypress folder configured");
3155
+ const perm = dirHandle;
3156
+ let state = await perm.queryPermission({ mode: "readwrite" });
3157
+ if (state !== "granted") state = await perm.requestPermission({ mode: "readwrite" });
3158
+ if (state !== "granted") throw new Error("Write permission denied");
3159
+ const fixturesDir = await dirHandle.getDirectoryHandle("fixtures", { create: true });
3160
+ let written = 0;
3161
+ for (const fx of fixtures) {
3162
+ const fileHandle = await fixturesDir.getFileHandle(fx.name, { create: true });
3163
+ const writable = await fileHandle.createWritable();
3164
+ await writable.write(fx.content);
3165
+ await writable.close();
3166
+ written++;
3167
+ }
3168
+ return written;
3169
+ }
2336
3170
  async bulkInsertWithoutId(store, items) {
2337
3171
  if (!Array.isArray(items)) return;
2338
3172
  const db = await this.getDB();
@@ -2385,6 +3219,14 @@ function parseJsonObject(text) {
2385
3219
  }
2386
3220
  return null;
2387
3221
  }
3222
+ function prettyJsonOrNull(text) {
3223
+ try {
3224
+ const parsed = JSON.parse(text);
3225
+ if (parsed && typeof parsed === "object") return JSON.stringify(parsed, null, 2);
3226
+ } catch {
3227
+ }
3228
+ return null;
3229
+ }
2388
3230
  function parseRequestBody(init) {
2389
3231
  const body = init?.body;
2390
3232
  if (typeof body === "string") return parseJsonObject(body);
@@ -2418,6 +3260,9 @@ var HttpMonitor = class {
2418
3260
  isExtendedHttpEnabled() {
2419
3261
  return localStorage.getItem("extendedHttpCommands") === "true";
2420
3262
  }
3263
+ isFixtureModeEnabled() {
3264
+ return localStorage.getItem("fixtureMode") === "true";
3265
+ }
2421
3266
  installFetch() {
2422
3267
  if (this.originalFetch) return;
2423
3268
  const originalFetch = window.fetch;
@@ -2479,6 +3324,14 @@ var HttpMonitor = class {
2479
3324
  if (!INTERCEPTED_METHODS.includes(method)) return;
2480
3325
  const url = resolveUrl(input);
2481
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
+ }
2482
3335
  this.recording.registerInterceptor(method, url, alias);
2483
3336
  const extendedHttp = this.isExtendedHttpEnabled();
2484
3337
  const requestBody = extendedHttp ? parseRequestBody(init) : null;
@@ -2495,6 +3348,14 @@ var HttpMonitor = class {
2495
3348
  handleXhrInterception(method, url, requestBody, responseText) {
2496
3349
  if (!INTERCEPTED_METHODS.includes(method)) return;
2497
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
+ }
2498
3359
  this.recording.registerInterceptor(method, url, alias);
2499
3360
  const extendedHttp = this.isExtendedHttpEnabled();
2500
3361
  const responseBody = extendedHttp ? parseJsonObject(responseText) : null;
@@ -2613,6 +3474,10 @@ var LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES = `
2613
3474
  align-items: stretch;
2614
3475
  box-sizing: border-box;
2615
3476
  overflow: hidden !important;
3477
+ /* Left-align modal body text/code (SweetAlert defaults it to centre, which
3478
+ inherits into the panels \u2014 including their shadow DOM). Titles/buttons keep
3479
+ their own alignment. */
3480
+ text-align: left !important;
2616
3481
  }
2617
3482
  /* The single wrapper div we inject inside each modal fills the container */
2618
3483
  .swal2-html-container > div {
@@ -2816,6 +3681,177 @@ function boxTopLeftFor(toggleCentre, dir) {
2816
3681
  return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
2817
3682
  }
2818
3683
 
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
+
2819
3855
  // src/index.ts
2820
3856
  init_selector_picker();
2821
3857
 
@@ -3026,7 +4062,7 @@ var SAVE_TEST_STYLES = `
3026
4062
  :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; }
3027
4063
  * { box-sizing: border-box; }
3028
4064
  .container { padding: 24px 28px; text-align: left; }
3029
- .btn-row, p { text-align: center; }
4065
+ .btn-row { text-align: center; }
3030
4066
  p { margin: 0 0 20px; font-size: 14px; color: #8b949e; line-height: 1.5; }
3031
4067
  .btn-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
3032
4068
  button {
@@ -3096,10 +4132,11 @@ function renderSaveTestAsk(t) {
3096
4132
  </div>
3097
4133
  </div>`;
3098
4134
  }
3099
- function renderSaveTestDesc(description, notes, tags, t) {
4135
+ function renderSaveTestDesc(description, notes, tags, ticket, t) {
3100
4136
  const chipsHtml = tags.map(
3101
4137
  (tag) => `<span class="chip">${escHtml(tag)}<button class="chip-del" data-tag="${escAttr(tag)}" title="${t("SAVE_TEST.REMOVE_TAG_TITLE")}">\u2715</button></span>`
3102
4138
  ).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>` : "";
3103
4140
  return `
3104
4141
  <div class="container">
3105
4142
  <p>${t("SAVE_TEST.DESC_LABEL")} (<code>it()</code>):</p>
@@ -3107,6 +4144,10 @@ function renderSaveTestDesc(description, notes, tags, t) {
3107
4144
  value="${escAttr(description)}" autocomplete="off" />
3108
4145
  <span class="tag-label">${t("SAVE_TEST.NOTES_LABEL")}</span>
3109
4146
  <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}
3110
4151
  <span class="tag-label">${t("SAVE_TEST.TAGS_LABEL")}</span>
3111
4152
  <div class="tag-input-row">
3112
4153
  <input id="tag-input" type="text" placeholder="${t("SAVE_TEST.TAGS_PLACEHOLDER")}" autocomplete="off" />
@@ -3128,6 +4169,9 @@ var SaveTestElement = class extends HTMLElement {
3128
4169
  description = "";
3129
4170
  notes = "";
3130
4171
  tags = [];
4172
+ ticketId = "";
4173
+ /** Set by the host from config so the ticket placeholder/validation is provider-aware. */
4174
+ issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
3131
4175
  translation = translationService;
3132
4176
  constructor() {
3133
4177
  super();
@@ -3144,19 +4188,20 @@ var SaveTestElement = class extends HTMLElement {
3144
4188
  this.render();
3145
4189
  }
3146
4190
  confirmSave() {
3147
- this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
4191
+ this.dispatch("savetest", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() || null });
3148
4192
  }
3149
4193
  confirmSaveAndExport() {
3150
- this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags] });
4194
+ this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() || null });
3151
4195
  }
3152
4196
  cancel() {
3153
- this.dispatch("savetest", { description: null, notes: "", tags: [] });
4197
+ this.dispatch("savetest", { description: null, notes: "", tags: [], ticketId: null });
3154
4198
  }
3155
4199
  restartComponent() {
3156
4200
  this._step = "ask";
3157
4201
  this.description = "";
3158
4202
  this.notes = "";
3159
4203
  this.tags = [];
4204
+ this.ticketId = "";
3160
4205
  this.render();
3161
4206
  }
3162
4207
  addTag(tag) {
@@ -3173,6 +4218,20 @@ var SaveTestElement = class extends HTMLElement {
3173
4218
  t(key) {
3174
4219
  return this.translation.translate(key);
3175
4220
  }
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 };
4234
+ }
3176
4235
  dispatch(type, detail) {
3177
4236
  this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
3178
4237
  }
@@ -3182,9 +4241,10 @@ var SaveTestElement = class extends HTMLElement {
3182
4241
  this.shadow.getElementById("btn-yes")?.addEventListener("click", () => this.askSave());
3183
4242
  this.shadow.getElementById("btn-no")?.addEventListener("click", () => this.cancel());
3184
4243
  } else {
3185
- this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.t.bind(this))}`;
4244
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestDesc(this.description, this.notes, this.tags, this.ticketView(), this.t.bind(this))}`;
3186
4245
  const descInput = this.shadow.getElementById("desc-input");
3187
4246
  const notesInput = this.shadow.getElementById("notes-input");
4247
+ const ticketInput = this.shadow.getElementById("ticket-input");
3188
4248
  const tagInput = this.shadow.getElementById("tag-input");
3189
4249
  descInput.addEventListener("input", () => {
3190
4250
  this.description = descInput.value;
@@ -3192,6 +4252,13 @@ var SaveTestElement = class extends HTMLElement {
3192
4252
  notesInput.addEventListener("input", () => {
3193
4253
  this.notes = notesInput.value;
3194
4254
  });
4255
+ ticketInput.addEventListener("input", () => {
4256
+ this.ticketId = ticketInput.value;
4257
+ });
4258
+ ticketInput.addEventListener("change", () => {
4259
+ this.ticketId = ticketInput.value;
4260
+ this.render();
4261
+ });
3195
4262
  const tryAddTag = () => {
3196
4263
  this.addTag(tagInput.value);
3197
4264
  tagInput.value = "";
@@ -3421,7 +4488,7 @@ function highlightLine(line) {
3421
4488
 
3422
4489
  // src/components/test-editor/test-editor.template.ts
3423
4490
  function renderTestEditor(state, t) {
3424
- const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, locale } = state;
4491
+ const { tags, visible, selectedVisible, activeTag, selectMode, selectedIds, describeName, expandedIndex, interceptorsByTest, groupByTicket, ticketUrlByTest, locale } = state;
3425
4492
  const tagFilterHtml = tags.length ? `<div class="tag-filter">
3426
4493
  ${tags.map((tag) => `<button class="tag-chip${activeTag === tag ? " active" : ""}" data-filter-tag="${escAttr(tag)}">${escHtml(tag)}</button>`).join("")}
3427
4494
  </div>` : `<div class="tag-filter" style="color:#484f58;font-size:11px">${t("TEST_EDITOR.NO_TAGS")}</div>`;
@@ -3430,14 +4497,19 @@ function renderTestEditor(state, t) {
3430
4497
  <input id="describe-name" type="text" placeholder="${t("TEST_EDITOR.DESCRIBE_PLACEHOLDER")}" value="${escAttr(describeName)}" />
3431
4498
  <button class="btn-gen-describe" id="btn-gen-describe">${t("TEST_EDITOR.COPY_DESCRIBE")}</button>
3432
4499
  </div>` : "";
3433
- const rows = visible.map((test, i) => {
4500
+ const renderRow = (test, i) => {
3434
4501
  const expanded = expandedIndex === i;
3435
4502
  const date = new Date(test.createdAt).toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
3436
4503
  const icps = interceptorsByTest[test.id] ?? test.interceptors ?? [];
3437
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>`;
3438
4508
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
3439
4509
  const hasIcps = (test.interceptors ?? []).length > 0;
3440
- const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
4510
+ const ticketComment = ticketId ? `${buildTicketComment(ticketId, ticketUrl)}
4511
+ ` : "";
4512
+ const itBlockCode = `${ticketComment}it('${escapeSingleQuotes(test.name)}', () => {
3441
4513
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
3442
4514
  });`;
3443
4515
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -3457,6 +4529,7 @@ ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
3457
4529
  ${checkbox}
3458
4530
  <span class="test-name">${escHtml(test.name)}</span>
3459
4531
  ${tagsHtml}
4532
+ ${ticketHtml}
3460
4533
  <span class="test-date">${date}</span>
3461
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>
3462
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>` : ""}
@@ -3464,17 +4537,40 @@ ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
3464
4537
  </div>
3465
4538
  ${body}
3466
4539
  </div>`;
3467
- }).join("");
4540
+ };
4541
+ const indexed = visible.map((test, i) => ({ test, i }));
4542
+ const groupedRows = () => {
4543
+ 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]);
4549
+ }
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>`;
4560
+ }).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("");
3468
4563
  return `
3469
4564
  <div class="toolbar">
3470
4565
  ${tagFilterHtml}
4566
+ <button class="btn-select${groupByTicket ? " active" : ""}" id="btn-group-ticket">${t("TEST_EDITOR.GROUP_BY_TICKET")}</button>
3471
4567
  <button class="btn-select${selectMode ? " active" : ""}" id="btn-select-mode">
3472
4568
  ${selectMode ? t("TEST_EDITOR.CANCEL_SELECT") : t("TEST_EDITOR.MULTI_SELECT")}
3473
4569
  </button>
3474
4570
  </div>
3475
4571
  ${describeBarHtml}
3476
4572
  <div class="list">
3477
- ${visible.length ? rows : `<div class="empty">${t("TEST_EDITOR.NO_TESTS")}</div>`}
4573
+ ${listBody}
3478
4574
  </div>`;
3479
4575
  }
3480
4576
 
@@ -3490,6 +4586,9 @@ var TestEditorElement = class extends HTMLElement {
3490
4586
  selectMode = false;
3491
4587
  selectedIds = /* @__PURE__ */ new Set();
3492
4588
  describeName = "";
4589
+ groupByTicket = false;
4590
+ issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
4591
+ issueTrackerParams = {};
3493
4592
  constructor() {
3494
4593
  super();
3495
4594
  this.shadow = this.attachShadow({ mode: "open" });
@@ -3499,7 +4598,17 @@ var TestEditorElement = class extends HTMLElement {
3499
4598
  this.loadTests();
3500
4599
  }
3501
4600
  async loadTests() {
3502
- this.tests = await this.persistence.getAllTests();
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;
3503
4612
  this.render();
3504
4613
  }
3505
4614
  async deleteTest(id) {
@@ -3574,6 +4683,12 @@ ${beforeEach}${itBlocks}
3574
4683
  const tags = this.allTags;
3575
4684
  const visible = this.visibleTests;
3576
4685
  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
+ }
3577
4692
  this.shadow.innerHTML = `<style>${TEST_EDITOR_STYLES}</style>${renderTestEditor({
3578
4693
  tags,
3579
4694
  visible,
@@ -3584,9 +4699,15 @@ ${beforeEach}${itBlocks}
3584
4699
  describeName: this.describeName,
3585
4700
  expandedIndex: this.expandedIndex,
3586
4701
  interceptorsByTest: this.interceptorsByTest,
4702
+ groupByTicket: this.groupByTicket,
4703
+ ticketUrlByTest,
3587
4704
  locale: localeForLang(this.translation.getLang())
3588
4705
  }, this.t.bind(this))}`;
3589
4706
  this.shadow.getElementById("btn-select-mode")?.addEventListener("click", () => this.toggleSelectMode());
4707
+ 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
+ );
3590
4711
  this.shadow.querySelectorAll("[data-filter-tag]").forEach((el) => {
3591
4712
  el.addEventListener("click", () => {
3592
4713
  const tag = el.dataset["filterTag"] ?? "";
@@ -3654,12 +4775,22 @@ var CONFIGURATION_STYLES = `
3654
4775
  * { box-sizing: border-box; }
3655
4776
 
3656
4777
  /* \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. */
3657
4780
  .cfg-grid {
3658
4781
  display: grid;
3659
4782
  grid-template-columns: 1fr 1fr;
3660
4783
  gap: 10px;
3661
4784
  padding: 14px;
4785
+ max-height: 72vh;
4786
+ overflow-y: auto;
4787
+ scrollbar-width: thin;
4788
+ scrollbar-color: #30363d transparent;
3662
4789
  }
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; }
3663
4794
 
3664
4795
  /* \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 */
3665
4796
  .card {
@@ -3873,8 +5004,33 @@ function renderExportOverlay(state, t) {
3873
5004
  </div>
3874
5005
  </div>`;
3875
5006
  }
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
+ }
3876
5032
  function renderConfiguration(state, t) {
3877
- const { selectedLanguage, advancedHttpConfig, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
5033
+ const { selectedLanguage, advancedHttpConfig, fixtureMode, selectorStrategy, filesystemGranted, cypressFolderName, smartSelectorEnabled, startHidden, resumeTtlMinutes } = state;
3878
5034
  const langOptions = LANGS.map(
3879
5035
  (l) => `<option value="${l.value}" ${selectedLanguage === l.value ? "selected" : ""}>${l.label}</option>`
3880
5036
  ).join("");
@@ -3902,6 +5058,18 @@ function renderConfiguration(state, t) {
3902
5058
  </label>
3903
5059
  </div>
3904
5060
 
5061
+ <!-- HTTP Fixtures -->
5062
+ <div class="card">
5063
+ <div class="card-hd">${t("CONFIG.FIXTURE_SECTION")}</div>
5064
+ <label class="check-row">
5065
+ <input type="checkbox" id="fixture-toggle" ${fixtureMode ? "checked" : ""} />
5066
+ <div>
5067
+ <div class="check-title">${t("CONFIG.FIXTURE_TITLE")}</div>
5068
+ <div class="check-sub">${t("CONFIG.FIXTURE_SUB")}</div>
5069
+ </div>
5070
+ </label>
5071
+ </div>
5072
+
3905
5073
  <!-- Smart Selector -->
3906
5074
  <div class="card">
3907
5075
  <div class="card-hd">${t("CONFIG.SMART_SELECTOR_SECTION")}</div>
@@ -3983,6 +5151,9 @@ function renderConfiguration(state, t) {
3983
5151
  </div>
3984
5152
  </div>
3985
5153
 
5154
+ <!-- Issue tracker (spec 014) -->
5155
+ ${renderIssueTrackerCard(state, t)}
5156
+
3986
5157
  <!-- Data -->
3987
5158
  <div class="card card-wide">
3988
5159
  <div class="card-hd">${t("CONFIG.DATA_SECTION")}</div>
@@ -4006,10 +5177,13 @@ var ConfigurationElement = class extends HTMLElement {
4006
5177
  translation;
4007
5178
  selectedLanguage = "es";
4008
5179
  advancedHttpConfig = false;
5180
+ fixtureMode = false;
4009
5181
  selectorStrategy = "data-cy";
4010
5182
  smartSelectorEnabled = true;
4011
5183
  startHidden = false;
4012
5184
  resumeTtlMinutes = DEFAULT_RESUME_TTL_MINUTES;
5185
+ issueTrackerProviderId = DEFAULT_ISSUE_TRACKER_PROVIDER_ID;
5186
+ issueTrackerParams = {};
4013
5187
  isExporting = false;
4014
5188
  exportMode = "all";
4015
5189
  exportTests = [];
@@ -4021,6 +5195,7 @@ var ConfigurationElement = class extends HTMLElement {
4021
5195
  super();
4022
5196
  this.shadow = this.attachShadow({ mode: "open" });
4023
5197
  this.advancedHttpConfig = localStorage.getItem("extendedHttpCommands") === "true";
5198
+ this.fixtureMode = localStorage.getItem("fixtureMode") === "true";
4024
5199
  }
4025
5200
  connectedCallback() {
4026
5201
  if (!this.persistence) this.persistence = new PersistenceService();
@@ -4038,6 +5213,7 @@ var ConfigurationElement = class extends HTMLElement {
4038
5213
  this.translation.setLang(this.selectedLanguage);
4039
5214
  }
4040
5215
  this.advancedHttpConfig = localStorage.getItem("extendedHttpCommands") === "true";
5216
+ this.fixtureMode = localStorage.getItem("fixtureMode") === "true";
4041
5217
  this.selectorStrategy = config?.["selectorStrategy"] ?? "data-cy";
4042
5218
  this.smartSelectorEnabled = config?.["smartSelectorEnabled"] !== "false";
4043
5219
  this.startHidden = config?.["startHidden"] === "true";
@@ -4049,6 +5225,12 @@ var ConfigurationElement = class extends HTMLElement {
4049
5225
  this.filesystemGranted = config?.["allowReadWriteFiles"] === "true";
4050
5226
  const handle = config?.["cypressDirectoryHandle"];
4051
5227
  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
+ }
4052
5234
  this.render();
4053
5235
  }
4054
5236
  async onLanguageChange(lang) {
@@ -4063,6 +5245,12 @@ var ConfigurationElement = class extends HTMLElement {
4063
5245
  this.persistence.setConfig({ extendedHttpCommands: checked ? "true" : "false" });
4064
5246
  this.render();
4065
5247
  }
5248
+ onFixtureModeChange(checked) {
5249
+ this.fixtureMode = checked;
5250
+ localStorage.setItem("fixtureMode", checked ? "true" : "false");
5251
+ this.persistence.setConfig({ fixtureMode: checked ? "true" : "false" });
5252
+ this.render();
5253
+ }
4066
5254
  async onStartHiddenChange(checked) {
4067
5255
  this.startHidden = checked;
4068
5256
  await this.persistence.setConfig({ startHidden: checked ? "true" : "false" });
@@ -4075,6 +5263,20 @@ var ConfigurationElement = class extends HTMLElement {
4075
5263
  await this.persistence.setConfig({ [RESUME_TTL_CONFIG_KEY]: safe });
4076
5264
  this.render();
4077
5265
  }
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
+ }
4078
5280
  onResetWidgetPosition() {
4079
5281
  this.dispatchEvent(new CustomEvent("resetwidgetposition", { bubbles: true, composed: true }));
4080
5282
  showToast(this.t("CONFIG.WIDGET_POSITION_RESET_DONE"));
@@ -4181,12 +5383,15 @@ var ConfigurationElement = class extends HTMLElement {
4181
5383
  this.shadow.innerHTML = `<style>${CONFIGURATION_STYLES}</style>${renderConfiguration({
4182
5384
  selectedLanguage: this.selectedLanguage,
4183
5385
  advancedHttpConfig: this.advancedHttpConfig,
5386
+ fixtureMode: this.fixtureMode,
4184
5387
  selectorStrategy: this.selectorStrategy,
4185
5388
  filesystemGranted: this.filesystemGranted,
4186
5389
  cypressFolderName: this.cypressFolderName,
4187
5390
  smartSelectorEnabled: this.smartSelectorEnabled,
4188
5391
  startHidden: this.startHidden,
4189
5392
  resumeTtlMinutes: this.resumeTtlMinutes,
5393
+ issueTrackerProviderId: this.issueTrackerProviderId,
5394
+ issueTrackerParams: this.issueTrackerParams,
4190
5395
  isExporting: this.isExporting,
4191
5396
  exportMode: this.exportMode,
4192
5397
  exportTests: this.exportTests,
@@ -4202,6 +5407,10 @@ var ConfigurationElement = class extends HTMLElement {
4202
5407
  "change",
4203
5408
  (e) => this.onAdvancedHttpConfigChange(e.target.checked)
4204
5409
  );
5410
+ this.shadow.getElementById("fixture-toggle").addEventListener(
5411
+ "change",
5412
+ (e) => this.onFixtureModeChange(e.target.checked)
5413
+ );
4205
5414
  this.shadow.getElementById("smart-selector-toggle").addEventListener(
4206
5415
  "change",
4207
5416
  (e) => this.onSmartSelectorChange(e.target.checked)
@@ -4219,6 +5428,16 @@ var ConfigurationElement = class extends HTMLElement {
4219
5428
  "change",
4220
5429
  (e) => this.onSelectorStrategyChange(e.target.value)
4221
5430
  );
5431
+ this.shadow.getElementById("issue-tracker-provider")?.addEventListener(
5432
+ "change",
5433
+ (e) => this.onIssueTrackerProviderChange(e.target.value)
5434
+ );
5435
+ this.shadow.querySelectorAll("[data-tracker-field]").forEach(
5436
+ (el) => el.addEventListener(
5437
+ "change",
5438
+ () => this.onIssueTrackerParamChange(el.dataset["trackerField"] ?? "", el.value)
5439
+ )
5440
+ );
4222
5441
  this.shadow.getElementById("btn-change-folder")?.addEventListener("click", () => this.changeFolder());
4223
5442
  this.shadow.getElementById("btn-revoke")?.addEventListener("click", () => this.revokeAccess());
4224
5443
  this.shadow.getElementById("btn-export")?.addEventListener("click", () => this.openExportDialog());
@@ -4821,6 +6040,9 @@ var FILE_PREVIEW_STYLES = `
4821
6040
  .file-name { flex: 1; font-size: 12px; color: #8b949e;
4822
6041
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; }
4823
6042
  .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; }
4824
6046
  .editor {
4825
6047
  flex: 1; min-width: 0; padding: 14px;
4826
6048
  background: #0d1117; color: #c9d1d9; border: none; outline: none;
@@ -5010,7 +6232,7 @@ function renderFilePreview(state, t) {
5010
6232
  <button id="btn-copy" title="${t("FILE_PREVIEW.COPY_TITLE")}">\u{1F4CB}</button>
5011
6233
  </div>
5012
6234
  <div class="body">
5013
- ${showDiff ? `<div class="diff-panel" id="diff-panel">${buildDiffHtml(originalContent ?? "", currentContent, t)}</div>` : `<textarea class="editor" id="editor" spellcheck="false">${escHtml(fileContent ?? "")}</textarea>`}
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>`}
5014
6236
  ${blocksPanelHtml}
5015
6237
  </div>
5016
6238
  ${runResultHtml}
@@ -5028,6 +6250,7 @@ function renderFilePreview(state, t) {
5028
6250
  var FilePreviewElement = class extends HTMLElement {
5029
6251
  shadow;
5030
6252
  textarea = null;
6253
+ editor = null;
5031
6254
  fileName = null;
5032
6255
  closeLabel = "";
5033
6256
  translation = translationService;
@@ -5053,20 +6276,29 @@ var FilePreviewElement = class extends HTMLElement {
5053
6276
  if (!this.closeLabel) this.closeLabel = this.translation.translate("FILE_PREVIEW.CLOSE_BTN");
5054
6277
  this.render();
5055
6278
  }
6279
+ disconnectedCallback() {
6280
+ this.editor?.destroy();
6281
+ this.editor = null;
6282
+ }
5056
6283
  t(key) {
5057
6284
  return this.translation.translate(key);
5058
6285
  }
6286
+ /** Current editor content (CodeMirror if upgraded, else the textarea/fallback). */
6287
+ currentValue() {
6288
+ return this.editor?.getValue() ?? this.textarea?.value ?? this._fileContent ?? "";
6289
+ }
5059
6290
  get fileContent() {
5060
6291
  return this._fileContent;
5061
6292
  }
5062
6293
  set fileContent(v) {
5063
6294
  if (this._originalContent === null) this._originalContent = v;
5064
6295
  this._fileContent = v;
5065
- if (this.textarea) this.textarea.value = v ?? "";
6296
+ if (this.editor) this.editor.setValue(v ?? "");
6297
+ else if (this.textarea) this.textarea.value = v ?? "";
5066
6298
  else this.render();
5067
6299
  }
5068
6300
  saveFile() {
5069
- const content = this.textarea?.value ?? this._fileContent ?? "";
6301
+ const content = this.currentValue();
5070
6302
  this.dispatchEvent(new CustomEvent("save", { detail: content, bubbles: true, composed: true }));
5071
6303
  }
5072
6304
  onClose() {
@@ -5115,7 +6347,7 @@ var FilePreviewElement = class extends HTMLElement {
5115
6347
  * (and warns) when the file has no valid describe() block to insert into.
5116
6348
  */
5117
6349
  insertBlocks() {
5118
- const base = this.textarea?.value ?? this._fileContent ?? "";
6350
+ const base = this.currentValue();
5119
6351
  let content = base;
5120
6352
  if (this.interceptorsBlock) {
5121
6353
  const merged = advancedTestTransformationService.insertBeforeEach(content, this.interceptorsBlock);
@@ -5135,12 +6367,14 @@ var FilePreviewElement = class extends HTMLElement {
5135
6367
  showToast(this.t("FILE_PREVIEW.INSERT_DONE"));
5136
6368
  }
5137
6369
  render() {
6370
+ this.editor?.destroy();
6371
+ this.editor = null;
5138
6372
  this.shadow.innerHTML = `<style>${FILE_PREVIEW_STYLES}</style>${renderFilePreview({
5139
6373
  fileName: this.fileName,
5140
6374
  showDiff: this._showDiff,
5141
6375
  fileContent: this._fileContent,
5142
6376
  originalContent: this._originalContent,
5143
- currentContent: this.textarea?.value ?? this._fileContent ?? "",
6377
+ currentContent: this._fileContent ?? "",
5144
6378
  itBlock: this.itBlock,
5145
6379
  interceptorsBlock: this.interceptorsBlock,
5146
6380
  closeLabel: this.closeLabel,
@@ -5154,12 +6388,13 @@ var FilePreviewElement = class extends HTMLElement {
5154
6388
  this.textarea.addEventListener("input", () => {
5155
6389
  this._fileContent = ta.value;
5156
6390
  });
6391
+ void this.upgradeEditor();
5157
6392
  }
5158
6393
  this.shadow.getElementById("btn-save")?.addEventListener("click", () => this.saveFile());
5159
6394
  this.shadow.getElementById("btn-close")?.addEventListener("click", () => this.onClose());
5160
6395
  this.shadow.getElementById("btn-launch")?.addEventListener("click", () => this.launchTest());
5161
6396
  this.shadow.getElementById("btn-copy")?.addEventListener("click", () => {
5162
- this.copyToClipboard(this.textarea?.value ?? this._fileContent ?? "");
6397
+ this.copyToClipboard(this.currentValue());
5163
6398
  });
5164
6399
  this.shadow.getElementById("btn-diff")?.addEventListener("click", () => this.toggleDiff());
5165
6400
  this.shadow.getElementById("btn-insert")?.addEventListener("click", () => this.insertBlocks());
@@ -5170,11 +6405,188 @@ var FilePreviewElement = class extends HTMLElement {
5170
6405
  this.copyToClipboard(this.interceptorsBlock);
5171
6406
  });
5172
6407
  }
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
+ }
5173
6436
  };
5174
6437
  if (!customElements.get("file-preview")) {
5175
6438
  customElements.define("file-preview", FilePreviewElement);
5176
6439
  }
5177
6440
 
6441
+ // src/components/help-panel/help-panel.styles.ts
6442
+ var HELP_PANEL_STYLES = `
6443
+ :host { display: block; }
6444
+ *, *::before, *::after { box-sizing: border-box; }
6445
+
6446
+ .help-tabs {
6447
+ display: flex;
6448
+ gap: 4px;
6449
+ border-bottom: 1px solid #21262d;
6450
+ margin-bottom: 8px;
6451
+ }
6452
+ .help-tab {
6453
+ appearance: none;
6454
+ border: none;
6455
+ background: transparent;
6456
+ color: #8b949e;
6457
+ font-size: 12px;
6458
+ font-weight: 600;
6459
+ padding: 8px 12px;
6460
+ cursor: pointer;
6461
+ border-bottom: 2px solid transparent;
6462
+ margin-bottom: -1px;
6463
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
6464
+ transition: color .12s, border-color .12s;
6465
+ }
6466
+ .help-tab:hover { color: #c9d1d9; }
6467
+ .help-tab.active { color: #e6edf3; border-bottom-color: #2f81f7; }
6468
+
6469
+ .help-panel {
6470
+ max-height: 60vh;
6471
+ overflow-y: auto;
6472
+ padding: 4px 6px 8px;
6473
+ color: #c9d1d9;
6474
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
6475
+ font-size: 13px;
6476
+ line-height: 1.55;
6477
+ }
6478
+
6479
+ .help-intro {
6480
+ margin: 0 0 12px;
6481
+ color: #8b949e;
6482
+ font-size: 12px;
6483
+ }
6484
+
6485
+ .help-sec { margin: 0 0 14px; }
6486
+
6487
+ .help-sec-hd {
6488
+ margin: 0 0 6px;
6489
+ font-size: 12px;
6490
+ font-weight: 700;
6491
+ color: #e6edf3;
6492
+ letter-spacing: .3px;
6493
+ border-bottom: 1px solid #21262d;
6494
+ padding-bottom: 4px;
6495
+ }
6496
+
6497
+ .help-list {
6498
+ margin: 0;
6499
+ padding: 0;
6500
+ list-style: none;
6501
+ }
6502
+
6503
+ .help-list li {
6504
+ padding: 3px 0 3px 14px;
6505
+ position: relative;
6506
+ color: #c9d1d9;
6507
+ }
6508
+
6509
+ .help-list li::before {
6510
+ content: '\xB7';
6511
+ position: absolute;
6512
+ left: 3px;
6513
+ color: #2f81f7;
6514
+ font-weight: 700;
6515
+ }
6516
+ `;
6517
+
6518
+ // src/components/help-panel/help-panel.template.ts
6519
+ init_html_utils();
6520
+ var REFERENCE_SECTIONS = [
6521
+ { title: "HELP.SEC_RECORDING", items: ["HELP.RECORDING_1", "HELP.RECORDING_2"] },
6522
+ {
6523
+ title: "HELP.SEC_SHORTCUTS",
6524
+ items: ["HELP.SC_RECORD", "HELP.SC_PAUSE", "HELP.SC_TESTS", "HELP.SC_COMMANDS", "HELP.SC_CONFIG", "HELP.SC_VISIBILITY", "HELP.SC_HELP"]
6525
+ },
6526
+ {
6527
+ title: "HELP.SEC_CAPTURED",
6528
+ items: ["HELP.CAP_CLICK", "HELP.CAP_DBLCLICK", "HELP.CAP_RIGHTCLICK", "HELP.CAP_TYPE", "HELP.CAP_CHECK", "HELP.CAP_SELECT", "HELP.CAP_KEYS", "HELP.CAP_ROUTE", "HELP.CAP_HTTP"]
6529
+ },
6530
+ { title: "HELP.SEC_ASSERT", items: ["HELP.ASSERT_1", "HELP.ASSERT_2"] },
6531
+ { title: "HELP.SEC_PANELS", items: ["HELP.PANEL_TESTS", "HELP.PANEL_COMMANDS", "HELP.PANEL_CONFIG", "HELP.PANEL_FILES"] },
6532
+ { title: "HELP.SEC_SELECTOR", items: ["HELP.SELECTOR_1", "HELP.SELECTOR_2"] },
6533
+ { 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"] }
6535
+ ];
6536
+ var GUIDE_SECTIONS = [
6537
+ { 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"] }
6540
+ ];
6541
+ function sectionsHtml(sections, t) {
6542
+ return sections.map(
6543
+ (s) => `
6544
+ <section class="help-sec">
6545
+ <h3 class="help-sec-hd">${escHtml(t(s.title))}</h3>
6546
+ <ul class="help-list">${s.items.map((k) => `<li>${escHtml(t(k))}</li>`).join("")}</ul>
6547
+ </section>`
6548
+ ).join("");
6549
+ }
6550
+ function renderHelpPanel(t, activeTab = "reference") {
6551
+ const tab = (id, key) => `<button class="help-tab ${activeTab === id ? "active" : ""}" data-tab="${id}">${escHtml(t(key))}</button>`;
6552
+ const body = activeTab === "guide" ? `<p class="help-intro">${escHtml(t("HELP.GUIDE_INTRO"))}</p>${sectionsHtml(GUIDE_SECTIONS, t)}` : `<p class="help-intro">${escHtml(t("HELP.INTRO"))}</p>${sectionsHtml(REFERENCE_SECTIONS, t)}`;
6553
+ return `
6554
+ <div class="help-root">
6555
+ <div class="help-tabs" role="tablist">
6556
+ ${tab("reference", "HELP.TAB_REFERENCE")}
6557
+ ${tab("guide", "HELP.TAB_GUIDE")}
6558
+ </div>
6559
+ <div class="help-panel" data-tab-panel="${activeTab}">${body}</div>
6560
+ </div>`;
6561
+ }
6562
+
6563
+ // src/components/help-panel/help-panel.ts
6564
+ var HelpPanelElement = class extends HTMLElement {
6565
+ shadow;
6566
+ activeTab = "reference";
6567
+ translation;
6568
+ constructor() {
6569
+ super();
6570
+ this.shadow = this.attachShadow({ mode: "open" });
6571
+ }
6572
+ connectedCallback() {
6573
+ if (!this.translation) this.translation = new TranslationService();
6574
+ this.render();
6575
+ }
6576
+ render() {
6577
+ this.shadow.innerHTML = `<style>${HELP_PANEL_STYLES}</style>${renderHelpPanel(this.translation.translate.bind(this.translation), this.activeTab)}`;
6578
+ this.shadow.querySelectorAll("[data-tab]").forEach((btn) => {
6579
+ btn.addEventListener("click", () => {
6580
+ this.activeTab = btn.dataset["tab"] ?? "reference";
6581
+ this.render();
6582
+ });
6583
+ });
6584
+ }
6585
+ };
6586
+ if (!customElements.get("help-panel")) {
6587
+ customElements.define("help-panel", HelpPanelElement);
6588
+ }
6589
+
5178
6590
  // src/components/lib-e2e-recorder/lib-e2e-recorder.ts
5179
6591
  var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
5180
6592
 
@@ -5182,22 +6594,15 @@ var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
5182
6594
  var DIRECTIONS = ["up-left", "up-right", "down-left", "down-right"];
5183
6595
  function directionBlocks() {
5184
6596
  return DIRECTIONS.map((dir) => {
5185
- const sx = dir.endsWith("left") ? -1 : 1;
5186
- const sy = dir.startsWith("up") ? -1 : 1;
5187
6597
  const tv = dir.startsWith("up") ? "bottom" : "top";
5188
6598
  const th = dir.endsWith("left") ? "right" : "left";
5189
6599
  const ov = tv === "bottom" ? "top" : "bottom";
5190
6600
  const oh = th === "right" ? "left" : "right";
5191
6601
  const sel = `.widget[data-expand="${dir}"]`;
5192
- const labelGeneral = `${sel} .btn-action::after { ${oh}: calc(100% + 9px); ${th}: auto; top: 50%; bottom: auto; transform: translateY(-50%); }`;
5193
- const labelEnds = `${sel} .btn-action[data-n="1"]::after, ${sel} .btn-action[data-n="4"]::after { ${ov}: calc(100% + 9px); ${tv}: auto; left: 50%; right: auto; transform: translateX(-50%); }`;
5194
6602
  return `
5195
- ${sel} { --sx: ${sx}; --sy: ${sy}; }
5196
- ${sel} .btn-toggle { ${tv}: 24px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
5197
- ${sel} .btn-pause { ${tv}: 78px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
5198
- ${sel} .btn-action { ${tv}: 28px; ${th}: 28px; ${ov}: auto; ${oh}: auto; }
5199
- ${labelGeneral}
5200
- ${labelEnds}
6603
+ ${sel} .btn-toggle { ${tv}: 24px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
6604
+ ${sel} .btn-pause { ${tv}: 78px; ${th}: 24px; ${ov}: auto; ${oh}: auto; }
6605
+ ${sel} .action-menu { ${tv}: 118px; ${th}: 8px; ${ov}: auto; ${oh}: auto; transform-origin: ${tv} ${th}; }
5201
6606
  `;
5202
6607
  }).join("\n");
5203
6608
  }
@@ -5207,9 +6612,10 @@ function getRecorderStyles(rec, paused) {
5207
6612
  *, *::before, *::after { box-sizing: border-box; }
5208
6613
 
5209
6614
  /*
5210
- * Invisible 190\xD7190 hit area. Position (left/top) is set from JS so the
5211
- * widget can be dragged; --sx/--sy + data-expand orient the radial menu so
5212
- * it always expands toward the viewport interior. Defaults to bottom-right.
6615
+ * Hit area. Position (left/top) is set from JS so the widget can be dragged;
6616
+ * data-expand orients the action-menu popover toward the viewport interior.
6617
+ * The menu is a DOM child, so hovering it keeps :host(.widget):hover alive even
6618
+ * though it visually overflows this box. Defaults to bottom-right.
5213
6619
  */
5214
6620
  .widget {
5215
6621
  position: fixed;
@@ -5218,12 +6624,10 @@ function getRecorderStyles(rec, paused) {
5218
6624
  width: 190px;
5219
6625
  height: 190px;
5220
6626
  z-index: 2147483647;
5221
- --sx: -1;
5222
- --sy: -1;
5223
6627
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
5224
6628
  }
5225
6629
 
5226
- /* \u2500\u2500 Toggle \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 */
6630
+ /* \u2500\u2500 Toggle (FAB) \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 */
5227
6631
  .btn-toggle {
5228
6632
  position: absolute;
5229
6633
  bottom: 24px;
@@ -5276,83 +6680,58 @@ function getRecorderStyles(rec, paused) {
5276
6680
  .btn-pause:hover { transform: scale(1.12); }
5277
6681
  .btn-pause:active { transform: scale(0.93); }
5278
6682
 
5279
- /* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5280
- .btn-action {
6683
+ /* \u2500\u2500 Action menu (labelled grid popover) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6684
+ .action-menu {
5281
6685
  position: absolute;
5282
- bottom: 28px;
5283
- right: 28px;
5284
- width: 36px;
5285
- height: 36px;
5286
- border-radius: 50%;
5287
- border: none;
5288
- cursor: pointer;
5289
- font-size: 16px;
5290
- display: flex;
5291
- align-items: center;
5292
- justify-content: center;
5293
- background: rgba(13,17,23,.92);
6686
+ display: grid;
6687
+ grid-template-columns: repeat(3, 1fr);
6688
+ gap: 4px;
6689
+ padding: 8px;
6690
+ width: 174px;
6691
+ background: rgba(13,17,23,.97);
6692
+ border: 1px solid rgba(48,54,61,.9);
6693
+ border-radius: 12px;
6694
+ box-shadow: 0 8px 28px rgba(0,0,0,.5);
5294
6695
  backdrop-filter: blur(14px);
5295
6696
  -webkit-backdrop-filter: blur(14px);
5296
- color: #8b949e;
5297
- box-shadow: 0 4px 18px rgba(0,0,0,.45), 0 0 0 1px rgba(48,54,61,.75);
5298
6697
  opacity: 0;
5299
- transform: scale(0.35);
5300
- pointer-events: none;
5301
- transition: opacity .15s, transform .18s ease-in,
5302
- background .15s, color .12s, box-shadow .15s;
5303
- }
5304
-
5305
- /* Label chip */
5306
- .btn-action::after {
5307
- content: attr(data-label);
5308
- position: absolute;
5309
- right: calc(100% + 9px);
5310
- top: 50%;
5311
- transform: translateY(-50%);
5312
- background: rgba(13,17,23,.95);
5313
- color: #e6edf3;
5314
- font-size: 11px;
5315
- font-weight: 500;
5316
- padding: 3px 9px;
5317
- border-radius: 6px;
5318
- white-space: nowrap;
6698
+ transform: scale(0.9);
5319
6699
  pointer-events: none;
5320
- opacity: 0;
5321
- transition: opacity .15s .05s;
5322
- border: 1px solid rgba(48,54,61,.8);
5323
- box-shadow: 0 2px 8px rgba(0,0,0,.35);
6700
+ transition: opacity .16s, transform .2s cubic-bezier(.34,1.56,.64,1);
6701
+ z-index: 3;
5324
6702
  }
5325
- .btn-action:hover::after { opacity: 1; }
5326
- .btn-action:hover { background: #21262d; color: #e6edf3; }
5327
- .btn-action:active { background: #30363d !important; }
5328
-
5329
- /* Expand: spring + stagger */
5330
- .widget:hover .btn-action {
6703
+ .widget:hover .action-menu {
5331
6704
  opacity: 1;
6705
+ transform: scale(1);
5332
6706
  pointer-events: all;
5333
- transition: opacity .2s, transform .32s cubic-bezier(.34,1.56,.64,1),
5334
- background .15s, color .12s, box-shadow .15s;
5335
6707
  }
5336
6708
 
5337
- /* Arc positions \u2014 signs come from --sx/--sy (data-expand) */
5338
- .widget:hover .btn-action[data-n="1"] { /* vertical extreme */
5339
- transform: translateY(calc(var(--sy) * 90px)) scale(1);
5340
- transition-delay: .03s;
5341
- }
5342
- .widget:hover .btn-action[data-n="2"] { /* diagonal near-vertical */
5343
- transform: translate(calc(var(--sx) * 45px), calc(var(--sy) * 78px)) scale(1);
5344
- transition-delay: .07s;
5345
- }
5346
- .widget:hover .btn-action[data-n="3"] { /* diagonal near-horizontal */
5347
- transform: translate(calc(var(--sx) * 78px), calc(var(--sy) * 45px)) scale(1);
5348
- transition-delay: .11s;
6709
+ .action-item {
6710
+ display: flex;
6711
+ flex-direction: column;
6712
+ align-items: center;
6713
+ justify-content: flex-start;
6714
+ gap: 3px;
6715
+ padding: 8px 4px;
6716
+ border: none;
6717
+ border-radius: 8px;
6718
+ cursor: pointer;
6719
+ background: transparent;
6720
+ color: #c9d1d9;
6721
+ font-size: 18px;
6722
+ line-height: 1;
6723
+ transition: background .12s;
5349
6724
  }
5350
- .widget:hover .btn-action[data-n="4"] { /* horizontal extreme */
5351
- transform: translateX(calc(var(--sx) * 90px)) scale(1);
5352
- transition-delay: .15s;
6725
+ .action-item:hover { background: #21262d; }
6726
+ .action-item .label {
6727
+ font-size: 9px;
6728
+ font-weight: 500;
6729
+ color: #8b949e;
6730
+ white-space: nowrap;
5353
6731
  }
6732
+ .action-item:hover .label { color: #e6edf3; }
5354
6733
 
5355
- /* Per-direction anchors + label sides */
6734
+ /* Per-direction anchors */
5356
6735
  ${directionBlocks()}
5357
6736
 
5358
6737
  /* \u2500\u2500 REC / PAUSED badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
@@ -5386,14 +6765,18 @@ function getRecorderStyles(rec, paused) {
5386
6765
  function renderRecorderWidget(rec, paused, t) {
5387
6766
  return `
5388
6767
  <div class="widget">
5389
- <button class="btn-action" data-n="1" data-action="config"
5390
- data-label="${t("RECORDER.BTN_CONFIG")}">\u2699\uFE0F</button>
5391
- <button class="btn-action" data-n="2" data-action="browse"
5392
- data-label="${t("RECORDER.BTN_FILES")}">\u{1F4C1}</button>
5393
- <button class="btn-action" data-n="3" data-action="commands"
5394
- data-label="${t("RECORDER.BTN_COMMANDS")}">\u2328\uFE0F</button>
5395
- <button class="btn-action" data-n="4" data-action="tests"
5396
- data-label="${t("RECORDER.BTN_TESTS")}">\u{1F4CB}</button>
6768
+ <div class="action-menu">
6769
+ <button class="action-item" data-action="config">
6770
+ <span class="ico">\u2699\uFE0F</span><span class="label">${t("RECORDER.BTN_CONFIG")}</span></button>
6771
+ <button class="action-item" data-action="browse">
6772
+ <span class="ico">\u{1F4C1}</span><span class="label">${t("RECORDER.BTN_FILES")}</span></button>
6773
+ <button class="action-item" data-action="commands">
6774
+ <span class="ico">\u2328\uFE0F</span><span class="label">${t("RECORDER.BTN_COMMANDS")}</span></button>
6775
+ <button class="action-item" data-action="tests">
6776
+ <span class="ico">\u{1F4CB}</span><span class="label">${t("RECORDER.BTN_TESTS")}</span></button>
6777
+ <button class="action-item" data-action="help">
6778
+ <span class="ico">\u2753</span><span class="label">${t("RECORDER.BTN_HELP")}</span></button>
6779
+ </div>
5397
6780
  <button class="btn-pause" data-action="pause"
5398
6781
  title="${paused ? t("RECORDER.RESUME_TITLE") : t("RECORDER.PAUSE_TITLE")}">
5399
6782
  ${paused ? "\u25B6" : "\u23F8"}
@@ -5431,6 +6814,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
5431
6814
  _previsualizerRef = null;
5432
6815
  httpMonitor;
5433
6816
  smartSelectorEnabled = true;
6817
+ _needsRecordingRebuild = false;
5434
6818
  recording;
5435
6819
  persistence;
5436
6820
  translation;
@@ -5444,6 +6828,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
5444
6828
  isSaveTestDialogOpen = false;
5445
6829
  isSettingsDialogOpen = false;
5446
6830
  isAdvancedEditorDialogOpen = false;
6831
+ isHelpDialogOpen = false;
5447
6832
  constructor() {
5448
6833
  super();
5449
6834
  this.shadow = this.attachShadow({ mode: "open" });
@@ -5456,7 +6841,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
5456
6841
  if (!this.getAttribute("data-cy")) {
5457
6842
  this.setAttribute("data-cy", "lib-e2e-cypress-for-dummys");
5458
6843
  }
5459
- if (!this.recording) this.recording = new RecordingService();
6844
+ if (!this.recording || this._needsRecordingRebuild) {
6845
+ this.recording = new RecordingService();
6846
+ this._needsRecordingRebuild = false;
6847
+ }
5460
6848
  if (!this.persistence) this.persistence = new PersistenceService();
5461
6849
  if (!this.translation) this.translation = new TranslationService();
5462
6850
  this.httpMonitor = new HttpMonitor(this.recording);
@@ -5497,6 +6885,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
5497
6885
  if (this.widgetResize) window.removeEventListener("resize", this.widgetResize);
5498
6886
  this.httpMonitor?.uninstall();
5499
6887
  this.recording.destroy();
6888
+ this._needsRecordingRebuild = true;
5500
6889
  }
5501
6890
  async initHttpConfig() {
5502
6891
  const config = await this.persistence.getConfig("extendedHttpCommands");
@@ -5506,6 +6895,8 @@ var LibE2eRecorderElement = class extends HTMLElement {
5506
6895
  } else {
5507
6896
  localStorage.setItem("extendedHttpCommands", config["extendedHttpCommands"] ?? "true");
5508
6897
  }
6898
+ const fx = await this.persistence.getConfig("fixtureMode");
6899
+ localStorage.setItem("fixtureMode", fx?.["fixtureMode"] ?? "false");
5509
6900
  }
5510
6901
  async initLanguage() {
5511
6902
  const config = await this.persistence.getConfig("language");
@@ -5708,6 +7099,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
5708
7099
  widget.setAttribute("data-expand", this.expandDir);
5709
7100
  }
5710
7101
  beginWidgetDrag(e) {
7102
+ this.suppressNextToggleClick = false;
5711
7103
  const origin = this.togglePos ?? defaultTogglePosition(window.innerWidth, window.innerHeight);
5712
7104
  this.dragState = { startX: e.clientX, startY: e.clientY, origX: origin.x, origY: origin.y, moved: false };
5713
7105
  }
@@ -5772,6 +7164,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
5772
7164
  } else if (key === "3") {
5773
7165
  event.preventDefault();
5774
7166
  this.showSettingsDialog();
7167
+ } else if (key === "h" && event.shiftKey) {
7168
+ event.preventDefault();
7169
+ this.showHelpDialog();
5775
7170
  }
5776
7171
  }
5777
7172
  toggleVisibility() {
@@ -5955,7 +7350,9 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
5955
7350
  const type = document.getElementById("assert-type").value;
5956
7351
  const val = document.getElementById("assert-value").value.trim();
5957
7352
  if (!sel) return;
5958
- const cmd = NO_VALUE_ASSERTIONS.has(type) || !val ? `cy.get('${sel}').should('${type}')` : `cy.get('${sel}').should('${type}', '${val}')`;
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}')`;
5959
7356
  this.recording.appendCommand(cmd);
5960
7357
  document.getElementById("assert-selector").value = "";
5961
7358
  document.getElementById("assert-value").value = "";
@@ -6011,15 +7408,19 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6011
7408
  if (!container) return;
6012
7409
  const child = document.createElement("save-test");
6013
7410
  child.translation = this.translation;
7411
+ this.persistence.getGeneralConfig().then((config) => {
7412
+ const providerId = config?.["issueTrackerProviderId"];
7413
+ if (providerId) child.issueTrackerProviderId = providerId;
7414
+ });
6014
7415
  container.appendChild(child);
6015
7416
  child.addEventListener("savetest", (e) => {
6016
- const { description, notes, tags } = e.detail ?? {};
6017
- this.onSaveTest(description ?? null, tags ?? [], notes ?? "");
7417
+ const { description, notes, tags, ticketId } = e.detail ?? {};
7418
+ this.onSaveTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? null);
6018
7419
  import_sweetalert2.default.close();
6019
7420
  });
6020
7421
  child.addEventListener("saveandexport", (e) => {
6021
- const { description, notes, tags } = e.detail ?? {};
6022
- this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "");
7422
+ const { description, notes, tags, ticketId } = e.detail ?? {};
7423
+ this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? null);
6023
7424
  import_sweetalert2.default.close();
6024
7425
  });
6025
7426
  },
@@ -6063,6 +7464,31 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6063
7464
  this.resizePopup();
6064
7465
  });
6065
7466
  }
7467
+ showHelpDialog() {
7468
+ this.toggleModal("isHelpDialogOpen", () => {
7469
+ import_sweetalert2.default.fire({
7470
+ title: this.translation.translate("HELP.TITLE"),
7471
+ html: '<div id="help-modal-content" style="padding:0"></div>',
7472
+ 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
+ });
7491
+ }
6066
7492
  showAdvancedEditorDialog(testId) {
6067
7493
  this.toggleModal("isAdvancedEditorDialogOpen", () => {
6068
7494
  import_sweetalert2.default.fire({
@@ -6127,7 +7553,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6127
7553
  showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
6128
7554
  import_sweetalert2.default.fire({
6129
7555
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
6130
- html: '<div id="file-editor-modal-content" style="padding:0;height:540px"></div>',
7556
+ html: '<div id="file-editor-modal-content" style="flex:1;min-height:0;display:flex;flex-direction:column;padding:0"></div>',
6131
7557
  showCloseButton: false,
6132
7558
  showConfirmButton: false,
6133
7559
  allowOutsideClick: false,
@@ -6136,6 +7562,20 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6136
7562
  didOpen: () => {
6137
7563
  makeSwalDraggable();
6138
7564
  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
+ }
6139
7579
  const container = document.getElementById("file-editor-modal-content");
6140
7580
  if (!container) return;
6141
7581
  const child = document.createElement("file-preview");
@@ -6168,18 +7608,22 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6168
7608
  this.resizePopup();
6169
7609
  }
6170
7610
  // ── save handlers ─────────────────────────────────────────────────────────
6171
- async onSaveTest(description, tags = [], notes = "") {
7611
+ async onSaveTest(description, tags = [], notes = "", ticketId = null) {
6172
7612
  if (description) {
6173
- await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
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);
6174
7616
  this.recording.clearCommands();
6175
7617
  this.clearRecordingHistory();
6176
7618
  this.cypressCommands = [];
6177
7619
  this.interceptors = [];
6178
7620
  }
6179
7621
  }
6180
- async onSaveAndExportTest(description, tags = [], notes = "") {
7622
+ async onSaveAndExportTest(description, tags = [], notes = "", ticketId = null) {
6181
7623
  if (description) {
6182
- const id = await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes);
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);
6183
7627
  this.recording.clearCommands();
6184
7628
  this.clearRecordingHistory();
6185
7629
  this.cypressCommands = [];
@@ -6187,6 +7631,16 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6187
7631
  setTimeout(() => this.showAdvancedEditorDialog(id), 300);
6188
7632
  }
6189
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);
7642
+ }
7643
+ }
6190
7644
  // ── helpers ───────────────────────────────────────────────────────────────
6191
7645
  toggleModal(flag, openFn) {
6192
7646
  if (this[flag]) {
@@ -6222,6 +7676,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
6222
7676
  this.shadow.querySelector('[data-action="commands"]')?.addEventListener("click", () => this.showCommandsDialog());
6223
7677
  this.shadow.querySelector('[data-action="config"]')?.addEventListener("click", () => this.showSettingsDialog());
6224
7678
  this.shadow.querySelector('[data-action="browse"]')?.addEventListener("click", () => this.showAdvancedEditorDialog());
7679
+ this.shadow.querySelector('[data-action="help"]')?.addEventListener("click", () => this.showHelpDialog());
6225
7680
  this.applyWidgetPosition();
6226
7681
  }
6227
7682
  };
@@ -6230,20 +7685,26 @@ if (!customElements.get("lib-e2e-recorder")) {
6230
7685
  }
6231
7686
 
6232
7687
  // src/index.ts
6233
- var VERSION = "0.6.0";
7688
+ var VERSION = "0.8.0";
6234
7689
  // Annotate the CommonJS export names for ESM import in node:
6235
7690
  0 && (module.exports = {
6236
7691
  ACTIVE_SESSION_BREADCRUMB_KEY,
6237
7692
  AdvancedTestEditorElement,
6238
7693
  AdvancedTestTransformationService,
7694
+ CHAIN_METHODS,
7695
+ CYPRESS_COMPLETIONS,
7696
+ CY_METHODS,
6239
7697
  ConfigurationElement,
6240
7698
  DB_SCHEMA,
6241
7699
  DB_STORE_NAMES,
7700
+ DEFAULT_ISSUE_TRACKER_PROVIDER_ID,
6242
7701
  DEFAULT_RESUME_TTL_MINUTES,
6243
7702
  DRAG_THRESHOLD,
6244
7703
  FilePreviewElement,
7704
+ HelpPanelElement,
6245
7705
  HttpMonitor,
6246
7706
  INPUT_TYPES,
7707
+ ISSUE_TRACKER_PROVIDERS,
6247
7708
  LIB_E2E_CYPRESS_FOR_DUMMYS_SWAL2_STYLES,
6248
7709
  LOCALE_BY_LANG,
6249
7710
  LibE2eRecorderElement,
@@ -6256,6 +7717,7 @@ var VERSION = "0.6.0";
6256
7717
  SelectorPickerElement,
6257
7718
  Subject,
6258
7719
  TOGGLE_MARGIN,
7720
+ TOP_LEVEL,
6259
7721
  TestEditorElement,
6260
7722
  TestPrevisualizerElement,
6261
7723
  TransformationService,
@@ -6264,9 +7726,16 @@ var VERSION = "0.6.0";
6264
7726
  advancedTestTransformationService,
6265
7727
  boxOffsetForDirection,
6266
7728
  boxTopLeftFor,
7729
+ buildTicketComment,
7730
+ buildTicketUrl,
6267
7731
  clampTogglePosition,
7732
+ createCodeEditor,
6268
7733
  defaultTogglePosition,
7734
+ filterCompletions,
7735
+ filterCypressCompletions,
7736
+ findIssueTrackerProvider,
6269
7737
  generateAlias,
7738
+ inferAssertionCommand,
6270
7739
  injectStyles,
6271
7740
  isLang,
6272
7741
  isLocalHost,
@@ -6276,6 +7745,7 @@ var VERSION = "0.6.0";
6276
7745
  makeSwalDraggableByContentId,
6277
7746
  persistenceService,
6278
7747
  resolveExpandDirection,
7748
+ resolveTicketUrl,
6279
7749
  selectTestsForExport,
6280
7750
  setSwal2DataCyAttribute,
6281
7751
  showToast,