@immense/vue-pom-generator 1.0.58 → 1.0.60

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.
Files changed (58) hide show
  1. package/README.md +36 -25
  2. package/RELEASE_NOTES.md +29 -31
  3. package/class-generation/base-page.ts +49 -25
  4. package/class-generation/index.ts +243 -333
  5. package/class-generation/playwright-types.ts +1 -1
  6. package/click-instrumentation.ts +0 -4
  7. package/dist/class-generation/base-page.d.ts +8 -4
  8. package/dist/class-generation/base-page.d.ts.map +1 -1
  9. package/dist/class-generation/index.d.ts +2 -0
  10. package/dist/class-generation/index.d.ts.map +1 -1
  11. package/dist/class-generation/playwright-types.d.ts +1 -1
  12. package/dist/class-generation/playwright-types.d.ts.map +1 -1
  13. package/dist/click-instrumentation.d.ts +0 -1
  14. package/dist/click-instrumentation.d.ts.map +1 -1
  15. package/dist/index.cjs +1527 -1064
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1529 -1066
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/manifest-generator.d.ts +35 -1
  20. package/dist/manifest-generator.d.ts.map +1 -1
  21. package/dist/method-generation.d.ts +4 -2
  22. package/dist/method-generation.d.ts.map +1 -1
  23. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  24. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  25. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  26. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  27. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  28. package/dist/plugin/support/build-plugin.d.ts +2 -29
  29. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  31. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  32. package/dist/plugin/support/virtual-modules.d.ts +3 -1
  33. package/dist/plugin/support/virtual-modules.d.ts.map +1 -1
  34. package/dist/plugin/support-plugins.d.ts +4 -33
  35. package/dist/plugin/support-plugins.d.ts.map +1 -1
  36. package/dist/plugin/types.d.ts +6 -23
  37. package/dist/plugin/types.d.ts.map +1 -1
  38. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  39. package/dist/pom-discoverability.d.ts +10 -0
  40. package/dist/pom-discoverability.d.ts.map +1 -0
  41. package/dist/pom-params.d.ts +40 -0
  42. package/dist/pom-params.d.ts.map +1 -0
  43. package/dist/pom-patterns.d.ts +31 -0
  44. package/dist/pom-patterns.d.ts.map +1 -0
  45. package/dist/routing/to-directive.d.ts +21 -0
  46. package/dist/routing/to-directive.d.ts.map +1 -1
  47. package/dist/tests/base-page.test.d.ts +2 -0
  48. package/dist/tests/base-page.test.d.ts.map +1 -0
  49. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts +7 -4
  50. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts.map +1 -1
  51. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  52. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  53. package/dist/transform.d.ts +0 -1
  54. package/dist/transform.d.ts.map +1 -1
  55. package/dist/utils.d.ts +129 -63
  56. package/dist/utils.d.ts.map +1 -1
  57. package/package.json +6 -4
  58. package/sequence-diagram.md +6 -6
package/dist/index.cjs CHANGED
@@ -245,6 +245,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
245
245
  const nuxtOptions = await loadNuxtConfig({ cwd });
246
246
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
247
247
  }
