lib-e2e-cypress-for-dummys-ts 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,41 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/utils/code-format.utils.ts
34
+ function gcd(a, b) {
35
+ while (b) {
36
+ const t = b;
37
+ b = a % b;
38
+ a = t;
39
+ }
40
+ return a;
41
+ }
42
+ function escapeSingleQuotes(value) {
43
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
44
+ }
45
+ function escapeCssAttrValue(value) {
46
+ return value.replace(/"/g, '\\"');
47
+ }
48
+ function normalizeBlock(code, baseIndent) {
49
+ const lines = code.split("\n").map((l) => l.trimEnd());
50
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
51
+ if (!nonEmpty.length) return "";
52
+ const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
53
+ const minIndent = Math.min(...indents);
54
+ const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
55
+ const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
56
+ return lines.map((l) => {
57
+ if (!l.trim()) return "";
58
+ const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
59
+ return baseIndent + " ".repeat(level) + l.trimStart();
60
+ }).join("\n");
61
+ }
62
+ var init_code_format_utils = __esm({
63
+ "src/utils/code-format.utils.ts"() {
64
+ "use strict";
65
+ }
66
+ });
67
+
33
68
  // src/utils/selector-quality.utils.ts
34
69
  function getSelectorQuality(el) {
35
70
  if (el.hasAttribute("data-cy") || el.hasAttribute("data-testid") || el.hasAttribute("aria-label")) {
@@ -46,11 +81,11 @@ function getSelectorQuality(el) {
46
81
  }
47
82
  function buildPickerSelector(el) {
48
83
  const dataCy = el.getAttribute("data-cy");
49
- if (dataCy) return `[data-cy="${dataCy}"]`;
84
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
50
85
  const dataTestid = el.getAttribute("data-testid");
51
- if (dataTestid) return `[data-testid="${dataTestid}"]`;
86
+ if (dataTestid) return `[data-testid="${escapeCssAttrValue(dataTestid)}"]`;
52
87
  const ariaLabel = el.getAttribute("aria-label");
53
- if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
88
+ if (ariaLabel) return `[aria-label="${escapeCssAttrValue(ariaLabel)}"]`;
54
89
  const rawId = el.id;
55
90
  if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
56
91
  return `#${rawId}`;
@@ -84,6 +119,7 @@ var FORBIDDEN_ID_PREFIXES;
84
119
  var init_selector_quality_utils = __esm({
85
120
  "src/utils/selector-quality.utils.ts"() {
86
121
  "use strict";
122
+ init_code_format_utils();
87
123
  FORBIDDEN_ID_PREFIXES = [
88
124
  "cdk-",
89
125
  "mat-",
@@ -314,6 +350,7 @@ var init_selector_picker = __esm({
314
350
  "src/components/selector-picker/selector-picker.ts"() {
315
351
  "use strict";
316
352
  init_selector_quality_utils();
353
+ init_code_format_utils();
317
354
  init_selector_picker_styles();
318
355
  init_selector_picker_template();
319
356
  OWN_DATA_CY = "lib-e2e-cypress-for-dummys";
@@ -433,7 +470,7 @@ var init_selector_picker = __esm({
433
470
  confirm(index) {
434
471
  const row = this.rows[index];
435
472
  if (!row) return;
436
- const command = `cy.get('${row.selector}').click()`;
473
+ const command = `cy.get('${escapeSingleQuotes(row.selector)}').click()`;
437
474
  this.recording.appendCommand(command);
438
475
  this.dispatchEvent(new CustomEvent("selectorchosen", { detail: command, bubbles: true, composed: true }));
439
476
  this.closeSilently();
@@ -451,8 +488,8 @@ var init_selector_picker = __esm({
451
488
  this.remove();
452
489
  }
453
490
  };
454
- if (!customElements.get("selector-picker")) {
455
- customElements.define("selector-picker", SelectorPickerElement);
491
+ if (!customElements.get("lib-e2e-selector-picker")) {
492
+ customElements.define("lib-e2e-selector-picker", SelectorPickerElement);
456
493
  }
457
494
  }
458
495
  });
@@ -460,14 +497,11 @@ var init_selector_picker = __esm({
460
497
  // src/index.ts
461
498
  var index_exports = {};
462
499
  __export(index_exports, {
463
- ACTIVE_SESSION_BREADCRUMB_KEY: () => ACTIVE_SESSION_BREADCRUMB_KEY,
464
500
  AdvancedTestEditorElement: () => AdvancedTestEditorElement,
465
501
  AdvancedTestTransformationService: () => AdvancedTestTransformationService,
466
502
  ConfigurationElement: () => ConfigurationElement,
467
503
  DB_SCHEMA: () => DB_SCHEMA,
468
504
  DB_STORE_NAMES: () => DB_STORE_NAMES,
469
- DEFAULT_RESUME_TTL_MINUTES: () => DEFAULT_RESUME_TTL_MINUTES,
470
- DRAG_THRESHOLD: () => DRAG_THRESHOLD,
471
505
  FilePreviewElement: () => FilePreviewElement,
472
506
  HelpPanelElement: () => HelpPanelElement,
473
507
  HttpMonitor: () => HttpMonitor,
@@ -476,24 +510,16 @@ __export(index_exports, {
476
510
  LOCALE_BY_LANG: () => LOCALE_BY_LANG,
477
511
  LibE2eRecorderElement: () => LibE2eRecorderElement,
478
512
  PersistenceService: () => PersistenceService,
479
- RESUME_TTL_CONFIG_KEY: () => RESUME_TTL_CONFIG_KEY,
480
513
  RecordingService: () => RecordingService,
481
514
  SCROLLBAR_STYLES: () => SCROLLBAR_STYLES,
482
515
  SUPPORTED_LANGS: () => SUPPORTED_LANGS,
483
516
  SaveTestElement: () => SaveTestElement,
484
517
  SelectorPickerElement: () => SelectorPickerElement,
485
- Subject: () => Subject,
486
- TOGGLE_MARGIN: () => TOGGLE_MARGIN,
487
518
  TestEditorElement: () => TestEditorElement,
488
519
  TestPrevisualizerElement: () => TestPrevisualizerElement,
489
520
  TransformationService: () => TransformationService,
490
521
  TranslationService: () => TranslationService,
491
522
  VERSION: () => VERSION,
492
- advancedTestTransformationService: () => advancedTestTransformationService,
493
- boxOffsetForDirection: () => boxOffsetForDirection,
494
- boxTopLeftFor: () => boxTopLeftFor,
495
- clampTogglePosition: () => clampTogglePosition,
496
- defaultTogglePosition: () => defaultTogglePosition,
497
523
  generateAlias: () => generateAlias,
498
524
  inferAssertionCommand: () => inferAssertionCommand,
499
525
  injectStyles: () => injectStyles,
@@ -502,14 +528,8 @@ __export(index_exports, {
502
528
  localeForLang: () => localeForLang,
503
529
  makeModalResizable: () => makeModalResizable,
504
530
  makeSwalDraggable: () => makeSwalDraggable,
505
- makeSwalDraggableByContentId: () => makeSwalDraggableByContentId,
506
- persistenceService: () => persistenceService,
507
- resolveExpandDirection: () => resolveExpandDirection,
508
531
  selectTestsForExport: () => selectTestsForExport,
509
- setSwal2DataCyAttribute: () => setSwal2DataCyAttribute,
510
- showToast: () => showToast,
511
- transformationService: () => transformationService,
512
- translationService: () => translationService
532
+ showToast: () => showToast
513
533
  });
514
534
  module.exports = __toCommonJS(index_exports);
515
535
 
@@ -598,11 +618,6 @@ var DB_SCHEMA = {
598
618
  };
599
619
  var DB_STORE_NAMES = DB_SCHEMA.stores.map((s) => s.name);
600
620
 
601
- // src/models/active-session.model.ts
602
- var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
603
- var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
604
- var DEFAULT_RESUME_TTL_MINUTES = 30;
605
-
606
621
  // src/utils/subject.ts
607
622
  var Subject = class {
608
623
  _value;
@@ -650,7 +665,10 @@ var I18N_ES = {
650
665
  NOTES_PLACEHOLDER: "Qu\xE9 valida este test, condiciones previas, etc.",
651
666
  TICKET_LABEL: "Ticket / Issue (opcional):",
652
667
  TICKET_PLACEHOLDER: "Ej: PROJ-123, #42",
653
- TICKET_WARN: "ID no v\xE1lido para el proveedor configurado (se guardar\xE1 igualmente)"
668
+ TICKET_WARN: "ID no v\xE1lido para el proveedor configurado (se guardar\xE1 igualmente)",
669
+ CONFIRM_DISCARD: "\xBFSeguro que quieres descartar el test grabado? Se perder\xE1n todos los pasos registrados.",
670
+ CONFIRM_DISCARD_BTN: "S\xED, descartar",
671
+ BACK_BTN: "Volver"
654
672
  },
655
673
  TEST_EDITOR: {
656
674
  NO_TAGS: "Sin etiquetas",
@@ -732,7 +750,32 @@ var I18N_ES = {
732
750
  ISSUE_TRACKER_SUB: "A\xF1ade un ID de ticket al guardar y genera un enlace clickable en el listado.",
733
751
  ISSUE_TRACKER_PROVIDER: "Proveedor",
734
752
  ISSUE_TRACKER_BASE_URL: "URL base",
735
- ISSUE_TRACKER_BASE_URL_PH: "Ej: https://miproyecto.atlassian.net"
753
+ ISSUE_TRACKER_BASE_URL_PH: "Ej: https://miproyecto.atlassian.net",
754
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
755
+ LOGIN_SETUP_NOT_CONFIGURED: "No configurado",
756
+ LOGIN_SETUP_SET_UP_BTN: "Configurar",
757
+ LOGIN_SETUP_TITLE: "Configuraci\xF3n de login",
758
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Crear scaffold",
759
+ LOGIN_SETUP_SELECT_EXISTING: "Seleccionar fichero existente",
760
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nombre del fichero",
761
+ LOGIN_SETUP_FILE_NAME_PH: "Ej: cypress/common-services/login.service.ts",
762
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-escanear",
763
+ LOGIN_SETUP_FUNCTIONS_DETECTED: "Funciones detectadas",
764
+ LOGIN_SETUP_BLOCK_ASSIGN: "Asignaci\xF3n de bloques",
765
+ LOGIN_SETUP_NO_FUNCTIONS: "No se encontraron funciones exportadas. Rellena el scaffold y re-escanea.",
766
+ LOGIN_SETUP_BEFORE_LABEL: "Funci\xF3n before() (una vez por suite)",
767
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Funci\xF3n beforeEach() (antes de cada test)",
768
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Ninguna \u2014",
769
+ LOGIN_SETUP_SUMMARY: "Fichero: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
770
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Guardar",
771
+ LOGIN_SETUP_CLEAR_BTN: "Eliminar configuraci\xF3n",
772
+ LOGIN_SETUP_CANCEL_BTN: "Cancelar",
773
+ LOGIN_SETUP_SOURCE_LABEL: "Origen del fichero de login",
774
+ LOGIN_SETUP_CREATE_SUB: "Genera un scaffold con funciones de ejemplo",
775
+ LOGIN_SETUP_SELECT_SUB: "Apunta a un servicio de login existente",
776
+ LOGIN_SETUP_INJECT_TITLE: "\xBFA\xF1adir bloques de login?",
777
+ LOGIN_SETUP_INJECT_TEXT: "Este fichero no tiene los bloques de login configurados. \xBFQuieres a\xF1adir el import y los bloques before/beforeEach ahora?",
778
+ LOGIN_SETUP_INJECT_CONFIRM: "S\xED, a\xF1adir"
736
779
  },
737
780
  SELECTOR_PICKER: {
738
781
  TITLE: "Selecciona un elemento del DOM",
@@ -823,6 +866,9 @@ var I18N_ES = {
823
866
  FILE_SAVE_ERROR_TOAST: "Error al guardar el archivo",
824
867
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures escritos en cypress/fixtures",
825
868
  FIXTURES_NO_FOLDER_TOAST: "No se pudieron escribir los fixtures (configura la carpeta Cypress)",
869
+ FILE_UPLOAD_MANUAL_TOAST: "A\xF1ade {files} a cypress/fixtures/ antes de ejecutar el test.",
870
+ FILE_UPLOAD_COPIED_TOAST: "{files} copiado/s a cypress/fixtures/.",
871
+ FILE_UPLOAD_COPY_ERROR_TOAST: "No se pudo copiar {files} a cypress/fixtures/. A\xF1\xE1delo manualmente.",
826
872
  BTN_CONFIG: "Config",
827
873
  BTN_FILES: "Ficheros",
828
874
  BTN_COMMANDS: "Comandos",
@@ -870,7 +916,7 @@ var I18N_ES = {
870
916
  SEC_PANELS: "\u{1F5C2} Paneles",
871
917
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 guardados: filtrar por tag, copiar, combinar, exportar.",
872
918
  PANEL_COMMANDS: "\u2328\uFE0F Comandos \u2014 de la grabaci\xF3n actual: reordenar, borrar, a\xF1adir aserciones.",
873
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 idioma, selector, HTTP, carpeta Cypress, datos, visibilidad, posici\xF3n.",
919
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 idioma, selector, HTTP, carpeta Cypress, login setup, datos, visibilidad, posici\xF3n.",
874
920
  PANEL_FILES: "\u{1F4C1} Ficheros \u2014 inserta tests en tus .cy.ts y lanza el runner.",
875
921
  SEC_SELECTOR: "\u{1F50E} Selectores",
876
922
  SELECTOR_1: "Elige el atributo preferido (data-cy, data-testid, aria-label, id) en Config.",
@@ -883,6 +929,7 @@ var I18N_ES = {
883
929
  ADV_RUNNER: "Runner local: npx lib-e2e-cypress-runner para lanzar un spec headless desde el editor.",
884
930
  ADV_CROSSAPP: "Cross-app: la grabaci\xF3n sobrevive al cambiar de micro-frontend (mismo origen).",
885
931
  ADV_DRAG: "Arrastra el bot\xF3n \u23FA para mover el widget; recuerda su posici\xF3n.",
932
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
886
933
  CLOSE: "Cerrar",
887
934
  TAB_REFERENCE: "Referencia r\xE1pida",
888
935
  TAB_GUIDE: "Gu\xEDa de uso",
@@ -900,13 +947,13 @@ var I18N_ES = {
900
947
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
901
948
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
902
949
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
950
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
903
951
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
904
952
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
905
953
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
906
954
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
907
955
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
908
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
909
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
956
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
910
957
  }
911
958
  };
912
959
 
@@ -937,7 +984,10 @@ var I18N_EN = {
937
984
  NOTES_PLACEHOLDER: "What this test validates, preconditions, etc.",
938
985
  TICKET_LABEL: "Ticket / Issue (optional):",
939
986
  TICKET_PLACEHOLDER: "E.g.: PROJ-123, #42",
940
- TICKET_WARN: "Invalid ID for the configured provider (will be saved anyway)"
987
+ TICKET_WARN: "Invalid ID for the configured provider (will be saved anyway)",
988
+ CONFIRM_DISCARD: "Are you sure you want to discard this recording? All recorded steps will be lost.",
989
+ CONFIRM_DISCARD_BTN: "Yes, discard",
990
+ BACK_BTN: "Go back"
941
991
  },
942
992
  TEST_EDITOR: {
943
993
  NO_TAGS: "No tags",
@@ -1019,7 +1069,32 @@ var I18N_EN = {
1019
1069
  ISSUE_TRACKER_SUB: "Add a ticket ID when saving and generate a clickable link in the test list.",
1020
1070
  ISSUE_TRACKER_PROVIDER: "Provider",
1021
1071
  ISSUE_TRACKER_BASE_URL: "Base URL",
1022
- ISSUE_TRACKER_BASE_URL_PH: "E.g.: https://myproject.atlassian.net"
1072
+ ISSUE_TRACKER_BASE_URL_PH: "E.g.: https://myproject.atlassian.net",
1073
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1074
+ LOGIN_SETUP_NOT_CONFIGURED: "Not configured",
1075
+ LOGIN_SETUP_SET_UP_BTN: "Set up",
1076
+ LOGIN_SETUP_TITLE: "Login setup",
1077
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Create scaffold",
1078
+ LOGIN_SETUP_SELECT_EXISTING: "Select existing file",
1079
+ LOGIN_SETUP_FILE_NAME_LABEL: "File name",
1080
+ LOGIN_SETUP_FILE_NAME_PH: "E.g.: cypress/common-services/login.service.ts",
1081
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scan",
1082
+ LOGIN_SETUP_FUNCTIONS_DETECTED: "Detected functions",
1083
+ LOGIN_SETUP_BLOCK_ASSIGN: "Block assignment",
1084
+ LOGIN_SETUP_NO_FUNCTIONS: "No exported functions found. Fill in the scaffold and re-scan.",
1085
+ LOGIN_SETUP_BEFORE_LABEL: "before() function (once per suite)",
1086
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "beforeEach() function (before every test)",
1087
+ LOGIN_SETUP_NONE_OPTION: "\u2014 None \u2014",
1088
+ LOGIN_SETUP_SUMMARY: "File: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
1089
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Save",
1090
+ LOGIN_SETUP_CLEAR_BTN: "Remove configuration",
1091
+ LOGIN_SETUP_CANCEL_BTN: "Cancel",
1092
+ LOGIN_SETUP_SOURCE_LABEL: "Login file source",
1093
+ LOGIN_SETUP_CREATE_SUB: "Generate a scaffold with sample functions",
1094
+ LOGIN_SETUP_SELECT_SUB: "Point to an existing login service file",
1095
+ LOGIN_SETUP_INJECT_TITLE: "Add login blocks?",
1096
+ LOGIN_SETUP_INJECT_TEXT: "This file does not have the configured login blocks. Do you want to add the import and before/beforeEach blocks now?",
1097
+ LOGIN_SETUP_INJECT_CONFIRM: "Yes, add them"
1023
1098
  },
1024
1099
  SELECTOR_PICKER: {
1025
1100
  TITLE: "Select a DOM element",
@@ -1110,6 +1185,9 @@ var I18N_EN = {
1110
1185
  FILE_SAVE_ERROR_TOAST: "Error saving the file",
1111
1186
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures written to cypress/fixtures",
1112
1187
  FIXTURES_NO_FOLDER_TOAST: "Could not write fixtures (configure the Cypress folder)",
1188
+ FILE_UPLOAD_MANUAL_TOAST: "Add {files} to cypress/fixtures/ before running the test.",
1189
+ FILE_UPLOAD_COPIED_TOAST: "{files} copied to cypress/fixtures/.",
1190
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Could not copy {files} to cypress/fixtures/. Add it manually.",
1113
1191
  BTN_CONFIG: "Config",
1114
1192
  BTN_FILES: "Files",
1115
1193
  BTN_COMMANDS: "Commands",
@@ -1157,7 +1235,7 @@ var I18N_EN = {
1157
1235
  SEC_PANELS: "\u{1F5C2} Panels",
1158
1236
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 saved tests: filter by tag, copy, combine, export.",
1159
1237
  PANEL_COMMANDS: "\u2328\uFE0F Commands \u2014 current recording: reorder, delete, add assertions.",
1160
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 language, selector, HTTP, Cypress folder, data, visibility, position.",
1238
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 language, selector, HTTP, Cypress folder, login setup, data, visibility, position.",
1161
1239
  PANEL_FILES: "\u{1F4C1} Files \u2014 insert tests into your .cy.ts and launch the runner.",
1162
1240
  SEC_SELECTOR: "\u{1F50E} Selectors",
1163
1241
  SELECTOR_1: "Choose the preferred attribute (data-cy, data-testid, aria-label, id) in Config.",
@@ -1170,6 +1248,7 @@ var I18N_EN = {
1170
1248
  ADV_RUNNER: "Local runner: npx lib-e2e-cypress-runner to run a spec headless from the editor.",
1171
1249
  ADV_CROSSAPP: "Cross-app: the recording survives switching micro-frontend (same origin).",
1172
1250
  ADV_DRAG: "Drag the \u23FA button to move the widget; its position is remembered.",
1251
+ ADV_LOGIN: "Login Setup (Ctrl+3): point to your login service; the recorder automatically injects before()/beforeEach() blocks into each new test.",
1173
1252
  CLOSE: "Close",
1174
1253
  TAB_REFERENCE: "Quick reference",
1175
1254
  TAB_GUIDE: "Usage guide",
@@ -1187,13 +1266,13 @@ var I18N_EN = {
1187
1266
  G_COV_4: "Reliable selector (data-cy/testid/aria/id) and a picker when none is obvious.",
1188
1267
  G_COV_5: "Automatic assertions (Alt+click) and the manual builder.",
1189
1268
  G_COV_6: "Continuity when switching micro-frontend (same origin), invisible mode and i18n.",
1269
+ G_COV_7: "Login Setup: configure your login function under Ctrl+3 \u2192 Login Setup; the recorder adds before()/beforeEach() blocks to every new test.",
1190
1270
  G_SEC_NOTCOVERED: "\u{1F6AB} What it does NOT cover (yet)",
1191
1271
  G_NC_1: "Different origins / subdomains (same origin only).",
1192
1272
  G_NC_2: "File upload (selectFile), drag & drop, and hover.",
1193
1273
  G_NC_3: "The Tab key (Cypress can't .type() it).",
1194
1274
  G_NC_4: "DELETE requests (intentionally ignored).",
1195
- G_NC_5: "Third-party iframes and Shadow DOM may not be captured.",
1196
- G_NC_6: "Programmatic login generation (on the roadmap)."
1275
+ G_NC_5: "Third-party iframes and Shadow DOM may not be captured."
1197
1276
  }
1198
1277
  };
1199
1278
 
@@ -1224,8 +1303,11 @@ var I18N_FR = {
1224
1303
  NOTES_PLACEHOLDER: "Ce que ce test valide, conditions pr\xE9alables, etc.",
1225
1304
  TICKET_LABEL: "Ticket / Issue (optionnel) :",
1226
1305
  TICKET_PLACEHOLDER: "Ex : PROJ-123, #42",
1227
- TICKET_WARN: "ID non valide pour le fournisseur configur\xE9 (sera enregistr\xE9 quand m\xEAme)"
1306
+ TICKET_WARN: "ID non valide pour le fournisseur configur\xE9 (sera enregistr\xE9 quand m\xEAme)",
1228
1307
  // TODO: translate
1308
+ CONFIRM_DISCARD: "\xCAtes-vous s\xFBr de vouloir abandonner cet enregistrement ? Toutes les \xE9tapes enregistr\xE9es seront perdues.",
1309
+ CONFIRM_DISCARD_BTN: "Oui, abandonner",
1310
+ BACK_BTN: "Retour"
1229
1311
  },
1230
1312
  TEST_EDITOR: {
1231
1313
  NO_TAGS: "Sans \xE9tiquettes",
@@ -1307,7 +1389,32 @@ var I18N_FR = {
1307
1389
  ISSUE_TRACKER_SUB: "Ajoutez un ID de ticket lors de l'enregistrement et g\xE9n\xE9rez un lien cliquable.",
1308
1390
  ISSUE_TRACKER_PROVIDER: "Fournisseur",
1309
1391
  ISSUE_TRACKER_BASE_URL: "URL de base",
1310
- ISSUE_TRACKER_BASE_URL_PH: "Ex : https://monprojet.atlassian.net"
1392
+ ISSUE_TRACKER_BASE_URL_PH: "Ex : https://monprojet.atlassian.net",
1393
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1394
+ LOGIN_SETUP_NOT_CONFIGURED: "Non configur\xE9",
1395
+ LOGIN_SETUP_SET_UP_BTN: "Configurer",
1396
+ LOGIN_SETUP_TITLE: "Configuration du login",
1397
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Cr\xE9er un scaffold",
1398
+ LOGIN_SETUP_SELECT_EXISTING: "S\xE9lectionner un fichier existant",
1399
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nom du fichier",
1400
+ LOGIN_SETUP_FILE_NAME_PH: "Ex : cypress/common-services/login.service.ts",
1401
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scanner",
1402
+ LOGIN_SETUP_FUNCTIONS_DETECTED: "Fonctions d\xE9tect\xE9es",
1403
+ LOGIN_SETUP_BLOCK_ASSIGN: "Affectation des blocs",
1404
+ LOGIN_SETUP_NO_FUNCTIONS: "Aucune fonction export\xE9e trouv\xE9e. Remplissez le scaffold et re-scannez.",
1405
+ LOGIN_SETUP_BEFORE_LABEL: "Fonction before() (une fois par suite)",
1406
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Fonction beforeEach() (avant chaque test)",
1407
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Aucune \u2014",
1408
+ LOGIN_SETUP_SUMMARY: "Fichier : {file} \xB7 before : {before} \xB7 beforeEach : {beforeEach}",
1409
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Enregistrer",
1410
+ LOGIN_SETUP_CLEAR_BTN: "Supprimer la configuration",
1411
+ LOGIN_SETUP_CANCEL_BTN: "Annuler",
1412
+ LOGIN_SETUP_SOURCE_LABEL: "Source du fichier de login",
1413
+ LOGIN_SETUP_CREATE_SUB: "G\xE9n\xE9rer un scaffold avec des fonctions exemples",
1414
+ LOGIN_SETUP_SELECT_SUB: "Pointer vers un fichier de service de login existant",
1415
+ LOGIN_SETUP_INJECT_TITLE: "Ajouter les blocs de login ?",
1416
+ LOGIN_SETUP_INJECT_TEXT: "Ce fichier ne contient pas les blocs de login configur\xE9s. Voulez-vous ajouter l'import et les blocs before/beforeEach maintenant ?",
1417
+ LOGIN_SETUP_INJECT_CONFIRM: "Oui, ajouter"
1311
1418
  },
1312
1419
  SELECTOR_PICKER: {
1313
1420
  TITLE: "S\xE9lectionnez un \xE9l\xE9ment du DOM",
@@ -1398,6 +1505,12 @@ var I18N_FR = {
1398
1505
  FILE_SAVE_ERROR_TOAST: "Erreur lors de l'enregistrement",
1399
1506
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures \xE9crites dans cypress/fixtures",
1400
1507
  FIXTURES_NO_FOLDER_TOAST: "Impossible d'\xE9crire les fixtures (configure le dossier Cypress)",
1508
+ FILE_UPLOAD_MANUAL_TOAST: "Ajoutez {files} \xE0 cypress/fixtures/ avant d'ex\xE9cuter le test.",
1509
+ // TODO: translate
1510
+ FILE_UPLOAD_COPIED_TOAST: "{files} copi\xE9(s) dans cypress/fixtures/.",
1511
+ // TODO: translate
1512
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Impossible de copier {files} vers cypress/fixtures/. Ajoutez-le manuellement.",
1513
+ // TODO: translate
1401
1514
  BTN_CONFIG: "Config",
1402
1515
  BTN_FILES: "Fichiers",
1403
1516
  BTN_COMMANDS: "Commandes",
@@ -1445,7 +1558,7 @@ var I18N_FR = {
1445
1558
  SEC_PANELS: "\u{1F5C2} Panneaux",
1446
1559
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 enregistr\xE9s : filtrer par tag, copier, combiner, exporter.",
1447
1560
  PANEL_COMMANDS: "\u2328\uFE0F Commandes \u2014 enregistrement en cours : r\xE9ordonner, supprimer, ajouter des assertions.",
1448
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 langue, s\xE9lecteur, HTTP, dossier Cypress, donn\xE9es, visibilit\xE9, position.",
1561
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 langue, s\xE9lecteur, HTTP, dossier Cypress, login setup, donn\xE9es, visibilit\xE9, position.",
1449
1562
  PANEL_FILES: "\u{1F4C1} Fichiers \u2014 ins\xE8re des tests dans tes .cy.ts et lance le runner.",
1450
1563
  SEC_SELECTOR: "\u{1F50E} S\xE9lecteurs",
1451
1564
  SELECTOR_1: "Choisis l'attribut pr\xE9f\xE9r\xE9 (data-cy, data-testid, aria-label, id) dans Config.",
@@ -1458,6 +1571,8 @@ var I18N_FR = {
1458
1571
  ADV_RUNNER: "Runner local : npx lib-e2e-cypress-runner pour lancer un spec headless depuis l'\xE9diteur.",
1459
1572
  ADV_CROSSAPP: "Cross-app : l'enregistrement survit au changement de micro-frontend (m\xEAme origine).",
1460
1573
  ADV_DRAG: "Fais glisser le bouton \u23FA pour d\xE9placer le widget ; sa position est m\xE9moris\xE9e.",
1574
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
1575
+ // TODO: translate (fr)
1461
1576
  CLOSE: "Fermer",
1462
1577
  TAB_REFERENCE: "R\xE9f\xE9rence rapide",
1463
1578
  TAB_GUIDE: "Guide d'utilisation",
@@ -1476,13 +1591,14 @@ var I18N_FR = {
1476
1591
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1477
1592
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1478
1593
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1594
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
1595
+ // TODO: translate (fr)
1479
1596
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1480
1597
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1481
1598
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1482
1599
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1483
1600
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
1484
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
1485
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
1601
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
1486
1602
  }
1487
1603
  };
1488
1604
 
@@ -1513,8 +1629,11 @@ var I18N_IT = {
1513
1629
  NOTES_PLACEHOLDER: "Cosa valida questo test, condizioni preliminari, ecc.",
1514
1630
  TICKET_LABEL: "Ticket / Issue (opzionale):",
1515
1631
  TICKET_PLACEHOLDER: "Es: PROJ-123, #42",
1516
- TICKET_WARN: "ID non valido per il provider configurato (verr\xE0 salvato comunque)"
1632
+ TICKET_WARN: "ID non valido per il provider configurato (verr\xE0 salvato comunque)",
1517
1633
  // TODO: translate
1634
+ CONFIRM_DISCARD: "Sei sicuro di voler scartare questa registrazione? Tutti i passi registrati andranno persi.",
1635
+ CONFIRM_DISCARD_BTN: "S\xEC, scarta",
1636
+ BACK_BTN: "Indietro"
1518
1637
  },
1519
1638
  TEST_EDITOR: {
1520
1639
  NO_TAGS: "Nessuna etichetta",
@@ -1596,7 +1715,32 @@ var I18N_IT = {
1596
1715
  ISSUE_TRACKER_SUB: "Aggiungi un ID ticket al salvataggio e genera un link cliccabile nell'elenco.",
1597
1716
  ISSUE_TRACKER_PROVIDER: "Provider",
1598
1717
  ISSUE_TRACKER_BASE_URL: "URL base",
1599
- ISSUE_TRACKER_BASE_URL_PH: "Es: https://mioprogetto.atlassian.net"
1718
+ ISSUE_TRACKER_BASE_URL_PH: "Es: https://mioprogetto.atlassian.net",
1719
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
1720
+ LOGIN_SETUP_NOT_CONFIGURED: "Non configurato",
1721
+ LOGIN_SETUP_SET_UP_BTN: "Configura",
1722
+ LOGIN_SETUP_TITLE: "Configurazione del login",
1723
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Crea scaffold",
1724
+ LOGIN_SETUP_SELECT_EXISTING: "Seleziona file esistente",
1725
+ LOGIN_SETUP_FILE_NAME_LABEL: "Nome del file",
1726
+ LOGIN_SETUP_FILE_NAME_PH: "Es: cypress/common-services/login.service.ts",
1727
+ LOGIN_SETUP_RESCAN: "\u{1F504} Re-scansiona",
1728
+ LOGIN_SETUP_FUNCTIONS_DETECTED: "Funzioni rilevate",
1729
+ LOGIN_SETUP_BLOCK_ASSIGN: "Assegnazione dei blocchi",
1730
+ LOGIN_SETUP_NO_FUNCTIONS: "Nessuna funzione esportata trovata. Compila lo scaffold e ri-scansiona.",
1731
+ LOGIN_SETUP_BEFORE_LABEL: "Funzione before() (una volta per suite)",
1732
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "Funzione beforeEach() (prima di ogni test)",
1733
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Nessuna \u2014",
1734
+ LOGIN_SETUP_SUMMARY: "File: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
1735
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Salva",
1736
+ LOGIN_SETUP_CLEAR_BTN: "Rimuovi configurazione",
1737
+ LOGIN_SETUP_CANCEL_BTN: "Annulla",
1738
+ LOGIN_SETUP_SOURCE_LABEL: "Sorgente del file di login",
1739
+ LOGIN_SETUP_CREATE_SUB: "Genera uno scaffold con funzioni di esempio",
1740
+ LOGIN_SETUP_SELECT_SUB: "Punta a un file di servizio di login esistente",
1741
+ LOGIN_SETUP_INJECT_TITLE: "Aggiungere i blocchi di login?",
1742
+ LOGIN_SETUP_INJECT_TEXT: "Questo file non contiene i blocchi di login configurati. Vuoi aggiungere l'import e i blocchi before/beforeEach ora?",
1743
+ LOGIN_SETUP_INJECT_CONFIRM: "S\xEC, aggiungi"
1600
1744
  },
1601
1745
  SELECTOR_PICKER: {
1602
1746
  TITLE: "Seleziona un elemento DOM",
@@ -1687,6 +1831,12 @@ var I18N_IT = {
1687
1831
  FILE_SAVE_ERROR_TOAST: "Errore durante il salvataggio",
1688
1832
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixture scritte in cypress/fixtures",
1689
1833
  FIXTURES_NO_FOLDER_TOAST: "Impossibile scrivere le fixture (configura la cartella Cypress)",
1834
+ FILE_UPLOAD_MANUAL_TOAST: "Aggiungi {files} a cypress/fixtures/ prima di eseguire il test.",
1835
+ // TODO: translate
1836
+ FILE_UPLOAD_COPIED_TOAST: "{files} copiato/i in cypress/fixtures/.",
1837
+ // TODO: translate
1838
+ FILE_UPLOAD_COPY_ERROR_TOAST: "Impossibile copiare {files} in cypress/fixtures/. Aggiungilo manualmente.",
1839
+ // TODO: translate
1690
1840
  BTN_CONFIG: "Config",
1691
1841
  BTN_FILES: "File",
1692
1842
  BTN_COMMANDS: "Comandi",
@@ -1734,7 +1884,7 @@ var I18N_IT = {
1734
1884
  SEC_PANELS: "\u{1F5C2} Pannelli",
1735
1885
  PANEL_TESTS: "\u{1F4CB} Test \u2014 salvati: filtra per tag, copia, combina, esporta.",
1736
1886
  PANEL_COMMANDS: "\u2328\uFE0F Comandi \u2014 registrazione corrente: riordina, elimina, aggiungi asserzioni.",
1737
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 lingua, selettore, HTTP, cartella Cypress, dati, visibilit\xE0, posizione.",
1887
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 lingua, selettore, HTTP, cartella Cypress, login setup, dati, visibilit\xE0, posizione.",
1738
1888
  PANEL_FILES: "\u{1F4C1} File \u2014 inserisci test nei tuoi .cy.ts e avvia il runner.",
1739
1889
  SEC_SELECTOR: "\u{1F50E} Selettori",
1740
1890
  SELECTOR_1: "Scegli l'attributo preferito (data-cy, data-testid, aria-label, id) in Config.",
@@ -1747,6 +1897,8 @@ var I18N_IT = {
1747
1897
  ADV_RUNNER: "Runner locale: npx lib-e2e-cypress-runner per eseguire uno spec headless dall'editor.",
1748
1898
  ADV_CROSSAPP: "Cross-app: la registrazione sopravvive al cambio di micro-frontend (stessa origine).",
1749
1899
  ADV_DRAG: "Trascina il pulsante \u23FA per spostare il widget; la posizione viene ricordata.",
1900
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
1901
+ // TODO: translate (it)
1750
1902
  CLOSE: "Chiudi",
1751
1903
  TAB_REFERENCE: "Riferimento rapido",
1752
1904
  TAB_GUIDE: "Guida all'uso",
@@ -1765,13 +1917,14 @@ var I18N_IT = {
1765
1917
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
1766
1918
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
1767
1919
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
1920
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
1921
+ // TODO: translate (it)
1768
1922
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
1769
1923
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
1770
1924
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
1771
1925
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
1772
1926
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
1773
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
1774
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
1927
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
1775
1928
  }
1776
1929
  };
1777
1930
 
@@ -1802,8 +1955,11 @@ var I18N_DE = {
1802
1955
  NOTES_PLACEHOLDER: "Was dieser Test validiert, Voraussetzungen usw.",
1803
1956
  TICKET_LABEL: "Ticket / Issue (optional):",
1804
1957
  TICKET_PLACEHOLDER: "Z. B.: PROJ-123, #42",
1805
- TICKET_WARN: "Ung\xFCltige ID f\xFCr den konfigurierten Anbieter (wird trotzdem gespeichert)"
1958
+ TICKET_WARN: "Ung\xFCltige ID f\xFCr den konfigurierten Anbieter (wird trotzdem gespeichert)",
1806
1959
  // TODO: translate
1960
+ CONFIRM_DISCARD: "Sind Sie sicher, dass Sie diese Aufzeichnung verwerfen m\xF6chten? Alle aufgezeichneten Schritte gehen verloren.",
1961
+ CONFIRM_DISCARD_BTN: "Ja, verwerfen",
1962
+ BACK_BTN: "Zur\xFCck"
1807
1963
  },
1808
1964
  TEST_EDITOR: {
1809
1965
  NO_TAGS: "Keine Tags",
@@ -1885,7 +2041,32 @@ var I18N_DE = {
1885
2041
  ISSUE_TRACKER_SUB: "F\xFCge beim Speichern eine Ticket-ID hinzu und erstelle einen klickbaren Link.",
1886
2042
  ISSUE_TRACKER_PROVIDER: "Anbieter",
1887
2043
  ISSUE_TRACKER_BASE_URL: "Basis-URL",
1888
- ISSUE_TRACKER_BASE_URL_PH: "Z. B.: https://meinprojekt.atlassian.net"
2044
+ ISSUE_TRACKER_BASE_URL_PH: "Z. B.: https://meinprojekt.atlassian.net",
2045
+ LOGIN_SETUP_SECTION: "\u{1F510} Login setup",
2046
+ LOGIN_SETUP_NOT_CONFIGURED: "Nicht konfiguriert",
2047
+ LOGIN_SETUP_SET_UP_BTN: "Einrichten",
2048
+ LOGIN_SETUP_TITLE: "Login-Konfiguration",
2049
+ LOGIN_SETUP_CREATE_SCAFFOLD: "Scaffold erstellen",
2050
+ LOGIN_SETUP_SELECT_EXISTING: "Vorhandene Datei ausw\xE4hlen",
2051
+ LOGIN_SETUP_FILE_NAME_LABEL: "Dateiname",
2052
+ LOGIN_SETUP_FILE_NAME_PH: "Z. B.: cypress/common-services/login.service.ts",
2053
+ LOGIN_SETUP_RESCAN: "\u{1F504} Erneut scannen",
2054
+ LOGIN_SETUP_FUNCTIONS_DETECTED: "Erkannte Funktionen",
2055
+ LOGIN_SETUP_BLOCK_ASSIGN: "Blockzuweisung",
2056
+ LOGIN_SETUP_NO_FUNCTIONS: "Keine exportierten Funktionen gefunden. Scaffold ausf\xFCllen und erneut scannen.",
2057
+ LOGIN_SETUP_BEFORE_LABEL: "before()-Funktion (einmal pro Suite)",
2058
+ LOGIN_SETUP_BEFORE_EACH_LABEL: "beforeEach()-Funktion (vor jedem Test)",
2059
+ LOGIN_SETUP_NONE_OPTION: "\u2014 Keine \u2014",
2060
+ LOGIN_SETUP_SUMMARY: "Datei: {file} \xB7 before: {before} \xB7 beforeEach: {beforeEach}",
2061
+ LOGIN_SETUP_SAVE_BTN: "\u{1F4BE} Speichern",
2062
+ LOGIN_SETUP_CLEAR_BTN: "Konfiguration entfernen",
2063
+ LOGIN_SETUP_CANCEL_BTN: "Abbrechen",
2064
+ LOGIN_SETUP_SOURCE_LABEL: "Quelle der Login-Datei",
2065
+ LOGIN_SETUP_CREATE_SUB: "Scaffold mit Beispielfunktionen erstellen",
2066
+ LOGIN_SETUP_SELECT_SUB: "Auf eine vorhandene Login-Service-Datei verweisen",
2067
+ LOGIN_SETUP_INJECT_TITLE: "Login-Bl\xF6cke hinzuf\xFCgen?",
2068
+ LOGIN_SETUP_INJECT_TEXT: "Diese Datei enth\xE4lt die konfigurierten Login-Bl\xF6cke nicht. M\xF6chten Sie jetzt den Import und die before/beforeEach-Bl\xF6cke hinzuf\xFCgen?",
2069
+ LOGIN_SETUP_INJECT_CONFIRM: "Ja, hinzuf\xFCgen"
1889
2070
  },
1890
2071
  SELECTOR_PICKER: {
1891
2072
  TITLE: "DOM-Element ausw\xE4hlen",
@@ -1976,6 +2157,12 @@ var I18N_DE = {
1976
2157
  FILE_SAVE_ERROR_TOAST: "Fehler beim Speichern",
1977
2158
  FIXTURES_WRITTEN_TOAST: "\u2713 Fixtures in cypress/fixtures geschrieben",
1978
2159
  FIXTURES_NO_FOLDER_TOAST: "Fixtures konnten nicht geschrieben werden (Cypress-Ordner konfigurieren)",
2160
+ FILE_UPLOAD_MANUAL_TOAST: "F\xFCge {files} zu cypress/fixtures/ hinzu, bevor du den Test ausf\xFChrst.",
2161
+ // TODO: translate
2162
+ FILE_UPLOAD_COPIED_TOAST: "{files} nach cypress/fixtures/ kopiert.",
2163
+ // TODO: translate
2164
+ FILE_UPLOAD_COPY_ERROR_TOAST: "{files} konnte nicht nach cypress/fixtures/ kopiert werden. F\xFCge es manuell hinzu.",
2165
+ // TODO: translate
1979
2166
  BTN_CONFIG: "Config",
1980
2167
  BTN_FILES: "Dateien",
1981
2168
  BTN_COMMANDS: "Befehle",
@@ -2023,7 +2210,7 @@ var I18N_DE = {
2023
2210
  SEC_PANELS: "\u{1F5C2} Panels",
2024
2211
  PANEL_TESTS: "\u{1F4CB} Tests \u2014 gespeichert: nach Tag filtern, kopieren, kombinieren, exportieren.",
2025
2212
  PANEL_COMMANDS: "\u2328\uFE0F Befehle \u2014 aktuelle Aufzeichnung: umsortieren, l\xF6schen, Assertions hinzuf\xFCgen.",
2026
- PANEL_CONFIG: "\u2699\uFE0F Config \u2014 Sprache, Selektor, HTTP, Cypress-Ordner, Daten, Sichtbarkeit, Position.",
2213
+ PANEL_CONFIG: "\u2699\uFE0F Config \u2014 Sprache, Selektor, HTTP, Cypress-Ordner, Login-Setup, Daten, Sichtbarkeit, Position.",
2027
2214
  PANEL_FILES: "\u{1F4C1} Dateien \u2014 Tests in deine .cy.ts einf\xFCgen und den Runner starten.",
2028
2215
  SEC_SELECTOR: "\u{1F50E} Selektoren",
2029
2216
  SELECTOR_1: "W\xE4hle das bevorzugte Attribut (data-cy, data-testid, aria-label, id) unter Config.",
@@ -2036,6 +2223,8 @@ var I18N_DE = {
2036
2223
  ADV_RUNNER: "Lokaler Runner: npx lib-e2e-cypress-runner, um einen Spec headless aus dem Editor zu starten.",
2037
2224
  ADV_CROSSAPP: "Cross-App: die Aufzeichnung \xFCbersteht den Wechsel des Micro-Frontends (gleicher Origin).",
2038
2225
  ADV_DRAG: "Ziehe den \u23FA-Button, um das Widget zu verschieben; die Position wird gemerkt.",
2226
+ ADV_LOGIN: "Login Setup (Ctrl+3): apunta a tu servicio de login; el grabador inyecta before()/beforeEach() autom\xE1ticamente en cada test nuevo.",
2227
+ // TODO: translate (de)
2039
2228
  CLOSE: "Schlie\xDFen",
2040
2229
  TAB_REFERENCE: "Kurzreferenz",
2041
2230
  TAB_GUIDE: "Nutzungsleitfaden",
@@ -2054,13 +2243,14 @@ var I18N_DE = {
2054
2243
  G_COV_4: "Selector fiable (data-cy/testid/aria/id) y picker cuando no hay uno claro.",
2055
2244
  G_COV_5: "Aserciones autom\xE1ticas (Alt+click) y constructor manual.",
2056
2245
  G_COV_6: "Continuidad al cambiar de micro-frontend (mismo origen), modo invisible e i18n.",
2246
+ G_COV_7: "Login Setup: configura tu funci\xF3n de login en Ctrl+3 \u2192 Login Setup y el grabador a\xF1ade los bloques before()/beforeEach() a cada test nuevo.",
2247
+ // TODO: translate (de)
2057
2248
  G_SEC_NOTCOVERED: "\u{1F6AB} Qu\xE9 NO cubre (por ahora)",
2058
2249
  G_NC_1: "Or\xEDgenes distintos / subdominios (solo mismo origen).",
2059
2250
  G_NC_2: "Subida de ficheros (selectFile), arrastrar y soltar, y hover.",
2060
2251
  G_NC_3: "La tecla Tab (Cypress no la soporta en .type()).",
2061
2252
  G_NC_4: "Peticiones DELETE (se ignoran a prop\xF3sito).",
2062
- G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse.",
2063
- G_NC_6: "Generaci\xF3n de login program\xE1tico (en el roadmap)."
2253
+ G_NC_5: "iframes y Shadow DOM de terceros pueden no capturarse."
2064
2254
  }
2065
2255
  };
2066
2256
 
@@ -2103,34 +2293,8 @@ var TranslationService = class {
2103
2293
  };
2104
2294
  var translationService = new TranslationService();
2105
2295
 
2106
- // src/utils/code-format.utils.ts
2107
- function gcd(a, b) {
2108
- while (b) {
2109
- const t = b;
2110
- b = a % b;
2111
- a = t;
2112
- }
2113
- return a;
2114
- }
2115
- function escapeSingleQuotes(value) {
2116
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2117
- }
2118
- function normalizeBlock(code, baseIndent) {
2119
- const lines = code.split("\n").map((l) => l.trimEnd());
2120
- const nonEmpty = lines.filter((l) => l.trim().length > 0);
2121
- if (!nonEmpty.length) return "";
2122
- const indents = nonEmpty.map((l) => l.length - l.trimStart().length);
2123
- const minIndent = Math.min(...indents);
2124
- const relIndents = [...new Set(indents.map((n) => n - minIndent).filter((n) => n > 0))];
2125
- const step = relIndents.length > 0 ? relIndents.reduce(gcd) : 2;
2126
- return lines.map((l) => {
2127
- if (!l.trim()) return "";
2128
- const level = Math.round((l.length - l.trimStart().length - minIndent) / step);
2129
- return baseIndent + " ".repeat(level) + l.trimStart();
2130
- }).join("\n");
2131
- }
2132
-
2133
2296
  // src/services/transformation.service.ts
2297
+ init_code_format_utils();
2134
2298
  var TransformationService = class {
2135
2299
  toLang(lang) {
2136
2300
  return isLang(lang) ? lang : "en";
@@ -2195,9 +2359,11 @@ var advancedTestTransformationService = new AdvancedTestTransformationService();
2195
2359
  init_selector_quality_utils();
2196
2360
 
2197
2361
  // src/utils/assertion.utils.ts
2362
+ init_code_format_utils();
2198
2363
  var MAX_TEXT_LENGTH = 60;
2199
2364
  function inferAssertionCommand(el, selector) {
2200
- const should = (body) => `cy.get('${selector}').should(${body})`;
2365
+ const safeSelector = escapeSingleQuotes(selector);
2366
+ const should = (body) => `cy.get('${safeSelector}').should(${body})`;
2201
2367
  const tag = el.tagName.toLowerCase();
2202
2368
  if (tag === "input") {
2203
2369
  const input = el;
@@ -2219,6 +2385,7 @@ function inferAssertionCommand(el, selector) {
2219
2385
  }
2220
2386
 
2221
2387
  // src/services/recording.service.ts
2388
+ init_code_format_utils();
2222
2389
  var OWN_SELECTOR = '[data-cy="lib-e2e-cypress-for-dummys"]';
2223
2390
  function createSessionId() {
2224
2391
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -2234,6 +2401,7 @@ var RecordingService = class {
2234
2401
  selectorNotFound$ = new Subject(null);
2235
2402
  inputDebounceTimers = /* @__PURE__ */ new Map();
2236
2403
  fixtures = /* @__PURE__ */ new Map();
2404
+ uploadedFiles = /* @__PURE__ */ new Map();
2237
2405
  abort = new AbortController();
2238
2406
  selectorStrategy = "data-cy";
2239
2407
  /** Stable id of the current live session (null when none has started). */
@@ -2260,7 +2428,7 @@ var RecordingService = class {
2260
2428
  this.isPaused$.next(false);
2261
2429
  this.isRecording$.next(true);
2262
2430
  this.addCommand(`cy.viewport(1900, 1200)`);
2263
- this.addCommand(`cy.visit('${window.location.pathname}')`);
2431
+ this.addCommand(`cy.visit('${escapeSingleQuotes(window.location.pathname)}')`);
2264
2432
  this.addCommand(`cy.get('[data-cy="lib-e2e-cypress-for-dummys"]').invoke('hide');`);
2265
2433
  }
2266
2434
  /**
@@ -2343,8 +2511,8 @@ var RecordingService = class {
2343
2511
  }
2344
2512
  registerInterceptor(method, url, alias, fixtureFile) {
2345
2513
  if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2346
- const wildcard = this.urlToWildcard(url, method);
2347
- const command = fixtureFile ? `cy.intercept('${method}', '${wildcard}', { fixture: '${fixtureFile}' }).as('${alias}')` : `cy.intercept('${method}', '${wildcard}').as('${alias}')`;
2514
+ const wildcard = escapeSingleQuotes(this.urlToWildcard(url, method));
2515
+ const command = fixtureFile ? `cy.intercept('${method}', '${wildcard}', { fixture: '${escapeSingleQuotes(fixtureFile)}' }).as('${alias}')` : `cy.intercept('${method}', '${wildcard}').as('${alias}')`;
2348
2516
  const current = this.interceptors$.getValue();
2349
2517
  if (!current.includes(command)) {
2350
2518
  this.interceptors$.next([...current, command]);
@@ -2358,10 +2526,20 @@ var RecordingService = class {
2358
2526
  getFixturesSnapshot() {
2359
2527
  return [...this.fixtures.entries()].map(([name, content]) => ({ name, content }));
2360
2528
  }
2529
+ addUploadedFile(filename, bytes) {
2530
+ this.uploadedFiles.set(filename, bytes);
2531
+ }
2532
+ getUploadedFilesSnapshot() {
2533
+ return [...this.uploadedFiles.entries()].map(([filename, bytes]) => ({ filename, bytes }));
2534
+ }
2535
+ clearUploadedFiles() {
2536
+ this.uploadedFiles.clear();
2537
+ }
2361
2538
  clearCommands() {
2362
2539
  this.commands$.next([]);
2363
2540
  this.interceptors$.next([]);
2364
2541
  this.fixtures.clear();
2542
+ this.uploadedFiles.clear();
2365
2543
  }
2366
2544
  clearInterceptors() {
2367
2545
  this.interceptors$.next([]);
@@ -2481,7 +2659,8 @@ var RecordingService = class {
2481
2659
  if (e.altKey) return;
2482
2660
  const selector = this.resolveSelectorFor(e.target);
2483
2661
  if (!selector) return;
2484
- const clickCmd = `cy.get('${selector}').click()`;
2662
+ const safeSelector = escapeSingleQuotes(selector);
2663
+ const clickCmd = `cy.get('${safeSelector}').click()`;
2485
2664
  let cmds = this.commands$.getValue();
2486
2665
  let removed = 0;
2487
2666
  while (removed < 2 && cmds.length > 0 && cmds[cmds.length - 1] === clickCmd) {
@@ -2489,7 +2668,7 @@ var RecordingService = class {
2489
2668
  removed++;
2490
2669
  }
2491
2670
  if (removed > 0) this.commands$.next(cmds);
2492
- this.addCommand(`cy.get('${selector}').dblclick()`);
2671
+ this.addCommand(`cy.get('${safeSelector}').dblclick()`);
2493
2672
  },
2494
2673
  { signal: this.abort.signal }
2495
2674
  );
@@ -2501,7 +2680,7 @@ var RecordingService = class {
2501
2680
  if (!this.isRecording$.getValue() || this.isPaused$.getValue()) return;
2502
2681
  const selector = this.resolveSelectorFor(e.target);
2503
2682
  if (!selector) return;
2504
- this.addCommand(`cy.get('${selector}').rightclick()`);
2683
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').rightclick()`);
2505
2684
  },
2506
2685
  { signal: this.abort.signal }
2507
2686
  );
@@ -2521,7 +2700,7 @@ var RecordingService = class {
2521
2700
  if (!selector) return;
2522
2701
  this.flushInputDebounce(target);
2523
2702
  const token = key === "Enter" ? "{enter}" : "{esc}";
2524
- this.addCommand(`cy.get('${selector}').type('${token}')`);
2703
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').type('${token}')`);
2525
2704
  },
2526
2705
  { signal: this.abort.signal }
2527
2706
  );
@@ -2545,19 +2724,52 @@ var RecordingService = class {
2545
2724
  if (!this.isRecording$.getValue()) return;
2546
2725
  const target = e.target;
2547
2726
  if (!target || this.isOwnElement(target)) return;
2548
- if (target.tagName.toLowerCase() === "select") {
2727
+ const tag = target.tagName.toLowerCase();
2728
+ if (tag === "select") {
2549
2729
  this.handleSelectEvent(target);
2730
+ } else if (tag === "input") {
2731
+ const type = (target.getAttribute("type") ?? "").toLowerCase();
2732
+ if (type === "file") {
2733
+ void this.handleFileInputChange(target);
2734
+ }
2550
2735
  }
2551
2736
  },
2552
2737
  { signal: this.abort.signal }
2553
2738
  );
2554
2739
  }
2740
+ async handleFileInputChange(target) {
2741
+ if (this.isPaused$.getValue()) return;
2742
+ const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2743
+ if (!container) return;
2744
+ const selector = this.getReliableSelector(container);
2745
+ if (!selector || this.isOwnSelector(selector)) return;
2746
+ const files = Array.from(target.files ?? []);
2747
+ if (!files.length) return;
2748
+ const paths = files.map((f) => `'cypress/fixtures/${escapeSingleQuotes(f.name)}'`);
2749
+ const pathArg = paths.length === 1 ? paths[0] : `[${paths.join(", ")}]`;
2750
+ this.addCommand(`cy.get('${escapeSingleQuotes(selector)}').selectFile(${pathArg})`);
2751
+ for (const file of files) {
2752
+ const bytes = await this.readFileAsArrayBuffer(file);
2753
+ this.addUploadedFile(file.name, bytes);
2754
+ }
2755
+ }
2756
+ readFileAsArrayBuffer(file) {
2757
+ if (typeof file.arrayBuffer === "function") {
2758
+ return file.arrayBuffer();
2759
+ }
2760
+ return new Promise((resolve, reject) => {
2761
+ const reader = new FileReader();
2762
+ reader.onload = () => resolve(reader.result);
2763
+ reader.onerror = () => reject(reader.error);
2764
+ reader.readAsArrayBuffer(file);
2765
+ });
2766
+ }
2555
2767
  listenToRouteChanges() {
2556
2768
  let lastUrl = window.location.pathname + window.location.search + window.location.hash;
2557
2769
  const addUrlCommand = (newUrl) => {
2558
2770
  if (!this.isRecording$.getValue()) return;
2559
2771
  if (newUrl === lastUrl) return;
2560
- this.addCommand(`cy.url().should('include', '${newUrl}')`);
2772
+ this.addCommand(`cy.url().should('include', '${escapeSingleQuotes(newUrl)}')`);
2561
2773
  lastUrl = newUrl;
2562
2774
  };
2563
2775
  const wrapMethod = (type) => {
@@ -2658,7 +2870,8 @@ var RecordingService = class {
2658
2870
  if (matSelect) dataCy = matSelect.closest("[data-cy]")?.getAttribute("data-cy") ?? null;
2659
2871
  if (!dataCy) dataCy = target.closest("[data-cy]")?.getAttribute("data-cy") ?? null;
2660
2872
  if (dataCy) {
2661
- this.addCommand(`cy.get('[data-cy="${dataCy}"]').click()`);
2873
+ const safeSel = escapeSingleQuotes(`[data-cy="${escapeCssAttrValue(dataCy)}"]`);
2874
+ this.addCommand(`cy.get('${safeSel}').click()`);
2662
2875
  return;
2663
2876
  }
2664
2877
  if (selector) {
@@ -2690,7 +2903,7 @@ var RecordingService = class {
2690
2903
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2691
2904
  if (!container) return;
2692
2905
  const selector = this.getReliableSelector(container);
2693
- const value = target.value.replace(/'/g, "\\'");
2906
+ const value = escapeSingleQuotes(target.value);
2694
2907
  this.addGenericCommand({
2695
2908
  selector,
2696
2909
  action: (s) => `cy.get('${s}').clear().type('${value}')`
@@ -2709,7 +2922,7 @@ var RecordingService = class {
2709
2922
  const container = target.closest("[data-cy], [data-testid], [aria-label], [id]");
2710
2923
  if (!container) return;
2711
2924
  const selector = this.getReliableSelector(container);
2712
- const value = target.value.replace(/'/g, "\\'");
2925
+ const value = escapeSingleQuotes(target.value);
2713
2926
  this.addGenericCommand({
2714
2927
  selector,
2715
2928
  action: (s) => `cy.get('${s}').select('${value}')`
@@ -2718,7 +2931,7 @@ var RecordingService = class {
2718
2931
  // ── Shared helpers ────────────────────────────────────────────────────────
2719
2932
  addGenericCommand(opts) {
2720
2933
  if (!opts.selector || this.isOwnSelector(opts.selector)) return;
2721
- this.addCommand(opts.action(opts.selector));
2934
+ this.addCommand(opts.action(escapeSingleQuotes(opts.selector)));
2722
2935
  }
2723
2936
  getReliableSelector(el) {
2724
2937
  const strategy = this.selectorStrategy;
@@ -2730,20 +2943,20 @@ var RecordingService = class {
2730
2943
  const validId = rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId) ? rawId : null;
2731
2944
  switch (strategy) {
2732
2945
  case "data-testid":
2733
- if (dataTestid) return `[data-testid="${dataTestid}"]`;
2734
- if (dataCy) return `[data-cy="${dataCy}"]`;
2946
+ if (dataTestid) return `[data-testid="${escapeCssAttrValue(dataTestid)}"]`;
2947
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2735
2948
  return validId ? `#${validId}` : null;
2736
2949
  case "aria-label":
2737
- if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
2738
- if (dataCy) return `[data-cy="${dataCy}"]`;
2950
+ if (ariaLabel) return `[aria-label="${escapeCssAttrValue(ariaLabel)}"]`;
2951
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2739
2952
  return validId ? `#${validId}` : null;
2740
2953
  case "id":
2741
2954
  if (validId) return `#${validId}`;
2742
- if (dataCy) return `[data-cy="${dataCy}"]`;
2743
- return dataTestid ? `[data-testid="${dataTestid}"]` : null;
2955
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2956
+ return dataTestid ? `[data-testid="${escapeCssAttrValue(dataTestid)}"]` : null;
2744
2957
  default:
2745
- if (dataCy) return `[data-cy="${dataCy}"]`;
2746
- if (dataDotCy) return `[data.cy="${dataDotCy}"]`;
2958
+ if (dataCy) return `[data-cy="${escapeCssAttrValue(dataCy)}"]`;
2959
+ if (dataDotCy) return `[data.cy="${escapeCssAttrValue(dataDotCy)}"]`;
2747
2960
  return validId ? `#${validId}` : null;
2748
2961
  }
2749
2962
  }
@@ -2769,8 +2982,41 @@ var RecordingService = class {
2769
2982
 
2770
2983
  // src/services/persistence.service.ts
2771
2984
  var import_idb = require("idb");
2985
+ init_code_format_utils();
2986
+
2987
+ // src/models/issue-tracker.model.ts
2988
+ var ISSUE_TRACKER_PRESETS = {
2989
+ jira: { id: "jira", label: "Jira", urlPattern: "{baseUrl}/browse/{id}", idPattern: /^[A-Z][A-Z0-9]+-\d+$/ },
2990
+ github: { id: "github", label: "GitHub Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
2991
+ gitlab: { id: "gitlab", label: "GitLab Issues", urlPattern: "{baseUrl}/-/issues/{id}", idPattern: /^\d+$/ },
2992
+ azure: { id: "azure", label: "Azure DevOps", urlPattern: "{baseUrl}/_workitems/edit/{id}", idPattern: /^\d+$/ },
2993
+ bitbucket: { id: "bitbucket", label: "Bitbucket Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
2994
+ custom: { id: "custom", label: "Custom", urlPattern: "{baseUrl}/{id}", idPattern: /.+/ }
2995
+ };
2996
+ var DEFAULT_ISSUE_TRACKER_CONFIG = {
2997
+ enabled: false,
2998
+ provider: "jira",
2999
+ baseUrl: ""
3000
+ };
3001
+
3002
+ // src/utils/ticket.utils.ts
3003
+ function buildTicketUrl(ticketId, config) {
3004
+ if (!config.enabled || !ticketId.trim() || !config.baseUrl.trim()) return null;
3005
+ const preset = ISSUE_TRACKER_PRESETS[config.provider];
3006
+ const base = config.baseUrl.replace(/\/+$/, "");
3007
+ return preset.urlPattern.replace("{baseUrl}", base).replace("{id}", encodeURIComponent(ticketId.trim()));
3008
+ }
3009
+ function buildTicketComment(ticketId) {
3010
+ return `// Ticket: ${ticketId.trim()}`;
3011
+ }
3012
+ function isValidTicketId(ticketId, provider) {
3013
+ if (!ticketId.trim()) return false;
3014
+ return ISSUE_TRACKER_PRESETS[provider].idPattern.test(ticketId.trim());
3015
+ }
3016
+
3017
+ // src/services/persistence.service.ts
2772
3018
  var ACTIVE_SESSION_ID = 1;
2773
- var PersistenceService = class {
3019
+ var PersistenceService = class _PersistenceService {
2774
3020
  // dbName is overridable so tests can use unique names for isolation.
2775
3021
  constructor(dbName = DB_SCHEMA.name) {
2776
3022
  this.dbName = dbName;
@@ -2823,7 +3069,8 @@ var PersistenceService = class {
2823
3069
  if (!record) return null;
2824
3070
  const commands = await this.getCommandStrings(testId);
2825
3071
  const interceptors = await this.getInterceptorStrings(testId);
2826
- const itBlock = `it('${escapeSingleQuotes(record.name)}', () => {
3072
+ const ticketComment = record.ticketId ? buildTicketComment(record.ticketId) + "\n" : "";
3073
+ const itBlock = `${ticketComment}it('${escapeSingleQuotes(record.name)}', () => {
2827
3074
  ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2828
3075
  });`;
2829
3076
  const interceptorsBlock = interceptors.length ? " // Auto-generated Cypress interceptors\n" + interceptors.map((i) => normalizeBlock(i, " ")).join("\n") + "\n" : "";
@@ -2904,6 +3151,30 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2904
3151
  const records = await db.getAll("configuration");
2905
3152
  return records[0] ?? null;
2906
3153
  }
3154
+ // ── Login setup config ───────────────────────────────────────────────────
3155
+ static LOGIN_SETUP_KEY = "loginSetupConfig";
3156
+ async saveLoginSetup(config) {
3157
+ await this.setConfigKey(_PersistenceService.LOGIN_SETUP_KEY, JSON.stringify(config));
3158
+ }
3159
+ async getLoginSetup() {
3160
+ const record = await this.getConfig(_PersistenceService.LOGIN_SETUP_KEY);
3161
+ if (!record) return null;
3162
+ const raw = record[_PersistenceService.LOGIN_SETUP_KEY];
3163
+ if (typeof raw !== "string") return null;
3164
+ try {
3165
+ return JSON.parse(raw);
3166
+ } catch {
3167
+ return null;
3168
+ }
3169
+ }
3170
+ async clearLoginSetup() {
3171
+ const db = await this.getDB();
3172
+ const records = await db.getAll("configuration");
3173
+ if (!records.length) return;
3174
+ const current = { ...records[0] };
3175
+ delete current[_PersistenceService.LOGIN_SETUP_KEY];
3176
+ await db.put("configuration", current);
3177
+ }
2907
3178
  // ── Active recording session ──────────────────────────────────────────────
2908
3179
  /** Upserts the live recording session (single record, fixed key). */
2909
3180
  async saveActiveSession(state) {
@@ -2946,6 +3217,24 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2946
3217
  }
2947
3218
  await this.bulkInsertWithoutId("interceptors", interceptors);
2948
3219
  }
3220
+ async hasDirectoryAccess() {
3221
+ const config = await this.getGeneralConfig();
3222
+ return !!config?.["cypressDirectoryHandle"];
3223
+ }
3224
+ async writeUploadedFile(filename, bytes) {
3225
+ const config = await this.getGeneralConfig();
3226
+ const dirHandle = config?.["cypressDirectoryHandle"];
3227
+ if (!dirHandle) throw new Error("No Cypress folder configured");
3228
+ const perm = dirHandle;
3229
+ let state = await perm.queryPermission({ mode: "readwrite" });
3230
+ if (state !== "granted") state = await perm.requestPermission({ mode: "readwrite" });
3231
+ if (state !== "granted") throw new Error("Write permission denied");
3232
+ const fixturesDir = await dirHandle.getDirectoryHandle("fixtures", { create: true });
3233
+ const fileHandle = await fixturesDir.getFileHandle(filename, { create: true });
3234
+ const writable = await fileHandle.createWritable();
3235
+ await writable.write(bytes);
3236
+ await writable.close();
3237
+ }
2949
3238
  async requestDirectoryPermissions() {
2950
3239
  if (!("showDirectoryPicker" in window)) {
2951
3240
  throw new Error("File System Access API not supported");
@@ -2988,7 +3277,57 @@ ${commands.map((c) => normalizeBlock(c, " ")).join("\n")}
2988
3277
  }
2989
3278
  }
2990
3279
  };
2991
- var persistenceService = new PersistenceService();
3280
+
3281
+ // src/services/http-monitor.ts
3282
+ init_code_format_utils();
3283
+
3284
+ // src/utils/redact.utils.ts
3285
+ var SENSITIVE_SUBSTRINGS = [
3286
+ "password",
3287
+ "passwd",
3288
+ "passphrase",
3289
+ "secret",
3290
+ "token",
3291
+ // access_token, refresh_token, id_token, csrf_token…
3292
+ "apikey",
3293
+ // api_key, x-api-key — NOT bare "key" (avoids "normalKey")
3294
+ "authorization",
3295
+ "cookie",
3296
+ "credential",
3297
+ "privatekey",
3298
+ "sessionid",
3299
+ "creditcard",
3300
+ "cardnumber",
3301
+ "cvv"
3302
+ ];
3303
+ var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
3304
+ "auth",
3305
+ "pwd",
3306
+ "pin",
3307
+ "otp",
3308
+ "ssn"
3309
+ ]);
3310
+ function normalizeKey(key) {
3311
+ return key.toLowerCase().replace(/[^a-z0-9]/g, "");
3312
+ }
3313
+ function isSensitiveKey(key) {
3314
+ const normalized = normalizeKey(key);
3315
+ if (SENSITIVE_EXACT.has(normalized)) return true;
3316
+ return SENSITIVE_SUBSTRINGS.some((s) => normalized.includes(s));
3317
+ }
3318
+ function redactSensitiveFields(obj) {
3319
+ if (Array.isArray(obj)) {
3320
+ return obj.map(redactSensitiveFields);
3321
+ }
3322
+ if (obj !== null && typeof obj === "object") {
3323
+ const result = {};
3324
+ for (const [key, value] of Object.entries(obj)) {
3325
+ result[key] = isSensitiveKey(key) ? "[REDACTED]" : redactSensitiveFields(value);
3326
+ }
3327
+ return result;
3328
+ }
3329
+ return obj;
3330
+ }
2992
3331
 
2993
3332
  // src/services/http-monitor.ts
2994
3333
  var INTERCEPTED_METHODS = ["GET", "POST", "PUT"];
@@ -3001,8 +3340,12 @@ function generateAlias(method, url) {
3001
3340
  return `${method.toLowerCase()}-intercepted-request`;
3002
3341
  }
3003
3342
  }
3343
+ var SAFE_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3004
3344
  function buildValidations(base, obj) {
3005
- return Object.keys(obj).filter((key) => key !== "id" && key !== "uid").map((key) => `expect(${base}.${key}).to.equal(${JSON.stringify(obj[key])});`).join("\n");
3345
+ return Object.keys(obj).filter((key) => key !== "id" && key !== "uid").map((key) => {
3346
+ const accessor = SAFE_IDENTIFIER.test(key) ? `.${key}` : `['${escapeSingleQuotes(key)}']`;
3347
+ return `expect(${base}${accessor}).to.equal(${JSON.stringify(obj[key])});`;
3348
+ }).join("\n");
3006
3349
  }
3007
3350
  function buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody) {
3008
3351
  if (extendedHttp && method === "GET" && responseBody) {
@@ -3054,20 +3397,143 @@ function resolveMethod(input, init) {
3054
3397
  if (input instanceof Request) return input.method.toUpperCase();
3055
3398
  return "GET";
3056
3399
  }
3400
+ async function _handleFetchInterception(recording, input, init, responseClone) {
3401
+ const method = resolveMethod(input, init);
3402
+ if (!INTERCEPTED_METHODS.includes(method)) return;
3403
+ const url = resolveUrl(input);
3404
+ const alias = generateAlias(method, url);
3405
+ recording.registerInterceptor(method, url, alias);
3406
+ const extendedHttp = localStorage.getItem("extendedHttpCommands") === "true";
3407
+ const fixtureMode = localStorage.getItem("fixtureMode") === "true";
3408
+ const requestBody = extendedHttp ? parseRequestBody(init) : null;
3409
+ let responseText = null;
3410
+ let responseBody = null;
3411
+ if (extendedHttp || fixtureMode && method === "GET") {
3412
+ try {
3413
+ responseText = await responseClone.text();
3414
+ if (extendedHttp) responseBody = parseJsonObject(responseText);
3415
+ } catch {
3416
+ }
3417
+ }
3418
+ if (fixtureMode && method === "GET" && responseText !== null) {
3419
+ const pretty = prettyJsonOrNull(responseText);
3420
+ if (pretty !== null) {
3421
+ const redacted = redactSensitiveFields(JSON.parse(pretty));
3422
+ recording.registerFixture(`${alias}.json`, JSON.stringify(redacted, null, 2));
3423
+ }
3424
+ }
3425
+ const cmd = buildCyWaitCommand(
3426
+ method,
3427
+ alias,
3428
+ extendedHttp,
3429
+ requestBody ? redactSensitiveFields(requestBody) : null,
3430
+ responseBody ? redactSensitiveFields(responseBody) : null
3431
+ );
3432
+ recording.addCommand(cmd);
3433
+ }
3434
+ function _handleXhrInterception(recording, method, url, requestBody, responseText) {
3435
+ if (!INTERCEPTED_METHODS.includes(method)) return;
3436
+ const alias = generateAlias(method, url);
3437
+ recording.registerInterceptor(method, url, alias);
3438
+ if (localStorage.getItem("fixtureMode") === "true" && method === "GET") {
3439
+ const pretty = prettyJsonOrNull(responseText);
3440
+ if (pretty !== null) {
3441
+ const redacted = redactSensitiveFields(JSON.parse(pretty));
3442
+ recording.registerFixture(`${alias}.json`, JSON.stringify(redacted, null, 2));
3443
+ }
3444
+ }
3445
+ const extendedHttp = localStorage.getItem("extendedHttpCommands") === "true";
3446
+ const responseBody = extendedHttp ? parseJsonObject(responseText) : null;
3447
+ const cmd = buildCyWaitCommand(
3448
+ method,
3449
+ alias,
3450
+ extendedHttp,
3451
+ requestBody ? redactSensitiveFields(requestBody) : null,
3452
+ responseBody ? redactSensitiveFields(responseBody) : null
3453
+ );
3454
+ recording.addCommand(cmd);
3455
+ }
3456
+ var _refCount = 0;
3457
+ var _originalFetch = null;
3458
+ var _originalXHR = null;
3459
+ var _recordings = /* @__PURE__ */ new Set();
3460
+ function _patchFetch() {
3461
+ const orig = window.fetch;
3462
+ _originalFetch = orig;
3463
+ window.fetch = async (input, init) => {
3464
+ const response = await orig(input, init);
3465
+ try {
3466
+ const active = [..._recordings];
3467
+ if (active.length > 0) {
3468
+ await Promise.all(
3469
+ active.map((rec) => _handleFetchInterception(rec, input, init, response.clone()).catch(() => {
3470
+ }))
3471
+ );
3472
+ }
3473
+ } catch {
3474
+ }
3475
+ return response;
3476
+ };
3477
+ }
3478
+ function _restoreFetch() {
3479
+ if (!_originalFetch) return;
3480
+ window.fetch = _originalFetch;
3481
+ _originalFetch = null;
3482
+ }
3483
+ function _patchXhr() {
3484
+ const OrigXHR = window.XMLHttpRequest;
3485
+ _originalXHR = OrigXHR;
3486
+ window.XMLHttpRequest = class extends OrigXHR {
3487
+ _xhrMethod = "GET";
3488
+ _xhrUrl = "";
3489
+ _xhrRequestBody = null;
3490
+ open(method, url, async = true, user, password) {
3491
+ this._xhrMethod = method.toUpperCase();
3492
+ this._xhrUrl = url instanceof URL ? url.href : url;
3493
+ super.open(method, url, async, user, password);
3494
+ }
3495
+ send(body) {
3496
+ if (typeof body === "string") {
3497
+ this._xhrRequestBody = parseJsonObject(body);
3498
+ }
3499
+ this.addEventListener("load", () => {
3500
+ try {
3501
+ for (const rec of _recordings) {
3502
+ _handleXhrInterception(rec, this._xhrMethod, this._xhrUrl, this._xhrRequestBody, this.responseText);
3503
+ }
3504
+ } catch {
3505
+ }
3506
+ });
3507
+ super.send(body);
3508
+ }
3509
+ };
3510
+ }
3511
+ function _restoreXhr() {
3512
+ if (!_originalXHR) return;
3513
+ window.XMLHttpRequest = _originalXHR;
3514
+ _originalXHR = null;
3515
+ }
3057
3516
  var HttpMonitor = class {
3058
3517
  constructor(recording) {
3059
3518
  this.recording = recording;
3060
3519
  }
3061
3520
  recording;
3062
- originalFetch = null;
3063
- originalXHR = null;
3064
3521
  install() {
3065
- this.installFetch();
3066
- this.installXhr();
3522
+ _recordings.add(this.recording);
3523
+ _refCount++;
3524
+ if (_refCount === 1) {
3525
+ _patchFetch();
3526
+ _patchXhr();
3527
+ }
3067
3528
  }
3068
3529
  uninstall() {
3069
- this.uninstallFetch();
3070
- this.uninstallXhr();
3530
+ _recordings.delete(this.recording);
3531
+ if (_refCount <= 0) return;
3532
+ _refCount--;
3533
+ if (_refCount === 0) {
3534
+ _restoreFetch();
3535
+ _restoreXhr();
3536
+ }
3071
3537
  }
3072
3538
  isExtendedHttpEnabled() {
3073
3539
  return localStorage.getItem("extendedHttpCommands") === "true";
@@ -3075,100 +3541,6 @@ var HttpMonitor = class {
3075
3541
  isFixtureModeEnabled() {
3076
3542
  return localStorage.getItem("fixtureMode") === "true";
3077
3543
  }
3078
- installFetch() {
3079
- if (this.originalFetch) return;
3080
- const originalFetch = window.fetch;
3081
- this.originalFetch = originalFetch;
3082
- window.fetch = async (input, init) => {
3083
- const response = await originalFetch(input, init);
3084
- try {
3085
- await this.handleFetchInterception(input, init, response.clone());
3086
- } catch {
3087
- }
3088
- return response;
3089
- };
3090
- }
3091
- uninstallFetch() {
3092
- if (!this.originalFetch) return;
3093
- window.fetch = this.originalFetch;
3094
- this.originalFetch = null;
3095
- }
3096
- installXhr() {
3097
- if (this.originalXHR) return;
3098
- this.originalXHR = window.XMLHttpRequest;
3099
- const OrigXHR = this.originalXHR;
3100
- const handleXhrInterception = this.handleXhrInterception.bind(this);
3101
- window.XMLHttpRequest = class extends OrigXHR {
3102
- _xhrMethod = "GET";
3103
- _xhrUrl = "";
3104
- _xhrRequestBody = null;
3105
- open(method, url, async = true, user, password) {
3106
- this._xhrMethod = method.toUpperCase();
3107
- this._xhrUrl = url instanceof URL ? url.href : url;
3108
- super.open(method, url, async, user, password);
3109
- }
3110
- send(body) {
3111
- if (typeof body === "string") {
3112
- this._xhrRequestBody = parseJsonObject(body);
3113
- }
3114
- this.addEventListener("load", () => {
3115
- try {
3116
- handleXhrInterception(
3117
- this._xhrMethod,
3118
- this._xhrUrl,
3119
- this._xhrRequestBody,
3120
- this.responseText
3121
- );
3122
- } catch {
3123
- }
3124
- });
3125
- super.send(body);
3126
- }
3127
- };
3128
- }
3129
- uninstallXhr() {
3130
- if (!this.originalXHR) return;
3131
- window.XMLHttpRequest = this.originalXHR;
3132
- this.originalXHR = null;
3133
- }
3134
- async handleFetchInterception(input, init, responseClone) {
3135
- const method = resolveMethod(input, init);
3136
- if (!INTERCEPTED_METHODS.includes(method)) return;
3137
- const url = resolveUrl(input);
3138
- const alias = generateAlias(method, url);
3139
- this.recording.registerInterceptor(method, url, alias);
3140
- const extendedHttp = this.isExtendedHttpEnabled();
3141
- const fixtureMode = this.isFixtureModeEnabled();
3142
- const requestBody = extendedHttp ? parseRequestBody(init) : null;
3143
- let responseText = null;
3144
- let responseBody = null;
3145
- if (extendedHttp || fixtureMode && method === "GET") {
3146
- try {
3147
- responseText = await responseClone.text();
3148
- if (extendedHttp) responseBody = parseJsonObject(responseText);
3149
- } catch {
3150
- }
3151
- }
3152
- if (fixtureMode && method === "GET" && responseText !== null) {
3153
- const pretty = prettyJsonOrNull(responseText);
3154
- if (pretty !== null) this.recording.registerFixture(`${alias}.json`, pretty);
3155
- }
3156
- const cmd = buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody);
3157
- this.recording.addCommand(cmd);
3158
- }
3159
- handleXhrInterception(method, url, requestBody, responseText) {
3160
- if (!INTERCEPTED_METHODS.includes(method)) return;
3161
- const alias = generateAlias(method, url);
3162
- this.recording.registerInterceptor(method, url, alias);
3163
- if (this.isFixtureModeEnabled() && method === "GET") {
3164
- const pretty = prettyJsonOrNull(responseText);
3165
- if (pretty !== null) this.recording.registerFixture(`${alias}.json`, pretty);
3166
- }
3167
- const extendedHttp = this.isExtendedHttpEnabled();
3168
- const responseBody = extendedHttp ? parseJsonObject(responseText) : null;
3169
- const cmd = buildCyWaitCommand(method, alias, extendedHttp, requestBody, responseBody);
3170
- this.recording.addCommand(cmd);
3171
- }
3172
3544
  };
3173
3545
 
3174
3546
  // src/utils/styles.utils.ts
@@ -3400,23 +3772,24 @@ function makeSwalDraggable() {
3400
3772
  if (!dragArea) return;
3401
3773
  applyDraggable(swal, dragArea);
3402
3774
  }
3403
- function makeSwalDraggableByContentId(contentId) {
3404
- const content = document.getElementById(contentId);
3405
- if (!content) {
3406
- makeSwalDraggable();
3407
- return;
3408
- }
3409
- const swal = content.closest(".swal2-popup") ?? document.querySelector(".swal2-popup");
3410
- if (!swal) return;
3411
- const dragArea = resolveDragArea(swal);
3412
- if (!dragArea) return;
3413
- applyDraggable(swal, dragArea);
3414
- }
3415
3775
  function setSwal2DataCyAttribute(dataCy = "lib-e2e-cypress-for-dummys") {
3416
3776
  document.querySelector(".swal2-container")?.setAttribute("data-cy", dataCy);
3417
3777
  document.querySelector(".swal2-html-container")?.setAttribute("data-cy", dataCy);
3418
3778
  document.querySelector(".swal2-title")?.setAttribute("data-cy", dataCy);
3419
3779
  }
3780
+ function ensurePopupDimensions(popup, heightPx) {
3781
+ if (!popup) return;
3782
+ popup.style.height = `${heightPx}px`;
3783
+ const htmlContainer = popup.querySelector(".swal2-html-container");
3784
+ if (!htmlContainer) return;
3785
+ htmlContainer.style.flex = "1";
3786
+ htmlContainer.style.minHeight = "0";
3787
+ htmlContainer.style.overflow = "hidden";
3788
+ htmlContainer.style.padding = "0";
3789
+ htmlContainer.style.margin = "0";
3790
+ htmlContainer.style.display = "flex";
3791
+ htmlContainer.style.flexDirection = "column";
3792
+ }
3420
3793
 
3421
3794
  // src/utils/toast.utils.ts
3422
3795
  function showToast(message, isSuccess = true) {
@@ -3457,37 +3830,6 @@ function isLocalHost(hostname) {
3457
3830
  return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
3458
3831
  }
3459
3832
 
3460
- // src/utils/widget-position.utils.ts
3461
- var TOGGLE_MARGIN = 30;
3462
- var DRAG_THRESHOLD = 5;
3463
- var DEFAULT_EDGE_OFFSET = 46;
3464
- function boxOffsetForDirection(dir) {
3465
- return {
3466
- x: dir.endsWith("left") ? 144 : 46,
3467
- y: dir.startsWith("up") ? 144 : 46
3468
- };
3469
- }
3470
- function clampTogglePosition(x, y, vw, vh, margin = TOGGLE_MARGIN) {
3471
- const maxX = Math.max(margin, vw - margin);
3472
- const maxY = Math.max(margin, vh - margin);
3473
- return {
3474
- x: Math.min(Math.max(x, margin), maxX),
3475
- y: Math.min(Math.max(y, margin), maxY)
3476
- };
3477
- }
3478
- function resolveExpandDirection(x, y, vw, vh) {
3479
- const vertical = y < vh / 2 ? "down" : "up";
3480
- const horizontal = x < vw / 2 ? "right" : "left";
3481
- return `${vertical}-${horizontal}`;
3482
- }
3483
- function defaultTogglePosition(vw, vh) {
3484
- return { x: vw - DEFAULT_EDGE_OFFSET, y: vh - DEFAULT_EDGE_OFFSET };
3485
- }
3486
- function boxTopLeftFor(toggleCentre, dir) {
3487
- const off = boxOffsetForDirection(dir);
3488
- return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
3489
- }
3490
-
3491
3833
  // src/index.ts
3492
3834
  init_selector_picker();
3493
3835
 
@@ -3689,35 +4031,8 @@ var TestPrevisualizerElement = class extends HTMLElement {
3689
4031
  }
3690
4032
  }
3691
4033
  };
3692
- if (!customElements.get("test-previsualizer")) {
3693
- customElements.define("test-previsualizer", TestPrevisualizerElement);
3694
- }
3695
-
3696
- // src/models/issue-tracker.model.ts
3697
- var ISSUE_TRACKER_PRESETS = {
3698
- jira: { id: "jira", label: "Jira", urlPattern: "{baseUrl}/browse/{id}", idPattern: /^[A-Z][A-Z0-9]+-\d+$/ },
3699
- github: { id: "github", label: "GitHub Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
3700
- gitlab: { id: "gitlab", label: "GitLab Issues", urlPattern: "{baseUrl}/-/issues/{id}", idPattern: /^\d+$/ },
3701
- azure: { id: "azure", label: "Azure DevOps", urlPattern: "{baseUrl}/_workitems/edit/{id}", idPattern: /^\d+$/ },
3702
- bitbucket: { id: "bitbucket", label: "Bitbucket Issues", urlPattern: "{baseUrl}/issues/{id}", idPattern: /^\d+$/ },
3703
- custom: { id: "custom", label: "Custom", urlPattern: "{baseUrl}/{id}", idPattern: /.+/ }
3704
- };
3705
- var DEFAULT_ISSUE_TRACKER_CONFIG = {
3706
- enabled: false,
3707
- provider: "jira",
3708
- baseUrl: ""
3709
- };
3710
-
3711
- // src/utils/ticket.utils.ts
3712
- function buildTicketUrl(ticketId, config) {
3713
- if (!config.enabled || !ticketId.trim() || !config.baseUrl.trim()) return null;
3714
- const preset = ISSUE_TRACKER_PRESETS[config.provider];
3715
- const base = config.baseUrl.replace(/\/+$/, "");
3716
- return preset.urlPattern.replace("{baseUrl}", base).replace("{id}", encodeURIComponent(ticketId.trim()));
3717
- }
3718
- function isValidTicketId(ticketId, provider) {
3719
- if (!ticketId.trim()) return false;
3720
- return ISSUE_TRACKER_PRESETS[provider].idPattern.test(ticketId.trim());
4034
+ if (!customElements.get("lib-e2e-test-previsualizer")) {
4035
+ customElements.define("lib-e2e-test-previsualizer", TestPrevisualizerElement);
3721
4036
  }
3722
4037
 
3723
4038
  // src/components/save-test/save-test.styles.ts
@@ -3781,10 +4096,21 @@ var SAVE_TEST_STYLES = `
3781
4096
  transition: color 0.12s; padding: 0; background: none; border: none;
3782
4097
  }
3783
4098
  .chip-del:hover { color: #f85149; filter: none; }
4099
+ .discard-warn { color: #e6edf3; font-size: 13px; margin-bottom: 20px; line-height: 1.5; }
3784
4100
  `;
3785
4101
 
3786
4102
  // src/components/save-test/save-test.template.ts
3787
4103
  init_html_utils();
4104
+ function renderSaveTestConfirmDiscard(t) {
4105
+ return `
4106
+ <div class="container">
4107
+ <p class="discard-warn">\u26A0 ${t("SAVE_TEST.CONFIRM_DISCARD")}</p>
4108
+ <div class="btn-row">
4109
+ <button class="btn-danger" id="btn-confirm-discard">${t("SAVE_TEST.CONFIRM_DISCARD_BTN")}</button>
4110
+ <button id="btn-back-discard">${t("SAVE_TEST.BACK_BTN")}</button>
4111
+ </div>
4112
+ </div>`;
4113
+ }
3788
4114
  function renderSaveTestAsk(t) {
3789
4115
  return `
3790
4116
  <div class="container">
@@ -3830,6 +4156,7 @@ function renderSaveTestDesc(description, notes, tags, ticketId, issueTrackerEnab
3830
4156
  var SaveTestElement = class extends HTMLElement {
3831
4157
  shadow;
3832
4158
  _step = "ask";
4159
+ _stepBeforeDiscard = "ask";
3833
4160
  description = "";
3834
4161
  notes = "";
3835
4162
  tags = [];
@@ -3857,8 +4184,17 @@ var SaveTestElement = class extends HTMLElement {
3857
4184
  this.dispatch("saveandexport", { description: this.description.trim(), notes: this.notes, tags: [...this.tags], ticketId: this.ticketId.trim() });
3858
4185
  }
3859
4186
  cancel() {
4187
+ this._stepBeforeDiscard = this._step === "confirm-discard" ? "ask" : this._step;
4188
+ this._step = "confirm-discard";
4189
+ this.render();
4190
+ }
4191
+ confirmDiscard() {
3860
4192
  this.dispatch("savetest", { description: null, notes: "", tags: [], ticketId: "" });
3861
4193
  }
4194
+ backFromDiscard() {
4195
+ this._step = this._stepBeforeDiscard;
4196
+ this.render();
4197
+ }
3862
4198
  restartComponent() {
3863
4199
  this._step = "ask";
3864
4200
  this.description = "";
@@ -3889,6 +4225,12 @@ var SaveTestElement = class extends HTMLElement {
3889
4225
  this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
3890
4226
  }
3891
4227
  render() {
4228
+ if (this._step === "confirm-discard") {
4229
+ this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestConfirmDiscard(this.t.bind(this))}`;
4230
+ this.shadow.getElementById("btn-confirm-discard")?.addEventListener("click", () => this.confirmDiscard());
4231
+ this.shadow.getElementById("btn-back-discard")?.addEventListener("click", () => this.backFromDiscard());
4232
+ return;
4233
+ }
3892
4234
  if (this._step === "ask") {
3893
4235
  this.shadow.innerHTML = `<style>${SAVE_TEST_STYLES}</style>${renderSaveTestAsk(this.t.bind(this))}`;
3894
4236
  this.shadow.getElementById("btn-yes")?.addEventListener("click", () => this.askSave());
@@ -3930,10 +4272,13 @@ var SaveTestElement = class extends HTMLElement {
3930
4272
  }
3931
4273
  }
3932
4274
  };
3933
- if (!customElements.get("save-test")) {
3934
- customElements.define("save-test", SaveTestElement);
4275
+ if (!customElements.get("lib-e2e-save-test")) {
4276
+ customElements.define("lib-e2e-save-test", SaveTestElement);
3935
4277
  }
3936
4278
 
4279
+ // src/components/test-editor/test-editor.ts
4280
+ init_code_format_utils();
4281
+
3937
4282
  // src/components/test-editor/test-editor.styles.ts
3938
4283
  var TEST_EDITOR_STYLES = `
3939
4284
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -4156,6 +4501,7 @@ function highlightLine(line) {
4156
4501
  }
4157
4502
 
4158
4503
  // src/components/test-editor/test-editor.template.ts
4504
+ init_code_format_utils();
4159
4505
  function renderTestRow(test, i, state, t) {
4160
4506
  const { selectMode, selectedIds, expandedIndex, interceptorsByTest, locale, issueTrackerConfig } = state;
4161
4507
  const expanded = expandedIndex === i;
@@ -4164,7 +4510,9 @@ function renderTestRow(test, i, state, t) {
4164
4510
  const tagsHtml = (test.tags ?? []).length ? `<span class="test-tags">${(test.tags ?? []).map((tag) => `<span class="test-tag">${escHtml(tag)}</span>`).join("")}</span>` : "";
4165
4511
  const checkbox = selectMode ? `<input type="checkbox" ${selectedIds.has(test.id) ? "checked" : ""} data-select="${test.id}" />` : "";
4166
4512
  const hasIcps = (test.interceptors ?? []).length > 0;
4167
- const itBlockCode = `it('${escapeSingleQuotes(test.name)}', () => {
4513
+ const ticketId = test.ticketId;
4514
+ const ticketComment = ticketId ? buildTicketComment(ticketId) + "\n" : "";
4515
+ const itBlockCode = `${ticketComment}it('${escapeSingleQuotes(test.name)}', () => {
4168
4516
  ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
4169
4517
  });`;
4170
4518
  const icpBlockCode = icps.length ? `beforeEach(() => {
@@ -4172,7 +4520,6 @@ ${(test.commands ?? []).map((c) => normalizeBlock(c, " ")).join("\n")}
4172
4520
  ${icps.map((c) => normalizeBlock(c, " ")).join("\n")}
4173
4521
  });` : "";
4174
4522
  const notesHtml = expanded && test.notes ? `<p class="test-notes">${escHtml(test.notes)}</p>` : "";
4175
- const ticketId = test.ticketId;
4176
4523
  const ticketUrl = ticketId ? buildTicketUrl(ticketId, issueTrackerConfig) : null;
4177
4524
  const ticketHtml = ticketId ? ticketUrl ? `<a class="ticket-link" href="${escAttr(ticketUrl)}" target="_blank" rel="noopener noreferrer" title="${t("TEST_EDITOR.TICKET_LINK_TITLE")}">${escHtml(ticketId)}</a>` : `<span class="ticket-badge">${escHtml(ticketId)}</span>` : "";
4178
4525
  const body = expanded ? `
@@ -4421,8 +4768,90 @@ ${beforeEach}${itBlocks}
4421
4768
  });
4422
4769
  }
4423
4770
  };
4424
- if (!customElements.get("test-editor")) {
4425
- customElements.define("test-editor", TestEditorElement);
4771
+ if (!customElements.get("lib-e2e-test-editor")) {
4772
+ customElements.define("lib-e2e-test-editor", TestEditorElement);
4773
+ }
4774
+
4775
+ // src/models/active-session.model.ts
4776
+ var ACTIVE_SESSION_BREADCRUMB_KEY = "e2e-active-session";
4777
+ var RESUME_TTL_CONFIG_KEY = "resumeRecencyTtlMinutes";
4778
+ var DEFAULT_RESUME_TTL_MINUTES = 30;
4779
+
4780
+ // src/utils/login-setup.utils.ts
4781
+ var EXPORT_FN_RE = /^(?!\/\/).*\bexport\s+(?:async\s+)?function\s+(\w+)|^(?!\/\/).*\bexport\s+const\s+(\w+)\s*=\s*(?:async\s*)?\(/gm;
4782
+ function extractExportedFunctions(fileContent) {
4783
+ const names = [];
4784
+ let match;
4785
+ const re = new RegExp(EXPORT_FN_RE.source, EXPORT_FN_RE.flags);
4786
+ while ((match = re.exec(fileContent)) !== null) {
4787
+ const name = match[1] ?? match[2];
4788
+ if (name) names.push(name);
4789
+ }
4790
+ return names;
4791
+ }
4792
+ function buildLoginScaffold() {
4793
+ return [
4794
+ "/**",
4795
+ " * Login service scaffold \u2014 fill in your own auth logic.",
4796
+ " * This file is not committed to source control.",
4797
+ " */",
4798
+ "",
4799
+ "export function fetchAuthToken() {",
4800
+ " // TODO: implement \u2014 retrieve and return your auth token",
4801
+ "}",
4802
+ "",
4803
+ "export function setupRequestInterceptors() {",
4804
+ " // TODO: implement \u2014 configure cy.intercept() or set localStorage/cookies",
4805
+ "}",
4806
+ ""
4807
+ ].join("\n");
4808
+ }
4809
+ function buildLoginImportPath(fromFilePath, toServiceFilePath) {
4810
+ const fromParts = fromFilePath.replace(/\\/g, "/").split("/");
4811
+ const toParts = toServiceFilePath.replace(/\\/g, "/").split("/");
4812
+ fromParts.pop();
4813
+ let common = 0;
4814
+ while (common < fromParts.length && common < toParts.length - 1 && fromParts[common] === toParts[common]) {
4815
+ common++;
4816
+ }
4817
+ const ups = fromParts.length - common;
4818
+ const downs = toParts.slice(common);
4819
+ const serviceFile = downs[downs.length - 1].replace(/\.ts$/, "");
4820
+ const prefix = ups === 0 ? "." : Array(ups).fill("..").join("/");
4821
+ const middle = downs.slice(0, -1);
4822
+ return [prefix, ...middle, serviceFile].join("/");
4823
+ }
4824
+ function hasLoginBlocks(content, fnNames) {
4825
+ return fnNames.some((fn) => new RegExp(`\\b${fn}\\s*\\(`).test(content));
4826
+ }
4827
+ function injectLoginBlocksIntoExisting(content, importLine, beforeBlock, beforeEachBlock) {
4828
+ let result = content;
4829
+ if (importLine && !content.includes(importLine)) {
4830
+ result = `${importLine}
4831
+
4832
+ ${result}`;
4833
+ }
4834
+ const blocks = [beforeBlock, beforeEachBlock].filter(Boolean).join("");
4835
+ if (blocks) {
4836
+ const describeMatch = result.match(/(describe\s*\(.*?{)/s);
4837
+ if (describeMatch) {
4838
+ const insertPos = (describeMatch.index ?? 0) + describeMatch[0].length;
4839
+ result = result.slice(0, insertPos) + "\n" + blocks + result.slice(insertPos);
4840
+ }
4841
+ }
4842
+ return result;
4843
+ }
4844
+ function buildLoginBlocks(importPath, beforeFn, beforeEachFn) {
4845
+ if (!beforeFn && !beforeEachFn) {
4846
+ return { importLine: "", beforeBlock: "", beforeEachBlock: "" };
4847
+ }
4848
+ const fns = [...new Set([beforeFn, beforeEachFn].filter(Boolean))];
4849
+ const importLine = `import { ${fns.join(", ")} } from '${importPath}';`;
4850
+ const beforeBlock = beforeFn ? ` before(() => { ${beforeFn}(); });
4851
+ ` : "";
4852
+ const beforeEachBlock = beforeEachFn ? ` beforeEach(() => { ${beforeEachFn}(); });
4853
+ ` : "";
4854
+ return { importLine, beforeBlock, beforeEachBlock };
4426
4855
  }
4427
4856
 
4428
4857
  // src/components/configuration/configuration.styles.ts
@@ -4583,6 +5012,99 @@ var CONFIGURATION_STYLES = `
4583
5012
  .btn-export-confirm { background: #238636; border-color: #238636; color: #fff; }
4584
5013
  .btn-export-confirm:hover:not(:disabled) { background: #2ea043; border-color: #2ea043; }
4585
5014
  .btn-export-confirm:disabled { opacity: 0.45; cursor: not-allowed; }
5015
+
5016
+ /* \u2500\u2500 Form-element font reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5017
+ input, select, button, textarea { font-family: inherit; }
5018
+
5019
+ /* \u2500\u2500 Primary action button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5020
+ .btn-primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
5021
+ .btn-primary:hover { background: #388bfd; border-color: #388bfd; color: #fff; }
5022
+ .btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
5023
+
5024
+ /* \u2500\u2500 Login Setup overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5025
+ .ls-modal { max-width: 500px; }
5026
+ .ls-hd { display: flex; align-items: center; gap: 8px; }
5027
+ .ls-hd-icon { font-size: 15px; }
5028
+
5029
+ .ls-body {
5030
+ padding: 20px; overflow-y: auto; flex: 1;
5031
+ display: flex; flex-direction: column; gap: 20px;
5032
+ scrollbar-width: thin; scrollbar-color: #30363d transparent;
5033
+ }
5034
+
5035
+ .ls-section { display: flex; flex-direction: column; gap: 8px; }
5036
+ .ls-section-label {
5037
+ font-size: 10px; font-weight: 700; text-transform: uppercase;
5038
+ letter-spacing: 0.8px; color: #484f58;
5039
+ }
5040
+
5041
+ .ls-actions { display: flex; gap: 10px; }
5042
+ .ls-action-btn {
5043
+ flex: 1; padding: 14px 12px; border-radius: 8px;
5044
+ background: #0d1117; border: 1px solid #21262d; color: #8b949e;
5045
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
5046
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
5047
+ }
5048
+ .ls-action-btn:hover { background: #161b22; color: #e6edf3; border-color: #388bfd; }
5049
+ .ls-action-icon { font-size: 22px; line-height: 1; }
5050
+ .ls-action-label { font-size: 12px; font-weight: 600; color: #c9d1d9; }
5051
+ .ls-action-btn:hover .ls-action-label { color: #e6edf3; }
5052
+ .ls-action-sub { font-size: 10px; color: #484f58; line-height: 1.4; text-align: center; }
5053
+
5054
+ .ls-file-row {
5055
+ display: flex; align-items: center; gap: 10px;
5056
+ padding: 10px 12px; background: #0d1117;
5057
+ border: 1px solid #21262d; border-radius: 8px;
5058
+ }
5059
+ .ls-file-icon { font-size: 14px; flex-shrink: 0; }
5060
+ .ls-file-name {
5061
+ flex: 1; font-size: 11px; color: #c9d1d9;
5062
+ font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
5063
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
5064
+ min-width: 0;
5065
+ }
5066
+ .ls-btn-rescan {
5067
+ flex-shrink: 0; padding: 4px 10px; font-size: 11px;
5068
+ background: #161b22; border-color: #30363d; color: #8b949e;
5069
+ }
5070
+ .ls-btn-rescan:hover { background: #21262d; color: #e6edf3; }
5071
+
5072
+ .ls-input {
5073
+ width: 100%; padding: 8px 10px; background: #0d1117;
5074
+ border: 1px solid #30363d; border-radius: 6px;
5075
+ color: #c9d1d9; font-size: 12px; outline: none; font-family: inherit;
5076
+ }
5077
+ .ls-input:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.12); }
5078
+
5079
+ .ls-fns {
5080
+ display: flex; flex-wrap: wrap; gap: 6px;
5081
+ padding: 10px; background: #0d1117;
5082
+ border: 1px solid #21262d; border-radius: 6px; min-height: 38px;
5083
+ }
5084
+ .ls-fn-chip {
5085
+ padding: 3px 10px; font-size: 11px; color: #79c0ff;
5086
+ background: rgba(47,129,247,0.1); border: 1px solid rgba(47,129,247,0.2);
5087
+ border-radius: 12px; font-family: 'Cascadia Code','Fira Code','Consolas',monospace;
5088
+ }
5089
+
5090
+ .ls-field-block { display: flex; flex-direction: column; gap: 6px; }
5091
+ .ls-field-block + .ls-field-block { margin-top: 12px; }
5092
+ .ls-label { font-size: 11px; color: #8b949e; }
5093
+ .ls-select {
5094
+ width: 100%; padding: 8px 10px; background: #0d1117;
5095
+ border: 1px solid #30363d; border-radius: 6px;
5096
+ color: #c9d1d9; font-size: 12px; outline: none; cursor: pointer;
5097
+ }
5098
+ .ls-select:focus { border-color: #2f81f7; box-shadow: 0 0 0 3px rgba(47,129,247,0.12); }
5099
+
5100
+ .ls-empty-state {
5101
+ display: flex; align-items: center; gap: 10px;
5102
+ padding: 14px; background: #0d1117; border: 1px solid #21262d;
5103
+ border-radius: 8px; font-size: 12px; color: #484f58;
5104
+ }
5105
+ .ls-empty-icon { font-size: 16px; }
5106
+
5107
+ .ls-ft { padding: 14px 20px; }
4586
5108
  `;
4587
5109
 
4588
5110
  // src/components/configuration/configuration.template.ts
@@ -4800,6 +5322,13 @@ function renderConfiguration(state, t) {
4800
5322
  </div>` : ""}
4801
5323
  </div>
4802
5324
 
5325
+ <!-- Login Setup -->
5326
+ <div class="card card-wide">
5327
+ <div class="card-hd">${t("CONFIG.LOGIN_SETUP_SECTION")}</div>
5328
+ ${state.loginSetupConfig ? `<p style="font-size:12px;color:#8b949e;margin:0 0 8px">${escHtml(state.loginSetupConfig.filePath)}</p>` : `<p style="font-size:12px;color:#8b949e;margin:0 0 8px">${t("CONFIG.LOGIN_SETUP_NOT_CONFIGURED")}</p>`}
5329
+ <button id="btn-open-login-setup" class="btn-sm">${t("CONFIG.LOGIN_SETUP_SET_UP_BTN")}</button>
5330
+ </div>
5331
+
4803
5332
  <!-- Data -->
4804
5333
  <div class="card card-wide">
4805
5334
  <div class="card-hd">${t("CONFIG.DATA_SECTION")}</div>
@@ -4813,7 +5342,93 @@ function renderConfiguration(state, t) {
4813
5342
  </div>
4814
5343
  </div>
4815
5344
 
4816
- </div>${renderExportOverlay(state, t)}`;
5345
+ </div>${renderExportOverlay(state, t)}${renderLoginSetupOverlay(state, t)}`;
5346
+ }
5347
+ function renderLoginSetupOverlay(state, t) {
5348
+ if (!state.isLoginSetupOpen) return "";
5349
+ const cfg = state.loginSetupConfig;
5350
+ const path = state.loginSetupDraftPath ?? cfg?.filePath ?? "";
5351
+ const fns = state.loginSetupDraftFunctions ?? cfg?.detectedFunctions ?? [];
5352
+ const beforeFn = state.loginSetupDraftBeforeFn !== void 0 ? state.loginSetupDraftBeforeFn : cfg?.beforeFn ?? null;
5353
+ const beforeEachFn = state.loginSetupDraftBeforeEachFn !== void 0 ? state.loginSetupDraftBeforeEachFn : cfg?.beforeEachFn ?? null;
5354
+ const hasContent = state.loginSetupDraftHasContent ?? !!cfg?.fileContent;
5355
+ const fnOption = (name, selected) => `<option value="${escAttr(name ?? "")}" ${selected === name ? "selected" : ""}>${name ? escHtml(name) : t("CONFIG.LOGIN_SETUP_NONE_OPTION")}</option>`;
5356
+ const fnDropdown = (id, label, selected) => `
5357
+ <div class="ls-field-block">
5358
+ <label class="ls-label" for="${id}">${escHtml(label)}</label>
5359
+ <select id="${id}" class="ls-select">
5360
+ ${fnOption(null, selected)}
5361
+ ${fns.map((fn) => fnOption(fn, selected)).join("")}
5362
+ </select>
5363
+ </div>`;
5364
+ const functionsSection = hasContent ? fns.length > 0 ? `<div class="ls-section">
5365
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_FUNCTIONS_DETECTED")}</span>
5366
+ <div class="ls-fns">${fns.map((fn) => `<span class="ls-fn-chip">${escHtml(fn)}</span>`).join("")}</div>
5367
+ </div>
5368
+ <div class="ls-section">
5369
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_BLOCK_ASSIGN")}</span>
5370
+ ${fnDropdown("login-setup-before-fn", t("CONFIG.LOGIN_SETUP_BEFORE_LABEL"), beforeFn)}
5371
+ ${fnDropdown("login-setup-before-each-fn", t("CONFIG.LOGIN_SETUP_BEFORE_EACH_LABEL"), beforeEachFn)}
5372
+ </div>` : `<div class="ls-empty-state">
5373
+ <span class="ls-empty-icon">\u{1F50D}</span>
5374
+ <span>${t("CONFIG.LOGIN_SETUP_NO_FUNCTIONS")}</span>
5375
+ </div>` : "";
5376
+ const fileSection = hasContent ? `<div class="ls-section">
5377
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_FILE_NAME_LABEL")}</span>
5378
+ <div class="ls-file-row">
5379
+ <span class="ls-file-icon">\u{1F4C4}</span>
5380
+ <span class="ls-file-name">${escHtml(path)}</span>
5381
+ <button id="btn-login-setup-rescan" class="ls-btn-rescan">${t("CONFIG.LOGIN_SETUP_RESCAN")}</button>
5382
+ </div>
5383
+ </div>` : `<div class="ls-section">
5384
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_FILE_NAME_LABEL")}</span>
5385
+ <input id="login-setup-filepath" type="text" class="ls-input"
5386
+ placeholder="${escAttr(t("CONFIG.LOGIN_SETUP_FILE_NAME_PH"))}"
5387
+ value="${escAttr(path)}" />
5388
+ </div>`;
5389
+ return `
5390
+ <div class="export-overlay" id="login-setup-overlay">
5391
+ <div class="export-modal ls-modal">
5392
+
5393
+ <div class="export-hd ls-hd">
5394
+ <span class="ls-hd-icon">\u{1F510}</span>
5395
+ ${t("CONFIG.LOGIN_SETUP_TITLE")}
5396
+ </div>
5397
+
5398
+ <div class="ls-body">
5399
+
5400
+ <div class="ls-section">
5401
+ <span class="ls-section-label">${t("CONFIG.LOGIN_SETUP_SOURCE_LABEL")}</span>
5402
+ <div class="ls-actions">
5403
+ <button id="btn-login-setup-create-scaffold" class="ls-action-btn">
5404
+ <span class="ls-action-icon">\u2728</span>
5405
+ <span class="ls-action-label">${t("CONFIG.LOGIN_SETUP_CREATE_SCAFFOLD")}</span>
5406
+ <span class="ls-action-sub">${t("CONFIG.LOGIN_SETUP_CREATE_SUB")}</span>
5407
+ </button>
5408
+ <button id="btn-login-setup-pick-file" class="ls-action-btn">
5409
+ <span class="ls-action-icon">\u{1F4C2}</span>
5410
+ <span class="ls-action-label">${t("CONFIG.LOGIN_SETUP_SELECT_EXISTING")}</span>
5411
+ <span class="ls-action-sub">${t("CONFIG.LOGIN_SETUP_SELECT_SUB")}</span>
5412
+ </button>
5413
+ </div>
5414
+ </div>
5415
+
5416
+ ${fileSection}
5417
+
5418
+ ${functionsSection}
5419
+
5420
+ </div>
5421
+
5422
+ <div class="export-ft ls-ft">
5423
+ <button id="btn-login-setup-clear" class="btn-danger">${t("CONFIG.LOGIN_SETUP_CLEAR_BTN")}</button>
5424
+ <span class="export-ft-actions">
5425
+ <button id="btn-login-setup-cancel">${t("CONFIG.LOGIN_SETUP_CANCEL_BTN")}</button>
5426
+ <button id="btn-login-setup-save" class="btn-primary">${t("CONFIG.LOGIN_SETUP_SAVE_BTN")}</button>
5427
+ </span>
5428
+ </div>
5429
+
5430
+ </div>
5431
+ </div>`;
4817
5432
  }
4818
5433
 
4819
5434
  // src/components/configuration/configuration.ts
@@ -4834,6 +5449,15 @@ var ConfigurationElement = class extends HTMLElement {
4834
5449
  exportSelectedIds = /* @__PURE__ */ new Set();
4835
5450
  exportSelectedTags = /* @__PURE__ */ new Set();
4836
5451
  issueTrackerConfig = { ...DEFAULT_ISSUE_TRACKER_CONFIG };
5452
+ isLoginSetupOpen = false;
5453
+ loginSetupConfig = null;
5454
+ loginSetupDraftPath = "";
5455
+ loginSetupDraftFunctions = [];
5456
+ loginSetupDraftBeforeFn = null;
5457
+ loginSetupDraftBeforeEachFn = null;
5458
+ loginSetupDraftHasContent = false;
5459
+ loginSetupDraftHandle = null;
5460
+ loginSetupDraftContent = "";
4837
5461
  filesystemGranted = false;
4838
5462
  cypressFolderName = null;
4839
5463
  constructor() {
@@ -4875,6 +5499,7 @@ var ConfigurationElement = class extends HTMLElement {
4875
5499
  provider: config?.["issueTrackerProvider"] ?? DEFAULT_ISSUE_TRACKER_CONFIG.provider,
4876
5500
  baseUrl: config?.["issueTrackerBaseUrl"] ?? ""
4877
5501
  };
5502
+ this.loginSetupConfig = await this.persistence.getLoginSetup();
4878
5503
  this.render();
4879
5504
  }
4880
5505
  async onLanguageChange(lang) {
@@ -4940,6 +5565,85 @@ var ConfigurationElement = class extends HTMLElement {
4940
5565
  await this.persistence.setConfig({ issueTrackerBaseUrl: baseUrl });
4941
5566
  this.dispatchEvent(new CustomEvent("issuetrackerchange", { detail: { ...this.issueTrackerConfig }, bubbles: true, composed: true }));
4942
5567
  }
5568
+ openLoginSetupPanel() {
5569
+ this.isLoginSetupOpen = true;
5570
+ const cfg = this.loginSetupConfig;
5571
+ this.loginSetupDraftHandle = null;
5572
+ this.loginSetupDraftContent = cfg?.fileContent ?? "";
5573
+ this.loginSetupDraftPath = cfg?.filePath ?? "";
5574
+ this.loginSetupDraftFunctions = cfg?.detectedFunctions ?? [];
5575
+ this.loginSetupDraftBeforeFn = cfg?.beforeFn ?? null;
5576
+ this.loginSetupDraftBeforeEachFn = cfg?.beforeEachFn ?? null;
5577
+ this.loginSetupDraftHasContent = !!cfg?.fileContent;
5578
+ this.render();
5579
+ }
5580
+ closeLoginSetupPanel() {
5581
+ this.isLoginSetupOpen = false;
5582
+ this.render();
5583
+ }
5584
+ async saveLoginSetupConfig(config) {
5585
+ await this.persistence.saveLoginSetup(config);
5586
+ this.loginSetupConfig = config;
5587
+ this.isLoginSetupOpen = false;
5588
+ this.render();
5589
+ }
5590
+ async clearLoginSetupConfig() {
5591
+ await this.persistence.clearLoginSetup();
5592
+ this.loginSetupConfig = null;
5593
+ this.render();
5594
+ }
5595
+ async loginSetupPickFile() {
5596
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim() ?? "";
5597
+ try {
5598
+ const [handle] = await window.showOpenFilePicker({
5599
+ types: [{ description: "TypeScript files", accept: { "text/plain": [".ts"] } }],
5600
+ multiple: false
5601
+ });
5602
+ this.loginSetupDraftHandle = handle;
5603
+ const file = await handle.getFile();
5604
+ this.loginSetupDraftContent = await file.text();
5605
+ this.loginSetupDraftHasContent = true;
5606
+ this.loginSetupDraftPath = currentPath || `cypress/common-services/${handle.name}`;
5607
+ this.loginSetupDraftFunctions = extractExportedFunctions(this.loginSetupDraftContent);
5608
+ this.render();
5609
+ } catch (e) {
5610
+ if (e?.name !== "AbortError") showToast(this.t("CONFIG.FOLDER_ERROR_TOAST"), false);
5611
+ }
5612
+ }
5613
+ async loginSetupCreateScaffold() {
5614
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim() ?? "";
5615
+ try {
5616
+ const handle = await window.showSaveFilePicker({
5617
+ suggestedName: "login.service.ts",
5618
+ types: [{ description: "TypeScript files", accept: { "text/plain": [".ts"] } }]
5619
+ });
5620
+ const content = buildLoginScaffold();
5621
+ const writable = await handle.createWritable();
5622
+ await writable.write(content);
5623
+ await writable.close();
5624
+ this.loginSetupDraftHandle = handle;
5625
+ this.loginSetupDraftContent = content;
5626
+ this.loginSetupDraftHasContent = true;
5627
+ this.loginSetupDraftPath = currentPath || `cypress/common-services/${handle.name}`;
5628
+ this.loginSetupDraftFunctions = extractExportedFunctions(content);
5629
+ this.render();
5630
+ } catch (e) {
5631
+ if (e?.name !== "AbortError") showToast(this.t("CONFIG.FOLDER_ERROR_TOAST"), false);
5632
+ }
5633
+ }
5634
+ async loginSetupRescan() {
5635
+ const currentPath = this.shadow.getElementById("login-setup-filepath")?.value.trim();
5636
+ if (currentPath !== void 0) this.loginSetupDraftPath = currentPath;
5637
+ try {
5638
+ if (this.loginSetupDraftHandle) {
5639
+ const file = await this.loginSetupDraftHandle.getFile();
5640
+ this.loginSetupDraftContent = await file.text();
5641
+ }
5642
+ this.loginSetupDraftFunctions = extractExportedFunctions(this.loginSetupDraftContent);
5643
+ this.render();
5644
+ } catch {
5645
+ }
5646
+ }
4943
5647
  async changeFolder() {
4944
5648
  try {
4945
5649
  await this.persistence.requestDirectoryPermissions();
@@ -5042,7 +5746,14 @@ var ConfigurationElement = class extends HTMLElement {
5042
5746
  exportTests: this.exportTests,
5043
5747
  exportSelectedIds: this.exportSelectedIds,
5044
5748
  exportSelectedTags: this.exportSelectedTags,
5045
- issueTrackerConfig: this.issueTrackerConfig
5749
+ issueTrackerConfig: this.issueTrackerConfig,
5750
+ isLoginSetupOpen: this.isLoginSetupOpen,
5751
+ loginSetupConfig: this.loginSetupConfig,
5752
+ loginSetupDraftPath: this.loginSetupDraftPath,
5753
+ loginSetupDraftFunctions: this.loginSetupDraftFunctions,
5754
+ loginSetupDraftBeforeFn: this.loginSetupDraftBeforeFn,
5755
+ loginSetupDraftBeforeEachFn: this.loginSetupDraftBeforeEachFn,
5756
+ loginSetupDraftHasContent: this.loginSetupDraftHasContent
5046
5757
  }, this.t.bind(this))}`;
5047
5758
  ;
5048
5759
  this.shadow.getElementById("lang-select").addEventListener(
@@ -5101,24 +5812,48 @@ var ConfigurationElement = class extends HTMLElement {
5101
5812
  this.shadow.querySelectorAll("[data-export-tag]").forEach(
5102
5813
  (el) => el.addEventListener("click", () => this.toggleExportTag(el.dataset["exportTag"] ?? ""))
5103
5814
  );
5815
+ this.shadow.getElementById("btn-open-login-setup")?.addEventListener("click", () => this.openLoginSetupPanel());
5816
+ this.shadow.getElementById("btn-login-setup-cancel")?.addEventListener("click", () => this.closeLoginSetupPanel());
5817
+ this.shadow.getElementById("btn-login-setup-pick-file")?.addEventListener("click", () => this.loginSetupPickFile());
5818
+ this.shadow.getElementById("btn-login-setup-create-scaffold")?.addEventListener("click", () => this.loginSetupCreateScaffold());
5819
+ this.shadow.getElementById("btn-login-setup-rescan")?.addEventListener("click", () => this.loginSetupRescan());
5820
+ this.shadow.getElementById("btn-login-setup-clear")?.addEventListener("click", () => this.clearLoginSetupConfig());
5821
+ this.shadow.getElementById("btn-login-setup-save")?.addEventListener("click", async () => {
5822
+ const pathEl = this.shadow.getElementById("login-setup-filepath");
5823
+ const beforeEl = this.shadow.getElementById("login-setup-before-fn");
5824
+ const beforeEachEl = this.shadow.getElementById("login-setup-before-each-fn");
5825
+ await this.saveLoginSetupConfig({
5826
+ enabled: true,
5827
+ filePath: pathEl?.value.trim() ?? this.loginSetupDraftPath,
5828
+ fileContent: this.loginSetupDraftContent,
5829
+ detectedFunctions: this.loginSetupDraftFunctions,
5830
+ beforeFn: beforeEl?.value || null,
5831
+ beforeEachFn: beforeEachEl?.value || null
5832
+ });
5833
+ });
5834
+ ;
5104
5835
  this.shadow.getElementById("file-input").addEventListener("change", async (e) => {
5105
5836
  const file = e.target.files?.[0];
5106
5837
  if (!file) return;
5107
5838
  try {
5108
5839
  await this.importAllData(file);
5109
- alert(this.t("CONFIG.IMPORT_SUCCESS"));
5840
+ showToast(this.t("CONFIG.IMPORT_SUCCESS"));
5110
5841
  } catch (err) {
5111
- alert(err.message ?? this.t("CONFIG.IMPORT_ERROR"));
5842
+ showToast(err.message ?? this.t("CONFIG.IMPORT_ERROR"), false);
5112
5843
  }
5113
5844
  const target = e.target;
5114
5845
  if (target) target.value = "";
5115
5846
  });
5116
5847
  }
5117
5848
  };
5118
- if (!customElements.get("e2e-configuration")) {
5119
- customElements.define("e2e-configuration", ConfigurationElement);
5849
+ if (!customElements.get("lib-e2e-configuration")) {
5850
+ customElements.define("lib-e2e-configuration", ConfigurationElement);
5120
5851
  }
5121
5852
 
5853
+ // src/components/advanced-test-editor/advanced-test-editor.ts
5854
+ var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
5855
+ init_code_format_utils();
5856
+
5122
5857
  // src/components/advanced-test-editor/advanced-test-editor.styles.ts
5123
5858
  var ADVANCED_TEST_EDITOR_STYLES = `
5124
5859
  :host { display: flex; flex-direction: column; flex: 1; min-height: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e6edf3; overflow: hidden; }
@@ -5377,6 +6112,7 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5377
6112
  isCreatingFolder = false;
5378
6113
  collapsedDirs = /* @__PURE__ */ new Set();
5379
6114
  sidebarWidth = 220;
6115
+ loginSetupConfig = null;
5380
6116
  hasPermission = false;
5381
6117
  needsReauth = false;
5382
6118
  _dirHandle = null;
@@ -5437,20 +6173,51 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5437
6173
  const name = rawName.trim().replace(/\.cy\.ts$/, "");
5438
6174
  if (!name) return;
5439
6175
  const fileName = `${name}.cy.ts`;
5440
- const template = `describe('${escapeSingleQuotes(name)}', () => {
6176
+ const cfg = this.loginSetupConfig;
6177
+ if (cfg?.enabled && (cfg.beforeFn || cfg.beforeEachFn)) {
6178
+ const e2eName = this._e2eHandle.name ?? "e2e";
6179
+ const fromPath = `cypress/${e2eName}/${fileName}`;
6180
+ const importPath = buildLoginImportPath(fromPath, cfg.filePath);
6181
+ const { importLine, beforeBlock, beforeEachBlock } = buildLoginBlocks(
6182
+ importPath,
6183
+ cfg.beforeFn,
6184
+ cfg.beforeEachFn
6185
+ );
6186
+ const header = importLine ? `${importLine}
5441
6187
 
6188
+ ` : "";
6189
+ const inner = [beforeBlock, beforeEachBlock].filter(Boolean).join("");
6190
+ const template = `${header}describe('${escapeSingleQuotes(name)}', () => {
6191
+ ${inner}
5442
6192
  it('should ', () => {
5443
6193
 
5444
6194
  });
5445
6195
 
5446
6196
  });
5447
6197
  `;
5448
- try {
5449
- const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
5450
- const writable = await fileHandle.createWritable();
5451
- await writable.write(template);
5452
- await writable.close();
5453
- } catch {
6198
+ try {
6199
+ const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
6200
+ const writable = await fileHandle.createWritable();
6201
+ await writable.write(template);
6202
+ await writable.close();
6203
+ } catch {
6204
+ }
6205
+ } else {
6206
+ const template = `describe('${escapeSingleQuotes(name)}', () => {
6207
+
6208
+ it('should ', () => {
6209
+
6210
+ });
6211
+
6212
+ });
6213
+ `;
6214
+ try {
6215
+ const fileHandle = await this._e2eHandle.getFileHandle(fileName, { create: true });
6216
+ const writable = await fileHandle.createWritable();
6217
+ await writable.write(template);
6218
+ await writable.close();
6219
+ } catch {
6220
+ }
5454
6221
  }
5455
6222
  this.isCreatingFile = false;
5456
6223
  await this.getFoldersData();
@@ -5478,6 +6245,30 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5478
6245
  const comment = this.testNotes ? this.transformationService.buildBlockComment(this.testNotes) + "\n" : "";
5479
6246
  content = this.transformationService.insertItBlock(content, comment + this.testItBlock);
5480
6247
  if (!content) return;
6248
+ const cfg = this.loginSetupConfig;
6249
+ if (cfg?.enabled && (cfg.beforeFn || cfg.beforeEachFn)) {
6250
+ const fns = [cfg.beforeFn, cfg.beforeEachFn].filter(Boolean);
6251
+ if (!hasLoginBlocks(content, fns)) {
6252
+ const e2eName = this._e2eHandle?.name ?? "e2e";
6253
+ const fileName = this.selectedFile?.name ?? "";
6254
+ const fromPath = `cypress/${e2eName}/${fileName}`;
6255
+ const importPath = buildLoginImportPath(fromPath, cfg.filePath);
6256
+ const { importLine, beforeBlock, beforeEachBlock } = buildLoginBlocks(importPath, cfg.beforeFn, cfg.beforeEachFn);
6257
+ const result = await import_sweetalert2.default.fire({
6258
+ title: this.t("CONFIG.LOGIN_SETUP_INJECT_TITLE"),
6259
+ html: `<div style="padding:10px 18px 14px;color:#8b949e;font-size:13px;line-height:1.6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">${this.t("CONFIG.LOGIN_SETUP_INJECT_TEXT")}</div>`,
6260
+ icon: "question",
6261
+ showCancelButton: true,
6262
+ confirmButtonText: this.t("CONFIG.LOGIN_SETUP_INJECT_CONFIRM"),
6263
+ cancelButtonText: this.t("CONFIG.LOGIN_SETUP_CANCEL_BTN"),
6264
+ confirmButtonColor: "#1f6feb",
6265
+ width: 460
6266
+ });
6267
+ if (result.isConfirmed) {
6268
+ content = injectLoginBlocksIntoExisting(content, importLine, beforeBlock, beforeEachBlock);
6269
+ }
6270
+ }
6271
+ }
5481
6272
  const writable = await this.selectedFileHandle.createWritable();
5482
6273
  await writable.write(content);
5483
6274
  await writable.close();
@@ -5672,8 +6463,8 @@ var AdvancedTestEditorElement = class extends HTMLElement {
5672
6463
  });
5673
6464
  }
5674
6465
  };
5675
- if (!customElements.get("advanced-test-editor")) {
5676
- customElements.define("advanced-test-editor", AdvancedTestEditorElement);
6466
+ if (!customElements.get("lib-e2e-advanced-test-editor")) {
6467
+ customElements.define("lib-e2e-advanced-test-editor", AdvancedTestEditorElement);
5677
6468
  }
5678
6469
 
5679
6470
  // src/components/file-preview/file-preview.styles.ts
@@ -6039,8 +6830,8 @@ var FilePreviewElement = class extends HTMLElement {
6039
6830
  });
6040
6831
  }
6041
6832
  };
6042
- if (!customElements.get("file-preview")) {
6043
- customElements.define("file-preview", FilePreviewElement);
6833
+ if (!customElements.get("lib-e2e-file-preview")) {
6834
+ customElements.define("lib-e2e-file-preview", FilePreviewElement);
6044
6835
  }
6045
6836
 
6046
6837
  // src/components/help-panel/help-panel.styles.ts
@@ -6136,12 +6927,12 @@ var REFERENCE_SECTIONS = [
6136
6927
  { title: "HELP.SEC_PANELS", items: ["HELP.PANEL_TESTS", "HELP.PANEL_COMMANDS", "HELP.PANEL_CONFIG", "HELP.PANEL_FILES"] },
6137
6928
  { title: "HELP.SEC_SELECTOR", items: ["HELP.SELECTOR_1", "HELP.SELECTOR_2"] },
6138
6929
  { title: "HELP.SEC_DATA", items: ["HELP.DATA_1", "HELP.DATA_2"] },
6139
- { title: "HELP.SEC_ADVANCED", items: ["HELP.ADV_INVISIBLE", "HELP.ADV_RUNNER", "HELP.ADV_CROSSAPP", "HELP.ADV_DRAG"] }
6930
+ { title: "HELP.SEC_ADVANCED", items: ["HELP.ADV_INVISIBLE", "HELP.ADV_RUNNER", "HELP.ADV_CROSSAPP", "HELP.ADV_DRAG", "HELP.ADV_LOGIN"] }
6140
6931
  ];
6141
6932
  var GUIDE_SECTIONS = [
6142
6933
  { 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"] },
6143
- { 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"] },
6144
- { 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"] }
6934
+ { title: "HELP.G_SEC_COVERED", items: ["HELP.G_COV_1", "HELP.G_COV_2", "HELP.G_COV_3", "HELP.G_COV_4", "HELP.G_COV_5", "HELP.G_COV_6", "HELP.G_COV_7"] },
6935
+ { 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"] }
6145
6936
  ];
6146
6937
  function sectionsHtml(sections, t) {
6147
6938
  return sections.map(
@@ -6188,12 +6979,12 @@ var HelpPanelElement = class extends HTMLElement {
6188
6979
  });
6189
6980
  }
6190
6981
  };
6191
- if (!customElements.get("help-panel")) {
6192
- customElements.define("help-panel", HelpPanelElement);
6982
+ if (!customElements.get("lib-e2e-help-panel")) {
6983
+ customElements.define("lib-e2e-help-panel", HelpPanelElement);
6193
6984
  }
6194
6985
 
6195
6986
  // src/components/lib-e2e-recorder/lib-e2e-recorder.ts
6196
- var import_sweetalert2 = __toESM(require("sweetalert2"), 1);
6987
+ var import_sweetalert22 = __toESM(require("sweetalert2"), 1);
6197
6988
 
6198
6989
  // src/components/lib-e2e-recorder/lib-e2e-recorder.styles.ts
6199
6990
  var DIRECTIONS = ["up-left", "up-right", "down-left", "down-right"];
@@ -6394,6 +7185,140 @@ function renderRecorderWidget(rec, paused, t) {
6394
7185
  <div class="rec-badge">${paused ? t("RECORDER.BADGE_PAUSED") : t("RECORDER.BADGE_REC")}</div>`;
6395
7186
  }
6396
7187
 
7188
+ // src/components/lib-e2e-recorder/assertion-builder.ts
7189
+ init_code_format_utils();
7190
+ var NO_VALUE_ASSERTIONS = /* @__PURE__ */ new Set(["be.visible", "not.exist", "be.disabled", "be.checked"]);
7191
+ function injectAssertionBuilder(container, t, onAddCommand) {
7192
+ container.insertAdjacentHTML("beforeend", `
7193
+ <div id="assertion-builder"
7194
+ style="padding:10px 12px;border-top:1px solid #21262d;background:#0d1117">
7195
+ <div style="font-size:10px;color:#484f58;text-transform:uppercase;letter-spacing:.8px;font-weight:600;margin-bottom:8px">
7196
+ ${t("RECORDER.ASSERTION_SECTION")}
7197
+ </div>
7198
+ <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end">
7199
+ <input id="assert-selector" type="text"
7200
+ placeholder="${t("RECORDER.ASSERT_SELECTOR_PH").replace(/"/g, "&quot;")}"
7201
+ style="flex:2;min-width:140px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7202
+ border-radius:5px;color:#c9d1d9;font-size:11px;outline:none;
7203
+ font-family:'Cascadia Code','Fira Code',monospace"/>
7204
+ <select id="assert-type"
7205
+ style="padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;
7206
+ color:#c9d1d9;font-size:11px;outline:none;cursor:pointer;flex-shrink:0">
7207
+ <option value="be.visible">be.visible</option>
7208
+ <option value="not.exist">not.exist</option>
7209
+ <option value="be.disabled">be.disabled</option>
7210
+ <option value="be.checked">be.checked</option>
7211
+ <option value="contain.text">contain.text</option>
7212
+ <option value="have.value">have.value</option>
7213
+ <option value="have.length">have.length</option>
7214
+ <option value="have.class">have.class</option>
7215
+ <option value="have.attr">have.attr</option>
7216
+ </select>
7217
+ <input id="assert-value" type="text" placeholder="${t("RECORDER.ASSERT_VALUE_PH")}"
7218
+ style="flex:2;min-width:110px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
7219
+ border-radius:5px;color:#c9d1d9;font-size:11px;outline:none"/>
7220
+ <button id="btn-add-assertion"
7221
+ style="padding:5px 12px;border:none;border-radius:5px;cursor:pointer;
7222
+ font-size:11px;font-weight:500;background:#2f81f7;color:#fff;
7223
+ white-space:nowrap;flex-shrink:0">
7224
+ ${t("RECORDER.ASSERT_ADD_BTN")}
7225
+ </button>
7226
+ </div>
7227
+ </div>`);
7228
+ document.getElementById("btn-add-assertion")?.addEventListener("click", () => {
7229
+ const sel = document.getElementById("assert-selector").value.trim();
7230
+ const type = document.getElementById("assert-type").value;
7231
+ const val = document.getElementById("assert-value").value.trim();
7232
+ if (!sel) return;
7233
+ const s = escapeSingleQuotes(sel);
7234
+ const v = escapeSingleQuotes(val);
7235
+ const cmd = NO_VALUE_ASSERTIONS.has(type) || !val ? `cy.get('${s}').should('${type}')` : `cy.get('${s}').should('${type}', '${v}')`;
7236
+ onAddCommand(cmd);
7237
+ document.getElementById("assert-selector").value = "";
7238
+ document.getElementById("assert-value").value = "";
7239
+ });
7240
+ }
7241
+
7242
+ // src/components/lib-e2e-recorder/filesystem-setup.ts
7243
+ function mountFilesystemSetupContent(container, t, onSkip, onSelect) {
7244
+ container.innerHTML = `
7245
+ <div style="padding:16px 20px 20px;color:#8b949e;font-size:13px;line-height:1.7">
7246
+ <p>${t("RECORDER.FS_INTRO_HTML")}</p>
7247
+ <p style="margin-top:10px;margin-bottom:6px;font-size:11px;color:#8b949e">
7248
+ ${t("RECORDER.FS_STRUCTURE_HINT_HTML")}
7249
+ </p>
7250
+ <pre style="margin:0;padding:10px 14px;background:#0d1117;border:1px solid #21262d;
7251
+ border-radius:8px;font-size:11px;color:#c9d1d9;line-height:1.8;
7252
+ font-family:'Cascadia Code','Fira Code','Consolas',monospace">
7253
+ cypress/ <span style="color:#484f58">${t("RECORDER.FS_TREE_PICK_HINT")}</span>
7254
+ \u2514\u2500\u2500 e2e/ <span style="color:#484f58">${t("RECORDER.FS_TREE_READ_HINT")}</span>
7255
+ \u2514\u2500\u2500 *.cy.ts</pre>
7256
+ <p style="margin-top:8px;font-size:11px;color:#484f58">
7257
+ ${t("RECORDER.FS_PERMISSION_NOTE")}
7258
+ </p>
7259
+ <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:20px">
7260
+ <button id="fs-skip"
7261
+ style="padding:7px 16px;border:1px solid #30363d;border-radius:6px;cursor:pointer;
7262
+ font-size:12px;font-weight:500;background:transparent;color:#8b949e">
7263
+ ${t("RECORDER.FS_LATER_BTN")}
7264
+ </button>
7265
+ <button id="fs-select"
7266
+ style="padding:7px 16px;border:none;border-radius:6px;cursor:pointer;
7267
+ font-size:12px;font-weight:500;background:#2f81f7;color:#fff">
7268
+ ${t("RECORDER.FS_SELECT_BTN")}
7269
+ </button>
7270
+ </div>
7271
+ </div>`;
7272
+ document.getElementById("fs-skip")?.addEventListener("click", () => {
7273
+ void onSkip();
7274
+ });
7275
+ document.getElementById("fs-select")?.addEventListener("click", () => {
7276
+ void onSelect();
7277
+ });
7278
+ }
7279
+
7280
+ // src/utils/swal-mount.utils.ts
7281
+ function mountComponentInSwal(tagName, container, props, events) {
7282
+ const el = document.createElement(tagName);
7283
+ Object.assign(el, props);
7284
+ container.appendChild(el);
7285
+ for (const { name, handler } of events ?? []) {
7286
+ el.addEventListener(name, handler);
7287
+ }
7288
+ return el;
7289
+ }
7290
+
7291
+ // src/utils/widget-position.utils.ts
7292
+ var TOGGLE_MARGIN = 30;
7293
+ var DRAG_THRESHOLD = 5;
7294
+ var DEFAULT_EDGE_OFFSET = 46;
7295
+ function boxOffsetForDirection(dir) {
7296
+ return {
7297
+ x: dir.endsWith("left") ? 144 : 46,
7298
+ y: dir.startsWith("up") ? 144 : 46
7299
+ };
7300
+ }
7301
+ function clampTogglePosition(x, y, vw, vh, margin = TOGGLE_MARGIN) {
7302
+ const maxX = Math.max(margin, vw - margin);
7303
+ const maxY = Math.max(margin, vh - margin);
7304
+ return {
7305
+ x: Math.min(Math.max(x, margin), maxX),
7306
+ y: Math.min(Math.max(y, margin), maxY)
7307
+ };
7308
+ }
7309
+ function resolveExpandDirection(x, y, vw, vh) {
7310
+ const vertical = y < vh / 2 ? "down" : "up";
7311
+ const horizontal = x < vw / 2 ? "right" : "left";
7312
+ return `${vertical}-${horizontal}`;
7313
+ }
7314
+ function defaultTogglePosition(vw, vh) {
7315
+ return { x: vw - DEFAULT_EDGE_OFFSET, y: vh - DEFAULT_EDGE_OFFSET };
7316
+ }
7317
+ function boxTopLeftFor(toggleCentre, dir) {
7318
+ const off = boxOffsetForDirection(dir);
7319
+ return { x: toggleCentre.x - off.x, y: toggleCentre.y - off.y };
7320
+ }
7321
+
6397
7322
  // src/utils/fixture-convert.utils.ts
6398
7323
  function toFixtureInterceptors(interceptors, fixtures) {
6399
7324
  const fixtureNames = new Set(fixtures.map((f) => f.name));
@@ -6513,6 +7438,10 @@ var LibE2eRecorderElement = class extends HTMLElement {
6513
7438
  this.recording.destroy();
6514
7439
  this._needsRecordingRebuild = true;
6515
7440
  }
7441
+ // HttpMonitor reads extendedHttpCommands and fixtureMode synchronously inside
7442
+ // fetch/XHR interception handlers, so they must live in localStorage (sync).
7443
+ // IndexedDB is the persistent source of truth; this method mirrors it into
7444
+ // localStorage on every mount so the sync cache is always fresh.
6516
7445
  async initHttpConfig() {
6517
7446
  const config = await this.persistence.getConfig("extendedHttpCommands");
6518
7447
  if (config === null) {
@@ -6560,9 +7489,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
6560
7489
  }
6561
7490
  showSelectorPicker(target) {
6562
7491
  Promise.resolve().then(() => (init_selector_picker(), selector_picker_exports)).then(() => {
6563
- const existing = document.querySelector("selector-picker");
7492
+ const existing = document.querySelector("lib-e2e-selector-picker");
6564
7493
  if (existing) existing.remove();
6565
- const picker = document.createElement("selector-picker");
7494
+ const picker = document.createElement("lib-e2e-selector-picker");
6566
7495
  picker.targetElement = target;
6567
7496
  picker.recording = this.recording;
6568
7497
  picker.translation = this.translation;
@@ -6624,7 +7553,7 @@ var LibE2eRecorderElement = class extends HTMLElement {
6624
7553
  }
6625
7554
  async promptResumeOrDiscard(session) {
6626
7555
  const t = this.translation.translate.bind(this.translation);
6627
- const result = await import_sweetalert2.default.fire({
7556
+ const result = await import_sweetalert22.default.fire({
6628
7557
  title: t("RECORDER.SESSION_RESUME_TITLE"),
6629
7558
  html: `<div style="padding:8px 4px;color:#8b949e;font-size:13px;line-height:1.6"><p>${t("RECORDER.SESSION_RESUME_TEXT")}</p><p style="margin-top:6px;color:#c9d1d9"><b>${session.commands.length}</b> ${t("RECORDER.SESSION_RESUME_COUNT")}</p></div>`,
6630
7559
  showCancelButton: true,
@@ -6843,8 +7772,9 @@ var LibE2eRecorderElement = class extends HTMLElement {
6843
7772
  this.showFilesystemSetupDialog();
6844
7773
  }
6845
7774
  showFilesystemSetupDialog() {
6846
- import_sweetalert2.default.fire({
6847
- title: this.translation.translate("RECORDER.FS_TITLE"),
7775
+ const t = this.translation.translate.bind(this.translation);
7776
+ import_sweetalert22.default.fire({
7777
+ title: t("RECORDER.FS_TITLE"),
6848
7778
  html: '<div id="fs-setup-content"></div>',
6849
7779
  showCloseButton: true,
6850
7780
  showConfirmButton: false,
@@ -6853,277 +7783,171 @@ var LibE2eRecorderElement = class extends HTMLElement {
6853
7783
  setSwal2DataCyAttribute();
6854
7784
  const container = document.getElementById("fs-setup-content");
6855
7785
  if (!container) return;
6856
- container.innerHTML = `
6857
- <div style="padding:16px 20px 20px;color:#8b949e;font-size:13px;line-height:1.7">
6858
- <p>${this.translation.translate("RECORDER.FS_INTRO_HTML")}</p>
6859
- <p style="margin-top:10px;margin-bottom:6px;font-size:11px;color:#8b949e">
6860
- ${this.translation.translate("RECORDER.FS_STRUCTURE_HINT_HTML")}
6861
- </p>
6862
- <pre style="margin:0;padding:10px 14px;background:#0d1117;border:1px solid #21262d;
6863
- border-radius:8px;font-size:11px;color:#c9d1d9;line-height:1.8;
6864
- font-family:'Cascadia Code','Fira Code','Consolas',monospace">
6865
- cypress/ <span style="color:#484f58">${this.translation.translate("RECORDER.FS_TREE_PICK_HINT")}</span>
6866
- \u2514\u2500\u2500 e2e/ <span style="color:#484f58">${this.translation.translate("RECORDER.FS_TREE_READ_HINT")}</span>
6867
- \u2514\u2500\u2500 *.cy.ts</pre>
6868
- <p style="margin-top:8px;font-size:11px;color:#484f58">
6869
- ${this.translation.translate("RECORDER.FS_PERMISSION_NOTE")}
6870
- </p>
6871
- <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:20px">
6872
- <button id="fs-skip"
6873
- style="padding:7px 16px;border:1px solid #30363d;border-radius:6px;cursor:pointer;
6874
- font-size:12px;font-weight:500;background:transparent;color:#8b949e">
6875
- ${this.translation.translate("RECORDER.FS_LATER_BTN")}
6876
- </button>
6877
- <button id="fs-select"
6878
- style="padding:7px 16px;border:none;border-radius:6px;cursor:pointer;
6879
- font-size:12px;font-weight:500;background:#2f81f7;color:#fff">
6880
- ${this.translation.translate("RECORDER.FS_SELECT_BTN")}
6881
- </button>
6882
- </div>
6883
- </div>`;
6884
- document.getElementById("fs-skip")?.addEventListener("click", async () => {
6885
- await this.persistence.setConfigKey("allowReadWriteFiles", "false");
6886
- import_sweetalert2.default.close();
6887
- });
6888
- document.getElementById("fs-select")?.addEventListener("click", async () => {
6889
- try {
6890
- await this.persistence.requestDirectoryPermissions();
6891
- import_sweetalert2.default.close();
6892
- showToast(this.translation.translate("RECORDER.FS_SUCCESS_TOAST"));
6893
- } catch (e) {
6894
- if (e?.name !== "AbortError") {
6895
- showToast(this.translation.translate("RECORDER.FS_ERROR_TOAST"), false);
7786
+ mountFilesystemSetupContent(
7787
+ container,
7788
+ t,
7789
+ async () => {
7790
+ await this.persistence.setConfigKey("allowReadWriteFiles", "false");
7791
+ import_sweetalert22.default.close();
7792
+ },
7793
+ async () => {
7794
+ try {
7795
+ await this.persistence.requestDirectoryPermissions();
7796
+ import_sweetalert22.default.close();
7797
+ showToast(t("RECORDER.FS_SUCCESS_TOAST"));
7798
+ } catch (e) {
7799
+ if (e?.name !== "AbortError") {
7800
+ showToast(t("RECORDER.FS_ERROR_TOAST"), false);
7801
+ }
6896
7802
  }
6897
7803
  }
6898
- });
7804
+ );
6899
7805
  }
6900
7806
  });
6901
7807
  }
6902
7808
  // ── dialogs ──────────────────────────────────────────────────────────────
6903
7809
  showCommandsDialog() {
6904
- this.toggleModal("isCommandsDialogOpen", () => {
6905
- import_sweetalert2.default.fire({
7810
+ this.openSwalDialog(
7811
+ "isCommandsDialogOpen",
7812
+ "commands-modal-content",
7813
+ {
6906
7814
  title: this.translation.translate("MAIN_FRAME.DIALOG_COMMANDS"),
6907
7815
  html: '<div id="commands-modal-content" style="min-height:250px;padding:0"></div>',
6908
7816
  showCloseButton: true,
6909
- showConfirmButton: false,
6910
7817
  allowOutsideClick: false,
6911
7818
  backdrop: false,
6912
- width: 640,
6913
- color: "#e6edf3",
6914
- didOpen: () => {
6915
- makeSwalDraggable();
6916
- setSwal2DataCyAttribute();
7819
+ width: 640
7820
+ },
7821
+ (container) => {
7822
+ const child = mountComponentInSwal("lib-e2e-test-previsualizer", container, {
7823
+ translation: this.translation,
7824
+ commands: this.cypressCommands,
7825
+ interceptors: this.interceptors,
7826
+ editable: true
7827
+ }, [
7828
+ { name: "deletecommand", handler: (e) => this.recording.removeCommand(e.detail) },
7829
+ { name: "movecommand", handler: (e) => this.recording.moveCommand(e.detail.from, e.detail.to) },
7830
+ { name: "deleteinterceptor", handler: (e) => this.recording.removeInterceptor(e.detail) }
7831
+ ]);
7832
+ this._previsualizerRef = child;
7833
+ injectAssertionBuilder(container, this.translation.translate.bind(this.translation), (cmd) => this.recording.appendCommand(cmd));
7834
+ },
7835
+ {
7836
+ // Make the container click-through so the page underneath remains interactive
7837
+ onDidOpenPreamble: () => {
6917
7838
  const swalContainer = document.querySelector(".swal2-container");
6918
7839
  if (swalContainer) {
6919
7840
  swalContainer.style.pointerEvents = "none";
6920
7841
  const popup = swalContainer.querySelector(".swal2-popup");
6921
7842
  if (popup) popup.style.pointerEvents = "all";
6922
7843
  }
6923
- const container = document.getElementById("commands-modal-content");
6924
- if (!container) return;
6925
- const child = document.createElement("test-previsualizer");
6926
- child.translation = this.translation;
6927
- child.commands = this.cypressCommands;
6928
- child.interceptors = this.interceptors;
6929
- child.editable = true;
6930
- container.appendChild(child);
6931
- this._previsualizerRef = child;
6932
- child.addEventListener("deletecommand", (e) => {
6933
- this.recording.removeCommand(e.detail);
6934
- });
6935
- child.addEventListener("movecommand", (e) => {
6936
- this.recording.moveCommand(e.detail.from, e.detail.to);
6937
- });
6938
- child.addEventListener("deleteinterceptor", (e) => {
6939
- this.recording.removeInterceptor(e.detail);
6940
- });
6941
- const assertionHtml = `
6942
- <div id="assertion-builder"
6943
- style="padding:10px 12px;border-top:1px solid #21262d;background:#0d1117">
6944
- <div style="font-size:10px;color:#484f58;text-transform:uppercase;letter-spacing:.8px;font-weight:600;margin-bottom:8px">
6945
- ${this.translation.translate("RECORDER.ASSERTION_SECTION")}
6946
- </div>
6947
- <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end">
6948
- <input id="assert-selector" type="text"
6949
- placeholder="${this.translation.translate("RECORDER.ASSERT_SELECTOR_PH").replace(/"/g, "&quot;")}"
6950
- style="flex:2;min-width:140px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
6951
- border-radius:5px;color:#c9d1d9;font-size:11px;outline:none;
6952
- font-family:'Cascadia Code','Fira Code',monospace"/>
6953
- <select id="assert-type"
6954
- style="padding:5px 8px;background:#161b22;border:1px solid #30363d;border-radius:5px;
6955
- color:#c9d1d9;font-size:11px;outline:none;cursor:pointer;flex-shrink:0">
6956
- <option value="be.visible">be.visible</option>
6957
- <option value="not.exist">not.exist</option>
6958
- <option value="be.disabled">be.disabled</option>
6959
- <option value="be.checked">be.checked</option>
6960
- <option value="contain.text">contain.text</option>
6961
- <option value="have.value">have.value</option>
6962
- <option value="have.length">have.length</option>
6963
- <option value="have.class">have.class</option>
6964
- <option value="have.attr">have.attr</option>
6965
- </select>
6966
- <input id="assert-value" type="text" placeholder="${this.translation.translate("RECORDER.ASSERT_VALUE_PH")}"
6967
- style="flex:2;min-width:110px;padding:5px 8px;background:#161b22;border:1px solid #30363d;
6968
- border-radius:5px;color:#c9d1d9;font-size:11px;outline:none"/>
6969
- <button id="btn-add-assertion"
6970
- style="padding:5px 12px;border:none;border-radius:5px;cursor:pointer;
6971
- font-size:11px;font-weight:500;background:#2f81f7;color:#fff;
6972
- white-space:nowrap;flex-shrink:0">
6973
- ${this.translation.translate("RECORDER.ASSERT_ADD_BTN")}
6974
- </button>
6975
- </div>
6976
- </div>`;
6977
- container.insertAdjacentHTML("beforeend", assertionHtml);
6978
- const NO_VALUE_ASSERTIONS = /* @__PURE__ */ new Set(["be.visible", "not.exist", "be.disabled", "be.checked"]);
6979
- document.getElementById("btn-add-assertion")?.addEventListener("click", () => {
6980
- const sel = document.getElementById("assert-selector").value.trim();
6981
- const type = document.getElementById("assert-type").value;
6982
- const val = document.getElementById("assert-value").value.trim();
6983
- if (!sel) return;
6984
- const s = escapeSingleQuotes(sel);
6985
- const v = escapeSingleQuotes(val);
6986
- const cmd = NO_VALUE_ASSERTIONS.has(type) || !val ? `cy.get('${s}').should('${type}')` : `cy.get('${s}').should('${type}', '${v}')`;
6987
- this.recording.appendCommand(cmd);
6988
- document.getElementById("assert-selector").value = "";
6989
- document.getElementById("assert-value").value = "";
6990
- });
6991
7844
  },
6992
- willClose: () => {
6993
- this.isCommandsDialogOpen = false;
7845
+ onWillClose: () => {
6994
7846
  this._previsualizerRef = null;
6995
7847
  }
6996
- });
6997
- this.resizePopup();
6998
- });
7848
+ }
7849
+ );
6999
7850
  }
7000
7851
  showSavedTestsDialog() {
7001
- this.toggleModal("isSavedTestsDialogOpen", () => {
7002
- import_sweetalert2.default.fire({
7852
+ this.openSwalDialog(
7853
+ "isSavedTestsDialogOpen",
7854
+ "saved-tests-modal-content",
7855
+ {
7003
7856
  title: this.translation.translate("MAIN_FRAME.DIALOG_SAVED_TESTS"),
7004
7857
  html: '<div id="saved-tests-modal-content" style="min-height:250px;padding:0"></div>',
7005
7858
  showCloseButton: true,
7006
- showConfirmButton: false,
7007
- width: 680,
7008
- color: "#e6edf3",
7009
- didOpen: () => {
7010
- makeSwalDraggable();
7011
- setSwal2DataCyAttribute();
7012
- const container = document.getElementById("saved-tests-modal-content");
7013
- if (!container) return;
7014
- const child = document.createElement("test-editor");
7015
- child.persistence = this.persistence;
7016
- child.translation = this.translation;
7017
- child.issueTrackerConfig = this.issueTrackerConfig;
7018
- container.appendChild(child);
7019
- },
7020
- willClose: () => {
7021
- this.isSavedTestsDialogOpen = false;
7022
- }
7023
- });
7024
- this.resizePopup();
7025
- });
7859
+ width: 680
7860
+ },
7861
+ (container) => {
7862
+ mountComponentInSwal("lib-e2e-test-editor", container, {
7863
+ persistence: this.persistence,
7864
+ translation: this.translation,
7865
+ issueTrackerConfig: this.issueTrackerConfig
7866
+ });
7867
+ }
7868
+ );
7026
7869
  }
7027
7870
  showSaveTestDialog() {
7028
- this.toggleModal("isSaveTestDialogOpen", () => {
7029
- import_sweetalert2.default.fire({
7871
+ this.openSwalDialog(
7872
+ "isSaveTestDialogOpen",
7873
+ "save-test-modal-content",
7874
+ {
7030
7875
  title: this.translation.translate("MAIN_FRAME.DIALOG_SAVE"),
7031
7876
  html: '<div id="save-test-modal-content" style="padding:0"></div>',
7032
- showCloseButton: true,
7033
- showConfirmButton: false,
7877
+ showCloseButton: false,
7034
7878
  allowOutsideClick: false,
7035
- color: "#e6edf3",
7036
- didOpen: () => {
7037
- makeSwalDraggable();
7038
- setSwal2DataCyAttribute();
7039
- const container = document.getElementById("save-test-modal-content");
7040
- if (!container) return;
7041
- const child = document.createElement("save-test");
7042
- child.translation = this.translation;
7043
- child.issueTrackerConfig = this.issueTrackerConfig;
7044
- container.appendChild(child);
7045
- child.addEventListener("savetest", (e) => {
7879
+ allowEscapeKey: false
7880
+ },
7881
+ (container) => {
7882
+ mountComponentInSwal("lib-e2e-save-test", container, {
7883
+ translation: this.translation,
7884
+ issueTrackerConfig: this.issueTrackerConfig
7885
+ }, [
7886
+ { name: "savetest", handler: (e) => {
7046
7887
  const { description, notes, tags, ticketId } = e.detail ?? {};
7047
7888
  this.onSaveTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? "");
7048
- import_sweetalert2.default.close();
7049
- });
7050
- child.addEventListener("saveandexport", (e) => {
7889
+ import_sweetalert22.default.close();
7890
+ } },
7891
+ { name: "saveandexport", handler: (e) => {
7051
7892
  const { description, notes, tags, ticketId } = e.detail ?? {};
7052
7893
  this.onSaveAndExportTest(description ?? null, tags ?? [], notes ?? "", ticketId ?? "");
7053
- import_sweetalert2.default.close();
7054
- });
7055
- },
7056
- willClose: () => {
7057
- this.isSaveTestDialogOpen = false;
7058
- }
7059
- });
7060
- });
7894
+ import_sweetalert22.default.close();
7895
+ } }
7896
+ ]);
7897
+ },
7898
+ { skipResize: true }
7899
+ );
7061
7900
  }
7062
7901
  showSettingsDialog() {
7063
- this.toggleModal("isSettingsDialogOpen", () => {
7064
- import_sweetalert2.default.fire({
7902
+ this.openSwalDialog(
7903
+ "isSettingsDialogOpen",
7904
+ "settings-modal-content",
7905
+ {
7065
7906
  title: this.translation.translate("MAIN_FRAME.SETTINGS"),
7066
7907
  html: '<div id="settings-modal-content" style="padding:0"></div>',
7067
7908
  showCloseButton: true,
7068
- showConfirmButton: false,
7069
- width: 520,
7070
- color: "#e6edf3",
7071
- didOpen: () => {
7072
- makeSwalDraggable();
7073
- setSwal2DataCyAttribute();
7074
- const container = document.getElementById("settings-modal-content");
7075
- if (!container) return;
7076
- const child = document.createElement("e2e-configuration");
7077
- child.persistence = this.persistence;
7078
- child.translation = this.translation;
7079
- container.appendChild(child);
7080
- child.addEventListener("smartselectorchange", (e) => {
7909
+ width: 520
7910
+ },
7911
+ (container) => {
7912
+ mountComponentInSwal("lib-e2e-configuration", container, {
7913
+ persistence: this.persistence,
7914
+ translation: this.translation
7915
+ }, [
7916
+ { name: "smartselectorchange", handler: (e) => {
7081
7917
  this.smartSelectorEnabled = e.detail;
7082
- });
7083
- child.addEventListener("starthiddenchange", (e) => {
7918
+ } },
7919
+ { name: "starthiddenchange", handler: (e) => {
7084
7920
  this.isVisible = !e.detail;
7085
7921
  this.style.display = this.isVisible ? "" : "none";
7086
- });
7087
- child.addEventListener("resetwidgetposition", () => this.resetWidgetPosition());
7088
- child.addEventListener("issuetrackerchange", (e) => {
7922
+ } },
7923
+ { name: "resetwidgetposition", handler: () => this.resetWidgetPosition() },
7924
+ { name: "issuetrackerchange", handler: (e) => {
7089
7925
  this.issueTrackerConfig = e.detail;
7090
- });
7091
- },
7092
- willClose: () => {
7093
- this.isSettingsDialogOpen = false;
7094
- }
7095
- });
7096
- this.resizePopup();
7097
- });
7926
+ } }
7927
+ ]);
7928
+ }
7929
+ );
7098
7930
  }
7099
7931
  showHelpDialog() {
7100
- this.toggleModal("isHelpDialogOpen", () => {
7101
- import_sweetalert2.default.fire({
7932
+ this.openSwalDialog(
7933
+ "isHelpDialogOpen",
7934
+ "help-modal-content",
7935
+ {
7102
7936
  title: this.translation.translate("HELP.TITLE"),
7103
7937
  html: '<div id="help-modal-content" style="padding:0"></div>',
7104
7938
  showCloseButton: true,
7105
- showConfirmButton: false,
7106
- width: 560,
7107
- color: "#e6edf3",
7108
- didOpen: () => {
7109
- makeSwalDraggable();
7110
- setSwal2DataCyAttribute();
7111
- const container = document.getElementById("help-modal-content");
7112
- if (!container) return;
7113
- const child = document.createElement("help-panel");
7114
- child.translation = this.translation;
7115
- container.appendChild(child);
7116
- },
7117
- willClose: () => {
7118
- this.isHelpDialogOpen = false;
7119
- }
7120
- });
7121
- this.resizePopup();
7122
- });
7939
+ width: 560
7940
+ },
7941
+ (container) => {
7942
+ mountComponentInSwal("lib-e2e-help-panel", container, {
7943
+ translation: this.translation
7944
+ });
7945
+ }
7946
+ );
7123
7947
  }
7124
7948
  showAdvancedEditorDialog(testId) {
7125
7949
  this.toggleModal("isAdvancedEditorDialogOpen", () => {
7126
- import_sweetalert2.default.fire({
7950
+ import_sweetalert22.default.fire({
7127
7951
  title: this.translation.translate("MAIN_FRAME.SHOW_ADVANCED_EDITOR"),
7128
7952
  html: '<div id="advanced-editor-modal-content" style="flex:1;min-height:0;display:flex;flex-direction:column;padding:0"></div>',
7129
7953
  showCloseButton: true,
@@ -7134,42 +7958,33 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7134
7958
  didOpen: () => {
7135
7959
  makeSwalDraggable();
7136
7960
  setSwal2DataCyAttribute();
7137
- const popup = import_sweetalert2.default.getPopup();
7138
- if (popup) {
7139
- popup.style.height = "600px";
7140
- const htmlContainer = popup.querySelector(".swal2-html-container");
7141
- if (htmlContainer) {
7142
- htmlContainer.style.flex = "1";
7143
- htmlContainer.style.minHeight = "0";
7144
- htmlContainer.style.overflow = "hidden";
7145
- htmlContainer.style.padding = "0";
7146
- htmlContainer.style.margin = "0";
7147
- htmlContainer.style.display = "flex";
7148
- htmlContainer.style.flexDirection = "column";
7149
- }
7150
- }
7961
+ ensurePopupDimensions(import_sweetalert22.default.getPopup(), 600);
7151
7962
  const container = document.getElementById("advanced-editor-modal-content");
7152
7963
  if (!container) return;
7153
- const child = document.createElement("advanced-test-editor");
7154
- child.persistence = this.persistence;
7155
- child.translation = this.translation;
7156
- if (testId !== void 0) child.testId = testId;
7157
- container.appendChild(child);
7158
- child.addEventListener("selectorstrategychange", (e) => {
7159
- this.recording.selectorStrategy = e.detail;
7160
- });
7161
- child.addEventListener("closemodal", () => import_sweetalert2.default.close());
7162
- child.addEventListener("openfileeditor", (e) => {
7163
- import_sweetalert2.default.close();
7164
- setTimeout(() => this.showFileEditorDialog(
7165
- e.detail.handle,
7166
- e.detail.content,
7167
- e.detail.fileName,
7168
- e.detail.testId,
7169
- e.detail.itBlock,
7170
- e.detail.interceptorsBlock,
7171
- e.detail.notes
7172
- ), 150);
7964
+ const child = mountComponentInSwal("lib-e2e-advanced-test-editor", container, {
7965
+ persistence: this.persistence,
7966
+ translation: this.translation,
7967
+ ...testId !== void 0 ? { testId } : {}
7968
+ }, [
7969
+ { name: "selectorstrategychange", handler: (e) => {
7970
+ this.recording.selectorStrategy = e.detail;
7971
+ } },
7972
+ { name: "closemodal", handler: () => import_sweetalert22.default.close() },
7973
+ { name: "openfileeditor", handler: (e) => {
7974
+ import_sweetalert22.default.close();
7975
+ setTimeout(() => this.showFileEditorDialog(
7976
+ e.detail.handle,
7977
+ e.detail.content,
7978
+ e.detail.fileName,
7979
+ e.detail.testId,
7980
+ e.detail.itBlock,
7981
+ e.detail.interceptorsBlock,
7982
+ e.detail.notes
7983
+ ), 150);
7984
+ } }
7985
+ ]);
7986
+ void this.persistence.getLoginSetup().then((cfg) => {
7987
+ child.loginSetupConfig = cfg;
7173
7988
  });
7174
7989
  },
7175
7990
  willClose: () => {
@@ -7177,13 +7992,13 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7177
7992
  }
7178
7993
  });
7179
7994
  setTimeout(() => {
7180
- const popup = import_sweetalert2.default.getPopup();
7995
+ const popup = import_sweetalert22.default.getPopup();
7181
7996
  if (popup) makeModalResizable(popup, { minWidth: 400, minHeight: 0 });
7182
7997
  }, 0);
7183
7998
  });
7184
7999
  }
7185
8000
  showFileEditorDialog(handle, content, fileName, testId, itBlock = "", interceptorsBlock = "", notes = "") {
7186
- import_sweetalert2.default.fire({
8001
+ import_sweetalert22.default.fire({
7187
8002
  title: this.translation.translate("RECORDER.FILE_EDITOR_TITLE"),
7188
8003
  html: '<div id="file-editor-modal-content" style="flex:1;min-height:0;display:flex;flex-direction:column;padding:0"></div>',
7189
8004
  showCloseButton: false,
@@ -7194,47 +8009,35 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7194
8009
  didOpen: () => {
7195
8010
  makeSwalDraggable();
7196
8011
  setSwal2DataCyAttribute();
7197
- const popup = import_sweetalert2.default.getPopup();
7198
- if (popup) {
7199
- popup.style.height = "640px";
7200
- const htmlContainer = popup.querySelector(".swal2-html-container");
7201
- if (htmlContainer) {
7202
- htmlContainer.style.flex = "1";
7203
- htmlContainer.style.minHeight = "0";
7204
- htmlContainer.style.overflow = "hidden";
7205
- htmlContainer.style.padding = "0";
7206
- htmlContainer.style.margin = "0";
7207
- htmlContainer.style.display = "flex";
7208
- htmlContainer.style.flexDirection = "column";
7209
- }
7210
- }
8012
+ ensurePopupDimensions(import_sweetalert22.default.getPopup(), 640);
7211
8013
  const container = document.getElementById("file-editor-modal-content");
7212
8014
  if (!container) return;
7213
- const child = document.createElement("file-preview");
7214
- child.translation = this.translation;
7215
- child.fileContent = content;
7216
- child.fileName = fileName;
7217
- child.closeLabel = this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR");
7218
- child.itBlock = itBlock;
7219
- child.interceptorsBlock = interceptorsBlock;
7220
- child.notes = notes;
7221
- container.appendChild(child);
7222
- child.addEventListener("close", () => {
7223
- import_sweetalert2.default.close();
7224
- setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
7225
- });
7226
- child.addEventListener("save", async (e) => {
7227
- try {
7228
- const writable = await handle.createWritable();
7229
- await writable.write(e.detail);
7230
- await writable.close();
7231
- showToast(this.translation.translate("RECORDER.FILE_SAVED_TOAST"));
7232
- } catch {
7233
- showToast(this.translation.translate("RECORDER.FILE_SAVE_ERROR_TOAST"), false);
7234
- }
7235
- import_sweetalert2.default.close();
7236
- setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
7237
- });
8015
+ mountComponentInSwal("lib-e2e-file-preview", container, {
8016
+ translation: this.translation,
8017
+ fileContent: content,
8018
+ fileName,
8019
+ closeLabel: this.translation.translate("FILE_PREVIEW.BACK_TO_EDITOR"),
8020
+ itBlock,
8021
+ interceptorsBlock,
8022
+ notes
8023
+ }, [
8024
+ { name: "close", handler: () => {
8025
+ import_sweetalert22.default.close();
8026
+ setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
8027
+ } },
8028
+ { name: "save", handler: async (e) => {
8029
+ try {
8030
+ const writable = await handle.createWritable();
8031
+ await writable.write(e.detail);
8032
+ await writable.close();
8033
+ showToast(this.translation.translate("RECORDER.FILE_SAVED_TOAST"));
8034
+ } catch {
8035
+ showToast(this.translation.translate("RECORDER.FILE_SAVE_ERROR_TOAST"), false);
8036
+ }
8037
+ import_sweetalert22.default.close();
8038
+ setTimeout(() => this.showAdvancedEditorDialog(testId), 150);
8039
+ } }
8040
+ ]);
7238
8041
  }
7239
8042
  });
7240
8043
  this.resizePopup();
@@ -7243,6 +8046,14 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7243
8046
  async onSaveTest(description, tags = [], notes = "", ticketId = "") {
7244
8047
  if (!description) return;
7245
8048
  await this.persistence.insertTest(description, this.cypressCommands, this.interceptors, tags, notes, ticketId || void 0);
8049
+ const uploadedFiles = this.recording.getUploadedFilesSnapshot();
8050
+ if (uploadedFiles.length > 0) {
8051
+ const filenames = uploadedFiles.map((f) => f.filename).join(", ");
8052
+ showToast(
8053
+ this.translation.translate("RECORDER.FILE_UPLOAD_MANUAL_TOAST").replace("{files}", filenames),
8054
+ false
8055
+ );
8056
+ }
7246
8057
  this.recording.clearCommands();
7247
8058
  this.clearRecordingHistory();
7248
8059
  this.cypressCommands = [];
@@ -7264,6 +8075,29 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7264
8075
  showToast(this.translation.translate("RECORDER.FIXTURES_NO_FOLDER_TOAST"), false);
7265
8076
  }
7266
8077
  }
8078
+ const uploadedFiles = this.recording.getUploadedFilesSnapshot();
8079
+ if (uploadedFiles.length > 0) {
8080
+ const filenames = uploadedFiles.map((f) => f.filename).join(", ");
8081
+ const hasAccess = await this.persistence.hasDirectoryAccess();
8082
+ if (hasAccess) {
8083
+ try {
8084
+ for (const uf of uploadedFiles) {
8085
+ await this.persistence.writeUploadedFile(uf.filename, uf.bytes);
8086
+ }
8087
+ showToast(this.translation.translate("RECORDER.FILE_UPLOAD_COPIED_TOAST").replace("{files}", filenames));
8088
+ } catch {
8089
+ showToast(
8090
+ this.translation.translate("RECORDER.FILE_UPLOAD_COPY_ERROR_TOAST").replace("{files}", filenames),
8091
+ false
8092
+ );
8093
+ }
8094
+ } else {
8095
+ showToast(
8096
+ this.translation.translate("RECORDER.FILE_UPLOAD_MANUAL_TOAST").replace("{files}", filenames),
8097
+ false
8098
+ );
8099
+ }
8100
+ }
7267
8101
  const id = await this.persistence.insertTest(description, finalCommands, finalInterceptors, tags, notes, ticketId || void 0);
7268
8102
  this.recording.clearCommands();
7269
8103
  this.clearRecordingHistory();
@@ -7274,7 +8108,7 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7274
8108
  // ── helpers ───────────────────────────────────────────────────────────────
7275
8109
  toggleModal(flag, openFn) {
7276
8110
  if (this[flag]) {
7277
- import_sweetalert2.default.close();
8111
+ import_sweetalert22.default.close();
7278
8112
  this[flag] = false;
7279
8113
  return;
7280
8114
  }
@@ -7283,10 +8117,32 @@ cypress/ <span style="color:#484f58">${this.translation.translate("RECOR
7283
8117
  }
7284
8118
  resizePopup() {
7285
8119
  setTimeout(() => {
7286
- const popup = import_sweetalert2.default.getPopup();
8120
+ const popup = import_sweetalert22.default.getPopup();
7287
8121
  if (popup) makeModalResizable(popup, { minWidth: 400, minHeight: 200 });
7288
8122
  }, 0);
7289
8123
  }
8124
+ openSwalDialog(flag, contentId, swalOptions, onMount, extra) {
8125
+ this.toggleModal(flag, () => {
8126
+ import_sweetalert22.default.fire({
8127
+ showConfirmButton: false,
8128
+ color: "#e6edf3",
8129
+ ...swalOptions,
8130
+ didOpen: () => {
8131
+ makeSwalDraggable();
8132
+ setSwal2DataCyAttribute();
8133
+ extra?.onDidOpenPreamble?.();
8134
+ const container = document.getElementById(contentId);
8135
+ if (!container) return;
8136
+ onMount(container);
8137
+ },
8138
+ willClose: () => {
8139
+ this[flag] = false;
8140
+ extra?.onWillClose?.();
8141
+ }
8142
+ });
8143
+ if (!extra?.skipResize) this.resizePopup();
8144
+ });
8145
+ }
7290
8146
  render() {
7291
8147
  const rec = this.isRecording;
7292
8148
  const paused = this.isPaused;
@@ -7315,17 +8171,14 @@ if (!customElements.get("lib-e2e-recorder")) {
7315
8171
  }
7316
8172
 
7317
8173
  // src/index.ts
7318
- var VERSION = "0.9.0";
8174
+ var VERSION = "1.0.0";
7319
8175
  // Annotate the CommonJS export names for ESM import in node:
7320
8176
  0 && (module.exports = {
7321
- ACTIVE_SESSION_BREADCRUMB_KEY,
7322
8177
  AdvancedTestEditorElement,
7323
8178
  AdvancedTestTransformationService,
7324
8179
  ConfigurationElement,
7325
8180
  DB_SCHEMA,
7326
8181
  DB_STORE_NAMES,
7327
- DEFAULT_RESUME_TTL_MINUTES,
7328
- DRAG_THRESHOLD,
7329
8182
  FilePreviewElement,
7330
8183
  HelpPanelElement,
7331
8184
  HttpMonitor,
@@ -7334,24 +8187,16 @@ var VERSION = "0.9.0";
7334
8187
  LOCALE_BY_LANG,
7335
8188
  LibE2eRecorderElement,
7336
8189
  PersistenceService,
7337
- RESUME_TTL_CONFIG_KEY,
7338
8190
  RecordingService,
7339
8191
  SCROLLBAR_STYLES,
7340
8192
  SUPPORTED_LANGS,
7341
8193
  SaveTestElement,
7342
8194
  SelectorPickerElement,
7343
- Subject,
7344
- TOGGLE_MARGIN,
7345
8195
  TestEditorElement,
7346
8196
  TestPrevisualizerElement,
7347
8197
  TransformationService,
7348
8198
  TranslationService,
7349
8199
  VERSION,
7350
- advancedTestTransformationService,
7351
- boxOffsetForDirection,
7352
- boxTopLeftFor,
7353
- clampTogglePosition,
7354
- defaultTogglePosition,
7355
8200
  generateAlias,
7356
8201
  inferAssertionCommand,
7357
8202
  injectStyles,
@@ -7360,13 +8205,7 @@ var VERSION = "0.9.0";
7360
8205
  localeForLang,
7361
8206
  makeModalResizable,
7362
8207
  makeSwalDraggable,
7363
- makeSwalDraggableByContentId,
7364
- persistenceService,
7365
- resolveExpandDirection,
7366
8208
  selectTestsForExport,
7367
- setSwal2DataCyAttribute,
7368
- showToast,
7369
- transformationService,
7370
- translationService
8209
+ showToast
7371
8210
  });
7372
8211
  //# sourceMappingURL=index.cjs.map