248
+ function resolveGenerationSupportOptions(options) {
249
+ return {
250
+ outDir: (options.outDir ?? "tests/playwright/__generated__").trim(),
251
+ emitLanguages: options.emitLanguages?.length ? options.emitLanguages : ["ts"],
252
+ typescriptOutputStructure: options.typescriptOutputStructure ?? "aggregated",
253
+ csharp: options.csharp,
254
+ generateFixtures: options.generateFixtures,
255
+ customPomAttachments: options.customPomAttachments ?? [],
256
+ customPomDir: options.customPomDir ?? "tests/playwright/pom/custom",
257
+ requireCustomPomDir: options.requireCustomPomDir ?? false,
258
+ customPomImportAliases: options.customPomImportAliases,
259
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior ?? "error",
260
+ nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
261
+ existingIdBehavior: options.existingIdBehavior ?? "error",
262
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
263
+ routerAwarePoms: options.routerAwarePoms ?? false,
264
+ routerEntry: options.routerEntry,
265
+ routerType: options.routerType ?? "vue-router",
266
+ routerModuleShims: options.routerModuleShims
267
+ };
268
+ }
269
+ function resolveInjectionSupportOptions(options) {
270
+ const isNuxt = options.isNuxt ?? false;
271
+ return {
272
+ pageDirs: isNuxt ? ["app/pages"] : [options.viewsDir ?? "src/views"],
273
+ componentDirs: isNuxt ? ["app/components"] : options.componentDirs ?? ["src/components"],
274
+ layoutDirs: isNuxt ? ["app/layouts"] : options.layoutDirs ?? ["src/layouts"],
275
+ wrapperSearchRoots: isNuxt ? [] : options.wrapperSearchRoots ?? [],
276
+ nativeWrappers: options.nativeWrappers ?? {},
277
+ excludedComponents: options.excludedComponents ?? [],
278
+ existingIdBehavior: options.existingIdBehavior ?? "error",
279
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid"
280
+ };
281
+ }
282
+ function applyNuxtDiscoveryToInjectionOptions(options, discovery) {
283
+ return {
284
+ ...options,
285
+ pageDirs: discovery.pageDirs.length ? discovery.pageDirs : [path.resolve(discovery.srcDir, "pages")],
286
+ componentDirs: discovery.componentDirs,
287
+ layoutDirs: discovery.layoutDirs,
288
+ wrapperSearchRoots: discovery.wrapperSearchRoots
289
+ };
290
+ }
248
291
  function createTypeScriptProject() {
249
292
  return new tsMorph.Project({
250
293
  useInMemoryFileSystem: true,
@@ -336,16 +379,7 @@ function createClassConstructor(constructorDeclaration) {
336
379
  ...constructorDeclaration
337
380
  };
338
381
  }
339
- function upperFirst$1(value) {
340
- if (!value) {
341
- return value;
342
- }
343
- return value.charAt(0).toUpperCase() + value.slice(1);
344
- }
345
- function hasParam(params, name) {
346
- return Object.prototype.hasOwnProperty.call(params, name);
347
- }
348
- function splitTypeAndInitializer(typeExpression) {
382
+ function splitPomParameterTypeExpression(typeExpression) {
349
383
  const trimmed = typeExpression.trim();
350
384
  const initializerIndex = trimmed.lastIndexOf("=");
351
385
  if (initializerIndex < 0) {
@@ -356,49 +390,306 @@ function splitTypeAndInitializer(typeExpression) {
356
390
  initializer: trimmed.slice(initializerIndex + 1).trim()
357
391
  };
358
392
  }
359
- function createParameter(name, typeExpression) {
360
- const { type, initializer } = splitTypeAndInitializer(typeExpression);
393
+ function createPomParameterSpec(name, typeExpression, options = {}) {
394
+ const normalizedTypeExpression = typeExpression?.trim();
395
+ const { type, initializer } = normalizedTypeExpression ? splitPomParameterTypeExpression(normalizedTypeExpression) : { type: void 0, initializer: void 0 };
361
396
  return {
362
397
  name,
363
- type: type || void 0,
364
- initializer
398
+ typeExpression: normalizedTypeExpression,
399
+ type,
400
+ initializer: options.initializer ?? initializer,
401
+ hasQuestionToken: options.hasQuestionToken,
402
+ isRestParameter: options.isRestParameter
365
403
  };
366
404
  }
367
- function createParameters(params) {
368
- return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
405
+ function normalizePomParameters(params) {
406
+ if (!params) {
407
+ return [];
408
+ }
409
+ return params.map((param) => createPomParameterSpec(param.name, param.typeExpression ?? param.type, {
410
+ initializer: param.initializer,
411
+ hasQuestionToken: param.hasQuestionToken,
412
+ isRestParameter: param.isRestParameter
413
+ }));
369
414
  }
370
- function createInlineParameter(name, options = {}) {
415
+ function getPomParameterNames(params) {
416
+ return normalizePomParameters(params).map((param) => param.name);
417
+ }
418
+ function getPomParameter(params, name) {
419
+ return normalizePomParameters(params).find((param) => param.name === name);
420
+ }
421
+ function hasPomParameter(params, name) {
422
+ return !!getPomParameter(params, name);
423
+ }
424
+ function setPomParameter(params, name, typeExpression, options = {}) {
425
+ const nextParam = createPomParameterSpec(name, typeExpression, options);
426
+ const normalizedParams = normalizePomParameters(params);
427
+ const existingIndex = normalizedParams.findIndex((param) => param.name === name);
428
+ if (existingIndex < 0) {
429
+ return [...normalizedParams, nextParam];
430
+ }
431
+ const nextParams = normalizedParams.slice();
432
+ nextParams[existingIndex] = nextParam;
433
+ return nextParams;
434
+ }
435
+ function removePomParameter(params, name) {
436
+ return normalizePomParameters(params).filter((param) => param.name !== name);
437
+ }
438
+ function toTypeScriptPomParameterStructures(params) {
439
+ return normalizePomParameters(params).map((param) => ({
440
+ name: param.name,
441
+ type: param.type || void 0,
442
+ initializer: param.initializer,
443
+ hasQuestionToken: param.hasQuestionToken,
444
+ isRestParameter: param.isRestParameter
445
+ }));
446
+ }
447
+ function getPomParameterArgumentNames(params) {
448
+ return normalizePomParameters(params).map((param) => param.isRestParameter ? `...${param.name}` : param.name);
449
+ }
450
+ function createPomMethodSignature(parameters) {
371
451
  return {
372
- name,
373
- type: options.type,
374
- initializer: options.initializer
452
+ parameters: normalizePomParameters(parameters)
375
453
  };
376
454
  }
377
- function removeByKeySegment(value) {
378
- const idx = value.lastIndexOf("ByKey");
379
- if (idx < 0) {
380
- return value;
455
+ function pomParameterSpecEquals(left, right) {
456
+ return left.name === right.name && left.typeExpression === right.typeExpression && left.type === right.type && left.initializer === right.initializer && left.hasQuestionToken === right.hasQuestionToken && left.isRestParameter === right.isRestParameter;
457
+ }
458
+ function pomParameterListEquals(left, right) {
459
+ const leftParams = normalizePomParameters(left);
460
+ const rightParams = normalizePomParameters(right);
461
+ if (leftParams.length !== rightParams.length) {
462
+ return false;
381
463
  }
382
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
464
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
465
+ }
466
+ function pomMethodSignatureEquals(left, right) {
467
+ return pomParameterListEquals(left.parameters, right.parameters);
468
+ }
469
+ function isParameterizedPomPattern(kind) {
470
+ return kind === "parameterized";
383
471
  }
384
- function uniqueAlternates(primary, alternates) {
472
+ function getTemplateVariables(formatted) {
385
473
  const out = [];
386
474
  const seen = /* @__PURE__ */ new Set();
387
- seen.add(primary);
388
- for (const a of alternates ?? []) {
389
- if (!a) {
475
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
476
+ for (const match of matches) {
477
+ const variableName = match[1];
478
+ if (seen.has(variableName)) {
390
479
  continue;
391
480
  }
392
- if (seen.has(a)) {
481
+ seen.add(variableName);
482
+ out.push(variableName);
483
+ }
484
+ return out;
485
+ }
486
+ function createPomStringPattern(formatted, patternKind) {
487
+ return {
488
+ formatted,
489
+ patternKind,
490
+ templateVariables: getTemplateVariables(formatted)
491
+ };
492
+ }
493
+ function getPomPatternVariables(patterns, options = {}) {
494
+ const out = [];
495
+ const seen = /* @__PURE__ */ new Set();
496
+ const omitted = new Set(options.omit ?? []);
497
+ for (const pattern of patterns) {
498
+ for (const variableName of pattern.templateVariables) {
499
+ if (omitted.has(variableName) || seen.has(variableName)) {
500
+ continue;
501
+ }
502
+ seen.add(variableName);
503
+ out.push(variableName);
504
+ }
505
+ }
506
+ return out;
507
+ }
508
+ function orderPomPatternParameters(params, patterns, options = {}) {
509
+ const currentParams = normalizePomParameters(params);
510
+ const orderedParams = [];
511
+ const seen = /* @__PURE__ */ new Set();
512
+ const missingParams = [];
513
+ for (const variableName of getPomPatternVariables(patterns, options)) {
514
+ seen.add(variableName);
515
+ const existingParam = currentParams.find((param) => param.name === variableName);
516
+ if (!existingParam) {
517
+ missingParams.push(variableName);
393
518
  continue;
394
519
  }
395
- seen.add(a);
396
- out.push(a);
520
+ orderedParams.push(existingParam);
521
+ }
522
+ if (missingParams.length > 0) {
523
+ const availableParams = currentParams.map((param) => JSON.stringify(param.name)).join(", ") || "<none>";
524
+ const patternSummary = patterns.map((pattern) => JSON.stringify(pattern.formatted)).join(", ");
525
+ throw new Error(
526
+ `[vue-pom-generator] Missing selector parameter(s) ${missingParams.map((name) => JSON.stringify(name)).join(", ")} for parameterized pattern(s) ${patternSummary}. Available parameters: ${availableParams}.`
527
+ );
528
+ }
529
+ for (const param of currentParams) {
530
+ if (seen.has(param.name)) {
531
+ continue;
532
+ }
533
+ seen.add(param.name);
534
+ orderedParams.push(param);
535
+ }
536
+ return orderedParams;
537
+ }
538
+ function getIndexedPomPatternVariable(pattern) {
539
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
540
+ return null;
541
+ }
542
+ if (pattern.templateVariables.length !== 1) {
543
+ throw new Error(
544
+ `[vue-pom-generator] Parameterized locator getters require exactly one template variable; got ${pattern.templateVariables.length} in ${JSON.stringify(pattern.formatted)}.`
545
+ );
546
+ }
547
+ return pattern.templateVariables[0];
548
+ }
549
+ function hasPomPatternVariables(pattern) {
550
+ return pattern.templateVariables.length > 0;
551
+ }
552
+ function toTypeScriptPomPatternExpression(pattern) {
553
+ return isParameterizedPomPattern(pattern.patternKind) ? `\`${pattern.formatted}\`` : JSON.stringify(pattern.formatted);
554
+ }
555
+ function toCSharpPomPatternExpression(pattern) {
556
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
557
+ return JSON.stringify(pattern.formatted);
558
+ }
559
+ const inner = pattern.formatted.replace(/\$\{/g, "{");
560
+ return `$${JSON.stringify(inner)}`;
561
+ }
562
+ function bindTypeScriptPomPattern(pattern, variableName) {
563
+ const expression = toTypeScriptPomPatternExpression(pattern);
564
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
565
+ return { expression, setupStatements: [] };
566
+ }
567
+ return {
568
+ expression: variableName,
569
+ setupStatements: [`const ${variableName} = ${expression};`]
570
+ };
571
+ }
572
+ function bindCSharpPomPattern(pattern, variableName) {
573
+ const expression = toCSharpPomPatternExpression(pattern);
574
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
575
+ return { expression, setupStatements: [] };
576
+ }
577
+ return {
578
+ expression: variableName,
579
+ setupStatements: [`var ${variableName} = ${expression};`]
580
+ };
581
+ }
582
+ function pomStringPatternEquals(left, right) {
583
+ return left.formatted === right.formatted && left.patternKind === right.patternKind;
584
+ }
585
+ function uniquePomStringPatterns(primary, alternates) {
586
+ const out = [];
587
+ const seen = /* @__PURE__ */ new Set();
588
+ const add = (pattern) => {
589
+ const key = JSON.stringify(pattern);
590
+ if (seen.has(key)) {
591
+ return;
592
+ }
593
+ seen.add(key);
594
+ out.push(pattern);
595
+ };
596
+ add(primary);
597
+ for (const alternate of alternates ?? []) {
598
+ add(alternate);
397
599
  }
398
600
  return out;
399
601
  }
400
- function testIdExpression(formattedDataTestId) {
401
- return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
602
+ function splitDiscoverabilityWords(value) {
603
+ const normalized = value.replace(/ByKey/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[._-]+/g, " ").trim();
604
+ if (!normalized) {
605
+ return [];
606
+ }
607
+ return normalized.split(/\s+/).map((word) => word.toLowerCase()).filter(Boolean);
608
+ }
609
+ function joinDiscoverabilityWords(words) {
610
+ return words.join(" ").replace(/\s+/g, " ").trim();
611
+ }
612
+ function toSentenceCase(value) {
613
+ return value.charAt(0).toUpperCase() + value.slice(1);
614
+ }
615
+ function stripComponentKindSuffix(componentName) {
616
+ for (const suffix of ["Page", "Component", "Layout"]) {
617
+ if (componentName.endsWith(suffix) && componentName.length > suffix.length) {
618
+ return componentName.slice(0, -suffix.length);
619
+ }
620
+ }
621
+ return componentName;
622
+ }
623
+ function removeLeadingWords(words, prefixWords) {
624
+ if (!prefixWords.length || words.length < prefixWords.length) {
625
+ return [...words];
626
+ }
627
+ for (let i = 0; i < prefixWords.length; i++) {
628
+ if (words[i] !== prefixWords[i]) {
629
+ return [...words];
630
+ }
631
+ }
632
+ return words.slice(prefixWords.length);
633
+ }
634
+ function removeTrailingRoleWord(words, roleWord) {
635
+ if (!words.length || words[words.length - 1] !== roleWord) {
636
+ return [...words];
637
+ }
638
+ return words.slice(0, -1);
639
+ }
640
+ function humanizePomMethodName(methodName) {
641
+ return joinDiscoverabilityWords(splitDiscoverabilityWords(methodName));
642
+ }
643
+ function stripPomActionPrefix(actionName) {
644
+ for (const prefix of ["click", "select", "type", "goTo"]) {
645
+ if (actionName.startsWith(prefix) && actionName.length > prefix.length) {
646
+ return actionName.slice(prefix.length);
647
+ }
648
+ }
649
+ return actionName;
650
+ }
651
+ function normalizePomRoleLabel(nativeRole) {
652
+ if (nativeRole === "vselect") {
653
+ return "select";
654
+ }
655
+ return nativeRole || "element";
656
+ }
657
+ function buildPomLocatorDescription(args) {
658
+ const componentWords = splitDiscoverabilityWords(args.componentName ? stripComponentKindSuffix(args.componentName) : "");
659
+ const roleWord = normalizePomRoleLabel(args.nativeRole).toLowerCase();
660
+ const semanticWords = removeLeadingWords(
661
+ removeTrailingRoleWord(splitDiscoverabilityWords(args.methodName), roleWord),
662
+ componentWords
663
+ );
664
+ const phrase = joinDiscoverabilityWords([
665
+ ...componentWords,
666
+ ...semanticWords,
667
+ roleWord
668
+ ]);
669
+ return toSentenceCase(phrase || "Generated element");
670
+ }
671
+ function upperFirst$1(value) {
672
+ if (!value) {
673
+ return value;
674
+ }
675
+ return value.charAt(0).toUpperCase() + value.slice(1);
676
+ }
677
+ function createParameters(params) {
678
+ return toTypeScriptPomParameterStructures(params);
679
+ }
680
+ function createInlineParameter(name, options = {}) {
681
+ return {
682
+ name,
683
+ type: options.type,
684
+ initializer: options.initializer
685
+ };
686
+ }
687
+ function removeByKeySegment$1(value) {
688
+ const idx = value.lastIndexOf("ByKey");
689
+ if (idx < 0) {
690
+ return value;
691
+ }
692
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
402
693
  }
403
694
  function createAsyncMethod(name, parameters, statements) {
404
695
  return createClassMethod({
@@ -408,17 +699,25 @@ function createAsyncMethod(name, parameters, statements) {
408
699
  statements
409
700
  });
410
701
  }
411
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
702
+ function generateClickMethod(componentName, methodName, selector, alternateSelectors, parameters) {
412
703
  const name = `click${methodName}`;
413
704
  const noWaitName = `${name}NoWait`;
414
- const baseParameters = createParameters(params);
415
- const argsForForward = Object.keys(params).join(", ");
416
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
705
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
706
+ componentName,
707
+ methodName,
708
+ nativeRole: "button"
709
+ }));
710
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
711
+ const hasSelectorVariables = hasPomPatternVariables(selector);
712
+ const baseParameters = createParameters(selectorParams);
713
+ const argsForForward = getPomParameterNames(selectorParams).join(", ");
714
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
715
+ const primaryTestIdExpr = toTypeScriptPomPatternExpression(selector);
417
716
  if (alternates.length > 0) {
418
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
717
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
419
718
  const clickMethod = createAsyncMethod(
420
719
  name,
421
- hasParam(params, "key") ? [
720
+ hasSelectorVariables ? [
422
721
  ...baseParameters,
423
722
  createInlineParameter("wait", { type: "boolean", initializer: "true" }),
424
723
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
@@ -430,7 +729,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
430
729
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
431
730
  writer.writeLine("let lastError: unknown;");
432
731
  writer.write("for (const testId of candidates) ").block(() => {
433
- writer.writeLine("const locator = this.locatorByTestId(testId);");
732
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
434
733
  writer.write("try ").block(() => {
435
734
  writer.write("if (await locator.count()) ").block(() => {
436
735
  writer.writeLine("await this.clickLocator(locator, annotationText, wait);");
@@ -447,14 +746,14 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
447
746
  const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
448
747
  const noWaitMethod = createAsyncMethod(
449
748
  noWaitName,
450
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
749
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
451
750
  (writer) => {
452
751
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
453
752
  }
454
753
  );
455
754
  return [clickMethod, noWaitMethod];
456
755
  }
457
- if (hasParam(params, "key")) {
756
+ if (hasSelectorVariables) {
458
757
  return [
459
758
  createAsyncMethod(
460
759
  name,
@@ -464,7 +763,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
464
763
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
465
764
  ],
466
765
  (writer) => {
467
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, annotationText, wait);`);
766
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait, ${locatorDescription});`);
468
767
  }
469
768
  ),
470
769
  createAsyncMethod(
@@ -484,7 +783,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
484
783
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
485
784
  ],
486
785
  (writer) => {
487
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", annotationText, wait);`);
786
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait, ${locatorDescription});`);
488
787
  }
489
788
  ),
490
789
  createAsyncMethod(
@@ -496,66 +795,76 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
496
795
  )
497
796
  ];
498
797
  }
499
- function generateRadioMethod(methodName, formattedDataTestId) {
798
+ function generateRadioMethod(componentName, methodName, selector, parameters) {
500
799
  const name = `select${methodName}`;
501
- const hasKey = formattedDataTestId.includes("${key}");
502
- const parameters = hasKey ? [
503
- createInlineParameter("key", { type: "string" }),
504
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
505
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
506
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
800
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
801
+ componentName,
802
+ methodName,
803
+ nativeRole: "radio"
804
+ }));
805
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
806
+ const methodParameters = createParameters(selectorParams);
807
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
507
808
  return [
508
- createAsyncMethod(name, parameters, (writer) => {
509
- writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
809
+ createAsyncMethod(name, methodParameters, (writer) => {
810
+ writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText, true, ${locatorDescription});`);
510
811
  })
511
812
  ];
512
813
  }
513
- function generateSelectMethod(methodName, formattedDataTestId) {
814
+ function generateSelectMethod(componentName, methodName, selector, parameters) {
514
815
  const name = `select${methodName}`;
515
- const needsKey = formattedDataTestId.includes("${key}");
516
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
816
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
817
+ componentName,
818
+ methodName,
819
+ nativeRole: "select"
820
+ }));
821
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
822
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
517
823
  return [
518
824
  createAsyncMethod(
519
825
  name,
520
- [
521
- createInlineParameter("value", { type: "string" }),
522
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
523
- ],
826
+ createParameters(selectorParams),
524
827
  (writer) => {
525
- writer.writeLine(`const selector = ${selectorExpr};`);
526
- writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
527
- writer.writeLine("await this.page.selectOption(selector, value);");
828
+ writer.writeLine(`const testId = ${testIdExpr};`);
829
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
830
+ writer.writeLine("await this.animateCursorToElement(locator, false, 500, annotationText);");
831
+ writer.writeLine("await locator.selectOption(value);");
528
832
  }
529
833
  )
530
834
  ];
531
835
  }
532
- function generateVSelectMethod(methodName, formattedDataTestId) {
836
+ function generateVSelectMethod(componentName, methodName, selector, parameters) {
533
837
  const name = `select${methodName}`;
838
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
839
+ componentName,
840
+ methodName,
841
+ nativeRole: "vselect"
842
+ }));
843
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
534
844
  return [
535
845
  createAsyncMethod(
536
846
  name,
537
- [
538
- createInlineParameter("value", { type: "string" }),
539
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
540
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
541
- ],
847
+ createParameters(selectorParams),
542
848
  (writer) => {
543
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
849
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText, ${locatorDescription});`);
544
850
  }
545
851
  )
546
852
  ];
547
853
  }
548
- function generateTypeMethod(methodName, formattedDataTestId) {
854
+ function generateTypeMethod(componentName, methodName, selector, parameters) {
549
855
  const name = `type${methodName}`;
856
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
857
+ componentName,
858
+ methodName,
859
+ nativeRole: "input"
860
+ }));
861
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
550
862
  return [
551
863
  createAsyncMethod(
552
864
  name,
553
- [
554
- createInlineParameter("text", { type: "string" }),
555
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
556
- ],
865
+ createParameters(selectorParams),
557
866
  (writer) => {
558
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
867
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText, ${locatorDescription});`);
559
868
  }
560
869
  )
561
870
  ];
@@ -570,62 +879,74 @@ function isAllDigits(value) {
570
879
  }
571
880
  return true;
572
881
  }
573
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
882
+ function generateGetElementByDataTestId(componentName, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
883
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
884
+ componentName,
885
+ methodName,
886
+ nativeRole
887
+ }));
574
888
  const roleSuffix = upperFirst$1(nativeRole || "Element");
575
889
  const baseName = upperFirst$1(methodName);
576
890
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
577
- const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
578
- const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
579
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
580
- if (needsKey) {
581
- const keyType = params.key || "string";
582
- const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
891
+ const hasRoleSuffix2 = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
892
+ const propertyName = hasRoleSuffix2 ? `${baseName}` : `${baseName}${roleSuffix}`;
893
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
894
+ const indexedVariable = getIndexedPomPatternVariable(selector);
895
+ if (indexedVariable) {
896
+ const keyType = getPomParameter(selectorParams, indexedVariable)?.typeExpression || "string";
897
+ const keyedPropertyName = getterNameOverride ?? removeByKeySegment$1(propertyName);
583
898
  return [
584
899
  createClassGetter({
585
900
  name: keyedPropertyName,
586
901
  statements: [
587
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
902
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription}));`
588
903
  ]
589
904
  })
590
905
  ];
591
906
  }
592
907
  const finalPropertyName = getterNameOverride ?? propertyName;
593
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
908
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
594
909
  if (alternates.length > 0) {
595
- const all = [formattedDataTestId, ...alternates];
596
- const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
910
+ const all = [selector, ...alternates];
911
+ const locatorExpr = all.map((id) => `this.locatorByTestId(${toTypeScriptPomPatternExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
597
912
  return [
598
913
  createClassGetter({
599
914
  name: finalPropertyName,
600
- statements: [`return ${locatorExpr};`]
915
+ statements: [`return this.describeLocator(${locatorExpr}, ${locatorDescription});`]
601
916
  })
602
917
  ];
603
918
  }
604
919
  return [
605
920
  createClassGetter({
606
921
  name: finalPropertyName,
607
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
922
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription});`]
608
923
  })
609
924
  ];
610
925
  }
611
926
  function generateNavigationMethod(args) {
612
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
927
+ const { componentName, targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
613
928
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
614
- const parameters = createParameters(params);
615
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
616
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
929
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
930
+ componentName,
931
+ methodName: baseMethodName,
932
+ nativeRole: "button"
933
+ }));
934
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
935
+ const methodParameters = createParameters(selectorParams);
936
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
937
+ const candidatesExpr = [toTypeScriptPomPatternExpression(selector), ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
617
938
  if (alternates.length > 0) {
618
939
  return [
619
940
  createClassMethod({
620
941
  name: methodName,
621
- parameters,
942
+ parameters: methodParameters,
622
943
  returnType: `Fluent<${target}>`,
623
944
  statements: (writer) => {
624
945
  writer.write("return this.fluent(async () => ").block(() => {
625
946
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
626
947
  writer.writeLine("let lastError: unknown;");
627
948
  writer.write("for (const testId of candidates) ").block(() => {
628
- writer.writeLine("const locator = this.locatorByTestId(testId);");
949
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
629
950
  writer.write("try ").block(() => {
630
951
  writer.write("if (await locator.count()) ").block(() => {
631
952
  writer.writeLine("await this.clickLocator(locator);");
@@ -646,11 +967,12 @@ function generateNavigationMethod(args) {
646
967
  return [
647
968
  createClassMethod({
648
969
  name: methodName,
649
- parameters,
970
+ parameters: methodParameters,
650
971
  returnType: `Fluent<${target}>`,
651
972
  statements: (writer) => {
652
973
  writer.write("return this.fluent(async () => ").block(() => {
653
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
974
+ writer.writeLine(`const locator = this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription});`);
975
+ writer.writeLine("await this.clickLocator(locator);");
654
976
  writer.writeLine(`return new ${target}(this.page);`);
655
977
  });
656
978
  writer.writeLine(");");
@@ -658,41 +980,43 @@ function generateNavigationMethod(args) {
658
980
  })
659
981
  ];
660
982
  }
661
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
983
+ function generateViewObjectModelMembers(componentName, targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
662
984
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
663
985
  const members = generateGetElementByDataTestId(
986
+ componentName,
664
987
  baseMethodName,
665
988
  nativeRole,
666
- formattedDataTestId,
667
- alternateFormattedDataTestIds,
989
+ selector,
990
+ alternateSelectors,
668
991
  getterNameOverride,
669
- params
992
+ parameters
670
993
  );
671
994
  if (targetPageObjectModelClass) {
672
995
  return [
673
996
  ...members,
674
997
  ...generateNavigationMethod({
998
+ componentName,
675
999
  targetPageObjectModelClass,
676
1000
  baseMethodName,
677
- formattedDataTestId,
678
- alternateFormattedDataTestIds,
679
- params
1001
+ selector,
1002
+ alternateSelectors,
1003
+ parameters
680
1004
  })
681
1005
  ];
682
1006
  }
683
1007
  if (nativeRole === "select") {
684
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
1008
+ return [...members, ...generateSelectMethod(componentName, baseMethodName, selector, parameters)];
685
1009
  }
686
1010
  if (nativeRole === "vselect") {
687
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
1011
+ return [...members, ...generateVSelectMethod(componentName, baseMethodName, selector, parameters)];
688
1012
  }
689
1013
  if (nativeRole === "input") {
690
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
1014
+ return [...members, ...generateTypeMethod(componentName, baseMethodName, selector, parameters)];
691
1015
  }
692
1016
  if (nativeRole === "radio") {
693
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
1017
+ return [...members, ...generateRadioMethod(componentName, baseMethodName || "Radio", selector, parameters)];
694
1018
  }
695
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
1019
+ return [...members, ...generateClickMethod(componentName, baseMethodName, selector, alternateSelectors, parameters)];
696
1020
  }
697
1021
  function isSimpleExpressionNode(value) {
698
1022
  return value !== null && "type" in value && value.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION;
@@ -732,43 +1056,72 @@ function buildPlaceholderParams(keys) {
732
1056
  params[k] = "__placeholder__";
733
1057
  return params;
734
1058
  }
735
- function getRouteLocationLikeFromToDirective(toDirective) {
736
- if (!toDirective.exp)
737
- return null;
738
- const exp = toDirective.exp;
739
- const rawSource = compilerCore.stringifyExpression(exp).trim();
1059
+ const isNodeType = (node, type) => {
1060
+ return node !== null && node.type === type;
1061
+ };
1062
+ const isStringLiteralNode = (node) => {
1063
+ return isNodeType(node, "StringLiteral") && typeof node.value === "string";
1064
+ };
1065
+ const isIdentifierNode = (node) => {
1066
+ return isNodeType(node, "Identifier") && typeof node.name === "string";
1067
+ };
1068
+ const isObjectPropertyNode = (node) => {
1069
+ if (!isNodeType(node, "ObjectProperty"))
1070
+ return false;
1071
+ const n = node;
1072
+ return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
1073
+ };
1074
+ const isObjectExpressionNode = (node) => {
1075
+ if (!isNodeType(node, "ObjectExpression"))
1076
+ return false;
1077
+ const n = node;
1078
+ return Array.isArray(n.properties);
1079
+ };
1080
+ function materializeResolvedRouteTarget(target, paramKeys) {
1081
+ if (typeof target === "string")
1082
+ return target;
1083
+ if (!paramKeys.length)
1084
+ return target;
1085
+ return {
1086
+ ...target,
1087
+ params: buildPlaceholderParams(paramKeys)
1088
+ };
1089
+ }
1090
+ function analyzeToDirectiveTarget(toDirective) {
1091
+ if (!toDirective.exp) {
1092
+ return {
1093
+ kind: "unsupported",
1094
+ rawSource: null,
1095
+ reason: "missing-expression"
1096
+ };
1097
+ }
1098
+ const rawSource = compilerCore.stringifyExpression(toDirective.exp).trim();
740
1099
  let expr;
741
1100
  try {
742
1101
  expr = parser.parseExpression(rawSource, { plugins: ["typescript"] });
743
- } catch {
744
- return null;
1102
+ } catch (error) {
1103
+ return {
1104
+ kind: "parse-error",
1105
+ rawSource,
1106
+ reason: "parse-error",
1107
+ error: error instanceof Error ? error.message : String(error)
1108
+ };
745
1109
  }
746
- const isNodeType = (node, type) => {
747
- return node !== null && node.type === type;
748
- };
749
- const isStringLiteralNode = (node) => {
750
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
751
- };
752
- const isIdentifierNode = (node) => {
753
- return isNodeType(node, "Identifier") && typeof node.name === "string";
754
- };
755
- const isObjectPropertyNode = (node) => {
756
- if (!isNodeType(node, "ObjectProperty"))
757
- return false;
758
- const n = node;
759
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
760
- };
761
- const isObjectExpressionNode = (node) => {
762
- if (!isNodeType(node, "ObjectExpression"))
763
- return false;
764
- const n = node;
765
- return Array.isArray(n.properties);
766
- };
767
1110
  if (isStringLiteralNode(expr)) {
768
- return expr.value;
1111
+ return {
1112
+ kind: "resolved",
1113
+ rawSource,
1114
+ target: expr.value,
1115
+ routeNameKey: null,
1116
+ paramKeys: []
1117
+ };
769
1118
  }
770
1119
  if (!isObjectExpressionNode(expr)) {
771
- return null;
1120
+ return {
1121
+ kind: "unsupported",
1122
+ rawSource,
1123
+ reason: "dynamic-expression"
1124
+ };
772
1125
  }
773
1126
  const getStringField = (fieldName) => {
774
1127
  const prop = expr.properties.find((p) => {
@@ -789,7 +1142,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
789
1142
  const key = p.key;
790
1143
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
791
1144
  });
792
- let params;
1145
+ let paramKeys = [];
793
1146
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
794
1147
  const keys = [];
795
1148
  for (const prop of paramsProp.value.properties) {
@@ -801,47 +1154,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
801
1154
  else if (isStringLiteralNode(key))
802
1155
  keys.push(key.value);
803
1156
  }
804
- if (keys.length) {
805
- params = buildPlaceholderParams(Array.from(new Set(keys)));
806
- }
1157
+ paramKeys = Array.from(new Set(keys));
807
1158
  }
808
1159
  if (name) {
809
- return { name, params };
1160
+ const trimmed = name.trim();
1161
+ if (!trimmed.length) {
1162
+ return {
1163
+ kind: "unsupported",
1164
+ rawSource,
1165
+ reason: "missing-name-or-path"
1166
+ };
1167
+ }
1168
+ return {
1169
+ kind: "resolved",
1170
+ rawSource,
1171
+ target: { name },
1172
+ routeNameKey: toPascalCaseRouteKey(trimmed),
1173
+ paramKeys
1174
+ };
810
1175
  }
811
1176
  if (path2) {
812
- return { path: path2, params };
1177
+ return {
1178
+ kind: "resolved",
1179
+ rawSource,
1180
+ target: { path: path2 },
1181
+ routeNameKey: null,
1182
+ paramKeys
1183
+ };
813
1184
  }
814
- return null;
815
- }
816
- function toDirectiveObjectFieldNameValue$1(toDirective) {
817
- const to = getRouteLocationLikeFromToDirective(toDirective);
818
- if (!to || typeof to === "string")
819
- return null;
820
- const name = to.name;
821
- if (typeof name !== "string")
822
- return null;
823
- const trimmed = name.trim();
824
- if (!trimmed.length)
825
- return null;
826
- return toPascalCaseRouteKey(trimmed);
827
- }
828
- function getRouteNameKeyFromToDirective(toDirective) {
829
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
830
- if (objectName)
831
- return objectName;
832
- return null;
1185
+ return {
1186
+ kind: "unsupported",
1187
+ rawSource,
1188
+ reason: "missing-name-or-path"
1189
+ };
833
1190
  }
834
1191
  function tryResolveToDirectiveTargetComponentName(toDirective) {
835
- const to = getRouteLocationLikeFromToDirective(toDirective);
836
- if (to && resolveToComponentName) {
837
- const resolved = resolveToComponentName(to);
1192
+ const analysis = analyzeToDirectiveTarget(toDirective);
1193
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1194
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
838
1195
  if (resolved)
839
1196
  return resolved;
840
1197
  }
841
- const key = getRouteNameKeyFromToDirective(toDirective);
842
- if (!key || !routeNameToComponentName)
1198
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
843
1199
  return null;
844
- return routeNameToComponentName.get(key) ?? null;
1200
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
845
1201
  }
846
1202
  function getDataTestIdFromGroupOption(text) {
847
1203
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -876,11 +1232,81 @@ function staticAttributeValue(value) {
876
1232
  return { kind: "static", value };
877
1233
  }
878
1234
  function templateAttributeValue(template) {
879
- return { kind: "template", template };
1235
+ const parsedTemplate = tryParseTemplateFragment(template);
1236
+ if (!parsedTemplate) {
1237
+ throw new Error(`[vue-pom-generator] Failed to parse generated template fragment: ${template}`);
1238
+ }
1239
+ return { kind: "template", template, parsedTemplate };
880
1240
  }
881
1241
  function getAttributeValueText(value) {
882
1242
  return value.kind === "static" ? value.value : value.template;
883
1243
  }
1244
+ function getVueExpressionSource(expression, ...preferredViews) {
1245
+ if (!expression) {
1246
+ return "";
1247
+ }
1248
+ for (const view of preferredViews) {
1249
+ let value = "";
1250
+ switch (view) {
1251
+ case "content":
1252
+ value = expression.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? expression.content : "";
1253
+ break;
1254
+ case "loc":
1255
+ value = expression.loc?.source ?? "";
1256
+ break;
1257
+ case "compiled":
1258
+ try {
1259
+ value = compilerCore.stringifyExpression(expression);
1260
+ } catch {
1261
+ value = "";
1262
+ }
1263
+ break;
1264
+ default:
1265
+ value = "";
1266
+ break;
1267
+ }
1268
+ const trimmed = value.trim();
1269
+ if (trimmed) {
1270
+ return trimmed;
1271
+ }
1272
+ }
1273
+ return "";
1274
+ }
1275
+ function tryGetExistingVueExpressionAst(expression) {
1276
+ if (!expression) {
1277
+ return null;
1278
+ }
1279
+ const ast = "ast" in expression ? expression.ast : null;
1280
+ return ast && "type" in ast ? ast : null;
1281
+ }
1282
+ function tryParseBabelExpressionFromSource(source, plugins) {
1283
+ const trimmed = source.trim();
1284
+ if (!trimmed) {
1285
+ return null;
1286
+ }
1287
+ try {
1288
+ return parser.parseExpression(trimmed, { plugins });
1289
+ } catch {
1290
+ return null;
1291
+ }
1292
+ }
1293
+ function tryGetVueExpressionAst(expression, options) {
1294
+ if (!expression) {
1295
+ return null;
1296
+ }
1297
+ if (options?.preferExistingAst !== false) {
1298
+ const existingAst = tryGetExistingVueExpressionAst(expression);
1299
+ if (existingAst) {
1300
+ return existingAst;
1301
+ }
1302
+ }
1303
+ const source = getVueExpressionSource(expression, ...options?.preferredViews ?? ["content", "loc", "compiled"]);
1304
+ return source ? tryParseBabelExpressionFromSource(source, options?.plugins ?? ["typescript"]) : null;
1305
+ }
1306
+ function tryGetDirectiveBabelAst(directive, options) {
1307
+ const exp = directive.exp && (directive.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || directive.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? directive.exp : null;
1308
+ return tryGetVueExpressionAst(exp, options);
1309
+ }
884
1310
  function toPascalCase(str) {
885
1311
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
886
1312
  if (!cleaned) {
@@ -922,32 +1348,14 @@ function tryGetClickDirective(node) {
922
1348
  function nodeHasClickDirective(node) {
923
1349
  return tryGetClickDirective(node) !== void 0;
924
1350
  }
925
- function getTemplateSlotScope(node) {
1351
+ function findTemplateSlotScopeExpression(node) {
926
1352
  if (node.tag !== "template") {
927
1353
  return null;
928
1354
  }
929
1355
  const slotProp = node.props.find((prop) => {
930
1356
  return prop.type === compilerCore.NodeTypes.DIRECTIVE && prop.name === "slot";
931
1357
  });
932
- if (slotProp?.exp) {
933
- if (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
934
- return slotProp.exp.content;
935
- }
936
- if (slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) {
937
- return compilerCore.stringifyExpression(slotProp.exp);
938
- }
939
- }
940
- return null;
941
- }
942
- function isSimpleScopeIdentifier(value) {
943
- if (!value) {
944
- return false;
945
- }
946
- try {
947
- return types.isIdentifier(parser.parseExpression(value, { plugins: ["typescript"] }));
948
- } catch {
949
- return false;
950
- }
1358
+ return slotProp?.exp && (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
951
1359
  }
952
1360
  function buildSlotScopeFallbackKeyExpression(identifier) {
953
1361
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -1049,43 +1457,64 @@ function tryGetSlotScopeKeyCandidate(node) {
1049
1457
  }
1050
1458
  return best;
1051
1459
  }
1052
- function tryGetTemplateSlotScopeKeyExpression(scope) {
1053
- const trimmed = scope.trim();
1054
- if (!trimmed) {
1460
+ function tryGetTemplateSlotScopeBindingNode(expression) {
1461
+ const ast = tryGetExistingVueExpressionAst(expression);
1462
+ if (ast) {
1463
+ if (types.isArrowFunctionExpression(ast)) {
1464
+ return ast.params[0] ?? null;
1465
+ }
1466
+ return ast;
1467
+ }
1468
+ const rawSource = getVueExpressionSource(expression, "content", "loc", "compiled");
1469
+ if (!rawSource) {
1055
1470
  return null;
1056
1471
  }
1057
- if (isSimpleScopeIdentifier(trimmed)) {
1058
- return buildSlotScopeFallbackKeyExpression(trimmed);
1472
+ try {
1473
+ return parser.parseExpression(rawSource, { plugins: ["typescript"] });
1474
+ } catch {
1059
1475
  }
1060
1476
  try {
1061
- const parsed = parser.parse(`(${trimmed}) => {}`, {
1477
+ const parsed = parser.parse(`(${rawSource}) => {}`, {
1062
1478
  sourceType: "module",
1063
1479
  plugins: ["typescript"]
1064
1480
  });
1065
1481
  const statement = parsed.program.body[0];
1066
1482
  if (statement && types.isExpressionStatement(statement) && types.isArrowFunctionExpression(statement.expression)) {
1067
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1483
+ return statement.expression.params[0] ?? null;
1068
1484
  }
1069
1485
  } catch {
1486
+ return null;
1070
1487
  }
1071
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1072
- const inner = trimmed.slice(1, -1).trim();
1073
- let cutIdx = -1;
1074
- const commaIdx = inner.indexOf(",");
1075
- const colonIdx = inner.indexOf(":");
1076
- if (commaIdx !== -1 && colonIdx !== -1) {
1077
- cutIdx = Math.min(commaIdx, colonIdx);
1078
- } else if (commaIdx !== -1) {
1079
- cutIdx = commaIdx;
1080
- } else if (colonIdx !== -1) {
1081
- cutIdx = colonIdx;
1082
- }
1083
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
1084
- if (first && isSimpleScopeIdentifier(first)) {
1085
- return buildSlotScopeFallbackKeyExpression(first);
1086
- }
1488
+ return null;
1489
+ }
1490
+ function toResolvedTemplateFragment(source) {
1491
+ const templateLiteral = tryUnwrapTemplateLiteralSource(source);
1492
+ if (templateLiteral) {
1493
+ return {
1494
+ template: templateLiteral.template,
1495
+ rawExpression: null
1496
+ };
1087
1497
  }
1088
- return trimmed;
1498
+ return toInterpolatedTemplateFragment(source);
1499
+ }
1500
+ function toResolvedKeyInfo(selectorSource, runtimeSource = selectorSource) {
1501
+ const selectorFragment = selectorSource ? toResolvedTemplateFragment(selectorSource) : null;
1502
+ const runtimeFragment = runtimeSource ? toResolvedTemplateFragment(runtimeSource) : null;
1503
+ const selectorTemplate = selectorFragment?.template ?? runtimeFragment?.template ?? null;
1504
+ const runtimeTemplate = runtimeFragment?.template ?? selectorFragment?.template ?? null;
1505
+ if (!selectorTemplate || !runtimeTemplate) {
1506
+ return null;
1507
+ }
1508
+ return {
1509
+ selectorFragment: selectorTemplate,
1510
+ runtimeFragment: runtimeTemplate,
1511
+ rawExpression: runtimeFragment?.rawExpression ?? selectorFragment?.rawExpression ?? null
1512
+ };
1513
+ }
1514
+ function tryGetTemplateSlotScopeKeyInfo(expression) {
1515
+ const bindingNode = tryGetTemplateSlotScopeBindingNode(expression);
1516
+ const candidateExpression = bindingNode ? tryGetSlotScopeKeyCandidate(bindingNode)?.expression ?? null : null;
1517
+ return candidateExpression ? toResolvedKeyInfo(candidateExpression) : null;
1089
1518
  }
1090
1519
  function nodeHasToDirective(node) {
1091
1520
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1094,51 +1523,160 @@ function nodeHasToDirective(node) {
1094
1523
  }
1095
1524
  return void 0;
1096
1525
  }
1097
- function nodeHasForDirective(node) {
1098
- return node.props.some(
1099
- (attr) => attr.type === compilerCore.NodeTypes.DIRECTIVE && attr.name === "for"
1100
- );
1101
- }
1102
1526
  function getKeyDirective(node) {
1103
1527
  return findDirectiveByName(node, "bind", "key") ?? null;
1104
1528
  }
1105
- function getKeyDirectiveValue(node, _context = null) {
1106
- const keyDirective = getKeyDirective(node);
1107
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1108
- if (rawSource) {
1109
- return `\${${rawSource}}`;
1529
+ function tryUnwrapTemplateLiteralSource(source) {
1530
+ const rawSource = source.trim();
1531
+ if (!rawSource) {
1532
+ return null;
1110
1533
  }
1111
- if (keyDirective?.exp) {
1112
- const value = compilerCore.stringifyExpression(keyDirective.exp);
1113
- if (value)
1114
- return `\${${value}}`;
1534
+ let ast = null;
1535
+ try {
1536
+ ast = parser.parseExpression(rawSource, { plugins: ["typescript"] });
1537
+ } catch {
1538
+ return null;
1115
1539
  }
1116
- return null;
1540
+ if (!ast || !types.isTemplateLiteral(ast)) {
1541
+ return null;
1542
+ }
1543
+ const cooked = ast.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
1544
+ try {
1545
+ const start = typeof ast.start === "number" ? ast.start + 1 : 1;
1546
+ const end = typeof ast.end === "number" ? ast.end - 1 : rawSource.length - 1;
1547
+ return {
1548
+ template: rawSource.slice(start, end) || cooked,
1549
+ expressionCount: ast.expressions.length
1550
+ };
1551
+ } catch {
1552
+ return {
1553
+ template: cooked,
1554
+ expressionCount: ast.expressions.length
1555
+ };
1556
+ }
1557
+ }
1558
+ function tryUnwrapTemplateLiteralExpressionSource(expression) {
1559
+ const rawSource = getVueExpressionSource(expression, "loc", "compiled");
1560
+ return rawSource ? tryUnwrapTemplateLiteralSource(rawSource) : null;
1561
+ }
1562
+ function tryParseTemplateFragment(fragment) {
1563
+ if (!fragment) {
1564
+ return null;
1565
+ }
1566
+ try {
1567
+ const source = `\`${fragment}\``;
1568
+ const ast = parser.parseExpression(source, { plugins: ["typescript"] });
1569
+ return types.isTemplateLiteral(ast) ? { source, templateLiteral: ast } : null;
1570
+ } catch {
1571
+ return null;
1572
+ }
1573
+ }
1574
+ function getTemplateExpressionSource(parsedTemplate, index) {
1575
+ const expression = parsedTemplate.templateLiteral.expressions[index];
1576
+ if (!expression) {
1577
+ return null;
1578
+ }
1579
+ const start = typeof expression.start === "number" ? expression.start : null;
1580
+ const end = typeof expression.end === "number" ? expression.end : null;
1581
+ if (start === null || end === null) {
1582
+ return null;
1583
+ }
1584
+ return parsedTemplate.source.slice(start, end);
1585
+ }
1586
+ function getSingleExpressionTemplateFragment(parsedTemplate) {
1587
+ const { templateLiteral } = parsedTemplate;
1588
+ if (templateLiteral.expressions.length !== 1 || templateLiteral.quasis.length !== 2) {
1589
+ return null;
1590
+ }
1591
+ const expressionSource = getTemplateExpressionSource(parsedTemplate, 0);
1592
+ if (expressionSource === null) {
1593
+ return null;
1594
+ }
1595
+ return {
1596
+ prefix: templateLiteral.quasis[0]?.value.raw ?? "",
1597
+ expressionSource,
1598
+ suffix: templateLiteral.quasis[1]?.value.raw ?? ""
1599
+ };
1600
+ }
1601
+ function templateFragmentContainsSingleExpression(container, candidate) {
1602
+ const containerFragment = getSingleExpressionTemplateFragment(container);
1603
+ const candidateFragment = getSingleExpressionTemplateFragment(candidate);
1604
+ if (!containerFragment || !candidateFragment) {
1605
+ return false;
1606
+ }
1607
+ return containerFragment.expressionSource === candidateFragment.expressionSource && containerFragment.prefix.endsWith(candidateFragment.prefix) && containerFragment.suffix.startsWith(candidateFragment.suffix);
1608
+ }
1609
+ function hasTemplateInterpolationExpressions(fragment) {
1610
+ return (tryParseTemplateFragment(fragment)?.templateLiteral.expressions.length ?? 0) > 0;
1611
+ }
1612
+ function toInterpolatedTemplateFragment(fragment) {
1613
+ if (!fragment) {
1614
+ return null;
1615
+ }
1616
+ if (hasTemplateInterpolationExpressions(fragment)) {
1617
+ return { template: fragment, rawExpression: null };
1618
+ }
1619
+ return {
1620
+ template: `\${${fragment}}`,
1621
+ rawExpression: fragment
1622
+ };
1623
+ }
1624
+ function renderTemplateLiteralExpression(templateValue) {
1625
+ const templateLiteralSource = templateValue.parsedTemplate.source;
1626
+ const templateLiteral = templateValue.parsedTemplate.templateLiteral;
1627
+ const writer = createTypeScriptWriter();
1628
+ writer.write("`");
1629
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
1630
+ writer.write(templateLiteral.quasis[i]?.value.raw ?? "");
1631
+ const interpolation = templateLiteral.expressions[i];
1632
+ if (!interpolation) {
1633
+ continue;
1634
+ }
1635
+ const start = typeof interpolation.start === "number" ? interpolation.start : null;
1636
+ const end = typeof interpolation.end === "number" ? interpolation.end : null;
1637
+ if (start === null || end === null) {
1638
+ return templateLiteralSource;
1639
+ }
1640
+ writer.write("${");
1641
+ writer.write(templateLiteralSource.slice(start, end));
1642
+ writer.write("}");
1643
+ }
1644
+ writer.write("`");
1645
+ return writer.toString();
1646
+ }
1647
+ function getKeyDirectiveExpression(node) {
1648
+ const keyDirective = getKeyDirective(node);
1649
+ return keyDirective?.exp && (keyDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || keyDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? keyDirective.exp : null;
1650
+ }
1651
+ function getKeyDirectiveInfo(node) {
1652
+ const keyExpression = getKeyDirectiveExpression(node);
1653
+ if (!keyExpression) {
1654
+ return null;
1655
+ }
1656
+ const selectorSource = getVueExpressionSource(keyExpression, "compiled", "loc");
1657
+ const runtimeSource = getVueExpressionSource(keyExpression, "loc", "compiled");
1658
+ return toResolvedKeyInfo(selectorSource, runtimeSource);
1117
1659
  }
1118
1660
  function getModelBindingValues(node) {
1119
1661
  let vModel = "";
1120
1662
  const vModelDirective = findDirectiveByName(node, "model");
1121
- if (vModelDirective?.exp?.loc.source) {
1122
- vModel = toPascalCase(vModelDirective.exp.loc.source);
1663
+ if (vModelDirective?.exp && (vModelDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || vModelDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
1664
+ vModel = toPascalCase(getVueExpressionSource(vModelDirective.exp, "loc", "content"));
1123
1665
  }
1124
1666
  let modelValue = null;
1125
1667
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1126
- if (modelValueDirective?.exp?.ast) {
1127
- const { name: mv } = getClickHandlerNameFromAst(modelValueDirective.exp.ast);
1668
+ const modelValueAst = modelValueDirective ? tryGetDirectiveBabelAst(modelValueDirective, {
1669
+ preferredViews: ["loc", "compiled"],
1670
+ plugins: ["typescript"],
1671
+ preferExistingAst: false
1672
+ }) : null;
1673
+ if (modelValueAst) {
1674
+ const { name: mv } = getClickHandlerNameFromAst(modelValueAst);
1128
1675
  modelValue = mv;
1129
1676
  }
1130
1677
  return { vModel, modelValue };
1131
1678
  }
1132
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1133
- if (node.isSelfClosing) {
1134
- const hasForDirective = nodeHasForDirective(node);
1135
- if (hasForDirective) {
1136
- return getKeyDirectiveValue(node);
1137
- }
1138
- }
1139
- return null;
1140
- }
1141
- function getIdOrName(node) {
1679
+ function getStaticIdOrNameHint(node) {
1142
1680
  let idAttr = findAttributeByKey(node, "id");
1143
1681
  if (!idAttr) {
1144
1682
  idAttr = findAttributeByKey(node, "name");
@@ -1146,8 +1684,9 @@ function getIdOrName(node) {
1146
1684
  let identifier = idAttr?.value?.content ?? "";
1147
1685
  if (!identifier) {
1148
1686
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1149
- if (dynamicIdAttr?.exp) {
1150
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1687
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1688
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1689
+ return "";
1151
1690
  }
1152
1691
  }
1153
1692
  if (identifier.includes("-")) {
@@ -1158,20 +1697,20 @@ function getIdOrName(node) {
1158
1697
  }
1159
1698
  return identifier;
1160
1699
  }
1161
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1700
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1162
1701
  let parent = getParent(hierarchyMap2, node);
1163
1702
  while (parent) {
1164
1703
  if (parent.type === compilerCore.NodeTypes.ELEMENT && parent.tag === "template") {
1165
- const scope = getTemplateSlotScope(parent);
1166
- if (scope) {
1167
- return tryGetTemplateSlotScopeKeyExpression(scope);
1704
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1705
+ if (slotScopeExpression) {
1706
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1168
1707
  }
1169
1708
  }
1170
1709
  parent = getParent(hierarchyMap2, parent);
1171
1710
  }
1172
1711
  return null;
1173
1712
  }
1174
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1713
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1175
1714
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1176
1715
  return null;
1177
1716
  }
@@ -1180,8 +1719,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1180
1719
  if (parent.type === compilerCore.NodeTypes.ELEMENT) {
1181
1720
  const forDirective = findDirectiveByName(parent, "for");
1182
1721
  if (forDirective) {
1183
- const keyValue = getKeyDirectiveValue(parent);
1184
- return keyValue;
1722
+ return getKeyDirectiveInfo(parent);
1185
1723
  }
1186
1724
  }
1187
1725
  parent = getParent(hierarchyMap2, parent);
@@ -1207,13 +1745,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1207
1745
  if (simpleSourceExp.constType === compilerCore.ConstantTypes.NOT_CONSTANT) {
1208
1746
  return null;
1209
1747
  }
1210
- const iterableRaw = (() => {
1211
- try {
1212
- return compilerCore.stringifyExpression(simpleSourceExp).trim();
1213
- } catch {
1214
- return (simpleSourceExp.loc?.source ?? "").trim();
1215
- }
1216
- })();
1748
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1217
1749
  if (!iterableRaw) {
1218
1750
  return null;
1219
1751
  }
@@ -1269,91 +1801,95 @@ function nodeHandlerAttributeInfo(node) {
1269
1801
  return null;
1270
1802
  }
1271
1803
  const exp = handlerDirective.exp;
1272
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
1804
+ const source = getVueExpressionSource(exp, "content", "compiled");
1273
1805
  if (!source) {
1274
1806
  return null;
1275
1807
  }
1276
1808
  const mergeKey = `handler:expr:${source}`;
1277
- let expr;
1278
- try {
1279
- expr = parser.parseExpression(source, { plugins: ["typescript", "jsx"] });
1280
- } catch {
1809
+ const expr = tryGetDirectiveBabelAst(handlerDirective, {
1810
+ preferredViews: ["content", "compiled"],
1811
+ plugins: ["typescript", "jsx"],
1812
+ // Vue's compiler AST can encode `_ctx.foo` as an Identifier name instead of a MemberExpression.
1813
+ // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1814
+ preferExistingAst: false
1815
+ });
1816
+ if (!expr) {
1281
1817
  return null;
1282
1818
  }
1283
- const isNodeType = (node2, type) => {
1819
+ const isNodeType2 = (node2, type) => {
1284
1820
  return node2 !== null && node2.type === type;
1285
1821
  };
1286
- const isIdentifierNode = (node2) => {
1287
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1822
+ const isIdentifierNode2 = (node2) => {
1823
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1288
1824
  };
1289
- const isStringLiteralNode = (node2) => {
1290
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1825
+ const isStringLiteralNode2 = (node2) => {
1826
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1291
1827
  };
1292
1828
  const isBooleanLiteralNode = (node2) => {
1293
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1829
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1294
1830
  };
1295
1831
  const isNumericLiteralNode = (node2) => {
1296
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1832
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1297
1833
  };
1298
1834
  const isNullLiteralNode = (node2) => {
1299
- return isNodeType(node2, "NullLiteral");
1835
+ return isNodeType2(node2, "NullLiteral");
1300
1836
  };
1301
1837
  const isMemberExpressionNode = (node2) => {
1302
- if (!isNodeType(node2, "MemberExpression"))
1838
+ if (!isNodeType2(node2, "MemberExpression"))
1303
1839
  return false;
1304
1840
  const n = node2;
1305
1841
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1306
1842
  };
1307
1843
  const isCallExpressionNode = (node2) => {
1308
- if (!isNodeType(node2, "CallExpression"))
1844
+ if (!isNodeType2(node2, "CallExpression"))
1309
1845
  return false;
1310
1846
  const n = node2;
1311
1847
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1312
1848
  };
1313
1849
  const isAwaitExpressionNode = (node2) => {
1314
- if (!isNodeType(node2, "AwaitExpression"))
1850
+ if (!isNodeType2(node2, "AwaitExpression"))
1315
1851
  return false;
1316
1852
  const n = node2;
1317
1853
  return typeof n.argument === "object" && n.argument !== null;
1318
1854
  };
1319
1855
  const isAssignmentExpressionNode = (node2) => {
1320
- if (!isNodeType(node2, "AssignmentExpression"))
1856
+ if (!isNodeType2(node2, "AssignmentExpression"))
1321
1857
  return false;
1322
1858
  const n = node2;
1323
1859
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1324
1860
  };
1325
1861
  const isArrowFunctionExpressionNode = (node2) => {
1326
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1862
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1327
1863
  return false;
1328
1864
  const n = node2;
1329
1865
  return typeof n.body === "object" && n.body !== null;
1330
1866
  };
1331
1867
  const isBlockStatementNode = (node2) => {
1332
- if (!isNodeType(node2, "BlockStatement"))
1868
+ if (!isNodeType2(node2, "BlockStatement"))
1333
1869
  return false;
1334
1870
  const n = node2;
1335
1871
  return Array.isArray(n.body);
1336
1872
  };
1337
1873
  const isExpressionStatementNode = (node2) => {
1338
- if (!isNodeType(node2, "ExpressionStatement"))
1874
+ if (!isNodeType2(node2, "ExpressionStatement"))
1339
1875
  return false;
1340
1876
  const n = node2;
1341
1877
  return typeof n.expression === "object" && n.expression !== null;
1342
1878
  };
1343
1879
  const isReturnStatementNode = (node2) => {
1344
- if (!isNodeType(node2, "ReturnStatement"))
1880
+ if (!isNodeType2(node2, "ReturnStatement"))
1345
1881
  return false;
1346
1882
  const n = node2;
1347
1883
  return typeof n.argument === "object" || n.argument === null;
1348
1884
  };
1349
- const isObjectExpressionNode = (node2) => {
1350
- if (!isNodeType(node2, "ObjectExpression"))
1885
+ const isObjectExpressionNode2 = (node2) => {
1886
+ if (!isNodeType2(node2, "ObjectExpression"))
1351
1887
  return false;
1352
1888
  const n = node2;
1353
1889
  return Array.isArray(n.properties);
1354
1890
  };
1355
- const isObjectPropertyNode = (node2) => {
1356
- if (!isNodeType(node2, "ObjectProperty"))
1891
+ const isObjectPropertyNode2 = (node2) => {
1892
+ if (!isNodeType2(node2, "ObjectProperty"))
1357
1893
  return false;
1358
1894
  const n = node2;
1359
1895
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1361,16 +1897,16 @@ function nodeHandlerAttributeInfo(node) {
1361
1897
  const getLastIdentifierFromMemberChain = (node2) => {
1362
1898
  if (!node2)
1363
1899
  return null;
1364
- if (isIdentifierNode(node2))
1900
+ if (isIdentifierNode2(node2))
1365
1901
  return node2.name;
1366
1902
  if (isMemberExpressionNode(node2)) {
1367
1903
  const prop = node2.property;
1368
1904
  if (node2.computed === false) {
1369
- if (isIdentifierNode(prop))
1905
+ if (isIdentifierNode2(prop))
1370
1906
  return prop.name;
1371
1907
  }
1372
1908
  if (node2.computed === true) {
1373
- if (isStringLiteralNode(prop))
1909
+ if (isStringLiteralNode2(prop))
1374
1910
  return prop.value;
1375
1911
  }
1376
1912
  }
@@ -1380,7 +1916,7 @@ function nodeHandlerAttributeInfo(node) {
1380
1916
  if (!node2) {
1381
1917
  return null;
1382
1918
  }
1383
- if (isIdentifierNode(node2)) {
1919
+ if (isIdentifierNode2(node2)) {
1384
1920
  return node2.name;
1385
1921
  }
1386
1922
  if (isMemberExpressionNode(node2)) {
@@ -1392,11 +1928,11 @@ function nodeHandlerAttributeInfo(node) {
1392
1928
  if (!lhs) {
1393
1929
  return null;
1394
1930
  }
1395
- if (isIdentifierNode(lhs)) {
1931
+ if (isIdentifierNode2(lhs)) {
1396
1932
  return lhs.name;
1397
1933
  }
1398
1934
  if (isMemberExpressionNode(lhs)) {
1399
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1935
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1400
1936
  return getLastIdentifierFromMemberChain(lhs.object);
1401
1937
  }
1402
1938
  return getLastIdentifierFromMemberChain(lhs);
@@ -1404,7 +1940,7 @@ function nodeHandlerAttributeInfo(node) {
1404
1940
  return null;
1405
1941
  };
1406
1942
  const isTemplateLiteralNode = (node2) => {
1407
- if (!isNodeType(node2, "TemplateLiteral")) {
1943
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1408
1944
  return false;
1409
1945
  }
1410
1946
  const n = node2;
@@ -1423,7 +1959,7 @@ function nodeHandlerAttributeInfo(node) {
1423
1959
  if (isNullLiteralNode(arg)) {
1424
1960
  return "Null";
1425
1961
  }
1426
- if (isStringLiteralNode(arg)) {
1962
+ if (isStringLiteralNode2(arg)) {
1427
1963
  const cleaned = (arg.value ?? "").trim();
1428
1964
  if (!cleaned) {
1429
1965
  return null;
@@ -1449,7 +1985,7 @@ function nodeHandlerAttributeInfo(node) {
1449
1985
  return toPascalCase(stableName.slice(0, 24));
1450
1986
  }
1451
1987
  }
1452
- if (isIdentifierNode(arg)) {
1988
+ if (isIdentifierNode2(arg)) {
1453
1989
  const firstChar = arg.name.charAt(0);
1454
1990
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1455
1991
  if (isUpperAlpha) {
@@ -1461,7 +1997,7 @@ function nodeHandlerAttributeInfo(node) {
1461
1997
  const getStableSuffixFromCall = (call) => {
1462
1998
  const args = call.arguments ?? [];
1463
1999
  const first = args.length > 0 ? args[0] : null;
1464
- if (!isObjectExpressionNode(first)) {
2000
+ if (!isObjectExpressionNode2(first)) {
1465
2001
  const parts2 = [];
1466
2002
  for (const arg of args.slice(0, 4)) {
1467
2003
  const w = stableWordFromValue(arg ?? null);
@@ -1480,20 +2016,20 @@ function nodeHandlerAttributeInfo(node) {
1480
2016
  }
1481
2017
  const parts = [];
1482
2018
  for (const prop of first.properties ?? []) {
1483
- if (!isObjectPropertyNode(prop)) {
2019
+ if (!isObjectPropertyNode2(prop)) {
1484
2020
  continue;
1485
2021
  }
1486
2022
  if (prop.computed) {
1487
2023
  continue;
1488
2024
  }
1489
- const keyName = isIdentifierNode(prop.key) ? prop.key.name : isStringLiteralNode(prop.key) ? prop.key.value : null;
2025
+ const keyName = isIdentifierNode2(prop.key) ? prop.key.name : isStringLiteralNode2(prop.key) ? prop.key.value : null;
1490
2026
  if (!keyName) {
1491
2027
  continue;
1492
2028
  }
1493
2029
  let valueWord = null;
1494
2030
  if (isBooleanLiteralNode(prop.value)) {
1495
2031
  valueWord = prop.value.value ? "True" : "False";
1496
- } else if (isStringLiteralNode(prop.value)) {
2032
+ } else if (isStringLiteralNode2(prop.value)) {
1497
2033
  const cleaned = (prop.value.value ?? "").trim();
1498
2034
  if (cleaned) {
1499
2035
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1622,13 +2158,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1622
2158
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1623
2159
  }
1624
2160
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1625
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1626
- let value = attrDynamic.exp.loc.source;
1627
- if (attrDynamic.exp.ast?.type === "MemberExpression") {
2161
+ const attrDynamicAst = attrDynamic ? tryGetDirectiveBabelAst(attrDynamic, {
2162
+ preferredViews: ["loc", "compiled"],
2163
+ plugins: ["typescript"],
2164
+ preferExistingAst: false
2165
+ }) : null;
2166
+ if (attrDynamic?.exp && attrDynamicAst) {
2167
+ let value = getVueExpressionSource(attrDynamic.exp, "loc", "compiled");
2168
+ if (types.isMemberExpression(attrDynamicAst) || types.isOptionalMemberExpression(attrDynamicAst)) {
1628
2169
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1629
2170
  }
1630
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1631
- value = compilerCore.stringifyExpression(attrDynamic.exp);
2171
+ if (types.isCallExpression(attrDynamicAst) || types.isOptionalCallExpression(attrDynamicAst)) {
2172
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1632
2173
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1633
2174
  }
1634
2175
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1636,16 +2177,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1636
2177
  return null;
1637
2178
  }
1638
2179
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1639
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1640
- if (key) {
1641
- return templateAttributeValue(`${componentName}-${key}-${formatTagName(node, nativeWrappers)}`);
2180
+ const keyInfo = getKeyDirectiveInfo(node) || getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2);
2181
+ if (keyInfo) {
2182
+ return templateAttributeValue(`${componentName}-${keyInfo.selectorFragment}-${formatTagName(node, nativeWrappers)}`);
1642
2183
  } else {
1643
2184
  let name = toDirectiveObjectFieldNameValue(toDirective);
1644
2185
  if (!name) {
1645
2186
  if (toDirective.exp == null) {
1646
2187
  return null;
1647
2188
  }
1648
- const source = compilerCore.stringifyExpression(toDirective.exp);
2189
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1649
2190
  const toAst = toDirective.exp.ast;
1650
2191
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && types.isTemplateLiteral(toAst);
1651
2192
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1669,44 +2210,46 @@ function toDirectiveObjectFieldNameValue(node) {
1669
2210
  if (!node.exp || node.exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
1670
2211
  return null;
1671
2212
  }
1672
- const source = node.exp.loc.source.trim();
1673
- try {
1674
- const expr = parser.parseExpression(source, { plugins: ["typescript"] });
1675
- const isNodeType = (n, type) => {
1676
- return n !== null && n.type === type;
1677
- };
1678
- const isStringLiteralNode = (n) => {
1679
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1680
- };
1681
- const isIdentifierNode = (n) => {
1682
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1683
- };
1684
- const isObjectPropertyNode = (n) => {
1685
- if (!isNodeType(n, "ObjectProperty"))
1686
- return false;
1687
- const nn = n;
1688
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1689
- };
1690
- const isObjectExpressionNode = (n) => {
1691
- if (!isNodeType(n, "ObjectExpression"))
1692
- return false;
1693
- const nn = n;
1694
- return Array.isArray(nn.properties);
1695
- };
1696
- if (!isObjectExpressionNode(expr))
1697
- return null;
1698
- const nameProp = expr.properties.find((p) => {
1699
- if (!isObjectPropertyNode(p))
1700
- return false;
1701
- const key = p.key;
1702
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1703
- });
1704
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1705
- return null;
1706
- return toPascalCase(nameProp.value.value);
1707
- } catch {
2213
+ const expr = tryGetDirectiveBabelAst(node, {
2214
+ preferredViews: ["loc", "compiled"],
2215
+ plugins: ["typescript"],
2216
+ preferExistingAst: false
2217
+ });
2218
+ if (!expr) {
1708
2219
  return null;
1709
2220
  }
2221
+ const isNodeType2 = (n, type) => {
2222
+ return n !== null && n.type === type;
2223
+ };
2224
+ const isStringLiteralNode2 = (n) => {
2225
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
2226
+ };
2227
+ const isIdentifierNode2 = (n) => {
2228
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
2229
+ };
2230
+ const isObjectPropertyNode2 = (n) => {
2231
+ if (!isNodeType2(n, "ObjectProperty"))
2232
+ return false;
2233
+ const nn = n;
2234
+ return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
2235
+ };
2236
+ const isObjectExpressionNode2 = (n) => {
2237
+ if (!isNodeType2(n, "ObjectExpression"))
2238
+ return false;
2239
+ const nn = n;
2240
+ return Array.isArray(nn.properties);
2241
+ };
2242
+ if (!isObjectExpressionNode2(expr))
2243
+ return null;
2244
+ const nameProp = expr.properties.find((p) => {
2245
+ if (!isObjectPropertyNode2(p))
2246
+ return false;
2247
+ const key = p.key;
2248
+ return isIdentifierNode2(key) && key.name === "name" || isStringLiteralNode2(key) && key.value === "name";
2249
+ });
2250
+ if (!nameProp || !isObjectPropertyNode2(nameProp) || !isStringLiteralNode2(nameProp.value))
2251
+ return null;
2252
+ return toPascalCase(nameProp.value.value);
1710
2253
  }
1711
2254
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1712
2255
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1716,7 +2259,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1716
2259
  let handlerName = "";
1717
2260
  if (click.exp) {
1718
2261
  const exp = click.exp;
1719
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
2262
+ const source = getVueExpressionSource(exp, "content", "compiled");
1720
2263
  if (source) {
1721
2264
  const parsed = tryParseBabelAstFromHandlerSource(source);
1722
2265
  if (parsed) {
@@ -1737,9 +2280,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1737
2280
  const trimmed = source.trim();
1738
2281
  if (!trimmed)
1739
2282
  return null;
1740
- try {
1741
- return parser.parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1742
- } catch {
2283
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2284
+ if (expressionAst) {
2285
+ return expressionAst;
1743
2286
  }
1744
2287
  try {
1745
2288
  return parser.parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -2078,23 +2621,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2078
2621
  return null;
2079
2622
  }
2080
2623
  const simpleExp = exp;
2081
- const ast = simpleExp.ast;
2082
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
2083
- const tl = ast;
2084
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2085
- const expressionCount = (tl.expressions ?? []).length;
2086
- const isStatic = expressionCount === 0;
2087
- const raw2 = (simpleExp.content ?? "").trim();
2088
- const unwrappedTemplate = raw2.startsWith("`") && raw2.endsWith("`") && raw2.length >= 2 ? raw2.slice(1, -1) : cooked;
2624
+ const ast = tryGetVueExpressionAst(simpleExp, {
2625
+ preferredViews: ["content", "loc", "compiled"],
2626
+ plugins: ["typescript"]
2627
+ });
2628
+ const unwrappedTemplateLiteral = tryUnwrapTemplateLiteralExpressionSource(simpleExp);
2629
+ if (unwrappedTemplateLiteral) {
2630
+ const isStatic = unwrappedTemplateLiteral.expressionCount === 0;
2089
2631
  if (isStatic) {
2090
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2632
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2091
2633
  }
2634
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2092
2635
  return {
2093
- value: unwrappedTemplate,
2636
+ value: templateValue.template,
2094
2637
  isDynamic: true,
2095
2638
  isStaticLiteral: false,
2096
- template: unwrappedTemplate,
2097
- templateExpressionCount: expressionCount
2639
+ template: templateValue.template,
2640
+ parsedTemplate: templateValue.parsedTemplate,
2641
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2098
2642
  };
2099
2643
  }
2100
2644
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2107,11 +2651,13 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2107
2651
  }
2108
2652
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2109
2653
  if (preservableReference) {
2654
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2110
2655
  return {
2111
2656
  value: preservableReference,
2112
2657
  isDynamic: true,
2113
2658
  isStaticLiteral: false,
2114
- template: `\${${preservableReference}}`,
2659
+ template: templateValue.template,
2660
+ parsedTemplate: templateValue.parsedTemplate,
2115
2661
  templateExpressionCount: 1,
2116
2662
  rawExpression: preservableReference
2117
2663
  };
@@ -2120,46 +2666,12 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2120
2666
  if (!raw) {
2121
2667
  return null;
2122
2668
  }
2123
- try {
2124
- const ast2 = parser.parseExpression(raw, { plugins: ["typescript"] });
2125
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2126
- const tl = ast2;
2127
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2128
- const expressionCount = (tl.expressions ?? []).length;
2129
- const isStatic = expressionCount === 0;
2130
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2131
- if (isStatic) {
2132
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2133
- }
2134
- return {
2135
- value: unwrappedTemplate,
2136
- isDynamic: true,
2137
- isStaticLiteral: false,
2138
- template: unwrappedTemplate,
2139
- templateExpressionCount: expressionCount
2140
- };
2141
- }
2142
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2143
- const sl = ast2;
2144
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2145
- }
2146
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2147
- if (preservableReference2) {
2148
- return {
2149
- value: preservableReference2,
2150
- isDynamic: true,
2151
- isStaticLiteral: false,
2152
- template: `\${${preservableReference2}}`,
2153
- templateExpressionCount: 1,
2154
- rawExpression: preservableReference2
2155
- };
2156
- }
2157
- } catch {
2158
- }
2159
2669
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2160
2670
  }
2161
2671
  function isTemplatePlaceholder(part) {
2162
- return part.startsWith("${") && part.endsWith("}") && part.length >= 3;
2672
+ const parsedTemplate = tryParseTemplateFragment(part);
2673
+ const templateFragment = parsedTemplate ? getSingleExpressionTemplateFragment(parsedTemplate) : null;
2674
+ return !!templateFragment && templateFragment.prefix === "" && templateFragment.suffix === "";
2163
2675
  }
2164
2676
  function isAllCapsOrDigits(value) {
2165
2677
  if (value.length <= 1) {
@@ -2212,33 +2724,14 @@ function safeMethodNameFromParts(parts) {
2212
2724
  }
2213
2725
  return name;
2214
2726
  }
2215
- function replaceAllTemplateExpressionsWithKey(template) {
2727
+ function toPomKeyPattern(templateValue) {
2728
+ const { templateLiteral } = templateValue.parsedTemplate;
2216
2729
  let out = "";
2217
- let i = 0;
2218
- while (i < template.length) {
2219
- const start = template.indexOf("${", i);
2220
- if (start < 0) {
2221
- out += template.slice(i);
2222
- break;
2223
- }
2224
- out += template.slice(i, start);
2225
- let depth = 1;
2226
- let j = start + 2;
2227
- while (j < template.length && depth > 0) {
2228
- if (template[j] === "{") {
2229
- depth++;
2230
- } else if (template[j] === "}") {
2231
- depth--;
2232
- }
2233
- j++;
2234
- }
2235
- const end = depth === 0 ? j - 1 : -1;
2236
- if (end < 0) {
2237
- out += template.slice(start);
2238
- break;
2730
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
2731
+ out += templateLiteral.quasis[i]?.value.raw ?? "";
2732
+ if (templateLiteral.expressions[i]) {
2733
+ out += "${key}";
2239
2734
  }
2240
- out += "${key}";
2241
- i = end + 1;
2242
2735
  }
2243
2736
  return out;
2244
2737
  }
@@ -2246,8 +2739,8 @@ function applyResolvedDataTestId(args) {
2246
2739
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2247
2740
  const entryOverrides = args.entryOverrides ?? {};
2248
2741
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2249
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2250
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2742
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2743
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2251
2744
  const warn = args.warn;
2252
2745
  const getBestKeyAccessCandidates = (expr) => {
2253
2746
  if (!expr) {
@@ -2256,7 +2749,10 @@ function applyResolvedDataTestId(args) {
2256
2749
  return splitNullishCoalescingExpression(expr);
2257
2750
  };
2258
2751
  let dataTestId = args.preferredGeneratedValue;
2752
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2259
2753
  let fromExisting = false;
2754
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2755
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2260
2756
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2261
2757
  if (existing) {
2262
2758
  const loc = args.element.loc?.start;
@@ -2277,8 +2773,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2277
2773
  if (existingIdBehavior === "preserve") {
2278
2774
  if (existing.isDynamic) {
2279
2775
  if (existing.template) {
2280
- const existingTemplate = existing.template;
2281
- if ((existing.templateExpressionCount ?? 0) !== 1) {
2776
+ const existingTemplateValue = existing.parsedTemplate ? { kind: "template", template: existing.template, parsedTemplate: existing.parsedTemplate } : templateAttributeValue(existing.template);
2777
+ const existingTemplateFragment = getSingleExpressionTemplateFragment(existingTemplateValue.parsedTemplate);
2778
+ const requiredKeyTemplateValue = bestKeyPreservePlaceholder ? templateAttributeValue(bestKeyPreservePlaceholder) : null;
2779
+ if ((existing.templateExpressionCount ?? 0) !== 1 || !existingTemplateFragment) {
2282
2780
  throw new Error(
2283
2781
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2284
2782
  Component: ${args.componentName}
@@ -2288,20 +2786,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2288
2786
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2289
2787
  );
2290
2788
  }
2291
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2292
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2293
- if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
2789
+ const hasExact = requiredKeyTemplateValue ? templateFragmentContainsSingleExpression(existingTemplateValue.parsedTemplate, requiredKeyTemplateValue.parsedTemplate) : false;
2790
+ const hasVarAccess = getBestKeyAccessCandidates(bestKeyVariable).some((candidate) => existingTemplateFragment.expressionSource === candidate);
2791
+ if (!hasExact && !hasVarAccess && bestKeyPreservePlaceholder) {
2294
2792
  throw new Error(
2295
2793
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2296
2794
  Component: ${args.componentName}
2297
2795
  File: ${file}:${locationHint}
2298
2796
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2299
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2797
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2300
2798
 
2301
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2799
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2302
2800
  );
2303
2801
  }
2304
- dataTestId = templateAttributeValue(existing.template);
2802
+ dataTestId = existingTemplateValue;
2803
+ runtimeDataTestId = existingTemplateValue;
2305
2804
  fromExisting = true;
2306
2805
  } else {
2307
2806
  throw new Error(
@@ -2315,18 +2814,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2315
2814
  );
2316
2815
  }
2317
2816
  } else {
2318
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2817
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2319
2818
  throw new Error(
2320
2819
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2321
2820
  Component: ${args.componentName}
2322
2821
  File: ${file}:${locationHint}
2323
2822
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2324
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2823
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2325
2824
 
2326
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2825
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2327
2826
  );
2328
2827
  }
2329
2828
  dataTestId = staticAttributeValue(existing.value);
2829
+ runtimeDataTestId = staticAttributeValue(existing.value);
2330
2830
  fromExisting = true;
2331
2831
  }
2332
2832
  }
@@ -2356,8 +2856,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2356
2856
  };
2357
2857
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2358
2858
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2359
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2360
- const isKeyed = formattedDataTestIdForPom.includes("${key}");
2859
+ const formattedDataTestIdForPom = dataTestId.kind === "template" ? toPomKeyPattern(dataTestId) : dataTestId.value;
2860
+ const selectorPatternKind = dataTestId.kind === "template" ? "parameterized" : "static";
2861
+ const selectorPattern = createPomStringPattern(formattedDataTestIdForPom, selectorPatternKind);
2862
+ const selectorIsParameterized = selectorPatternKind === "parameterized";
2361
2863
  const deriveBaseMethodNameFromHint = (hint) => {
2362
2864
  const hintRaw = (hint ?? "").trim();
2363
2865
  const trimEdgeSeparators = (value) => {
@@ -2394,7 +2896,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2394
2896
  }
2395
2897
  return value.slice(0, idx) + value.slice(idx + "ByKey".length);
2396
2898
  };
2397
- const hasRoleSuffix = (baseName, roleSuffix) => {
2899
+ const hasRoleSuffix2 = (baseName, roleSuffix) => {
2398
2900
  if (baseName.endsWith(roleSuffix)) {
2399
2901
  return true;
2400
2902
  }
@@ -2404,14 +2906,14 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2404
2906
  const getPrimaryGetterName = (primaryMethodName) => {
2405
2907
  const roleSuffix = upperFirst(normalizedRole || "Element");
2406
2908
  const baseName = upperFirst(primaryMethodName);
2407
- const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2408
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2909
+ const propertyName = hasRoleSuffix2(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2910
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2409
2911
  };
2410
2912
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2411
2913
  const roleSuffix = upperFirst(normalizedRole || "Element");
2412
2914
  const baseName = upperFirst(primaryMethodName);
2413
- const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2414
- if (!isKeyed) {
2915
+ const propertyName = hasRoleSuffix2(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2916
+ if (!selectorIsParameterized) {
2415
2917
  return { primary: propertyName };
2416
2918
  }
2417
2919
  const stripped = removeByKeySegment2(propertyName);
@@ -2473,11 +2975,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2473
2975
  return false;
2474
2976
  }
2475
2977
  const existingSelectors = [
2476
- existingPom.formattedDataTestId,
2477
- ...existingPom.alternateFormattedDataTestIds ?? []
2978
+ existingPom.selector,
2979
+ ...existingPom.alternateSelectors ?? []
2478
2980
  ];
2479
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2480
- if (isKeyed && !sharesSelectorIdentity) {
2981
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2982
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2481
2983
  return false;
2482
2984
  }
2483
2985
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2492,12 +2994,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2492
2994
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2493
2995
  return false;
2494
2996
  }
2495
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2496
- existingPom.alternateFormattedDataTestIds ??= [];
2497
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2498
- existingPom.alternateFormattedDataTestIds.push(formattedDataTestIdForPom);
2997
+ if (!pomStringPatternEquals(existingPom.selector, selectorPattern)) {
2998
+ existingPom.alternateSelectors ??= [];
2999
+ if (!(existingPom.alternateSelectors ?? []).some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern))) {
3000
+ existingPom.alternateSelectors.push(selectorPattern);
2499
3001
  }
2500
3002
  }
3003
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
3004
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
3005
+ }
2501
3006
  return true;
2502
3007
  };
2503
3008
  let methodName = "";
@@ -2510,7 +3015,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2510
3015
  let suffix = 1;
2511
3016
  while (true) {
2512
3017
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2513
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
3018
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2514
3019
  const actionName = getPrimaryActionMethodName(candidate);
2515
3020
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2516
3021
  let chosenGetterName = getterCandidates.primary;
@@ -2534,9 +3039,9 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2534
3039
  if (conflicts && nameCollisionBehavior === "error") {
2535
3040
  const roleSuffix = upperFirst(normalizedRole || "Element");
2536
3041
  const baseNameUpper = upperFirst(baseWithSuffix);
2537
- if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
3042
+ if (!hasRoleSuffix2(baseNameUpper, roleSuffix)) {
2538
3043
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2539
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
3044
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2540
3045
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2541
3046
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2542
3047
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2604,51 +3109,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2604
3109
  Fix: make the element identifiable (e.g. add id/name/inner text or use a more specific click handler name), or switch generation.nameCollisionBehavior to "warn"/"suffix".`
2605
3110
  );
2606
3111
  }
2607
- const params = {};
2608
- if (isKeyed) {
2609
- params.key = keyTypeFromValues;
2610
- }
3112
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2611
3113
  switch (normalizedRole) {
2612
3114
  case "input":
2613
- params.text = "string";
2614
- params.annotationText = 'string = ""';
2615
- if (!isKeyed) delete params.key;
3115
+ parameters = setPomParameter(parameters, "text", "string");
3116
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2616
3117
  break;
2617
3118
  case "select":
2618
- params.value = "string";
2619
- params.annotationText = 'string = ""';
2620
- if (!isKeyed) delete params.key;
3119
+ parameters = setPomParameter(parameters, "value", "string");
3120
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2621
3121
  break;
2622
3122
  case "vselect":
2623
- params.value = "string";
2624
- params.timeOut = "number = 500";
2625
- params.annotationText = 'string = ""';
2626
- if (!isKeyed) delete params.key;
3123
+ parameters = setPomParameter(parameters, "value", "string");
3124
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
3125
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2627
3126
  break;
2628
3127
  case "radio":
2629
- params.annotationText = 'string = ""';
3128
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2630
3129
  break;
2631
3130
  }
2632
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2633
- params.key = keyTypeFromValues;
2634
- }
3131
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2635
3132
  if (addHtmlAttribute && !fromExisting) {
2636
3133
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2637
3134
  }
2638
3135
  const childComponentName = args.element.tag;
2639
3136
  const dataTestIdEntry = {
2640
- value: getAttributeValueText(dataTestId),
2641
- templateLiteral: void 0,
2642
- ...entryOverrides
3137
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
3138
+ getAttributeValueText(dataTestId),
3139
+ dataTestId.kind === "template" ? "parameterized" : "static"
3140
+ ),
3141
+ templateLiteral: entryOverrides.templateLiteral,
3142
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2643
3143
  };
2644
3144
  dataTestIdEntry.pom = {
2645
3145
  nativeRole: normalizedRole,
2646
3146
  methodName,
2647
3147
  getterNameOverride,
2648
- formattedDataTestId: formattedDataTestIdForPom,
2649
- alternateFormattedDataTestIds: void 0,
3148
+ selector: selectorPattern,
3149
+ alternateSelectors: void 0,
2650
3150
  mergeKey: args.pomMergeKey,
2651
- params,
3151
+ parameters: normalizedParameters,
2652
3152
  keyValuesOverride: args.keyValuesOverride ?? null
2653
3153
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2654
3154
  };
@@ -2679,43 +3179,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2679
3179
  }
2680
3180
  };
2681
3181
  const getSignatureForGeneratedMethod = () => {
2682
- const role = normalizedRole;
2683
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2684
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2685
- const keyType = keyTypeFromValues;
2686
- if (isNavigation2) {
2687
- if (needsKey2) {
2688
- return { params: `key: ${keyType}`, argNames: ["key"] };
2689
- }
2690
- return { params: "", argNames: [] };
2691
- }
2692
- switch (role) {
2693
- case "input":
2694
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2695
- case "select":
2696
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2697
- case "vselect":
2698
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2699
- case "radio":
2700
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2701
- default:
2702
- if (needsKey2) {
2703
- return { params: `key: ${keyType}`, argNames: ["key"] };
2704
- }
2705
- return { params: "", argNames: [] };
2706
- }
3182
+ return createPomMethodSignature(normalizedParameters);
2707
3183
  };
2708
3184
  const registerPrimaryOnce = (pom) => {
2709
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2710
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3185
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2711
3186
  const key = JSON.stringify({
2712
3187
  kind: "primary",
2713
3188
  role: pom.nativeRole,
2714
3189
  methodName: pom.methodName,
2715
3190
  getterNameOverride: pom.getterNameOverride ?? null,
2716
- formattedDataTestId: pom.formattedDataTestId,
2717
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2718
- params: stableParams,
3191
+ selector: pom.selector,
3192
+ alternateSelectors: alternates.length ? alternates : void 0,
3193
+ parameters: pom.parameters,
2719
3194
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2720
3195
  emitPrimary: pom.emitPrimary ?? true
2721
3196
  });
@@ -2729,8 +3204,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2729
3204
  }
2730
3205
  };
2731
3206
  const addExtraClickMethod = (spec) => {
2732
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2733
- const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, params: stableParams });
3207
+ const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, parameters: spec.parameters });
2734
3208
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2735
3209
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2736
3210
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2753,7 +3227,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2753
3227
  if (prev === null) {
2754
3228
  return;
2755
3229
  }
2756
- if (signature === null || prev.params !== signature.params) {
3230
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2757
3231
  args.dependencies.generatedMethods.set(name, null);
2758
3232
  }
2759
3233
  };
@@ -2769,23 +3243,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2769
3243
  return candidate;
2770
3244
  };
2771
3245
  const tryGetDirectiveExpressionAst = (dir) => {
2772
- const exp = dir.exp;
2773
- if (!exp) {
2774
- return null;
2775
- }
2776
- if (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
2777
- const simple = exp;
2778
- const ast = simple.ast;
2779
- if (ast && "type" in ast) {
2780
- return ast;
2781
- }
2782
- }
2783
- try {
2784
- const raw = args.context ? compilerCore.stringifyExpression(exp) : exp.loc.source;
2785
- return parser.parseExpression(raw, { plugins: ["typescript"] });
2786
- } catch {
2787
- return null;
2788
- }
3246
+ return tryGetDirectiveBabelAst(dir, {
3247
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3248
+ plugins: ["typescript"]
3249
+ });
2789
3250
  };
2790
3251
  const tryGetStaticStringFromBabel = (node) => {
2791
3252
  if (!node) {
@@ -2882,17 +3343,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2882
3343
  name: generatedName2,
2883
3344
  selector: {
2884
3345
  kind: "withinTestIdByLabel",
2885
- rootFormattedDataTestId: wrapperTestId,
2886
- formattedLabel: label,
3346
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3347
+ label: createPomStringPattern(label, "static"),
2887
3348
  exact: true
2888
3349
  },
2889
- params: { annotationText: `string = ""` }
3350
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2890
3351
  });
2891
3352
  if (added2) {
2892
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3353
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2893
3354
  }
2894
3355
  }
2895
- return;
3356
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2896
3357
  }
2897
3358
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2898
3359
  if (dataTestIdEntry.pom) {
@@ -2904,19 +3365,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2904
3365
  name: generatedName,
2905
3366
  selector: {
2906
3367
  kind: "withinTestIdByLabel",
2907
- rootFormattedDataTestId: wrapperTestId,
2908
- formattedLabel: "${value}",
3368
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3369
+ label: createPomStringPattern("${value}", "parameterized"),
2909
3370
  exact: true
2910
3371
  },
2911
- params: { value: "string", annotationText: `string = ""` }
3372
+ parameters: [
3373
+ createPomParameterSpec("value", "string"),
3374
+ createPomParameterSpec("annotationText", `string = ""`)
3375
+ ]
2912
3376
  });
2913
3377
  if (added) {
2914
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3378
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3379
+ createPomParameterSpec("value", "string"),
3380
+ createPomParameterSpec("annotationText", `string = ""`)
3381
+ ]));
2915
3382
  }
2916
- return;
3383
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2917
3384
  }
2918
3385
  const staticKeyValues = args.keyValuesOverride ?? null;
2919
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3386
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2920
3387
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2921
3388
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2922
3389
  if (dataTestIdEntry.pom) {
@@ -2935,19 +3402,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2935
3402
  name: generatedName,
2936
3403
  selector: {
2937
3404
  kind: "testId",
2938
- formattedDataTestId: formattedDataTestIdForPom
3405
+ testId: selectorPattern
2939
3406
  },
2940
3407
  keyLiteral: rawValue,
2941
- params: { wait: "boolean = true", annotationText: 'string = ""' }
3408
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2942
3409
  });
2943
3410
  if (added) {
2944
- registerGeneratedMethodSignature(generatedName, {
2945
- params: `wait: boolean = true, annotationText: string = ""`,
2946
- argNames: ["wait", "annotationText"]
2947
- });
3411
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3412
+ createPomParameterSpec("wait", "boolean = true"),
3413
+ createPomParameterSpec("annotationText", 'string = ""')
3414
+ ]));
2948
3415
  }
2949
3416
  }
2950
- return;
3417
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2951
3418
  }
2952
3419
  if (dataTestIdEntry.pom) {
2953
3420
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2961,6 +3428,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2961
3428
  const generatedName = getGeneratedMethodName();
2962
3429
  registerGeneratedMethodSignature(generatedName, signature);
2963
3430
  }
3431
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2964
3432
  }
2965
3433
  function safeRealpath(value) {
2966
3434
  try {
@@ -3933,127 +4401,20 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
3933
4401
  debugLog("closing internal vite server");
3934
4402
  await server.close();
3935
4403
  }
3936
- } finally {
3937
- restoreDomShim();
3938
- }
3939
- });
3940
- }
3941
- const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
3942
- const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
3943
- const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
3944
- class VuePomGeneratorError extends Error {
3945
- constructor(message) {
3946
- const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
3947
- super(normalized);
3948
- this.name = "VuePomGeneratorError";
3949
- }
3950
- }
3951
- function splitParameterList(parameters) {
3952
- const parts = [];
3953
- let current = "";
3954
- let braceDepth = 0;
3955
- let bracketDepth = 0;
3956
- let parenDepth = 0;
3957
- let angleDepth = 0;
3958
- let inSingleQuote = false;
3959
- let inDoubleQuote = false;
3960
- let inTemplateString = false;
3961
- for (let index = 0; index < parameters.length; index += 1) {
3962
- const char = parameters[index];
3963
- const previous = index > 0 ? parameters[index - 1] : "";
3964
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3965
- inSingleQuote = !inSingleQuote;
3966
- current += char;
3967
- continue;
3968
- }
3969
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3970
- inDoubleQuote = !inDoubleQuote;
3971
- current += char;
3972
- continue;
3973
- }
3974
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3975
- inTemplateString = !inTemplateString;
3976
- current += char;
3977
- continue;
3978
- }
3979
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3980
- current += char;
3981
- continue;
3982
- }
3983
- switch (char) {
3984
- case "{":
3985
- braceDepth += 1;
3986
- break;
3987
- case "}":
3988
- braceDepth -= 1;
3989
- break;
3990
- case "[":
3991
- bracketDepth += 1;
3992
- break;
3993
- case "]":
3994
- bracketDepth -= 1;
3995
- break;
3996
- case "(":
3997
- parenDepth += 1;
3998
- break;
3999
- case ")":
4000
- parenDepth -= 1;
4001
- break;
4002
- case "<":
4003
- angleDepth += 1;
4004
- break;
4005
- case ">":
4006
- angleDepth -= 1;
4007
- break;
4008
- case ",":
4009
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
4010
- const trimmed2 = current.trim();
4011
- if (trimmed2) {
4012
- parts.push(trimmed2);
4013
- }
4014
- current = "";
4015
- continue;
4016
- }
4017
- break;
4018
- }
4019
- current += char;
4020
- }
4021
- const trimmed = current.trim();
4022
- if (trimmed) {
4023
- parts.push(trimmed);
4024
- }
4025
- return parts;
4026
- }
4027
- function parseParameterSignature(parameter) {
4028
- const colonIndex = parameter.indexOf(":");
4029
- if (colonIndex < 0) {
4030
- return { name: parameter.trim() };
4031
- }
4032
- const rawName = parameter.slice(0, colonIndex).trim();
4033
- const hasQuestionToken = rawName.endsWith("?");
4034
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
4035
- const remainder = parameter.slice(colonIndex + 1).trim();
4036
- const initializerIndex = remainder.lastIndexOf("=");
4037
- if (initializerIndex < 0) {
4038
- return {
4039
- name,
4040
- hasQuestionToken,
4041
- type: remainder || void 0
4042
- };
4043
- }
4044
- return {
4045
- name,
4046
- hasQuestionToken,
4047
- type: remainder.slice(0, initializerIndex).trim() || void 0,
4048
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
4049
- };
4404
+ } finally {
4405
+ restoreDomShim();
4406
+ }
4407
+ });
4050
4408
  }
4051
- function parseParameterSignatures(parameters) {
4052
- const trimmed = parameters.trim();
4053
- if (!trimmed) {
4054
- return [];
4409
+ const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
4410
+ const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
4411
+ const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
4412
+ class VuePomGeneratorError extends Error {
4413
+ constructor(message) {
4414
+ const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
4415
+ super(normalized);
4416
+ this.name = "VuePomGeneratorError";
4055
4417
  }
4056
- return splitParameterList(trimmed).map(parseParameterSignature);
4057
4418
  }
4058
4419
  function toPosixRelativePath(fromDir, toFile) {
4059
4420
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
@@ -4074,6 +4435,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
4074
4435
  const root = projectRoot ?? process.cwd();
4075
4436
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
4076
4437
  }
4438
+ function createMissingCustomPomDirectoryError(configuredDir, resolvedDir) {
4439
+ return new VuePomGeneratorError(
4440
+ `Custom POM directory "${configuredDir}" does not exist.
4441
+ Resolved path: ${resolvedDir}
4442
+ Create the directory, point generation.playwright.customPoms.dir at the correct location, or remove the customPoms configuration.`
4443
+ );
4444
+ }
4445
+ function createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir) {
4446
+ const renderedClassNames = missingClassNames.map((name) => `"${name}"`).join(", ");
4447
+ return new VuePomGeneratorError(
4448
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.
4449
+ Expected matching helper files/exports under "${configuredDir}".
4450
+ Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.`
4451
+ );
4452
+ }
4077
4453
  function createCustomPomImportCollisionError(exportName, requested) {
4078
4454
  return new VuePomGeneratorError(
4079
4455
  `Custom POM import name collision detected for "${exportName}".
@@ -4212,41 +4588,31 @@ function generateGoToSelfMethod(componentName) {
4212
4588
  })
4213
4589
  ];
4214
4590
  }
4215
- function formatMethodParams(params) {
4216
- if (!params)
4217
- return "";
4218
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4219
- const entries = Object.entries(params);
4220
- if (!entries.length)
4221
- return "";
4222
- const score = (name) => {
4223
- const idx = preferredOrder.indexOf(name);
4224
- return idx < 0 ? 999 : idx;
4225
- };
4226
- return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
4591
+ function getSelectorPatterns(selector) {
4592
+ return selector.kind === "testId" ? [selector.testId] : [selector.rootTestId, selector.label];
4227
4593
  }
4228
- function generateExtraClickMethodMembers(spec) {
4594
+ function generateExtraClickMethodMembers(spec, componentName) {
4229
4595
  if (spec.kind !== "click") {
4230
4596
  return [];
4231
4597
  }
4232
- const params = spec.params ?? {};
4233
- const signatureParams = formatMethodParams(params);
4234
- const parameters = parseParameterSignatures(signatureParams);
4235
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4236
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
4598
+ const selectorPatterns = getSelectorPatterns(spec.selector);
4599
+ const signatureSpecs = orderPomPatternParameters(
4600
+ spec.parameters,
4601
+ selectorPatterns,
4602
+ { omit: spec.keyLiteral !== void 0 ? ["key"] : [] }
4603
+ );
4604
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
4605
+ const hasAnnotationText = signatureSpecs.some((param) => param.name === "annotationText");
4606
+ const hasWait = signatureSpecs.some((param) => param.name === "wait");
4237
4607
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4238
4608
  const waitArg = hasWait ? "wait" : "true";
4609
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
4610
+ componentName,
4611
+ methodName: stripPomActionPrefix(spec.name),
4612
+ nativeRole: "button"
4613
+ }));
4239
4614
  if (spec.selector.kind === "testId") {
4240
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4241
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4242
- const clickArgs = [];
4243
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4244
- if (hasAnnotationText || hasWait) {
4245
- clickArgs.push(annotationArg);
4246
- }
4247
- if (hasWait) {
4248
- clickArgs.push(waitArg);
4249
- }
4615
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4250
4616
  return [
4251
4617
  createClassMethod({
4252
4618
  name: spec.name,
@@ -4256,20 +4622,16 @@ function generateExtraClickMethodMembers(spec) {
4256
4622
  if (spec.keyLiteral !== void 0) {
4257
4623
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4258
4624
  }
4259
- if (needsTemplate) {
4260
- writer.writeLine(`const testId = ${testIdExpr};`);
4625
+ for (const statement of testIdBinding.setupStatements) {
4626
+ writer.writeLine(statement);
4261
4627
  }
4262
- writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4628
+ writer.writeLine(`await this.clickByTestId(${testIdBinding.expression}, ${annotationArg}, ${waitArg}, ${locatorDescription});`);
4263
4629
  }
4264
4630
  })
4265
4631
  ];
4266
4632
  }
4267
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4268
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4269
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4270
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4271
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4272
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4633
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4634
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4273
4635
  return [
4274
4636
  createClassMethod({
4275
4637
  name: spec.name,
@@ -4279,45 +4641,45 @@ function generateExtraClickMethodMembers(spec) {
4279
4641
  if (spec.keyLiteral !== void 0) {
4280
4642
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4281
4643
  }
4282
- if (rootNeedsTemplate) {
4283
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4644
+ for (const statement of rootBinding.setupStatements) {
4645
+ writer.writeLine(statement);
4284
4646
  }
4285
- if (labelNeedsTemplate) {
4286
- writer.writeLine(`const label = ${labelExpr};`);
4647
+ for (const statement of labelBinding.setupStatements) {
4648
+ writer.writeLine(statement);
4287
4649
  }
4288
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4650
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg}, { description: ${locatorDescription} });`);
4289
4651
  }
4290
4652
  })
4291
4653
  ];
4292
4654
  }
4293
- function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4655
+ function generateMethodMembersFromPom(componentName, primary, targetPageObjectModelClass) {
4294
4656
  if (primary.emitPrimary === false) {
4295
4657
  return [];
4296
4658
  }
4297
4659
  return generateViewObjectModelMembers(
4660
+ componentName,
4298
4661
  targetPageObjectModelClass,
4299
4662
  primary.methodName,
4300
4663
  primary.nativeRole,
4301
- primary.formattedDataTestId,
4302
- primary.alternateFormattedDataTestIds,
4664
+ primary.selector,
4665
+ primary.alternateSelectors,
4303
4666
  primary.getterNameOverride,
4304
- primary.params ?? {}
4667
+ primary.parameters
4305
4668
  );
4306
4669
  }
4307
- function generateMethodsContentForDependencies(dependencies) {
4670
+ function generateMethodsContentForDependencies(componentName, dependencies) {
4308
4671
  const entries = Array.from(dependencies.dataTestIdSet ?? []);
4309
4672
  const primarySpecsAll = entries.map((e) => ({ pom: e.pom, target: e.targetPageObjectModelClass })).filter((x) => !!x.pom).sort((a, b) => a.pom.methodName.localeCompare(b.pom.methodName));
4310
4673
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4311
4674
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4312
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4313
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4675
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4314
4676
  const key = JSON.stringify({
4315
4677
  role: pom.nativeRole,
4316
4678
  methodName: pom.methodName,
4317
4679
  getterNameOverride: pom.getterNameOverride ?? null,
4318
- testId: pom.formattedDataTestId,
4319
- alternateTestIds: alternates.length ? alternates : void 0,
4320
- params: stableParams,
4680
+ selector: pom.selector,
4681
+ alternateSelectors: alternates.length ? alternates : void 0,
4682
+ parameters: pom.parameters,
4321
4683
  target: target ?? null,
4322
4684
  emitPrimary: pom.emitPrimary ?? true
4323
4685
  });
@@ -4330,10 +4692,10 @@ function generateMethodsContentForDependencies(dependencies) {
4330
4692
  const extras = (dependencies.pomExtraMethods ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
4331
4693
  const members = [];
4332
4694
  for (const { pom, target } of primarySpecs) {
4333
- members.push(...generateMethodMembersFromPom(pom, target));
4695
+ members.push(...generateMethodMembersFromPom(componentName, pom, target));
4334
4696
  }
4335
4697
  for (const extra of extras) {
4336
- members.push(...generateExtraClickMethodMembers(extra));
4698
+ members.push(...generateExtraClickMethodMembers(extra, componentName));
4337
4699
  }
4338
4700
  return members;
4339
4701
  }
@@ -4344,6 +4706,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4344
4706
  customPomAttachments = [],
4345
4707
  projectRoot,
4346
4708
  customPomDir,
4709
+ requireCustomPomDir,
4347
4710
  customPomImportAliases,
4348
4711
  customPomImportNameCollisionBehavior = "error",
4349
4712
  testIdAttribute,
@@ -4376,6 +4739,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4376
4739
  customPomAttachments,
4377
4740
  projectRoot,
4378
4741
  customPomDir,
4742
+ requireCustomPomDir,
4379
4743
  customPomImportAliases,
4380
4744
  customPomImportNameCollisionBehavior,
4381
4745
  testIdAttribute,
@@ -4385,6 +4749,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4385
4749
  customPomAttachments,
4386
4750
  projectRoot,
4387
4751
  customPomDir,
4752
+ requireCustomPomDir,
4388
4753
  customPomImportAliases,
4389
4754
  customPomImportNameCollisionBehavior,
4390
4755
  testIdAttribute,
@@ -4440,9 +4805,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4440
4805
  }
4441
4806
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4442
4807
  customPomDir: options.customPomDir,
4808
+ requireCustomPomDir: options.requireCustomPomDir,
4443
4809
  customPomImportAliases: options.customPomImportAliases,
4444
4810
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4445
4811
  });
4812
+ assertCustomPomAttachmentsResolved(
4813
+ options.customPomAttachments ?? [],
4814
+ customPomImportResolution.classIdentifierMap,
4815
+ options.customPomDir ?? "tests/playwright/pom/custom"
4816
+ );
4446
4817
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4447
4818
  const files = [];
4448
4819
  for (const [name, deps] of entries) {
@@ -4601,21 +4972,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4601
4972
  return { filePath, content };
4602
4973
  });
4603
4974
  }
4604
- function toCSharpTestIdExpression(formattedDataTestId) {
4605
- const needsInterpolation = formattedDataTestId.includes("${");
4606
- if (!needsInterpolation) {
4607
- return JSON.stringify(formattedDataTestId);
4608
- }
4609
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4610
- const quoted = JSON.stringify(inner);
4611
- return `$${quoted}`;
4612
- }
4613
- function toCSharpParam(paramTypeExpr) {
4614
- const trimmed = (paramTypeExpr ?? "").trim();
4615
- const eqIdx = trimmed.indexOf("=");
4616
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4617
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4618
- const typePart = left.includes("|") ? "string" : left;
4975
+ function toCSharpParam(param) {
4976
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4619
4977
  let type = "string";
4620
4978
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4621
4979
  type = "bool";
@@ -4623,19 +4981,17 @@ function toCSharpParam(paramTypeExpr) {
4623
4981
  type = "string";
4624
4982
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4625
4983
  type = "int";
4626
- else if (/\d+/.test(typePart) && typePart === "")
4627
- type = "int";
4628
4984
  else if (/\btimeOut\b/i.test(typePart))
4629
4985
  type = "int";
4630
4986
  let defaultExpr;
4631
- if (right !== void 0) {
4987
+ if (param.initializer !== void 0) {
4632
4988
  if (type === "bool") {
4633
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : void 0;
4989
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : void 0;
4634
4990
  } else if (type === "int") {
4635
- const m = right.match(/\d+/);
4991
+ const m = param.initializer.match(/\d+/);
4636
4992
  defaultExpr = m ? m[0] : void 0;
4637
4993
  } else {
4638
- if (right === '""' || right === '""' || right === "''") {
4994
+ if (param.initializer === '""' || param.initializer === "''") {
4639
4995
  defaultExpr = '""';
4640
4996
  }
4641
4997
  }
@@ -4643,17 +4999,15 @@ function toCSharpParam(paramTypeExpr) {
4643
4999
  return { type, defaultExpr };
4644
5000
  }
4645
5001
  function formatCSharpParams(params) {
4646
- if (!params)
4647
- return { signature: "", argNames: [] };
4648
- const entries = Object.entries(params);
4649
- if (!entries.length)
5002
+ const normalizedParams = normalizePomParameters(params);
5003
+ if (!normalizedParams.length)
4650
5004
  return { signature: "", argNames: [] };
4651
5005
  const signatureParts = [];
4652
5006
  const argNames = [];
4653
- for (const [name, typeExpr] of entries) {
4654
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4655
- argNames.push(name);
4656
- signatureParts.push(defaultExpr !== void 0 ? `${type} ${name} = ${defaultExpr}` : `${type} ${name}`);
5007
+ for (const param of normalizedParams) {
5008
+ const { type, defaultExpr } = toCSharpParam(param);
5009
+ argNames.push(param.name);
5010
+ signatureParts.push(defaultExpr !== void 0 ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
4657
5011
  }
4658
5012
  return { signature: signatureParts.join(", "), argNames };
4659
5013
  }
@@ -4744,23 +5098,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4744
5098
  const baseMethodName = upperFirst(pom.methodName);
4745
5099
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4746
5100
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4747
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4748
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4749
- const templateVars = templateVarMatches.map((m) => m[1]);
4750
- const augmentedParams = { ...pom.params };
4751
- for (const v of templateVars) {
4752
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4753
- augmentedParams[v] = "string";
4754
- }
4755
- }
4756
- const orderedParams = Object.fromEntries([
4757
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4758
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4759
- ]);
5101
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
5102
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
5103
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4760
5104
  const { signature, argNames } = formatCSharpParams(orderedParams);
4761
5105
  const args = argNames.join(", ");
4762
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4763
- if (pom.formattedDataTestId.includes("${")) {
5106
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
5107
+ if (selectorIsParameterized) {
4764
5108
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4765
5109
  } else {
4766
5110
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4771,12 +5115,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4771
5115
  if (target) {
4772
5116
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4773
5117
  chunks.push(" {");
4774
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4775
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
5118
+ if (selectorIsParameterized || allTestIds.length <= 1) {
5119
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4776
5120
  chunks.push(` return new ${target}(Page);`);
4777
5121
  } else {
4778
5122
  chunks.push(" Exception? lastError = null;");
4779
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5123
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4780
5124
  chunks.push(" {");
4781
5125
  chunks.push(" try");
4782
5126
  chunks.push(" {");
@@ -4800,7 +5144,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4800
5144
  }
4801
5145
  chunks.push(` public async Task ${actionName}(${sig})`);
4802
5146
  chunks.push(" {");
4803
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5147
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4804
5148
  const emitActionCall = (locatorAccess) => {
4805
5149
  if (pom.nativeRole === "input") {
4806
5150
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4813,9 +5157,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4813
5157
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4814
5158
  }
4815
5159
  };
4816
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5160
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4817
5161
  chunks.push(" Exception? lastError = null;");
4818
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5162
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4819
5163
  chunks.push(" {");
4820
5164
  chunks.push(" try");
4821
5165
  chunks.push(" {");
@@ -4861,7 +5205,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4861
5205
  for (const extra of extras) {
4862
5206
  if (extra.kind !== "click")
4863
5207
  continue;
4864
- const { signature } = formatCSharpParams(extra.params);
5208
+ const extraParams = orderPomPatternParameters(
5209
+ extra.parameters,
5210
+ getSelectorPatterns(extra.selector),
5211
+ { omit: extra.keyLiteral !== void 0 ? ["key"] : [] }
5212
+ );
5213
+ const { signature } = formatCSharpParams(extraParams);
4865
5214
  const extraName = upperFirst(extra.name);
4866
5215
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4867
5216
  chunks.push(" {");
@@ -4869,29 +5218,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4869
5218
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4870
5219
  }
4871
5220
  if (extra.selector.kind === "testId") {
4872
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4873
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4874
- if (needsTemplate) {
4875
- chunks.push(` var testId = ${testIdExpr};`);
4876
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4877
- } else {
4878
- chunks.push(` await LocatorByTestId(${testIdExpr}).ClickAsync();`);
5221
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
5222
+ for (const statement of testIdBinding.setupStatements) {
5223
+ chunks.push(` ${statement}`);
4879
5224
  }
5225
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4880
5226
  } else {
4881
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4882
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4883
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4884
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5227
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5228
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4885
5229
  const exactArg = extra.selector.exact === false ? "false" : "true";
4886
- if (rootNeedsTemplate) {
4887
- chunks.push(` var rootTestId = ${rootExpr};`);
5230
+ for (const statement of rootBinding.setupStatements) {
5231
+ chunks.push(` ${statement}`);
4888
5232
  }
4889
- if (labelNeedsTemplate) {
4890
- chunks.push(` var label = ${labelExpr};`);
5233
+ for (const statement of labelBinding.setupStatements) {
5234
+ chunks.push(` ${statement}`);
4891
5235
  }
4892
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4893
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4894
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5236
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4895
5237
  }
4896
5238
  chunks.push(" }");
4897
5239
  chunks.push("");
@@ -5207,7 +5549,7 @@ function prepareViewObjectModelClass(componentName, dependencies, componentHiera
5207
5549
  members.push(...generateRouteProperty(routeMeta));
5208
5550
  members.push(...generateGoToSelfMethod(className));
5209
5551
  }
5210
- members.push(...generateMethodsContentForDependencies(dependencies));
5552
+ members.push(...generateMethodsContentForDependencies(componentName, dependencies));
5211
5553
  return {
5212
5554
  className,
5213
5555
  componentRefsForInstances,
@@ -5335,7 +5677,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5335
5677
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5336
5678
  continue;
5337
5679
  const list = methodToChildren.get(name) ?? [];
5338
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5680
+ list.push({ childProp, signature: sig });
5339
5681
  methodToChildren.set(name, list);
5340
5682
  }
5341
5683
  }
@@ -5345,12 +5687,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5345
5687
  return [];
5346
5688
  }
5347
5689
  return passthroughs.map(([methodName, candidates]) => {
5348
- const { childProp, params, argNames } = candidates[0];
5349
- const callArgs = argNames.join(", ");
5690
+ const { childProp, signature } = candidates[0];
5691
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5350
5692
  return createClassMethod({
5351
5693
  name: methodName,
5352
5694
  isAsync: true,
5353
- parameters: parseParameterSignatures(params),
5695
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5354
5696
  statements: [
5355
5697
  `return await this.${childProp}.${methodName}(${callArgs});`
5356
5698
  ]
@@ -5374,8 +5716,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5374
5716
  const list = methodToAttachments.get(methodName) ?? [];
5375
5717
  list.push({
5376
5718
  propertyName: attachment.propertyName,
5377
- params: signature.params,
5378
- argNames: signature.argNames
5719
+ signature
5379
5720
  });
5380
5721
  methodToAttachments.set(methodName, list);
5381
5722
  }
@@ -5386,12 +5727,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5386
5727
  return [];
5387
5728
  }
5388
5729
  return passthroughs.map(([methodName, candidates]) => {
5389
- const { propertyName, params, argNames } = candidates[0];
5390
- const callArgs = argNames.join(", ");
5730
+ const { propertyName, signature } = candidates[0];
5731
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5391
5732
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5392
5733
  return createClassMethod({
5393
5734
  name: methodName,
5394
- parameters: parseParameterSignatures(params),
5735
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5395
5736
  statements: [
5396
5737
  `return ${invocation};`
5397
5738
  ]
@@ -5405,15 +5746,44 @@ function sliceNodeSource(source, node) {
5405
5746
  const snippet = source.slice(node.start, node.end).trim();
5406
5747
  return snippet.length ? snippet : null;
5407
5748
  }
5408
- function getCustomPomCallArgumentName(param) {
5749
+ function getTypeAnnotationSource(source, node) {
5750
+ const rawTypeAnnotation = node.typeAnnotation;
5751
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
5752
+ return void 0;
5753
+ }
5754
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
5755
+ return typeAnnotation && typeof typeAnnotation === "object" ? sliceNodeSource(source, typeAnnotation) ?? void 0 : void 0;
5756
+ }
5757
+ function getCustomPomParameterSpec(source, param) {
5409
5758
  if (param.type === "Identifier") {
5410
- return param.name;
5759
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5760
+ hasQuestionToken: !!param.optional
5761
+ });
5411
5762
  }
5412
5763
  if (param.type === "AssignmentPattern") {
5413
- return param.left.type === "Identifier" ? param.left.name : null;
5764
+ if (param.left.type !== "Identifier") {
5765
+ return null;
5766
+ }
5767
+ const initializer = sliceNodeSource(source, param.right);
5768
+ if (!initializer) {
5769
+ return null;
5770
+ }
5771
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
5772
+ initializer,
5773
+ hasQuestionToken: !!param.left.optional
5774
+ });
5414
5775
  }
5415
5776
  if (param.type === "RestElement") {
5416
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
5777
+ if (param.argument.type !== "Identifier") {
5778
+ return null;
5779
+ }
5780
+ const typeExpression = getTypeAnnotationSource(
5781
+ source,
5782
+ param
5783
+ ) ?? getTypeAnnotationSource(source, param.argument);
5784
+ return createPomParameterSpec(param.argument.name, typeExpression, {
5785
+ isRestParameter: true
5786
+ });
5417
5787
  }
5418
5788
  return null;
5419
5789
  }
@@ -5446,29 +5816,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5446
5816
  if (member.key.type !== "Identifier") {
5447
5817
  continue;
5448
5818
  }
5449
- const params = [];
5450
- const argNames = [];
5819
+ const parameters = [];
5451
5820
  let supported = true;
5452
5821
  member.params.forEach((param) => {
5453
5822
  if (!supported) {
5454
5823
  return;
5455
5824
  }
5456
- const paramSource = sliceNodeSource(source, param);
5457
- const argName = getCustomPomCallArgumentName(param);
5458
- if (!paramSource || !argName) {
5825
+ const parameter = getCustomPomParameterSpec(source, param);
5826
+ if (!parameter) {
5459
5827
  supported = false;
5460
5828
  return;
5461
5829
  }
5462
- params.push(paramSource);
5463
- argNames.push(argName);
5830
+ parameters.push(parameter);
5464
5831
  });
5465
5832
  if (!supported) {
5466
5833
  continue;
5467
5834
  }
5468
- signatures.set(member.key.name, {
5469
- params: params.join(", "),
5470
- argNames
5471
- });
5835
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5472
5836
  }
5473
5837
  }
5474
5838
  return signatures;
@@ -5651,6 +6015,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5651
6015
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5652
6016
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5653
6017
  if (!fs.existsSync(customDirAbs)) {
6018
+ if (options.requireCustomPomDir) {
6019
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
6020
+ }
5654
6021
  return {
5655
6022
  classIdentifierMap,
5656
6023
  methodSignaturesByClass,
@@ -5693,6 +6060,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5693
6060
  importSpecifiersByClass
5694
6061
  };
5695
6062
  }
6063
+ function assertCustomPomAttachmentsResolved(attachments, classIdentifierMap, configuredDir) {
6064
+ const missingClassNames = Array.from(new Set(
6065
+ attachments.map((attachment) => attachment.className).filter((className) => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className))
6066
+ )).sort((left, right) => left.localeCompare(right));
6067
+ if (missingClassNames.length > 0) {
6068
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
6069
+ }
6070
+ }
5696
6071
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5697
6072
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5698
6073
  if (!filePath)
@@ -5721,7 +6096,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5721
6096
  if (!sig)
5722
6097
  continue;
5723
6098
  const list = methodToChildren.get(name) ?? [];
5724
- list.push({ child, params: sig.params, argNames: sig.argNames });
6099
+ list.push({ child, signature: sig });
5725
6100
  methodToChildren.set(name, list);
5726
6101
  }
5727
6102
  }
@@ -5729,12 +6104,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5729
6104
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5730
6105
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5731
6106
  continue;
5732
- const { child, params, argNames } = candidatesForMethod[0];
5733
- const callArgs = argNames.join(", ");
6107
+ const { child, signature } = candidatesForMethod[0];
6108
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5734
6109
  passthroughMembers.push(createClassMethod({
5735
6110
  name: methodName,
5736
6111
  isAsync: true,
5737
- parameters: parseParameterSignatures(params),
6112
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5738
6113
  statements: [
5739
6114
  `return await this.${child}.${methodName}(${callArgs});`
5740
6115
  ]
@@ -5783,9 +6158,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5783
6158
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5784
6159
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5785
6160
  customPomDir: options.customPomDir,
6161
+ requireCustomPomDir: options.requireCustomPomDir,
5786
6162
  customPomImportAliases: options.customPomImportAliases,
5787
6163
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5788
6164
  });
6165
+ assertCustomPomAttachmentsResolved(
6166
+ options.customPomAttachments ?? [],
6167
+ customPomImportResolution.classIdentifierMap,
6168
+ options.customPomDir ?? "tests/playwright/pom/custom"
6169
+ );
5789
6170
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5790
6171
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5791
6172
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5935,8 +6316,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5935
6316
  return candidate;
5936
6317
  };
5937
6318
  for (const dt of dataTestIdSet) {
5938
- const raw = dt.value;
5939
- if (raw.includes("${")) {
6319
+ const raw = dt.selectorValue.formatted;
6320
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5940
6321
  continue;
5941
6322
  }
5942
6323
  const toggleSuffix = "-toggle";
@@ -6026,7 +6407,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
6026
6407
  });
6027
6408
  }
6028
6409
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
6029
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
6030
6410
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
6031
6411
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
6032
6412
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6288,7 +6668,7 @@ function getConditionalDirectiveInfo(element) {
6288
6668
  if (directive.name === "else") {
6289
6669
  const exp2 = directive.exp;
6290
6670
  if (exp2 && (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || exp2.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6291
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6671
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6292
6672
  return { kind: "else-if", source: source2 };
6293
6673
  }
6294
6674
  return { kind: "else", source: "" };
@@ -6297,13 +6677,13 @@ function getConditionalDirectiveInfo(element) {
6297
6677
  const exp2 = directive.exp;
6298
6678
  if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp2.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6299
6679
  return null;
6300
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6680
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6301
6681
  return { kind: "else-if", source: source2 };
6302
6682
  }
6303
6683
  const exp = directive.exp;
6304
6684
  if (!exp || exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6305
6685
  return null;
6306
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
6686
+ const source = getVueExpressionSource(exp, "content", "compiled");
6307
6687
  return { kind: directive.name, source };
6308
6688
  }
6309
6689
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6329,26 +6709,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6329
6709
  };
6330
6710
  try {
6331
6711
  const expr = parser.parseExpression(src, { plugins: ["typescript"] });
6332
- const isNodeType = (n, type) => {
6712
+ const isNodeType2 = (n, type) => {
6333
6713
  return n !== null && n.type === type;
6334
6714
  };
6335
- const isStringLiteralNode = (n) => {
6336
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6715
+ const isStringLiteralNode2 = (n) => {
6716
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6337
6717
  };
6338
- const isIdentifierNode = (n) => {
6339
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6718
+ const isIdentifierNode2 = (n) => {
6719
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6340
6720
  };
6341
6721
  const results = [];
6342
6722
  const walk = (n) => {
6343
6723
  if (!n)
6344
6724
  return;
6345
- if (isStringLiteralNode(n)) {
6725
+ if (isStringLiteralNode2(n)) {
6346
6726
  const v = (n.value ?? "").trim();
6347
6727
  if (isIdentifierish(v)) {
6348
6728
  results.push(v);
6349
6729
  }
6350
6730
  }
6351
- if (isIdentifierNode(n)) {
6731
+ if (isIdentifierNode2(n)) {
6352
6732
  const v = (n.name ?? "").trim();
6353
6733
  if (isIdentifierish(v)) {
6354
6734
  results.push(v);
@@ -6498,34 +6878,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6498
6878
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6499
6879
  return null;
6500
6880
  }
6501
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6881
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6502
6882
  const jsStringLiteral = (value) => {
6503
6883
  return JSON.stringify(value);
6504
6884
  };
6505
6885
  const getTestIdExpressionForNode = () => {
6506
- const existing = findTestIdAttribute(element, testIdAttribute);
6507
- if (!existing) {
6508
- return "undefined";
6509
- }
6510
- if (existing.type === compilerCore.NodeTypes.ATTRIBUTE) {
6511
- const v = existing.value?.content;
6512
- if (!v) {
6513
- return "undefined";
6514
- }
6515
- return jsStringLiteral(v);
6516
- }
6517
- const directive = existing;
6518
- const exp2 = directive.exp;
6519
- if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
6886
+ if (!resolvedRuntimeTestId) {
6520
6887
  return "undefined";
6521
6888
  }
6522
- const content = (exp2.content ?? "").trim();
6523
- if (!content) {
6524
- return "undefined";
6889
+ if (resolvedRuntimeTestId.kind === "static") {
6890
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6525
6891
  }
6526
- return `(${content})`;
6892
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6527
6893
  };
6528
- const testIdExpression2 = getTestIdExpressionForNode();
6894
+ const testIdExpression = getTestIdExpressionForNode();
6529
6895
  const clickDirective = tryGetClickDirective(element);
6530
6896
  if (!clickDirective)
6531
6897
  return;
@@ -6536,10 +6902,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6536
6902
  const exp = clickDirective.exp;
6537
6903
  if (!exp)
6538
6904
  return;
6539
- const existingSource = (exp.loc?.source ?? (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6905
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6540
6906
  if (existingSource.includes(CLICK_EVENT_NAME))
6541
6907
  return;
6542
- const originalExpression = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6908
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6543
6909
  if (!originalExpression)
6544
6910
  return;
6545
6911
  const isStatementBody = (() => {
@@ -6556,7 +6922,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6556
6922
  const statementWrappedHandler = `($event) => {
6557
6923
  const __win = ($event && $event.view) ? $event.view : undefined;
6558
6924
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6559
- const __testIdFromNode = ${testIdExpression2};
6925
+ const __testIdFromNode = ${testIdExpression};
6560
6926
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6561
6927
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6562
6928
  const __emit = (phase, err) => {
@@ -6567,16 +6933,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6567
6933
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6568
6934
  }
6569
6935
  } catch (e) {
6570
- // Instrumentation must never hide failures during e2e strict mode.
6571
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6572
- // Outside strict mode we log and continue so we don't break real user clicks.
6936
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6573
6937
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6574
6938
  if (__w && __w.console && typeof __w.console.error === 'function') {
6575
6939
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6576
6940
  }
6577
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6578
- throw e;
6579
- }
6941
+ throw e;
6580
6942
  }
6581
6943
  };
6582
6944
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6607,7 +6969,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6607
6969
  const expressionWrappedHandler = `($event) => {
6608
6970
  const __win = ($event && $event.view) ? $event.view : undefined;
6609
6971
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6610
- const __testIdFromNode = ${testIdExpression2};
6972
+ const __testIdFromNode = ${testIdExpression};
6611
6973
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6612
6974
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6613
6975
  const __emit = (phase, err) => {
@@ -6618,16 +6980,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6618
6980
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6619
6981
  }
6620
6982
  } catch (e) {
6621
- // Instrumentation must never hide failures during e2e strict mode.
6622
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6623
- // Outside strict mode we log and continue so we don't break real user clicks.
6983
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6624
6984
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6625
6985
  if (__w && __w.console && typeof __w.console.error === 'function') {
6626
6986
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6627
6987
  }
6628
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6629
- throw e;
6630
- }
6988
+ throw e;
6631
6989
  }
6632
6990
  };
6633
6991
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6665,10 +7023,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6665
7023
  let previousFileName = "";
6666
7024
  const hierarchyMap = /* @__PURE__ */ new Map();
6667
7025
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6668
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
7026
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6669
7027
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6670
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6671
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
7028
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6672
7029
  const warn = options.warn;
6673
7030
  const vueFilesPathMap = options.vueFilesPathMap;
6674
7031
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6744,7 +7101,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6744
7101
  conditionalHintByIfBranch.set(branch, hint);
6745
7102
  continue;
6746
7103
  }
6747
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
7104
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6748
7105
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6749
7106
  if (stable) {
6750
7107
  conditionalHintByIfBranch.set(branch, stable);
@@ -6806,17 +7163,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6806
7163
  nativeWrappers[element.tag] = { role: "grid" };
6807
7164
  }
6808
7165
  }
6809
- const getBestAvailableKeyValue = () => {
7166
+ const getBestAvailableKeyInfo = () => {
6810
7167
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6811
7168
  const isDirectVForChild = parentNode?.type === compilerCore.NodeTypes.FOR;
6812
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6813
- if (vForKey) return vForKey;
6814
- return getContainedInSlotDataKeyValue(element, hierarchyMap);
7169
+ const vForKeyInfo = (isDirectVForChild ? getKeyDirectiveInfo(element) : null) || getContainedInVForDirectiveKeyInfo(context, element, hierarchyMap);
7170
+ if (vForKeyInfo) {
7171
+ return vForKeyInfo;
7172
+ }
7173
+ return getContainedInSlotDataKeyInfo(element, hierarchyMap);
6815
7174
  };
6816
- const bestKeyInferred = getBestAvailableKeyValue();
6817
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6818
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6819
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7175
+ const bestKeyInfo = getBestAvailableKeyInfo();
7176
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7177
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6820
7178
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6821
7179
  const parentKey = context?.parent ? context.parent : null;
6822
7180
  const conditional = getConditionalDirectiveInfo(element);
@@ -6859,7 +7217,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6859
7217
  if (!cond) {
6860
7218
  conditionalHint = "else";
6861
7219
  } else {
6862
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
7220
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6863
7221
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6864
7222
  }
6865
7223
  }
@@ -6869,7 +7227,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6869
7227
  });
6870
7228
  if (showDirective?.exp && (showDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6871
7229
  const exp = showDirective.exp;
6872
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
7230
+ const source = getVueExpressionSource(exp, "content", "compiled");
6873
7231
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6874
7232
  if (showHint) {
6875
7233
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6903,13 +7261,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6903
7261
  const tagSuffix = getTagSuffix();
6904
7262
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6905
7263
  };
7264
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7265
+ const tagSuffix = getTagSuffix();
7266
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7267
+ };
6906
7268
  const getSubmitDataTestId = (identifier) => {
6907
7269
  const tagSuffix = getTagSuffix();
6908
7270
  return `${componentName}-${identifier}${tagSuffix}`;
6909
7271
  };
6910
7272
  const applyResolvedDataTestIdForElement = (args) => {
6911
7273
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6912
- applyResolvedDataTestId({
7274
+ return applyResolvedDataTestId({
6913
7275
  element,
6914
7276
  componentName,
6915
7277
  parentComponentName,
@@ -6919,8 +7281,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6919
7281
  generatedMethodContentByComponent,
6920
7282
  nativeRole,
6921
7283
  preferredGeneratedValue: args.preferredGeneratedValue,
6922
- bestKeyPlaceholder,
6923
- bestKeyVariable,
7284
+ preferredRuntimeValue: args.preferredRuntimeValue,
7285
+ keyInfo: bestKeyInfo,
6924
7286
  keyValuesOverride,
6925
7287
  entryOverrides: args.entryOverrides,
6926
7288
  semanticNameHint: args.semanticNameHint,
@@ -6938,7 +7300,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6938
7300
  return p.type === compilerCore.NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6939
7301
  }) ?? null;
6940
7302
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6941
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7303
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6942
7304
  const loc = element.loc?.start;
6943
7305
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6944
7306
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6947,7 +7309,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6947
7309
  Element: <${element.tag}>
6948
7310
  Handler: ${handlerSource}
6949
7311
 
6950
- Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name. You can also set errorBehavior = "ignore" to keep generic fallback behavior.`
7312
+ Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name.`
6951
7313
  );
6952
7314
  }
6953
7315
  if (nativeWrappersValue) {
@@ -7074,20 +7436,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7074
7436
  contextFilename: context.filename
7075
7437
  });
7076
7438
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
7077
- const idOrName = getIdOrName(element) || void 0;
7439
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
7078
7440
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
7079
7441
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
7080
7442
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
7081
7443
  const testId = getClickDataTestId(clickSuffix);
7082
- applyResolvedDataTestIdForElement({
7444
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7445
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
7083
7446
  preferredGeneratedValue: testId,
7447
+ preferredRuntimeValue: runtimeTestId,
7084
7448
  semanticNameHint: semanticNameHint2,
7085
7449
  semanticNameHintAlternates,
7086
7450
  pomMergeKey
7087
7451
  });
7088
- {
7089
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7090
- }
7452
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7091
7453
  return;
7092
7454
  }
7093
7455
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7097,7 +7459,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7097
7459
  if (!isRecognizedInteractiveRole) {
7098
7460
  return;
7099
7461
  }
7100
- const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7462
+ const identifierHint = getStaticIdOrNameHint(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7101
7463
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7102
7464
  applyResolvedDataTestIdForElement({
7103
7465
  preferredGeneratedValue,
@@ -7107,7 +7469,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7107
7469
  }
7108
7470
  const isSubmit = element.props.find((p) => p.type === compilerCore.NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7109
7471
  if (isSubmit) {
7110
- const identifier = getIdOrName(element) || innerText;
7472
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7111
7473
  if (!identifier) {
7112
7474
  const loc = element.loc?.start;
7113
7475
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7159,29 +7521,32 @@ function createBuildProcessorPlugin(options) {
7159
7521
  getSourceDirs,
7160
7522
  basePageClassPath,
7161
7523
  normalizedBasePagePath,
7524
+ generation,
7525
+ projectRootRef,
7526
+ nativeWrappers,
7527
+ excludedComponents,
7528
+ getWrapperSearchRoots,
7529
+ getResolvedRouterEntry,
7530
+ loggerRef
7531
+ } = options;
7532
+ const {
7162
7533
  outDir,
7163
7534
  emitLanguages,
7164
7535
  typescriptOutputStructure,
7165
7536
  csharp,
7166
7537
  generateFixtures,
7167
7538
  customPomAttachments,
7168
- projectRootRef,
7169
7539
  customPomDir,
7540
+ requireCustomPomDir,
7170
7541
  customPomImportAliases,
7171
7542
  customPomImportNameCollisionBehavior,
7172
7543
  testIdAttribute,
7173
7544
  nameCollisionBehavior,
7174
- missingSemanticNameBehavior = "error",
7175
7545
  existingIdBehavior,
7176
- nativeWrappers,
7177
- excludedComponents,
7178
- getWrapperSearchRoots,
7179
7546
  routerAwarePoms,
7180
- getResolvedRouterEntry,
7181
7547
  routerType,
7182
- routerModuleShims,
7183
- loggerRef
7184
- } = options;
7548
+ routerModuleShims
7549
+ } = generation;
7185
7550
  let lastGeneratedMetrics = {
7186
7551
  entryCount: 0,
7187
7552
  interactiveComponentCount: 0,
@@ -7280,10 +7645,9 @@ function createBuildProcessorPlugin(options) {
7280
7645
  excludedComponents,
7281
7646
  getViewsDirAbs(),
7282
7647
  {
7283
- existingIdBehavior: existingIdBehavior ?? "preserve",
7648
+ existingIdBehavior: existingIdBehavior ?? "error",
7284
7649
  testIdAttribute,
7285
7650
  nameCollisionBehavior,
7286
- missingSemanticNameBehavior,
7287
7651
  warn: (message) => loggerRef.current.warn(message),
7288
7652
  vueFilesPathMap,
7289
7653
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7383,6 +7747,7 @@ function createBuildProcessorPlugin(options) {
7383
7747
  customPomAttachments,
7384
7748
  projectRoot: projectRootRef.current,
7385
7749
  customPomDir,
7750
+ requireCustomPomDir,
7386
7751
  customPomImportAliases,
7387
7752
  customPomImportNameCollisionBehavior,
7388
7753
  testIdAttribute,
@@ -7414,6 +7779,11 @@ function createDevProcessorPlugin(options) {
7414
7779
  projectRootRef,
7415
7780
  normalizedBasePagePath,
7416
7781
  basePageClassPath,
7782
+ generation,
7783
+ getResolvedRouterEntry,
7784
+ loggerRef
7785
+ } = options;
7786
+ const {
7417
7787
  outDir,
7418
7788
  emitLanguages,
7419
7789
  typescriptOutputStructure,
@@ -7421,18 +7791,16 @@ function createDevProcessorPlugin(options) {
7421
7791
  generateFixtures,
7422
7792
  customPomAttachments,
7423
7793
  customPomDir,
7794
+ requireCustomPomDir,
7424
7795
  customPomImportAliases,
7425
7796
  customPomImportNameCollisionBehavior,
7426
- nameCollisionBehavior = "suffix",
7427
- missingSemanticNameBehavior = "error",
7797
+ nameCollisionBehavior,
7428
7798
  existingIdBehavior,
7429
7799
  testIdAttribute,
7430
7800
  routerAwarePoms,
7431
- getResolvedRouterEntry,
7432
7801
  routerType,
7433
- routerModuleShims,
7434
- loggerRef
7435
- } = options;
7802
+ routerModuleShims
7803
+ } = generation;
7436
7804
  let scheduleVueFileRegen = null;
7437
7805
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7438
7806
  path.resolve(projectRootRef.current),
@@ -7619,9 +7987,8 @@ function createDevProcessorPlugin(options) {
7619
7987
  excludedComponents,
7620
7988
  getViewsDirAbs(),
7621
7989
  {
7622
- existingIdBehavior: existingIdBehavior ?? "preserve",
7990
+ existingIdBehavior: existingIdBehavior ?? "error",
7623
7991
  nameCollisionBehavior,
7624
- missingSemanticNameBehavior,
7625
7992
  testIdAttribute,
7626
7993
  warn: (message) => loggerRef.current.warn(message),
7627
7994
  vueFilesPathMap: provisionalVuePathMap,
@@ -7672,6 +8039,7 @@ function createDevProcessorPlugin(options) {
7672
8039
  customPomAttachments,
7673
8040
  projectRoot: projectRootRef.current,
7674
8041
  customPomDir,
8042
+ requireCustomPomDir,
7675
8043
  customPomImportAliases,
7676
8044
  customPomImportNameCollisionBehavior,
7677
8045
  pageDirs: getPageDirs(),
@@ -7846,8 +8214,119 @@ function createDevProcessorPlugin(options) {
7846
8214
  }
7847
8215
  };
7848
8216
  }
7849
- function generateTestIdsModule(componentTestIds) {
7850
- const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0]));
8217
+ function removeByKeySegment(value) {
8218
+ const idx = value.indexOf("ByKey");
8219
+ if (idx < 0) {
8220
+ return value;
8221
+ }
8222
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
8223
+ }
8224
+ function hasRoleSuffix(baseName, roleSuffix) {
8225
+ if (baseName.endsWith(roleSuffix)) {
8226
+ return true;
8227
+ }
8228
+ const re = new RegExp(`^${roleSuffix}\\d+$`);
8229
+ return re.test(baseName);
8230
+ }
8231
+ function getGeneratedPropertyName(pom) {
8232
+ if (pom.getterNameOverride) {
8233
+ return pom.getterNameOverride;
8234
+ }
8235
+ const roleSuffix = upperFirst(pom.nativeRole || "Element");
8236
+ const baseName = upperFirst(pom.methodName);
8237
+ const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
8238
+ return pom.selector.patternKind === "parameterized" ? removeByKeySegment(propertyName) : propertyName;
8239
+ }
8240
+ function getGeneratedActionName(entry, pom) {
8241
+ const methodNameUpper = upperFirst(pom.methodName);
8242
+ const radioMethodNameUpper = upperFirst(pom.methodName || "Radio");
8243
+ const isNavigation = !!entry.targetPageObjectModelClass;
8244
+ if (isNavigation) {
8245
+ return `goTo${methodNameUpper}`;
8246
+ }
8247
+ switch (pom.nativeRole) {
8248
+ case "input":
8249
+ return `type${methodNameUpper}`;
8250
+ case "select":
8251
+ case "vselect":
8252
+ return `select${methodNameUpper}`;
8253
+ case "radio":
8254
+ return `select${radioMethodNameUpper}`;
8255
+ default:
8256
+ return `click${methodNameUpper}`;
8257
+ }
8258
+ }
8259
+ function matchesPrimarySelector(extraMethod, pom) {
8260
+ if (extraMethod.selector.kind !== "testId") {
8261
+ return false;
8262
+ }
8263
+ return extraMethod.selector.testId.formatted === pom.selector.formatted && extraMethod.selector.testId.patternKind === pom.selector.patternKind;
8264
+ }
8265
+ function getManifestEntry(componentName, entry, componentMetadata, extraMethods) {
8266
+ const testId = entry.selectorValue.formatted;
8267
+ const metadata = componentMetadata?.get(testId);
8268
+ const pom = entry.pom;
8269
+ const generatedActionName = pom ? getGeneratedActionName(entry, pom) : null;
8270
+ const extraActionNames = pom ? extraMethods.filter((extraMethod) => matchesPrimarySelector(extraMethod, pom)).map((extraMethod) => extraMethod.name).sort((a, b) => a.localeCompare(b)) : [];
8271
+ const generatedActionNames = Array.from(/* @__PURE__ */ new Set([
8272
+ ...generatedActionName ? [generatedActionName] : [],
8273
+ ...extraActionNames.filter((name) => name !== generatedActionName)
8274
+ ]));
8275
+ return {
8276
+ testId,
8277
+ selectorPatternKind: entry.selectorValue.patternKind,
8278
+ semanticName: metadata?.semanticName ?? (pom ? humanizePomMethodName(pom.methodName) : testId),
8279
+ locatorDescription: pom ? buildPomLocatorDescription({
8280
+ componentName,
8281
+ methodName: pom.methodName,
8282
+ nativeRole: pom.nativeRole
8283
+ }) : componentName,
8284
+ inferredRole: pom?.nativeRole ?? null,
8285
+ generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
8286
+ generatedActionName,
8287
+ generatedActionNames,
8288
+ emitPrimary: pom?.emitPrimary !== false,
8289
+ ...entry.targetPageObjectModelClass ? { targetPageObjectModelClass: entry.targetPageObjectModelClass } : {},
8290
+ ...metadata?.tag ? { sourceTag: metadata.tag } : {},
8291
+ ...metadata ? { sourceTagType: metadata.tagType } : {},
8292
+ ...metadata?.patchFlag !== void 0 ? { patchFlag: metadata.patchFlag } : {},
8293
+ ...metadata?.dynamicProps?.length ? { dynamicProps: metadata.dynamicProps } : {},
8294
+ ...metadata?.hasClickHandler !== void 0 ? { hasClickHandler: metadata.hasClickHandler } : {},
8295
+ ...metadata?.hasDynamicClass !== void 0 ? { hasDynamicClass: metadata.hasDynamicClass } : {},
8296
+ ...metadata?.hasDynamicStyle !== void 0 ? { hasDynamicStyle: metadata.hasDynamicStyle } : {},
8297
+ ...metadata?.hasDynamicText !== void 0 ? { hasDynamicText: metadata.hasDynamicText } : {}
8298
+ };
8299
+ }
8300
+ function buildPomManifest(componentHierarchyMap, elementMetadata) {
8301
+ const manifestEntries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([componentName, dependencies]) => {
8302
+ const entries = Array.from(dependencies.dataTestIdSet).sort((a, b) => a.selectorValue.formatted.localeCompare(b.selectorValue.formatted)).map((entry) => getManifestEntry(componentName, entry, elementMetadata.get(componentName), dependencies.pomExtraMethods ?? []));
8303
+ if (!entries.length) {
8304
+ return null;
8305
+ }
8306
+ return [componentName, {
8307
+ componentName,
8308
+ className: componentName,
8309
+ sourceFile: dependencies.filePath,
8310
+ kind: dependencies.isView ? "view" : "component",
8311
+ testIds: Array.from(new Set(entries.map((entry) => entry.testId))),
8312
+ entries
8313
+ }];
8314
+ }).filter((entry) => entry !== null);
8315
+ return Object.fromEntries(manifestEntries);
8316
+ }
8317
+ function buildTestIdManifest(pomManifest) {
8318
+ return Object.fromEntries(
8319
+ Object.entries(pomManifest).map(([componentName, component]) => [componentName, Array.from(new Set(component.testIds)).sort((a, b) => a.localeCompare(b))])
8320
+ );
8321
+ }
8322
+ function writeConstJson(value) {
8323
+ return (writer) => {
8324
+ writer.write(`${JSON.stringify(value, null, 2)} as const`);
8325
+ };
8326
+ }
8327
+ function generateTestIdsModule(componentHierarchyMap, elementMetadata) {
8328
+ const pomManifest = buildPomManifest(componentHierarchyMap, elementMetadata);
8329
+ const testIdManifest = buildTestIdManifest(pomManifest);
7851
8330
  return renderSourceFile("virtual-testids.ts", (sourceFile) => {
7852
8331
  sourceFile.addStatements("// Virtual module: test id manifest");
7853
8332
  sourceFile.addVariableStatement({
@@ -7855,16 +8334,15 @@ function generateTestIdsModule(componentTestIds) {
7855
8334
  isExported: true,
7856
8335
  declarations: [{
7857
8336
  name: "testIdManifest",
7858
- initializer: (writer) => {
7859
- writer.write("{").newLine();
7860
- writer.indent(() => {
7861
- manifestEntries.forEach(([componentName, testIds], index) => {
7862
- const suffix = index === manifestEntries.length - 1 ? "" : ",";
7863
- writer.writeLine(`${JSON.stringify(componentName)}: ${JSON.stringify(Array.from(testIds).sort())}${suffix}`);
7864
- });
7865
- });
7866
- writer.write("} as const");
7867
- }
8337
+ initializer: writeConstJson(testIdManifest)
8338
+ }]
8339
+ });
8340
+ sourceFile.addVariableStatement({
8341
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
8342
+ isExported: true,
8343
+ declarations: [{
8344
+ name: "pomManifest",
8345
+ initializer: writeConstJson(pomManifest)
7868
8346
  }]
7869
8347
  });
7870
8348
  sourceFile.addTypeAlias({
@@ -7877,27 +8355,67 @@ function generateTestIdsModule(componentTestIds) {
7877
8355
  name: "ComponentName",
7878
8356
  type: "keyof TestIdManifest"
7879
8357
  });
8358
+ sourceFile.addTypeAlias({
8359
+ isExported: true,
8360
+ name: "PomManifest",
8361
+ type: "typeof pomManifest"
8362
+ });
8363
+ sourceFile.addTypeAlias({
8364
+ isExported: true,
8365
+ name: "PomManifestComponentName",
8366
+ type: "keyof PomManifest"
8367
+ });
8368
+ });
8369
+ }
8370
+ function generatePomManifestModule(componentHierarchyMap, elementMetadata) {
8371
+ const pomManifest = buildPomManifest(componentHierarchyMap, elementMetadata);
8372
+ return renderSourceFile("virtual-pom-manifest.ts", (sourceFile) => {
8373
+ sourceFile.addStatements("// Virtual module: richer POM discoverability manifest");
8374
+ sourceFile.addVariableStatement({
8375
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
8376
+ isExported: true,
8377
+ declarations: [{
8378
+ name: "pomManifest",
8379
+ initializer: writeConstJson(pomManifest)
8380
+ }]
8381
+ });
8382
+ sourceFile.addTypeAlias({
8383
+ isExported: true,
8384
+ name: "PomManifest",
8385
+ type: "typeof pomManifest"
8386
+ });
8387
+ sourceFile.addTypeAlias({
8388
+ isExported: true,
8389
+ name: "PomManifestComponentName",
8390
+ type: "keyof PomManifest"
8391
+ });
7880
8392
  });
7881
8393
  }
7882
- const VIRTUAL_ID = "virtual:testids";
7883
- const RESOLVED_ID = `\0${VIRTUAL_ID}`;
7884
- function createTestIdsVirtualModulesPlugin(componentTestIds) {
8394
+ const TEST_IDS_VIRTUAL_ID = "virtual:testids";
8395
+ const TEST_IDS_RESOLVED_ID = `\0${TEST_IDS_VIRTUAL_ID}`;
8396
+ const POM_MANIFEST_VIRTUAL_ID = "virtual:pom-manifest";
8397
+ const POM_MANIFEST_RESOLVED_ID = `\0${POM_MANIFEST_VIRTUAL_ID}`;
8398
+ function createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata) {
7885
8399
  return {
7886
8400
  name: "vue-pom-generator:virtual-testids",
7887
8401
  resolveId(id) {
7888
- if (id === VIRTUAL_ID)
7889
- return RESOLVED_ID;
8402
+ if (id === TEST_IDS_VIRTUAL_ID)
8403
+ return TEST_IDS_RESOLVED_ID;
8404
+ if (id === POM_MANIFEST_VIRTUAL_ID)
8405
+ return POM_MANIFEST_RESOLVED_ID;
7890
8406
  },
7891
8407
  load(id) {
7892
- if (id === RESOLVED_ID)
7893
- return generateTestIdsModule(componentTestIds);
8408
+ if (id === TEST_IDS_RESOLVED_ID)
8409
+ return generateTestIdsModule(componentHierarchyMap, elementMetadata);
8410
+ if (id === POM_MANIFEST_RESOLVED_ID)
8411
+ return generatePomManifestModule(componentHierarchyMap, elementMetadata);
7894
8412
  }
7895
8413
  };
7896
8414
  }
7897
8415
  function createSupportPlugins(options) {
7898
8416
  const {
7899
- componentTestIds,
7900
8417
  componentHierarchyMap,
8418
+ elementMetadata,
7901
8419
  vueFilesPathMap,
7902
8420
  nativeWrappers,
7903
8421
  excludedComponents,
@@ -7907,27 +8425,30 @@ function createSupportPlugins(options) {
7907
8425
  getViewsDir,
7908
8426
  getSourceDirs,
7909
8427
  getWrapperSearchRoots,
7910
- nameCollisionBehavior = "suffix",
7911
- missingSemanticNameBehavior = "error",
7912
- existingIdBehavior,
8428
+ generation,
8429
+ projectRootRef,
8430
+ basePageClassPath: basePageClassPathOverride,
8431
+ loggerRef
8432
+ } = options;
8433
+ const {
7913
8434
  outDir,
7914
8435
  emitLanguages,
7915
8436
  typescriptOutputStructure,
7916
8437
  csharp,
7917
- routerAwarePoms,
7918
- routerEntry,
7919
- routerType,
7920
- routerModuleShims,
7921
8438
  generateFixtures,
7922
8439
  customPomAttachments,
7923
- projectRootRef,
7924
- basePageClassPath: basePageClassPathOverride,
7925
8440
  customPomDir,
8441
+ requireCustomPomDir,
7926
8442
  customPomImportAliases,
7927
8443
  customPomImportNameCollisionBehavior,
8444
+ nameCollisionBehavior,
8445
+ existingIdBehavior,
7928
8446
  testIdAttribute,
7929
- loggerRef
7930
- } = options;
8447
+ routerAwarePoms,
8448
+ routerEntry,
8449
+ routerType,
8450
+ routerModuleShims
8451
+ } = generation;
7931
8452
  const resolveRouterEntry2 = () => {
7932
8453
  if (!routerAwarePoms)
7933
8454
  return void 0;
@@ -7956,27 +8477,12 @@ function createSupportPlugins(options) {
7956
8477
  getSourceDirs,
7957
8478
  basePageClassPath,
7958
8479
  normalizedBasePagePath,
7959
- outDir,
7960
- emitLanguages,
7961
- typescriptOutputStructure,
7962
- csharp,
7963
- generateFixtures,
7964
- customPomAttachments,
8480
+ generation,
7965
8481
  projectRootRef,
7966
- customPomDir,
7967
- customPomImportAliases,
7968
- customPomImportNameCollisionBehavior,
7969
- testIdAttribute,
7970
- nameCollisionBehavior,
7971
- missingSemanticNameBehavior,
7972
- existingIdBehavior,
7973
8482
  nativeWrappers,
7974
8483
  excludedComponents,
7975
8484
  getWrapperSearchRoots,
7976
- routerAwarePoms,
7977
- routerType,
7978
8485
  getResolvedRouterEntry: resolveRouterEntry2,
7979
- routerModuleShims,
7980
8486
  loggerRef
7981
8487
  });
7982
8488
  const devProcessor = createDevProcessorPlugin({
@@ -7991,26 +8497,11 @@ function createSupportPlugins(options) {
7991
8497
  projectRootRef,
7992
8498
  normalizedBasePagePath,
7993
8499
  basePageClassPath,
7994
- outDir,
7995
- emitLanguages,
7996
- typescriptOutputStructure,
7997
- csharp,
7998
- generateFixtures,
7999
- customPomAttachments,
8000
- customPomDir,
8001
- customPomImportAliases,
8002
- customPomImportNameCollisionBehavior,
8003
- nameCollisionBehavior,
8004
- missingSemanticNameBehavior,
8005
- existingIdBehavior,
8006
- testIdAttribute,
8007
- routerAwarePoms,
8008
- routerType,
8500
+ generation,
8009
8501
  getResolvedRouterEntry: resolveRouterEntry2,
8010
- routerModuleShims,
8011
8502
  loggerRef
8012
8503
  });
8013
- const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
8504
+ const virtualModules = createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata);
8014
8505
  return [tsProcessor, devProcessor, virtualModules];
8015
8506
  }
8016
8507
  function findDataTestIdProp(element, attributeName = "data-testid") {
@@ -8344,8 +8835,7 @@ function createVuePluginWithTestIds(options) {
8344
8835
  });
8345
8836
  const api = viteVuePlugin?.api;
8346
8837
  if (!api) {
8347
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8348
- return;
8838
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8349
8839
  }
8350
8840
  const currentOptions = api.options ?? {};
8351
8841
  const currentTemplate = currentOptions.template ?? {};
@@ -8411,33 +8901,6 @@ function assertOneOf(value, allowed, name) {
8411
8901
  }
8412
8902
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8413
8903
  }
8414
- function assertErrorBehavior(value, name) {
8415
- if (!value) {
8416
- return;
8417
- }
8418
- if (value === "ignore" || value === "error") {
8419
- return;
8420
- }
8421
- if (typeof value !== "object" || Array.isArray(value)) {
8422
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8423
- }
8424
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8425
- for (const key of Object.keys(value)) {
8426
- if (!supportedKeys.has(key)) {
8427
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8428
- }
8429
- }
8430
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8431
- }
8432
- function resolveMissingSemanticNameBehavior(value) {
8433
- if (!value) {
8434
- return "error";
8435
- }
8436
- if (value === "ignore" || value === "error") {
8437
- return value;
8438
- }
8439
- return value.missingSemanticNameBehavior ?? "error";
8440
- }
8441
8904
  function readPackageJson(projectRoot) {
8442
8905
  const packageJsonPath = path.join(projectRoot, "package.json");
8443
8906
  if (!fs.existsSync(packageJsonPath)) {
@@ -8528,7 +8991,6 @@ function getSharedGeneratorState(key) {
8528
8991
  let state = sharedGeneratorStateRegistry.get(key);
8529
8992
  if (!state) {
8530
8993
  state = {
8531
- componentTestIds: /* @__PURE__ */ new Map(),
8532
8994
  elementMetadata: /* @__PURE__ */ new Map(),
8533
8995
  semanticNameMap: /* @__PURE__ */ new Map(),
8534
8996
  componentHierarchyMap: /* @__PURE__ */ new Map(),
@@ -8546,7 +9008,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8546
9008
  '[vue-pom-generator] vuePluginOwnership="external" requires the resolved Vite Vue plugin, but none was found. Add vue() to your Vite plugins before spreading createVuePomGeneratorPlugins(...).'
8547
9009
  );
8548
9010
  }
8549
- throw new Error("[vue-pom-generator] Nuxt mode requires the resolved Vite Vue plugin, but none was found.");
9011
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8550
9012
  }
8551
9013
  const currentOptions = viteVuePlugin.api.options ?? {};
8552
9014
  const currentTemplate = currentOptions.template ?? {};
@@ -8603,18 +9065,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8603
9065
  const vueGenerationOptions = generationOptions;
8604
9066
  const verbosity = options.logging?.verbosity ?? "warn";
8605
9067
  const vueOptions = options.vueOptions;
8606
- const legacyVueOptions = options;
8607
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8608
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8609
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8610
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8611
- const nativeWrappers = injection.nativeWrappers ?? {};
8612
- const excludedComponents = injection.excludeComponents ?? [];
8613
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8614
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8615
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8616
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8617
- const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
9068
+ const resolvedInjectionOptionsRef = {
9069
+ current: resolveInjectionSupportOptions({
9070
+ isNuxt,
9071
+ viewsDir: injection.viewsDir,
9072
+ componentDirs: injection.componentDirs,
9073
+ layoutDirs: injection.layoutDirs,
9074
+ wrapperSearchRoots: injection.wrapperSearchRoots,
9075
+ nativeWrappers: injection.nativeWrappers,
9076
+ excludedComponents: injection.excludeComponents,
9077
+ existingIdBehavior: injection.existingIdBehavior,
9078
+ testIdAttribute: injection.attribute
9079
+ })
9080
+ };
9081
+ const resolvedInjectionOptions = resolvedInjectionOptionsRef.current;
9082
+ const nativeWrappers = resolvedInjectionOptions.nativeWrappers;
9083
+ const excludedComponents = resolvedInjectionOptions.excludedComponents;
9084
+ const testIdAttribute = resolvedInjectionOptions.testIdAttribute;
8618
9085
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8619
9086
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8620
9087
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8623,27 +9090,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8623
9090
  }
8624
9091
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8625
9092
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8626
- const csharp = generationOptions?.csharp;
8627
- const errorBehavior = options.errorBehavior;
8628
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8629
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8630
9093
  const generateFixtures = generationOptions?.playwright?.fixtures;
8631
9094
  const customPoms = generationOptions?.playwright?.customPoms;
8632
9095
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8633
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8634
9096
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8635
- const resolvedCustomPomImportCollisionBehavior = customPoms?.importNameCollisionBehavior ?? "error";
9097
+ const requireCustomPomDir = customPoms?.dir !== void 0 || resolvedCustomPomAttachments.length > 0 || Object.keys(resolvedCustomPomImportAliases ?? {}).length > 0;
9098
+ const resolvedGenerationOptions = resolveGenerationSupportOptions({
9099
+ outDir: generationOptions?.outDir,
9100
+ emitLanguages: generationOptions?.emit,
9101
+ typescriptOutputStructure: generationOptions?.playwright?.outputStructure,
9102
+ csharp: generationOptions?.csharp,
9103
+ generateFixtures,
9104
+ customPomAttachments: resolvedCustomPomAttachments,
9105
+ customPomDir: customPoms?.dir,
9106
+ requireCustomPomDir,
9107
+ customPomImportAliases: resolvedCustomPomImportAliases,
9108
+ customPomImportNameCollisionBehavior: customPoms?.importNameCollisionBehavior,
9109
+ nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
9110
+ existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
9111
+ testIdAttribute,
9112
+ routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
9113
+ routerEntry,
9114
+ routerType,
9115
+ routerModuleShims
9116
+ });
8636
9117
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8637
- const getPageDirs = () => pageDirsRef.current;
9118
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8638
9119
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8639
- const getComponentDirs = () => componentDirsRef.current;
8640
- const getLayoutDirs = () => layoutDirsRef.current;
9120
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
9121
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8641
9122
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8642
9123
  ...getPageDirs(),
8643
9124
  ...getComponentDirs(),
8644
9125
  ...getLayoutDirs()
8645
9126
  ]));
8646
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
9127
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8647
9128
  const sharedStateKey = JSON.stringify({
8648
9129
  cwd: process.cwd(),
8649
9130
  mode: isNuxt ? "nuxt" : "vue",
@@ -8651,9 +9132,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8651
9132
  componentDirs: isNuxt ? null : getComponentDirs(),
8652
9133
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8653
9134
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8654
- outDir,
9135
+ outDir: resolvedGenerationOptions.outDir,
8655
9136
  testIdAttribute,
8656
- routerType,
9137
+ routerType: resolvedGenerationOptions.routerType,
8657
9138
  vuePluginOwnership
8658
9139
  });
8659
9140
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8674,23 +9155,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8674
9155
  if (isNuxt) {
8675
9156
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8676
9157
  projectRootRef.current = nuxtDiscovery.rootDir;
8677
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8678
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8679
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8680
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
9158
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
9159
+ resolvedInjectionOptionsRef.current,
9160
+ nuxtDiscovery
9161
+ );
8681
9162
  }
8682
9163
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8683
9164
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8684
9165
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8685
9166
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8686
9167
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8687
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8688
9168
  if (generationEnabled) {
8689
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8690
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8691
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8692
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8693
- assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
9169
+ assertNonEmptyString(resolvedGenerationOptions.outDir, "[vue-pom-generator] generation.outDir");
9170
+ assertOneOf(resolvedGenerationOptions.typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
9171
+ assertRouterModuleShims(resolvedGenerationOptions.routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
9172
+ if (!isNuxt && vueGenerationOptions?.router && resolvedGenerationOptions.routerType === "vue-router") {
9173
+ assertNonEmptyString(resolvedGenerationOptions.routerEntry, "[vue-pom-generator] generation.router.entry");
8694
9174
  }
8695
9175
  }
8696
9176
  if (usesExternalVuePlugin) {
@@ -8709,11 +9189,11 @@ function createVuePomGeneratorPlugins(options = {}) {
8709
9189
  };
8710
9190
  const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, getViewsDir());
8711
9191
  const getWrapperSearchRootsAbs = () => getWrapperSearchRoots().map((root) => resolveFromProjectRoot(projectRootRef.current, root));
8712
- const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
9192
+ const { elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8713
9193
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8714
9194
  vueOptions,
8715
- existingIdBehavior,
8716
- nameCollisionBehavior,
9195
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
9196
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8717
9197
  nativeWrappers,
8718
9198
  elementMetadata,
8719
9199
  semanticNameMap,
@@ -8728,10 +9208,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8728
9208
  getProjectRoot: () => projectRootRef.current
8729
9209
  });
8730
9210
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8731
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8732
9211
  const supportPlugins = createSupportPlugins({
8733
- componentTestIds,
8734
9212
  componentHierarchyMap,
9213
+ elementMetadata,
8735
9214
  vueFilesPathMap,
8736
9215
  nativeWrappers,
8737
9216
  excludedComponents,
@@ -8741,26 +9220,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8741
9220
  getViewsDir,
8742
9221
  getSourceDirs,
8743
9222
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8744
- nameCollisionBehavior,
8745
- missingSemanticNameBehavior,
8746
- existingIdBehavior,
8747
- outDir,
8748
- emitLanguages,
8749
- typescriptOutputStructure,
8750
- csharp,
8751
- routerAwarePoms,
8752
- routerEntry,
8753
- generateFixtures,
9223
+ generation: resolvedGenerationOptions,
8754
9224
  projectRootRef,
8755
9225
  basePageClassPath: basePageClassPathOverride,
8756
- customPomAttachments: resolvedCustomPomAttachments,
8757
- customPomDir: resolvedCustomPomDir,
8758
- customPomImportAliases: resolvedCustomPomImportAliases,
8759
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8760
- testIdAttribute,
8761
- loggerRef,
8762
- routerType,
8763
- routerModuleShims
9226
+ loggerRef
8764
9227
  });
8765
9228
  if (isNuxt) {
8766
9229
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");
@@ -8774,7 +9237,7 @@ function createVuePomGeneratorPlugins(options = {}) {
8774
9237
  ...supportPlugins
8775
9238
  ];
8776
9239
  if (!generationEnabled) {
8777
- const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
9240
+ const virtualModules = createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata);
8778
9241
  return [
8779
9242
  configPlugin,
8780
9243
  metadataCollectorPlugin,