@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.mjs CHANGED
@@ -8,8 +8,8 @@ import { parse as parse$2, NodeTypes as NodeTypes$1 } from "@vue/compiler-dom";
8
8
  import { parse as parse$1, compileScript } from "@vue/compiler-sfc";
9
9
  import { parseExpression, parse } from "@babel/parser";
10
10
  import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression, ElementTypes } from "@vue/compiler-core";
11
- import { Project, QuoteKind, NewLineKind, IndentationText, StructureKind, CodeBlockWriter, VariableDeclarationKind } from "ts-morph";
12
- import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isArrowFunctionExpression, isOptionalMemberExpression, isObjectExpression, isFile, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
11
+ import { Project, QuoteKind, NewLineKind, IndentationText, CodeBlockWriter, StructureKind, VariableDeclarationKind } from "ts-morph";
12
+ import { isArrayExpression, isStringLiteral, isTemplateLiteral, isMemberExpression, isOptionalMemberExpression, isCallExpression, isOptionalCallExpression, isAssignmentExpression, isIdentifier, isExpressionStatement, isObjectExpression, isFile, isArrowFunctionExpression, isBlockStatement, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
13
13
  import { performance } from "node:perf_hooks";
14
14
  import vue from "@vitejs/plugin-vue";
15
15
  const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
@@ -204,6 +204,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
204
204
  const nuxtOptions = await loadNuxtConfig({ cwd });
205
205
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
206
206
  }
207
+ function resolveGenerationSupportOptions(options) {
208
+ return {
209
+ outDir: (options.outDir ?? "tests/playwright/__generated__").trim(),
210
+ emitLanguages: options.emitLanguages?.length ? options.emitLanguages : ["ts"],
211
+ typescriptOutputStructure: options.typescriptOutputStructure ?? "aggregated",
212
+ csharp: options.csharp,
213
+ generateFixtures: options.generateFixtures,
214
+ customPomAttachments: options.customPomAttachments ?? [],
215
+ customPomDir: options.customPomDir ?? "tests/playwright/pom/custom",
216
+ requireCustomPomDir: options.requireCustomPomDir ?? false,
217
+ customPomImportAliases: options.customPomImportAliases,
218
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior ?? "error",
219
+ nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
220
+ existingIdBehavior: options.existingIdBehavior ?? "error",
221
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
222
+ routerAwarePoms: options.routerAwarePoms ?? false,
223
+ routerEntry: options.routerEntry,
224
+ routerType: options.routerType ?? "vue-router",
225
+ routerModuleShims: options.routerModuleShims
226
+ };
227
+ }
228
+ function resolveInjectionSupportOptions(options) {
229
+ const isNuxt = options.isNuxt ?? false;
230
+ return {
231
+ pageDirs: isNuxt ? ["app/pages"] : [options.viewsDir ?? "src/views"],
232
+ componentDirs: isNuxt ? ["app/components"] : options.componentDirs ?? ["src/components"],
233
+ layoutDirs: isNuxt ? ["app/layouts"] : options.layoutDirs ?? ["src/layouts"],
234
+ wrapperSearchRoots: isNuxt ? [] : options.wrapperSearchRoots ?? [],
235
+ nativeWrappers: options.nativeWrappers ?? {},
236
+ excludedComponents: options.excludedComponents ?? [],
237
+ existingIdBehavior: options.existingIdBehavior ?? "error",
238
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid"
239
+ };
240
+ }
241
+ function applyNuxtDiscoveryToInjectionOptions(options, discovery) {
242
+ return {
243
+ ...options,
244
+ pageDirs: discovery.pageDirs.length ? discovery.pageDirs : [path.resolve(discovery.srcDir, "pages")],
245
+ componentDirs: discovery.componentDirs,
246
+ layoutDirs: discovery.layoutDirs,
247
+ wrapperSearchRoots: discovery.wrapperSearchRoots
248
+ };
249
+ }
207
250
  function createTypeScriptProject() {
208
251
  return new Project({
209
252
  useInMemoryFileSystem: true,
@@ -295,16 +338,7 @@ function createClassConstructor(constructorDeclaration) {
295
338
  ...constructorDeclaration
296
339
  };
297
340
  }
298
- function upperFirst$1(value) {
299
- if (!value) {
300
- return value;
301
- }
302
- return value.charAt(0).toUpperCase() + value.slice(1);
303
- }
304
- function hasParam(params, name) {
305
- return Object.prototype.hasOwnProperty.call(params, name);
306
- }
307
- function splitTypeAndInitializer(typeExpression) {
341
+ function splitPomParameterTypeExpression(typeExpression) {
308
342
  const trimmed = typeExpression.trim();
309
343
  const initializerIndex = trimmed.lastIndexOf("=");
310
344
  if (initializerIndex < 0) {
@@ -315,49 +349,306 @@ function splitTypeAndInitializer(typeExpression) {
315
349
  initializer: trimmed.slice(initializerIndex + 1).trim()
316
350
  };
317
351
  }
318
- function createParameter(name, typeExpression) {
319
- const { type, initializer } = splitTypeAndInitializer(typeExpression);
352
+ function createPomParameterSpec(name, typeExpression, options = {}) {
353
+ const normalizedTypeExpression = typeExpression?.trim();
354
+ const { type, initializer } = normalizedTypeExpression ? splitPomParameterTypeExpression(normalizedTypeExpression) : { type: void 0, initializer: void 0 };
320
355
  return {
321
356
  name,
322
- type: type || void 0,
323
- initializer
357
+ typeExpression: normalizedTypeExpression,
358
+ type,
359
+ initializer: options.initializer ?? initializer,
360
+ hasQuestionToken: options.hasQuestionToken,
361
+ isRestParameter: options.isRestParameter
324
362
  };
325
363
  }
326
- function createParameters(params) {
327
- return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
364
+ function normalizePomParameters(params) {
365
+ if (!params) {
366
+ return [];
367
+ }
368
+ return params.map((param) => createPomParameterSpec(param.name, param.typeExpression ?? param.type, {
369
+ initializer: param.initializer,
370
+ hasQuestionToken: param.hasQuestionToken,
371
+ isRestParameter: param.isRestParameter
372
+ }));
328
373
  }
329
- function createInlineParameter(name, options = {}) {
374
+ function getPomParameterNames(params) {
375
+ return normalizePomParameters(params).map((param) => param.name);
376
+ }
377
+ function getPomParameter(params, name) {
378
+ return normalizePomParameters(params).find((param) => param.name === name);
379
+ }
380
+ function hasPomParameter(params, name) {
381
+ return !!getPomParameter(params, name);
382
+ }
383
+ function setPomParameter(params, name, typeExpression, options = {}) {
384
+ const nextParam = createPomParameterSpec(name, typeExpression, options);
385
+ const normalizedParams = normalizePomParameters(params);
386
+ const existingIndex = normalizedParams.findIndex((param) => param.name === name);
387
+ if (existingIndex < 0) {
388
+ return [...normalizedParams, nextParam];
389
+ }
390
+ const nextParams = normalizedParams.slice();
391
+ nextParams[existingIndex] = nextParam;
392
+ return nextParams;
393
+ }
394
+ function removePomParameter(params, name) {
395
+ return normalizePomParameters(params).filter((param) => param.name !== name);
396
+ }
397
+ function toTypeScriptPomParameterStructures(params) {
398
+ return normalizePomParameters(params).map((param) => ({
399
+ name: param.name,
400
+ type: param.type || void 0,
401
+ initializer: param.initializer,
402
+ hasQuestionToken: param.hasQuestionToken,
403
+ isRestParameter: param.isRestParameter
404
+ }));
405
+ }
406
+ function getPomParameterArgumentNames(params) {
407
+ return normalizePomParameters(params).map((param) => param.isRestParameter ? `...${param.name}` : param.name);
408
+ }
409
+ function createPomMethodSignature(parameters) {
330
410
  return {
331
- name,
332
- type: options.type,
333
- initializer: options.initializer
411
+ parameters: normalizePomParameters(parameters)
334
412
  };
335
413
  }
336
- function removeByKeySegment(value) {
337
- const idx = value.lastIndexOf("ByKey");
338
- if (idx < 0) {
339
- return value;
414
+ function pomParameterSpecEquals(left, right) {
415
+ 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;
416
+ }
417
+ function pomParameterListEquals(left, right) {
418
+ const leftParams = normalizePomParameters(left);
419
+ const rightParams = normalizePomParameters(right);
420
+ if (leftParams.length !== rightParams.length) {
421
+ return false;
340
422
  }
341
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
423
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
424
+ }
425
+ function pomMethodSignatureEquals(left, right) {
426
+ return pomParameterListEquals(left.parameters, right.parameters);
427
+ }
428
+ function isParameterizedPomPattern(kind) {
429
+ return kind === "parameterized";
342
430
  }
343
- function uniqueAlternates(primary, alternates) {
431
+ function getTemplateVariables(formatted) {
344
432
  const out = [];
345
433
  const seen = /* @__PURE__ */ new Set();
346
- seen.add(primary);
347
- for (const a of alternates ?? []) {
348
- if (!a) {
434
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
435
+ for (const match of matches) {
436
+ const variableName = match[1];
437
+ if (seen.has(variableName)) {
349
438
  continue;
350
439
  }
351
- if (seen.has(a)) {
440
+ seen.add(variableName);
441
+ out.push(variableName);
442
+ }
443
+ return out;
444
+ }
445
+ function createPomStringPattern(formatted, patternKind) {
446
+ return {
447
+ formatted,
448
+ patternKind,
449
+ templateVariables: getTemplateVariables(formatted)
450
+ };
451
+ }
452
+ function getPomPatternVariables(patterns, options = {}) {
453
+ const out = [];
454
+ const seen = /* @__PURE__ */ new Set();
455
+ const omitted = new Set(options.omit ?? []);
456
+ for (const pattern of patterns) {
457
+ for (const variableName of pattern.templateVariables) {
458
+ if (omitted.has(variableName) || seen.has(variableName)) {
459
+ continue;
460
+ }
461
+ seen.add(variableName);
462
+ out.push(variableName);
463
+ }
464
+ }
465
+ return out;
466
+ }
467
+ function orderPomPatternParameters(params, patterns, options = {}) {
468
+ const currentParams = normalizePomParameters(params);
469
+ const orderedParams = [];
470
+ const seen = /* @__PURE__ */ new Set();
471
+ const missingParams = [];
472
+ for (const variableName of getPomPatternVariables(patterns, options)) {
473
+ seen.add(variableName);
474
+ const existingParam = currentParams.find((param) => param.name === variableName);
475
+ if (!existingParam) {
476
+ missingParams.push(variableName);
352
477
  continue;
353
478
  }
354
- seen.add(a);
355
- out.push(a);
479
+ orderedParams.push(existingParam);
480
+ }
481
+ if (missingParams.length > 0) {
482
+ const availableParams = currentParams.map((param) => JSON.stringify(param.name)).join(", ") || "<none>";
483
+ const patternSummary = patterns.map((pattern) => JSON.stringify(pattern.formatted)).join(", ");
484
+ throw new Error(
485
+ `[vue-pom-generator] Missing selector parameter(s) ${missingParams.map((name) => JSON.stringify(name)).join(", ")} for parameterized pattern(s) ${patternSummary}. Available parameters: ${availableParams}.`
486
+ );
487
+ }
488
+ for (const param of currentParams) {
489
+ if (seen.has(param.name)) {
490
+ continue;
491
+ }
492
+ seen.add(param.name);
493
+ orderedParams.push(param);
494
+ }
495
+ return orderedParams;
496
+ }
497
+ function getIndexedPomPatternVariable(pattern) {
498
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
499
+ return null;
500
+ }
501
+ if (pattern.templateVariables.length !== 1) {
502
+ throw new Error(
503
+ `[vue-pom-generator] Parameterized locator getters require exactly one template variable; got ${pattern.templateVariables.length} in ${JSON.stringify(pattern.formatted)}.`
504
+ );
505
+ }
506
+ return pattern.templateVariables[0];
507
+ }
508
+ function hasPomPatternVariables(pattern) {
509
+ return pattern.templateVariables.length > 0;
510
+ }
511
+ function toTypeScriptPomPatternExpression(pattern) {
512
+ return isParameterizedPomPattern(pattern.patternKind) ? `\`${pattern.formatted}\`` : JSON.stringify(pattern.formatted);
513
+ }
514
+ function toCSharpPomPatternExpression(pattern) {
515
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
516
+ return JSON.stringify(pattern.formatted);
517
+ }
518
+ const inner = pattern.formatted.replace(/\$\{/g, "{");
519
+ return `$${JSON.stringify(inner)}`;
520
+ }
521
+ function bindTypeScriptPomPattern(pattern, variableName) {
522
+ const expression = toTypeScriptPomPatternExpression(pattern);
523
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
524
+ return { expression, setupStatements: [] };
525
+ }
526
+ return {
527
+ expression: variableName,
528
+ setupStatements: [`const ${variableName} = ${expression};`]
529
+ };
530
+ }
531
+ function bindCSharpPomPattern(pattern, variableName) {
532
+ const expression = toCSharpPomPatternExpression(pattern);
533
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
534
+ return { expression, setupStatements: [] };
535
+ }
536
+ return {
537
+ expression: variableName,
538
+ setupStatements: [`var ${variableName} = ${expression};`]
539
+ };
540
+ }
541
+ function pomStringPatternEquals(left, right) {
542
+ return left.formatted === right.formatted && left.patternKind === right.patternKind;
543
+ }
544
+ function uniquePomStringPatterns(primary, alternates) {
545
+ const out = [];
546
+ const seen = /* @__PURE__ */ new Set();
547
+ const add = (pattern) => {
548
+ const key = JSON.stringify(pattern);
549
+ if (seen.has(key)) {
550
+ return;
551
+ }
552
+ seen.add(key);
553
+ out.push(pattern);
554
+ };
555
+ add(primary);
556
+ for (const alternate of alternates ?? []) {
557
+ add(alternate);
356
558
  }
357
559
  return out;
358
560
  }
359
- function testIdExpression(formattedDataTestId) {
360
- return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
561
+ function splitDiscoverabilityWords(value) {
562
+ 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();
563
+ if (!normalized) {
564
+ return [];
565
+ }
566
+ return normalized.split(/\s+/).map((word) => word.toLowerCase()).filter(Boolean);
567
+ }
568
+ function joinDiscoverabilityWords(words) {
569
+ return words.join(" ").replace(/\s+/g, " ").trim();
570
+ }
571
+ function toSentenceCase(value) {
572
+ return value.charAt(0).toUpperCase() + value.slice(1);
573
+ }
574
+ function stripComponentKindSuffix(componentName) {
575
+ for (const suffix of ["Page", "Component", "Layout"]) {
576
+ if (componentName.endsWith(suffix) && componentName.length > suffix.length) {
577
+ return componentName.slice(0, -suffix.length);
578
+ }
579
+ }
580
+ return componentName;
581
+ }
582
+ function removeLeadingWords(words, prefixWords) {
583
+ if (!prefixWords.length || words.length < prefixWords.length) {
584
+ return [...words];
585
+ }
586
+ for (let i = 0; i < prefixWords.length; i++) {
587
+ if (words[i] !== prefixWords[i]) {
588
+ return [...words];
589
+ }
590
+ }
591
+ return words.slice(prefixWords.length);
592
+ }
593
+ function removeTrailingRoleWord(words, roleWord) {
594
+ if (!words.length || words[words.length - 1] !== roleWord) {
595
+ return [...words];
596
+ }
597
+ return words.slice(0, -1);
598
+ }
599
+ function humanizePomMethodName(methodName) {
600
+ return joinDiscoverabilityWords(splitDiscoverabilityWords(methodName));
601
+ }
602
+ function stripPomActionPrefix(actionName) {
603
+ for (const prefix of ["click", "select", "type", "goTo"]) {
604
+ if (actionName.startsWith(prefix) && actionName.length > prefix.length) {
605
+ return actionName.slice(prefix.length);
606
+ }
607
+ }
608
+ return actionName;
609
+ }
610
+ function normalizePomRoleLabel(nativeRole) {
611
+ if (nativeRole === "vselect") {
612
+ return "select";
613
+ }
614
+ return nativeRole || "element";
615
+ }
616
+ function buildPomLocatorDescription(args) {
617
+ const componentWords = splitDiscoverabilityWords(args.componentName ? stripComponentKindSuffix(args.componentName) : "");
618
+ const roleWord = normalizePomRoleLabel(args.nativeRole).toLowerCase();
619
+ const semanticWords = removeLeadingWords(
620
+ removeTrailingRoleWord(splitDiscoverabilityWords(args.methodName), roleWord),
621
+ componentWords
622
+ );
623
+ const phrase = joinDiscoverabilityWords([
624
+ ...componentWords,
625
+ ...semanticWords,
626
+ roleWord
627
+ ]);
628
+ return toSentenceCase(phrase || "Generated element");
629
+ }
630
+ function upperFirst$1(value) {
631
+ if (!value) {
632
+ return value;
633
+ }
634
+ return value.charAt(0).toUpperCase() + value.slice(1);
635
+ }
636
+ function createParameters(params) {
637
+ return toTypeScriptPomParameterStructures(params);
638
+ }
639
+ function createInlineParameter(name, options = {}) {
640
+ return {
641
+ name,
642
+ type: options.type,
643
+ initializer: options.initializer
644
+ };
645
+ }
646
+ function removeByKeySegment$1(value) {
647
+ const idx = value.lastIndexOf("ByKey");
648
+ if (idx < 0) {
649
+ return value;
650
+ }
651
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
361
652
  }
362
653
  function createAsyncMethod(name, parameters, statements) {
363
654
  return createClassMethod({
@@ -367,17 +658,25 @@ function createAsyncMethod(name, parameters, statements) {
367
658
  statements
368
659
  });
369
660
  }
370
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
661
+ function generateClickMethod(componentName, methodName, selector, alternateSelectors, parameters) {
371
662
  const name = `click${methodName}`;
372
663
  const noWaitName = `${name}NoWait`;
373
- const baseParameters = createParameters(params);
374
- const argsForForward = Object.keys(params).join(", ");
375
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
664
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
665
+ componentName,
666
+ methodName,
667
+ nativeRole: "button"
668
+ }));
669
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
670
+ const hasSelectorVariables = hasPomPatternVariables(selector);
671
+ const baseParameters = createParameters(selectorParams);
672
+ const argsForForward = getPomParameterNames(selectorParams).join(", ");
673
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
674
+ const primaryTestIdExpr = toTypeScriptPomPatternExpression(selector);
376
675
  if (alternates.length > 0) {
377
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
676
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
378
677
  const clickMethod = createAsyncMethod(
379
678
  name,
380
- hasParam(params, "key") ? [
679
+ hasSelectorVariables ? [
381
680
  ...baseParameters,
382
681
  createInlineParameter("wait", { type: "boolean", initializer: "true" }),
383
682
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
@@ -389,7 +688,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
389
688
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
390
689
  writer.writeLine("let lastError: unknown;");
391
690
  writer.write("for (const testId of candidates) ").block(() => {
392
- writer.writeLine("const locator = this.locatorByTestId(testId);");
691
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
393
692
  writer.write("try ").block(() => {
394
693
  writer.write("if (await locator.count()) ").block(() => {
395
694
  writer.writeLine("await this.clickLocator(locator, annotationText, wait);");
@@ -406,14 +705,14 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
406
705
  const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
407
706
  const noWaitMethod = createAsyncMethod(
408
707
  noWaitName,
409
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
708
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
410
709
  (writer) => {
411
710
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
412
711
  }
413
712
  );
414
713
  return [clickMethod, noWaitMethod];
415
714
  }
416
- if (hasParam(params, "key")) {
715
+ if (hasSelectorVariables) {
417
716
  return [
418
717
  createAsyncMethod(
419
718
  name,
@@ -423,7 +722,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
423
722
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
424
723
  ],
425
724
  (writer) => {
426
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, annotationText, wait);`);
725
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait, ${locatorDescription});`);
427
726
  }
428
727
  ),
429
728
  createAsyncMethod(
@@ -443,7 +742,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
443
742
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
444
743
  ],
445
744
  (writer) => {
446
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", annotationText, wait);`);
745
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait, ${locatorDescription});`);
447
746
  }
448
747
  ),
449
748
  createAsyncMethod(
@@ -455,66 +754,76 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
455
754
  )
456
755
  ];
457
756
  }
458
- function generateRadioMethod(methodName, formattedDataTestId) {
757
+ function generateRadioMethod(componentName, methodName, selector, parameters) {
459
758
  const name = `select${methodName}`;
460
- const hasKey = formattedDataTestId.includes("${key}");
461
- const parameters = hasKey ? [
462
- createInlineParameter("key", { type: "string" }),
463
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
464
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
465
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
759
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
760
+ componentName,
761
+ methodName,
762
+ nativeRole: "radio"
763
+ }));
764
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
765
+ const methodParameters = createParameters(selectorParams);
766
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
466
767
  return [
467
- createAsyncMethod(name, parameters, (writer) => {
468
- writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
768
+ createAsyncMethod(name, methodParameters, (writer) => {
769
+ writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText, true, ${locatorDescription});`);
469
770
  })
470
771
  ];
471
772
  }
472
- function generateSelectMethod(methodName, formattedDataTestId) {
773
+ function generateSelectMethod(componentName, methodName, selector, parameters) {
473
774
  const name = `select${methodName}`;
474
- const needsKey = formattedDataTestId.includes("${key}");
475
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
775
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
776
+ componentName,
777
+ methodName,
778
+ nativeRole: "select"
779
+ }));
780
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
781
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
476
782
  return [
477
783
  createAsyncMethod(
478
784
  name,
479
- [
480
- createInlineParameter("value", { type: "string" }),
481
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
482
- ],
785
+ createParameters(selectorParams),
483
786
  (writer) => {
484
- writer.writeLine(`const selector = ${selectorExpr};`);
485
- writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
486
- writer.writeLine("await this.page.selectOption(selector, value);");
787
+ writer.writeLine(`const testId = ${testIdExpr};`);
788
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
789
+ writer.writeLine("await this.animateCursorToElement(locator, false, 500, annotationText);");
790
+ writer.writeLine("await locator.selectOption(value);");
487
791
  }
488
792
  )
489
793
  ];
490
794
  }
491
- function generateVSelectMethod(methodName, formattedDataTestId) {
795
+ function generateVSelectMethod(componentName, methodName, selector, parameters) {
492
796
  const name = `select${methodName}`;
797
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
798
+ componentName,
799
+ methodName,
800
+ nativeRole: "vselect"
801
+ }));
802
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
493
803
  return [
494
804
  createAsyncMethod(
495
805
  name,
496
- [
497
- createInlineParameter("value", { type: "string" }),
498
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
499
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
500
- ],
806
+ createParameters(selectorParams),
501
807
  (writer) => {
502
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
808
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText, ${locatorDescription});`);
503
809
  }
504
810
  )
505
811
  ];
506
812
  }
507
- function generateTypeMethod(methodName, formattedDataTestId) {
813
+ function generateTypeMethod(componentName, methodName, selector, parameters) {
508
814
  const name = `type${methodName}`;
815
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
816
+ componentName,
817
+ methodName,
818
+ nativeRole: "input"
819
+ }));
820
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
509
821
  return [
510
822
  createAsyncMethod(
511
823
  name,
512
- [
513
- createInlineParameter("text", { type: "string" }),
514
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
515
- ],
824
+ createParameters(selectorParams),
516
825
  (writer) => {
517
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
826
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText, ${locatorDescription});`);
518
827
  }
519
828
  )
520
829
  ];
@@ -529,62 +838,74 @@ function isAllDigits(value) {
529
838
  }
530
839
  return true;
531
840
  }
532
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
841
+ function generateGetElementByDataTestId(componentName, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
842
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
843
+ componentName,
844
+ methodName,
845
+ nativeRole
846
+ }));
533
847
  const roleSuffix = upperFirst$1(nativeRole || "Element");
534
848
  const baseName = upperFirst$1(methodName);
535
849
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
536
- const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
537
- const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
538
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
539
- if (needsKey) {
540
- const keyType = params.key || "string";
541
- const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
850
+ const hasRoleSuffix2 = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
851
+ const propertyName = hasRoleSuffix2 ? `${baseName}` : `${baseName}${roleSuffix}`;
852
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
853
+ const indexedVariable = getIndexedPomPatternVariable(selector);
854
+ if (indexedVariable) {
855
+ const keyType = getPomParameter(selectorParams, indexedVariable)?.typeExpression || "string";
856
+ const keyedPropertyName = getterNameOverride ?? removeByKeySegment$1(propertyName);
542
857
  return [
543
858
  createClassGetter({
544
859
  name: keyedPropertyName,
545
860
  statements: [
546
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
861
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription}));`
547
862
  ]
548
863
  })
549
864
  ];
550
865
  }
551
866
  const finalPropertyName = getterNameOverride ?? propertyName;
552
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
867
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
553
868
  if (alternates.length > 0) {
554
- const all = [formattedDataTestId, ...alternates];
555
- const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
869
+ const all = [selector, ...alternates];
870
+ const locatorExpr = all.map((id) => `this.locatorByTestId(${toTypeScriptPomPatternExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
556
871
  return [
557
872
  createClassGetter({
558
873
  name: finalPropertyName,
559
- statements: [`return ${locatorExpr};`]
874
+ statements: [`return this.describeLocator(${locatorExpr}, ${locatorDescription});`]
560
875
  })
561
876
  ];
562
877
  }
563
878
  return [
564
879
  createClassGetter({
565
880
  name: finalPropertyName,
566
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
881
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription});`]
567
882
  })
568
883
  ];
569
884
  }
570
885
  function generateNavigationMethod(args) {
571
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
886
+ const { componentName, targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
572
887
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
573
- const parameters = createParameters(params);
574
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
575
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
888
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
889
+ componentName,
890
+ methodName: baseMethodName,
891
+ nativeRole: "button"
892
+ }));
893
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
894
+ const methodParameters = createParameters(selectorParams);
895
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
896
+ const candidatesExpr = [toTypeScriptPomPatternExpression(selector), ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
576
897
  if (alternates.length > 0) {
577
898
  return [
578
899
  createClassMethod({
579
900
  name: methodName,
580
- parameters,
901
+ parameters: methodParameters,
581
902
  returnType: `Fluent<${target}>`,
582
903
  statements: (writer) => {
583
904
  writer.write("return this.fluent(async () => ").block(() => {
584
905
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
585
906
  writer.writeLine("let lastError: unknown;");
586
907
  writer.write("for (const testId of candidates) ").block(() => {
587
- writer.writeLine("const locator = this.locatorByTestId(testId);");
908
+ writer.writeLine(`const locator = this.locatorByTestId(testId, ${locatorDescription});`);
588
909
  writer.write("try ").block(() => {
589
910
  writer.write("if (await locator.count()) ").block(() => {
590
911
  writer.writeLine("await this.clickLocator(locator);");
@@ -605,11 +926,12 @@ function generateNavigationMethod(args) {
605
926
  return [
606
927
  createClassMethod({
607
928
  name: methodName,
608
- parameters,
929
+ parameters: methodParameters,
609
930
  returnType: `Fluent<${target}>`,
610
931
  statements: (writer) => {
611
932
  writer.write("return this.fluent(async () => ").block(() => {
612
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
933
+ writer.writeLine(`const locator = this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}, ${locatorDescription});`);
934
+ writer.writeLine("await this.clickLocator(locator);");
613
935
  writer.writeLine(`return new ${target}(this.page);`);
614
936
  });
615
937
  writer.writeLine(");");
@@ -617,41 +939,43 @@ function generateNavigationMethod(args) {
617
939
  })
618
940
  ];
619
941
  }
620
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
942
+ function generateViewObjectModelMembers(componentName, targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
621
943
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
622
944
  const members = generateGetElementByDataTestId(
945
+ componentName,
623
946
  baseMethodName,
624
947
  nativeRole,
625
- formattedDataTestId,
626
- alternateFormattedDataTestIds,
948
+ selector,
949
+ alternateSelectors,
627
950
  getterNameOverride,
628
- params
951
+ parameters
629
952
  );
630
953
  if (targetPageObjectModelClass) {
631
954
  return [
632
955
  ...members,
633
956
  ...generateNavigationMethod({
957
+ componentName,
634
958
  targetPageObjectModelClass,
635
959
  baseMethodName,
636
- formattedDataTestId,
637
- alternateFormattedDataTestIds,
638
- params
960
+ selector,
961
+ alternateSelectors,
962
+ parameters
639
963
  })
640
964
  ];
641
965
  }
642
966
  if (nativeRole === "select") {
643
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
967
+ return [...members, ...generateSelectMethod(componentName, baseMethodName, selector, parameters)];
644
968
  }
645
969
  if (nativeRole === "vselect") {
646
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
970
+ return [...members, ...generateVSelectMethod(componentName, baseMethodName, selector, parameters)];
647
971
  }
648
972
  if (nativeRole === "input") {
649
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
973
+ return [...members, ...generateTypeMethod(componentName, baseMethodName, selector, parameters)];
650
974
  }
651
975
  if (nativeRole === "radio") {
652
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
976
+ return [...members, ...generateRadioMethod(componentName, baseMethodName || "Radio", selector, parameters)];
653
977
  }
654
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
978
+ return [...members, ...generateClickMethod(componentName, baseMethodName, selector, alternateSelectors, parameters)];
655
979
  }
656
980
  function isSimpleExpressionNode(value) {
657
981
  return value !== null && "type" in value && value.type === NodeTypes.SIMPLE_EXPRESSION;
@@ -691,43 +1015,72 @@ function buildPlaceholderParams(keys) {
691
1015
  params[k] = "__placeholder__";
692
1016
  return params;
693
1017
  }
694
- function getRouteLocationLikeFromToDirective(toDirective) {
695
- if (!toDirective.exp)
696
- return null;
697
- const exp = toDirective.exp;
698
- const rawSource = stringifyExpression(exp).trim();
1018
+ const isNodeType = (node, type) => {
1019
+ return node !== null && node.type === type;
1020
+ };
1021
+ const isStringLiteralNode = (node) => {
1022
+ return isNodeType(node, "StringLiteral") && typeof node.value === "string";
1023
+ };
1024
+ const isIdentifierNode = (node) => {
1025
+ return isNodeType(node, "Identifier") && typeof node.name === "string";
1026
+ };
1027
+ const isObjectPropertyNode = (node) => {
1028
+ if (!isNodeType(node, "ObjectProperty"))
1029
+ return false;
1030
+ const n = node;
1031
+ return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
1032
+ };
1033
+ const isObjectExpressionNode = (node) => {
1034
+ if (!isNodeType(node, "ObjectExpression"))
1035
+ return false;
1036
+ const n = node;
1037
+ return Array.isArray(n.properties);
1038
+ };
1039
+ function materializeResolvedRouteTarget(target, paramKeys) {
1040
+ if (typeof target === "string")
1041
+ return target;
1042
+ if (!paramKeys.length)
1043
+ return target;
1044
+ return {
1045
+ ...target,
1046
+ params: buildPlaceholderParams(paramKeys)
1047
+ };
1048
+ }
1049
+ function analyzeToDirectiveTarget(toDirective) {
1050
+ if (!toDirective.exp) {
1051
+ return {
1052
+ kind: "unsupported",
1053
+ rawSource: null,
1054
+ reason: "missing-expression"
1055
+ };
1056
+ }
1057
+ const rawSource = stringifyExpression(toDirective.exp).trim();
699
1058
  let expr;
700
1059
  try {
701
1060
  expr = parseExpression(rawSource, { plugins: ["typescript"] });
702
- } catch {
703
- return null;
1061
+ } catch (error) {
1062
+ return {
1063
+ kind: "parse-error",
1064
+ rawSource,
1065
+ reason: "parse-error",
1066
+ error: error instanceof Error ? error.message : String(error)
1067
+ };
704
1068
  }
705
- const isNodeType = (node, type) => {
706
- return node !== null && node.type === type;
707
- };
708
- const isStringLiteralNode = (node) => {
709
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
710
- };
711
- const isIdentifierNode = (node) => {
712
- return isNodeType(node, "Identifier") && typeof node.name === "string";
713
- };
714
- const isObjectPropertyNode = (node) => {
715
- if (!isNodeType(node, "ObjectProperty"))
716
- return false;
717
- const n = node;
718
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
719
- };
720
- const isObjectExpressionNode = (node) => {
721
- if (!isNodeType(node, "ObjectExpression"))
722
- return false;
723
- const n = node;
724
- return Array.isArray(n.properties);
725
- };
726
1069
  if (isStringLiteralNode(expr)) {
727
- return expr.value;
1070
+ return {
1071
+ kind: "resolved",
1072
+ rawSource,
1073
+ target: expr.value,
1074
+ routeNameKey: null,
1075
+ paramKeys: []
1076
+ };
728
1077
  }
729
1078
  if (!isObjectExpressionNode(expr)) {
730
- return null;
1079
+ return {
1080
+ kind: "unsupported",
1081
+ rawSource,
1082
+ reason: "dynamic-expression"
1083
+ };
731
1084
  }
732
1085
  const getStringField = (fieldName) => {
733
1086
  const prop = expr.properties.find((p) => {
@@ -748,7 +1101,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
748
1101
  const key = p.key;
749
1102
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
750
1103
  });
751
- let params;
1104
+ let paramKeys = [];
752
1105
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
753
1106
  const keys = [];
754
1107
  for (const prop of paramsProp.value.properties) {
@@ -760,47 +1113,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
760
1113
  else if (isStringLiteralNode(key))
761
1114
  keys.push(key.value);
762
1115
  }
763
- if (keys.length) {
764
- params = buildPlaceholderParams(Array.from(new Set(keys)));
765
- }
1116
+ paramKeys = Array.from(new Set(keys));
766
1117
  }
767
1118
  if (name) {
768
- return { name, params };
1119
+ const trimmed = name.trim();
1120
+ if (!trimmed.length) {
1121
+ return {
1122
+ kind: "unsupported",
1123
+ rawSource,
1124
+ reason: "missing-name-or-path"
1125
+ };
1126
+ }
1127
+ return {
1128
+ kind: "resolved",
1129
+ rawSource,
1130
+ target: { name },
1131
+ routeNameKey: toPascalCaseRouteKey(trimmed),
1132
+ paramKeys
1133
+ };
769
1134
  }
770
1135
  if (path2) {
771
- return { path: path2, params };
1136
+ return {
1137
+ kind: "resolved",
1138
+ rawSource,
1139
+ target: { path: path2 },
1140
+ routeNameKey: null,
1141
+ paramKeys
1142
+ };
772
1143
  }
773
- return null;
774
- }
775
- function toDirectiveObjectFieldNameValue$1(toDirective) {
776
- const to = getRouteLocationLikeFromToDirective(toDirective);
777
- if (!to || typeof to === "string")
778
- return null;
779
- const name = to.name;
780
- if (typeof name !== "string")
781
- return null;
782
- const trimmed = name.trim();
783
- if (!trimmed.length)
784
- return null;
785
- return toPascalCaseRouteKey(trimmed);
786
- }
787
- function getRouteNameKeyFromToDirective(toDirective) {
788
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
789
- if (objectName)
790
- return objectName;
791
- return null;
1144
+ return {
1145
+ kind: "unsupported",
1146
+ rawSource,
1147
+ reason: "missing-name-or-path"
1148
+ };
792
1149
  }
793
1150
  function tryResolveToDirectiveTargetComponentName(toDirective) {
794
- const to = getRouteLocationLikeFromToDirective(toDirective);
795
- if (to && resolveToComponentName) {
796
- const resolved = resolveToComponentName(to);
1151
+ const analysis = analyzeToDirectiveTarget(toDirective);
1152
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1153
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
797
1154
  if (resolved)
798
1155
  return resolved;
799
1156
  }
800
- const key = getRouteNameKeyFromToDirective(toDirective);
801
- if (!key || !routeNameToComponentName)
1157
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
802
1158
  return null;
803
- return routeNameToComponentName.get(key) ?? null;
1159
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
804
1160
  }
805
1161
  function getDataTestIdFromGroupOption(text) {
806
1162
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -835,11 +1191,81 @@ function staticAttributeValue(value) {
835
1191
  return { kind: "static", value };
836
1192
  }
837
1193
  function templateAttributeValue(template) {
838
- return { kind: "template", template };
1194
+ const parsedTemplate = tryParseTemplateFragment(template);
1195
+ if (!parsedTemplate) {
1196
+ throw new Error(`[vue-pom-generator] Failed to parse generated template fragment: ${template}`);
1197
+ }
1198
+ return { kind: "template", template, parsedTemplate };
839
1199
  }
840
1200
  function getAttributeValueText(value) {
841
1201
  return value.kind === "static" ? value.value : value.template;
842
1202
  }
1203
+ function getVueExpressionSource(expression, ...preferredViews) {
1204
+ if (!expression) {
1205
+ return "";
1206
+ }
1207
+ for (const view of preferredViews) {
1208
+ let value = "";
1209
+ switch (view) {
1210
+ case "content":
1211
+ value = expression.type === NodeTypes.SIMPLE_EXPRESSION ? expression.content : "";
1212
+ break;
1213
+ case "loc":
1214
+ value = expression.loc?.source ?? "";
1215
+ break;
1216
+ case "compiled":
1217
+ try {
1218
+ value = stringifyExpression(expression);
1219
+ } catch {
1220
+ value = "";
1221
+ }
1222
+ break;
1223
+ default:
1224
+ value = "";
1225
+ break;
1226
+ }
1227
+ const trimmed = value.trim();
1228
+ if (trimmed) {
1229
+ return trimmed;
1230
+ }
1231
+ }
1232
+ return "";
1233
+ }
1234
+ function tryGetExistingVueExpressionAst(expression) {
1235
+ if (!expression) {
1236
+ return null;
1237
+ }
1238
+ const ast = "ast" in expression ? expression.ast : null;
1239
+ return ast && "type" in ast ? ast : null;
1240
+ }
1241
+ function tryParseBabelExpressionFromSource(source, plugins) {
1242
+ const trimmed = source.trim();
1243
+ if (!trimmed) {
1244
+ return null;
1245
+ }
1246
+ try {
1247
+ return parseExpression(trimmed, { plugins });
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ }
1252
+ function tryGetVueExpressionAst(expression, options) {
1253
+ if (!expression) {
1254
+ return null;
1255
+ }
1256
+ if (options?.preferExistingAst !== false) {
1257
+ const existingAst = tryGetExistingVueExpressionAst(expression);
1258
+ if (existingAst) {
1259
+ return existingAst;
1260
+ }
1261
+ }
1262
+ const source = getVueExpressionSource(expression, ...options?.preferredViews ?? ["content", "loc", "compiled"]);
1263
+ return source ? tryParseBabelExpressionFromSource(source, options?.plugins ?? ["typescript"]) : null;
1264
+ }
1265
+ function tryGetDirectiveBabelAst(directive, options) {
1266
+ const exp = directive.exp && (directive.exp.type === NodeTypes.SIMPLE_EXPRESSION || directive.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? directive.exp : null;
1267
+ return tryGetVueExpressionAst(exp, options);
1268
+ }
843
1269
  function toPascalCase(str) {
844
1270
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
845
1271
  if (!cleaned) {
@@ -881,32 +1307,14 @@ function tryGetClickDirective(node) {
881
1307
  function nodeHasClickDirective(node) {
882
1308
  return tryGetClickDirective(node) !== void 0;
883
1309
  }
884
- function getTemplateSlotScope(node) {
1310
+ function findTemplateSlotScopeExpression(node) {
885
1311
  if (node.tag !== "template") {
886
1312
  return null;
887
1313
  }
888
1314
  const slotProp = node.props.find((prop) => {
889
1315
  return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot";
890
1316
  });
891
- if (slotProp?.exp) {
892
- if (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
893
- return slotProp.exp.content;
894
- }
895
- if (slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) {
896
- return stringifyExpression(slotProp.exp);
897
- }
898
- }
899
- return null;
900
- }
901
- function isSimpleScopeIdentifier(value) {
902
- if (!value) {
903
- return false;
904
- }
905
- try {
906
- return isIdentifier(parseExpression(value, { plugins: ["typescript"] }));
907
- } catch {
908
- return false;
909
- }
1317
+ return slotProp?.exp && (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
910
1318
  }
911
1319
  function buildSlotScopeFallbackKeyExpression(identifier) {
912
1320
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -1008,43 +1416,64 @@ function tryGetSlotScopeKeyCandidate(node) {
1008
1416
  }
1009
1417
  return best;
1010
1418
  }
1011
- function tryGetTemplateSlotScopeKeyExpression(scope) {
1012
- const trimmed = scope.trim();
1013
- if (!trimmed) {
1419
+ function tryGetTemplateSlotScopeBindingNode(expression) {
1420
+ const ast = tryGetExistingVueExpressionAst(expression);
1421
+ if (ast) {
1422
+ if (isArrowFunctionExpression(ast)) {
1423
+ return ast.params[0] ?? null;
1424
+ }
1425
+ return ast;
1426
+ }
1427
+ const rawSource = getVueExpressionSource(expression, "content", "loc", "compiled");
1428
+ if (!rawSource) {
1014
1429
  return null;
1015
1430
  }
1016
- if (isSimpleScopeIdentifier(trimmed)) {
1017
- return buildSlotScopeFallbackKeyExpression(trimmed);
1431
+ try {
1432
+ return parseExpression(rawSource, { plugins: ["typescript"] });
1433
+ } catch {
1018
1434
  }
1019
1435
  try {
1020
- const parsed = parse(`(${trimmed}) => {}`, {
1436
+ const parsed = parse(`(${rawSource}) => {}`, {
1021
1437
  sourceType: "module",
1022
1438
  plugins: ["typescript"]
1023
1439
  });
1024
1440
  const statement = parsed.program.body[0];
1025
1441
  if (statement && isExpressionStatement(statement) && isArrowFunctionExpression(statement.expression)) {
1026
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1442
+ return statement.expression.params[0] ?? null;
1027
1443
  }
1028
1444
  } catch {
1445
+ return null;
1029
1446
  }
1030
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1031
- const inner = trimmed.slice(1, -1).trim();
1032
- let cutIdx = -1;
1033
- const commaIdx = inner.indexOf(",");
1034
- const colonIdx = inner.indexOf(":");
1035
- if (commaIdx !== -1 && colonIdx !== -1) {
1036
- cutIdx = Math.min(commaIdx, colonIdx);
1037
- } else if (commaIdx !== -1) {
1038
- cutIdx = commaIdx;
1039
- } else if (colonIdx !== -1) {
1040
- cutIdx = colonIdx;
1041
- }
1042
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
1043
- if (first && isSimpleScopeIdentifier(first)) {
1044
- return buildSlotScopeFallbackKeyExpression(first);
1045
- }
1447
+ return null;
1448
+ }
1449
+ function toResolvedTemplateFragment(source) {
1450
+ const templateLiteral = tryUnwrapTemplateLiteralSource(source);
1451
+ if (templateLiteral) {
1452
+ return {
1453
+ template: templateLiteral.template,
1454
+ rawExpression: null
1455
+ };
1046
1456
  }
1047
- return trimmed;
1457
+ return toInterpolatedTemplateFragment(source);
1458
+ }
1459
+ function toResolvedKeyInfo(selectorSource, runtimeSource = selectorSource) {
1460
+ const selectorFragment = selectorSource ? toResolvedTemplateFragment(selectorSource) : null;
1461
+ const runtimeFragment = runtimeSource ? toResolvedTemplateFragment(runtimeSource) : null;
1462
+ const selectorTemplate = selectorFragment?.template ?? runtimeFragment?.template ?? null;
1463
+ const runtimeTemplate = runtimeFragment?.template ?? selectorFragment?.template ?? null;
1464
+ if (!selectorTemplate || !runtimeTemplate) {
1465
+ return null;
1466
+ }
1467
+ return {
1468
+ selectorFragment: selectorTemplate,
1469
+ runtimeFragment: runtimeTemplate,
1470
+ rawExpression: runtimeFragment?.rawExpression ?? selectorFragment?.rawExpression ?? null
1471
+ };
1472
+ }
1473
+ function tryGetTemplateSlotScopeKeyInfo(expression) {
1474
+ const bindingNode = tryGetTemplateSlotScopeBindingNode(expression);
1475
+ const candidateExpression = bindingNode ? tryGetSlotScopeKeyCandidate(bindingNode)?.expression ?? null : null;
1476
+ return candidateExpression ? toResolvedKeyInfo(candidateExpression) : null;
1048
1477
  }
1049
1478
  function nodeHasToDirective(node) {
1050
1479
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1053,51 +1482,160 @@ function nodeHasToDirective(node) {
1053
1482
  }
1054
1483
  return void 0;
1055
1484
  }
1056
- function nodeHasForDirective(node) {
1057
- return node.props.some(
1058
- (attr) => attr.type === NodeTypes.DIRECTIVE && attr.name === "for"
1059
- );
1060
- }
1061
1485
  function getKeyDirective(node) {
1062
1486
  return findDirectiveByName(node, "bind", "key") ?? null;
1063
1487
  }
1064
- function getKeyDirectiveValue(node, _context = null) {
1065
- const keyDirective = getKeyDirective(node);
1066
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1067
- if (rawSource) {
1068
- return `\${${rawSource}}`;
1488
+ function tryUnwrapTemplateLiteralSource(source) {
1489
+ const rawSource = source.trim();
1490
+ if (!rawSource) {
1491
+ return null;
1069
1492
  }
1070
- if (keyDirective?.exp) {
1071
- const value = stringifyExpression(keyDirective.exp);
1072
- if (value)
1073
- return `\${${value}}`;
1493
+ let ast = null;
1494
+ try {
1495
+ ast = parseExpression(rawSource, { plugins: ["typescript"] });
1496
+ } catch {
1497
+ return null;
1074
1498
  }
1075
- return null;
1499
+ if (!ast || !isTemplateLiteral(ast)) {
1500
+ return null;
1501
+ }
1502
+ const cooked = ast.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
1503
+ try {
1504
+ const start = typeof ast.start === "number" ? ast.start + 1 : 1;
1505
+ const end = typeof ast.end === "number" ? ast.end - 1 : rawSource.length - 1;
1506
+ return {
1507
+ template: rawSource.slice(start, end) || cooked,
1508
+ expressionCount: ast.expressions.length
1509
+ };
1510
+ } catch {
1511
+ return {
1512
+ template: cooked,
1513
+ expressionCount: ast.expressions.length
1514
+ };
1515
+ }
1516
+ }
1517
+ function tryUnwrapTemplateLiteralExpressionSource(expression) {
1518
+ const rawSource = getVueExpressionSource(expression, "loc", "compiled");
1519
+ return rawSource ? tryUnwrapTemplateLiteralSource(rawSource) : null;
1520
+ }
1521
+ function tryParseTemplateFragment(fragment) {
1522
+ if (!fragment) {
1523
+ return null;
1524
+ }
1525
+ try {
1526
+ const source = `\`${fragment}\``;
1527
+ const ast = parseExpression(source, { plugins: ["typescript"] });
1528
+ return isTemplateLiteral(ast) ? { source, templateLiteral: ast } : null;
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+ function getTemplateExpressionSource(parsedTemplate, index) {
1534
+ const expression = parsedTemplate.templateLiteral.expressions[index];
1535
+ if (!expression) {
1536
+ return null;
1537
+ }
1538
+ const start = typeof expression.start === "number" ? expression.start : null;
1539
+ const end = typeof expression.end === "number" ? expression.end : null;
1540
+ if (start === null || end === null) {
1541
+ return null;
1542
+ }
1543
+ return parsedTemplate.source.slice(start, end);
1544
+ }
1545
+ function getSingleExpressionTemplateFragment(parsedTemplate) {
1546
+ const { templateLiteral } = parsedTemplate;
1547
+ if (templateLiteral.expressions.length !== 1 || templateLiteral.quasis.length !== 2) {
1548
+ return null;
1549
+ }
1550
+ const expressionSource = getTemplateExpressionSource(parsedTemplate, 0);
1551
+ if (expressionSource === null) {
1552
+ return null;
1553
+ }
1554
+ return {
1555
+ prefix: templateLiteral.quasis[0]?.value.raw ?? "",
1556
+ expressionSource,
1557
+ suffix: templateLiteral.quasis[1]?.value.raw ?? ""
1558
+ };
1559
+ }
1560
+ function templateFragmentContainsSingleExpression(container, candidate) {
1561
+ const containerFragment = getSingleExpressionTemplateFragment(container);
1562
+ const candidateFragment = getSingleExpressionTemplateFragment(candidate);
1563
+ if (!containerFragment || !candidateFragment) {
1564
+ return false;
1565
+ }
1566
+ return containerFragment.expressionSource === candidateFragment.expressionSource && containerFragment.prefix.endsWith(candidateFragment.prefix) && containerFragment.suffix.startsWith(candidateFragment.suffix);
1567
+ }
1568
+ function hasTemplateInterpolationExpressions(fragment) {
1569
+ return (tryParseTemplateFragment(fragment)?.templateLiteral.expressions.length ?? 0) > 0;
1570
+ }
1571
+ function toInterpolatedTemplateFragment(fragment) {
1572
+ if (!fragment) {
1573
+ return null;
1574
+ }
1575
+ if (hasTemplateInterpolationExpressions(fragment)) {
1576
+ return { template: fragment, rawExpression: null };
1577
+ }
1578
+ return {
1579
+ template: `\${${fragment}}`,
1580
+ rawExpression: fragment
1581
+ };
1582
+ }
1583
+ function renderTemplateLiteralExpression(templateValue) {
1584
+ const templateLiteralSource = templateValue.parsedTemplate.source;
1585
+ const templateLiteral = templateValue.parsedTemplate.templateLiteral;
1586
+ const writer = createTypeScriptWriter();
1587
+ writer.write("`");
1588
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
1589
+ writer.write(templateLiteral.quasis[i]?.value.raw ?? "");
1590
+ const interpolation = templateLiteral.expressions[i];
1591
+ if (!interpolation) {
1592
+ continue;
1593
+ }
1594
+ const start = typeof interpolation.start === "number" ? interpolation.start : null;
1595
+ const end = typeof interpolation.end === "number" ? interpolation.end : null;
1596
+ if (start === null || end === null) {
1597
+ return templateLiteralSource;
1598
+ }
1599
+ writer.write("${");
1600
+ writer.write(templateLiteralSource.slice(start, end));
1601
+ writer.write("}");
1602
+ }
1603
+ writer.write("`");
1604
+ return writer.toString();
1605
+ }
1606
+ function getKeyDirectiveExpression(node) {
1607
+ const keyDirective = getKeyDirective(node);
1608
+ return keyDirective?.exp && (keyDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || keyDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? keyDirective.exp : null;
1609
+ }
1610
+ function getKeyDirectiveInfo(node) {
1611
+ const keyExpression = getKeyDirectiveExpression(node);
1612
+ if (!keyExpression) {
1613
+ return null;
1614
+ }
1615
+ const selectorSource = getVueExpressionSource(keyExpression, "compiled", "loc");
1616
+ const runtimeSource = getVueExpressionSource(keyExpression, "loc", "compiled");
1617
+ return toResolvedKeyInfo(selectorSource, runtimeSource);
1076
1618
  }
1077
1619
  function getModelBindingValues(node) {
1078
1620
  let vModel = "";
1079
1621
  const vModelDirective = findDirectiveByName(node, "model");
1080
- if (vModelDirective?.exp?.loc.source) {
1081
- vModel = toPascalCase(vModelDirective.exp.loc.source);
1622
+ if (vModelDirective?.exp && (vModelDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || vModelDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION)) {
1623
+ vModel = toPascalCase(getVueExpressionSource(vModelDirective.exp, "loc", "content"));
1082
1624
  }
1083
1625
  let modelValue = null;
1084
1626
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1085
- if (modelValueDirective?.exp?.ast) {
1086
- const { name: mv } = getClickHandlerNameFromAst(modelValueDirective.exp.ast);
1627
+ const modelValueAst = modelValueDirective ? tryGetDirectiveBabelAst(modelValueDirective, {
1628
+ preferredViews: ["loc", "compiled"],
1629
+ plugins: ["typescript"],
1630
+ preferExistingAst: false
1631
+ }) : null;
1632
+ if (modelValueAst) {
1633
+ const { name: mv } = getClickHandlerNameFromAst(modelValueAst);
1087
1634
  modelValue = mv;
1088
1635
  }
1089
1636
  return { vModel, modelValue };
1090
1637
  }
1091
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1092
- if (node.isSelfClosing) {
1093
- const hasForDirective = nodeHasForDirective(node);
1094
- if (hasForDirective) {
1095
- return getKeyDirectiveValue(node);
1096
- }
1097
- }
1098
- return null;
1099
- }
1100
- function getIdOrName(node) {
1638
+ function getStaticIdOrNameHint(node) {
1101
1639
  let idAttr = findAttributeByKey(node, "id");
1102
1640
  if (!idAttr) {
1103
1641
  idAttr = findAttributeByKey(node, "name");
@@ -1105,8 +1643,9 @@ function getIdOrName(node) {
1105
1643
  let identifier = idAttr?.value?.content ?? "";
1106
1644
  if (!identifier) {
1107
1645
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1108
- if (dynamicIdAttr?.exp) {
1109
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1646
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1647
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1648
+ return "";
1110
1649
  }
1111
1650
  }
1112
1651
  if (identifier.includes("-")) {
@@ -1117,20 +1656,20 @@ function getIdOrName(node) {
1117
1656
  }
1118
1657
  return identifier;
1119
1658
  }
1120
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1659
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1121
1660
  let parent = getParent(hierarchyMap2, node);
1122
1661
  while (parent) {
1123
1662
  if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
1124
- const scope = getTemplateSlotScope(parent);
1125
- if (scope) {
1126
- return tryGetTemplateSlotScopeKeyExpression(scope);
1663
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1664
+ if (slotScopeExpression) {
1665
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1127
1666
  }
1128
1667
  }
1129
1668
  parent = getParent(hierarchyMap2, parent);
1130
1669
  }
1131
1670
  return null;
1132
1671
  }
1133
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1672
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1134
1673
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1135
1674
  return null;
1136
1675
  }
@@ -1139,8 +1678,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1139
1678
  if (parent.type === NodeTypes.ELEMENT) {
1140
1679
  const forDirective = findDirectiveByName(parent, "for");
1141
1680
  if (forDirective) {
1142
- const keyValue = getKeyDirectiveValue(parent);
1143
- return keyValue;
1681
+ return getKeyDirectiveInfo(parent);
1144
1682
  }
1145
1683
  }
1146
1684
  parent = getParent(hierarchyMap2, parent);
@@ -1166,13 +1704,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1166
1704
  if (simpleSourceExp.constType === ConstantTypes.NOT_CONSTANT) {
1167
1705
  return null;
1168
1706
  }
1169
- const iterableRaw = (() => {
1170
- try {
1171
- return stringifyExpression(simpleSourceExp).trim();
1172
- } catch {
1173
- return (simpleSourceExp.loc?.source ?? "").trim();
1174
- }
1175
- })();
1707
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1176
1708
  if (!iterableRaw) {
1177
1709
  return null;
1178
1710
  }
@@ -1228,91 +1760,95 @@ function nodeHandlerAttributeInfo(node) {
1228
1760
  return null;
1229
1761
  }
1230
1762
  const exp = handlerDirective.exp;
1231
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
1763
+ const source = getVueExpressionSource(exp, "content", "compiled");
1232
1764
  if (!source) {
1233
1765
  return null;
1234
1766
  }
1235
1767
  const mergeKey = `handler:expr:${source}`;
1236
- let expr;
1237
- try {
1238
- expr = parseExpression(source, { plugins: ["typescript", "jsx"] });
1239
- } catch {
1768
+ const expr = tryGetDirectiveBabelAst(handlerDirective, {
1769
+ preferredViews: ["content", "compiled"],
1770
+ plugins: ["typescript", "jsx"],
1771
+ // Vue's compiler AST can encode `_ctx.foo` as an Identifier name instead of a MemberExpression.
1772
+ // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1773
+ preferExistingAst: false
1774
+ });
1775
+ if (!expr) {
1240
1776
  return null;
1241
1777
  }
1242
- const isNodeType = (node2, type) => {
1778
+ const isNodeType2 = (node2, type) => {
1243
1779
  return node2 !== null && node2.type === type;
1244
1780
  };
1245
- const isIdentifierNode = (node2) => {
1246
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1781
+ const isIdentifierNode2 = (node2) => {
1782
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1247
1783
  };
1248
- const isStringLiteralNode = (node2) => {
1249
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1784
+ const isStringLiteralNode2 = (node2) => {
1785
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1250
1786
  };
1251
1787
  const isBooleanLiteralNode = (node2) => {
1252
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1788
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1253
1789
  };
1254
1790
  const isNumericLiteralNode = (node2) => {
1255
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1791
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1256
1792
  };
1257
1793
  const isNullLiteralNode = (node2) => {
1258
- return isNodeType(node2, "NullLiteral");
1794
+ return isNodeType2(node2, "NullLiteral");
1259
1795
  };
1260
1796
  const isMemberExpressionNode = (node2) => {
1261
- if (!isNodeType(node2, "MemberExpression"))
1797
+ if (!isNodeType2(node2, "MemberExpression"))
1262
1798
  return false;
1263
1799
  const n = node2;
1264
1800
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1265
1801
  };
1266
1802
  const isCallExpressionNode = (node2) => {
1267
- if (!isNodeType(node2, "CallExpression"))
1803
+ if (!isNodeType2(node2, "CallExpression"))
1268
1804
  return false;
1269
1805
  const n = node2;
1270
1806
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1271
1807
  };
1272
1808
  const isAwaitExpressionNode = (node2) => {
1273
- if (!isNodeType(node2, "AwaitExpression"))
1809
+ if (!isNodeType2(node2, "AwaitExpression"))
1274
1810
  return false;
1275
1811
  const n = node2;
1276
1812
  return typeof n.argument === "object" && n.argument !== null;
1277
1813
  };
1278
1814
  const isAssignmentExpressionNode = (node2) => {
1279
- if (!isNodeType(node2, "AssignmentExpression"))
1815
+ if (!isNodeType2(node2, "AssignmentExpression"))
1280
1816
  return false;
1281
1817
  const n = node2;
1282
1818
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1283
1819
  };
1284
1820
  const isArrowFunctionExpressionNode = (node2) => {
1285
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1821
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1286
1822
  return false;
1287
1823
  const n = node2;
1288
1824
  return typeof n.body === "object" && n.body !== null;
1289
1825
  };
1290
1826
  const isBlockStatementNode = (node2) => {
1291
- if (!isNodeType(node2, "BlockStatement"))
1827
+ if (!isNodeType2(node2, "BlockStatement"))
1292
1828
  return false;
1293
1829
  const n = node2;
1294
1830
  return Array.isArray(n.body);
1295
1831
  };
1296
1832
  const isExpressionStatementNode = (node2) => {
1297
- if (!isNodeType(node2, "ExpressionStatement"))
1833
+ if (!isNodeType2(node2, "ExpressionStatement"))
1298
1834
  return false;
1299
1835
  const n = node2;
1300
1836
  return typeof n.expression === "object" && n.expression !== null;
1301
1837
  };
1302
1838
  const isReturnStatementNode = (node2) => {
1303
- if (!isNodeType(node2, "ReturnStatement"))
1839
+ if (!isNodeType2(node2, "ReturnStatement"))
1304
1840
  return false;
1305
1841
  const n = node2;
1306
1842
  return typeof n.argument === "object" || n.argument === null;
1307
1843
  };
1308
- const isObjectExpressionNode = (node2) => {
1309
- if (!isNodeType(node2, "ObjectExpression"))
1844
+ const isObjectExpressionNode2 = (node2) => {
1845
+ if (!isNodeType2(node2, "ObjectExpression"))
1310
1846
  return false;
1311
1847
  const n = node2;
1312
1848
  return Array.isArray(n.properties);
1313
1849
  };
1314
- const isObjectPropertyNode = (node2) => {
1315
- if (!isNodeType(node2, "ObjectProperty"))
1850
+ const isObjectPropertyNode2 = (node2) => {
1851
+ if (!isNodeType2(node2, "ObjectProperty"))
1316
1852
  return false;
1317
1853
  const n = node2;
1318
1854
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1320,16 +1856,16 @@ function nodeHandlerAttributeInfo(node) {
1320
1856
  const getLastIdentifierFromMemberChain = (node2) => {
1321
1857
  if (!node2)
1322
1858
  return null;
1323
- if (isIdentifierNode(node2))
1859
+ if (isIdentifierNode2(node2))
1324
1860
  return node2.name;
1325
1861
  if (isMemberExpressionNode(node2)) {
1326
1862
  const prop = node2.property;
1327
1863
  if (node2.computed === false) {
1328
- if (isIdentifierNode(prop))
1864
+ if (isIdentifierNode2(prop))
1329
1865
  return prop.name;
1330
1866
  }
1331
1867
  if (node2.computed === true) {
1332
- if (isStringLiteralNode(prop))
1868
+ if (isStringLiteralNode2(prop))
1333
1869
  return prop.value;
1334
1870
  }
1335
1871
  }
@@ -1339,7 +1875,7 @@ function nodeHandlerAttributeInfo(node) {
1339
1875
  if (!node2) {
1340
1876
  return null;
1341
1877
  }
1342
- if (isIdentifierNode(node2)) {
1878
+ if (isIdentifierNode2(node2)) {
1343
1879
  return node2.name;
1344
1880
  }
1345
1881
  if (isMemberExpressionNode(node2)) {
@@ -1351,11 +1887,11 @@ function nodeHandlerAttributeInfo(node) {
1351
1887
  if (!lhs) {
1352
1888
  return null;
1353
1889
  }
1354
- if (isIdentifierNode(lhs)) {
1890
+ if (isIdentifierNode2(lhs)) {
1355
1891
  return lhs.name;
1356
1892
  }
1357
1893
  if (isMemberExpressionNode(lhs)) {
1358
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1894
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1359
1895
  return getLastIdentifierFromMemberChain(lhs.object);
1360
1896
  }
1361
1897
  return getLastIdentifierFromMemberChain(lhs);
@@ -1363,7 +1899,7 @@ function nodeHandlerAttributeInfo(node) {
1363
1899
  return null;
1364
1900
  };
1365
1901
  const isTemplateLiteralNode = (node2) => {
1366
- if (!isNodeType(node2, "TemplateLiteral")) {
1902
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1367
1903
  return false;
1368
1904
  }
1369
1905
  const n = node2;
@@ -1382,7 +1918,7 @@ function nodeHandlerAttributeInfo(node) {
1382
1918
  if (isNullLiteralNode(arg)) {
1383
1919
  return "Null";
1384
1920
  }
1385
- if (isStringLiteralNode(arg)) {
1921
+ if (isStringLiteralNode2(arg)) {
1386
1922
  const cleaned = (arg.value ?? "").trim();
1387
1923
  if (!cleaned) {
1388
1924
  return null;
@@ -1408,7 +1944,7 @@ function nodeHandlerAttributeInfo(node) {
1408
1944
  return toPascalCase(stableName.slice(0, 24));
1409
1945
  }
1410
1946
  }
1411
- if (isIdentifierNode(arg)) {
1947
+ if (isIdentifierNode2(arg)) {
1412
1948
  const firstChar = arg.name.charAt(0);
1413
1949
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1414
1950
  if (isUpperAlpha) {
@@ -1420,7 +1956,7 @@ function nodeHandlerAttributeInfo(node) {
1420
1956
  const getStableSuffixFromCall = (call) => {
1421
1957
  const args = call.arguments ?? [];
1422
1958
  const first = args.length > 0 ? args[0] : null;
1423
- if (!isObjectExpressionNode(first)) {
1959
+ if (!isObjectExpressionNode2(first)) {
1424
1960
  const parts2 = [];
1425
1961
  for (const arg of args.slice(0, 4)) {
1426
1962
  const w = stableWordFromValue(arg ?? null);
@@ -1439,20 +1975,20 @@ function nodeHandlerAttributeInfo(node) {
1439
1975
  }
1440
1976
  const parts = [];
1441
1977
  for (const prop of first.properties ?? []) {
1442
- if (!isObjectPropertyNode(prop)) {
1978
+ if (!isObjectPropertyNode2(prop)) {
1443
1979
  continue;
1444
1980
  }
1445
1981
  if (prop.computed) {
1446
1982
  continue;
1447
1983
  }
1448
- const keyName = isIdentifierNode(prop.key) ? prop.key.name : isStringLiteralNode(prop.key) ? prop.key.value : null;
1984
+ const keyName = isIdentifierNode2(prop.key) ? prop.key.name : isStringLiteralNode2(prop.key) ? prop.key.value : null;
1449
1985
  if (!keyName) {
1450
1986
  continue;
1451
1987
  }
1452
1988
  let valueWord = null;
1453
1989
  if (isBooleanLiteralNode(prop.value)) {
1454
1990
  valueWord = prop.value.value ? "True" : "False";
1455
- } else if (isStringLiteralNode(prop.value)) {
1991
+ } else if (isStringLiteralNode2(prop.value)) {
1456
1992
  const cleaned = (prop.value.value ?? "").trim();
1457
1993
  if (cleaned) {
1458
1994
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1581,13 +2117,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1581
2117
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1582
2118
  }
1583
2119
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1584
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1585
- let value = attrDynamic.exp.loc.source;
1586
- if (attrDynamic.exp.ast?.type === "MemberExpression") {
2120
+ const attrDynamicAst = attrDynamic ? tryGetDirectiveBabelAst(attrDynamic, {
2121
+ preferredViews: ["loc", "compiled"],
2122
+ plugins: ["typescript"],
2123
+ preferExistingAst: false
2124
+ }) : null;
2125
+ if (attrDynamic?.exp && attrDynamicAst) {
2126
+ let value = getVueExpressionSource(attrDynamic.exp, "loc", "compiled");
2127
+ if (isMemberExpression(attrDynamicAst) || isOptionalMemberExpression(attrDynamicAst)) {
1587
2128
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1588
2129
  }
1589
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1590
- value = stringifyExpression(attrDynamic.exp);
2130
+ if (isCallExpression(attrDynamicAst) || isOptionalCallExpression(attrDynamicAst)) {
2131
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1591
2132
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1592
2133
  }
1593
2134
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1595,16 +2136,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1595
2136
  return null;
1596
2137
  }
1597
2138
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1598
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1599
- if (key) {
1600
- return templateAttributeValue(`${componentName}-${key}-${formatTagName(node, nativeWrappers)}`);
2139
+ const keyInfo = getKeyDirectiveInfo(node) || getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2);
2140
+ if (keyInfo) {
2141
+ return templateAttributeValue(`${componentName}-${keyInfo.selectorFragment}-${formatTagName(node, nativeWrappers)}`);
1601
2142
  } else {
1602
2143
  let name = toDirectiveObjectFieldNameValue(toDirective);
1603
2144
  if (!name) {
1604
2145
  if (toDirective.exp == null) {
1605
2146
  return null;
1606
2147
  }
1607
- const source = stringifyExpression(toDirective.exp);
2148
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1608
2149
  const toAst = toDirective.exp.ast;
1609
2150
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && isTemplateLiteral(toAst);
1610
2151
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1628,44 +2169,46 @@ function toDirectiveObjectFieldNameValue(node) {
1628
2169
  if (!node.exp || node.exp.type !== NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== NodeTypes.SIMPLE_EXPRESSION) {
1629
2170
  return null;
1630
2171
  }
1631
- const source = node.exp.loc.source.trim();
1632
- try {
1633
- const expr = parseExpression(source, { plugins: ["typescript"] });
1634
- const isNodeType = (n, type) => {
1635
- return n !== null && n.type === type;
1636
- };
1637
- const isStringLiteralNode = (n) => {
1638
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1639
- };
1640
- const isIdentifierNode = (n) => {
1641
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1642
- };
1643
- const isObjectPropertyNode = (n) => {
1644
- if (!isNodeType(n, "ObjectProperty"))
1645
- return false;
1646
- const nn = n;
1647
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1648
- };
1649
- const isObjectExpressionNode = (n) => {
1650
- if (!isNodeType(n, "ObjectExpression"))
1651
- return false;
1652
- const nn = n;
1653
- return Array.isArray(nn.properties);
1654
- };
1655
- if (!isObjectExpressionNode(expr))
1656
- return null;
1657
- const nameProp = expr.properties.find((p) => {
1658
- if (!isObjectPropertyNode(p))
1659
- return false;
1660
- const key = p.key;
1661
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1662
- });
1663
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1664
- return null;
1665
- return toPascalCase(nameProp.value.value);
1666
- } catch {
2172
+ const expr = tryGetDirectiveBabelAst(node, {
2173
+ preferredViews: ["loc", "compiled"],
2174
+ plugins: ["typescript"],
2175
+ preferExistingAst: false
2176
+ });
2177
+ if (!expr) {
1667
2178
  return null;
1668
2179
  }
2180
+ const isNodeType2 = (n, type) => {
2181
+ return n !== null && n.type === type;
2182
+ };
2183
+ const isStringLiteralNode2 = (n) => {
2184
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
2185
+ };
2186
+ const isIdentifierNode2 = (n) => {
2187
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
2188
+ };
2189
+ const isObjectPropertyNode2 = (n) => {
2190
+ if (!isNodeType2(n, "ObjectProperty"))
2191
+ return false;
2192
+ const nn = n;
2193
+ return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
2194
+ };
2195
+ const isObjectExpressionNode2 = (n) => {
2196
+ if (!isNodeType2(n, "ObjectExpression"))
2197
+ return false;
2198
+ const nn = n;
2199
+ return Array.isArray(nn.properties);
2200
+ };
2201
+ if (!isObjectExpressionNode2(expr))
2202
+ return null;
2203
+ const nameProp = expr.properties.find((p) => {
2204
+ if (!isObjectPropertyNode2(p))
2205
+ return false;
2206
+ const key = p.key;
2207
+ return isIdentifierNode2(key) && key.name === "name" || isStringLiteralNode2(key) && key.value === "name";
2208
+ });
2209
+ if (!nameProp || !isObjectPropertyNode2(nameProp) || !isStringLiteralNode2(nameProp.value))
2210
+ return null;
2211
+ return toPascalCase(nameProp.value.value);
1669
2212
  }
1670
2213
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1671
2214
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1675,7 +2218,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1675
2218
  let handlerName = "";
1676
2219
  if (click.exp) {
1677
2220
  const exp = click.exp;
1678
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
2221
+ const source = getVueExpressionSource(exp, "content", "compiled");
1679
2222
  if (source) {
1680
2223
  const parsed = tryParseBabelAstFromHandlerSource(source);
1681
2224
  if (parsed) {
@@ -1696,9 +2239,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1696
2239
  const trimmed = source.trim();
1697
2240
  if (!trimmed)
1698
2241
  return null;
1699
- try {
1700
- return parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1701
- } catch {
2242
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2243
+ if (expressionAst) {
2244
+ return expressionAst;
1702
2245
  }
1703
2246
  try {
1704
2247
  return parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -2037,23 +2580,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2037
2580
  return null;
2038
2581
  }
2039
2582
  const simpleExp = exp;
2040
- const ast = simpleExp.ast;
2041
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
2042
- const tl = ast;
2043
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2044
- const expressionCount = (tl.expressions ?? []).length;
2045
- const isStatic = expressionCount === 0;
2046
- const raw2 = (simpleExp.content ?? "").trim();
2047
- const unwrappedTemplate = raw2.startsWith("`") && raw2.endsWith("`") && raw2.length >= 2 ? raw2.slice(1, -1) : cooked;
2583
+ const ast = tryGetVueExpressionAst(simpleExp, {
2584
+ preferredViews: ["content", "loc", "compiled"],
2585
+ plugins: ["typescript"]
2586
+ });
2587
+ const unwrappedTemplateLiteral = tryUnwrapTemplateLiteralExpressionSource(simpleExp);
2588
+ if (unwrappedTemplateLiteral) {
2589
+ const isStatic = unwrappedTemplateLiteral.expressionCount === 0;
2048
2590
  if (isStatic) {
2049
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2591
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2050
2592
  }
2593
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2051
2594
  return {
2052
- value: unwrappedTemplate,
2595
+ value: templateValue.template,
2053
2596
  isDynamic: true,
2054
2597
  isStaticLiteral: false,
2055
- template: unwrappedTemplate,
2056
- templateExpressionCount: expressionCount
2598
+ template: templateValue.template,
2599
+ parsedTemplate: templateValue.parsedTemplate,
2600
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2057
2601
  };
2058
2602
  }
2059
2603
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2066,11 +2610,13 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2066
2610
  }
2067
2611
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2068
2612
  if (preservableReference) {
2613
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2069
2614
  return {
2070
2615
  value: preservableReference,
2071
2616
  isDynamic: true,
2072
2617
  isStaticLiteral: false,
2073
- template: `\${${preservableReference}}`,
2618
+ template: templateValue.template,
2619
+ parsedTemplate: templateValue.parsedTemplate,
2074
2620
  templateExpressionCount: 1,
2075
2621
  rawExpression: preservableReference
2076
2622
  };
@@ -2079,46 +2625,12 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2079
2625
  if (!raw) {
2080
2626
  return null;
2081
2627
  }
2082
- try {
2083
- const ast2 = parseExpression(raw, { plugins: ["typescript"] });
2084
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2085
- const tl = ast2;
2086
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2087
- const expressionCount = (tl.expressions ?? []).length;
2088
- const isStatic = expressionCount === 0;
2089
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2090
- if (isStatic) {
2091
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2092
- }
2093
- return {
2094
- value: unwrappedTemplate,
2095
- isDynamic: true,
2096
- isStaticLiteral: false,
2097
- template: unwrappedTemplate,
2098
- templateExpressionCount: expressionCount
2099
- };
2100
- }
2101
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2102
- const sl = ast2;
2103
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2104
- }
2105
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2106
- if (preservableReference2) {
2107
- return {
2108
- value: preservableReference2,
2109
- isDynamic: true,
2110
- isStaticLiteral: false,
2111
- template: `\${${preservableReference2}}`,
2112
- templateExpressionCount: 1,
2113
- rawExpression: preservableReference2
2114
- };
2115
- }
2116
- } catch {
2117
- }
2118
2628
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2119
2629
  }
2120
2630
  function isTemplatePlaceholder(part) {
2121
- return part.startsWith("${") && part.endsWith("}") && part.length >= 3;
2631
+ const parsedTemplate = tryParseTemplateFragment(part);
2632
+ const templateFragment = parsedTemplate ? getSingleExpressionTemplateFragment(parsedTemplate) : null;
2633
+ return !!templateFragment && templateFragment.prefix === "" && templateFragment.suffix === "";
2122
2634
  }
2123
2635
  function isAllCapsOrDigits(value) {
2124
2636
  if (value.length <= 1) {
@@ -2171,33 +2683,14 @@ function safeMethodNameFromParts(parts) {
2171
2683
  }
2172
2684
  return name;
2173
2685
  }
2174
- function replaceAllTemplateExpressionsWithKey(template) {
2686
+ function toPomKeyPattern(templateValue) {
2687
+ const { templateLiteral } = templateValue.parsedTemplate;
2175
2688
  let out = "";
2176
- let i = 0;
2177
- while (i < template.length) {
2178
- const start = template.indexOf("${", i);
2179
- if (start < 0) {
2180
- out += template.slice(i);
2181
- break;
2182
- }
2183
- out += template.slice(i, start);
2184
- let depth = 1;
2185
- let j = start + 2;
2186
- while (j < template.length && depth > 0) {
2187
- if (template[j] === "{") {
2188
- depth++;
2189
- } else if (template[j] === "}") {
2190
- depth--;
2191
- }
2192
- j++;
2193
- }
2194
- const end = depth === 0 ? j - 1 : -1;
2195
- if (end < 0) {
2196
- out += template.slice(start);
2197
- break;
2689
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
2690
+ out += templateLiteral.quasis[i]?.value.raw ?? "";
2691
+ if (templateLiteral.expressions[i]) {
2692
+ out += "${key}";
2198
2693
  }
2199
- out += "${key}";
2200
- i = end + 1;
2201
2694
  }
2202
2695
  return out;
2203
2696
  }
@@ -2205,8 +2698,8 @@ function applyResolvedDataTestId(args) {
2205
2698
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2206
2699
  const entryOverrides = args.entryOverrides ?? {};
2207
2700
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2208
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2209
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2701
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2702
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2210
2703
  const warn = args.warn;
2211
2704
  const getBestKeyAccessCandidates = (expr) => {
2212
2705
  if (!expr) {
@@ -2215,7 +2708,10 @@ function applyResolvedDataTestId(args) {
2215
2708
  return splitNullishCoalescingExpression(expr);
2216
2709
  };
2217
2710
  let dataTestId = args.preferredGeneratedValue;
2711
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2218
2712
  let fromExisting = false;
2713
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2714
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2219
2715
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2220
2716
  if (existing) {
2221
2717
  const loc = args.element.loc?.start;
@@ -2236,8 +2732,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2236
2732
  if (existingIdBehavior === "preserve") {
2237
2733
  if (existing.isDynamic) {
2238
2734
  if (existing.template) {
2239
- const existingTemplate = existing.template;
2240
- if ((existing.templateExpressionCount ?? 0) !== 1) {
2735
+ const existingTemplateValue = existing.parsedTemplate ? { kind: "template", template: existing.template, parsedTemplate: existing.parsedTemplate } : templateAttributeValue(existing.template);
2736
+ const existingTemplateFragment = getSingleExpressionTemplateFragment(existingTemplateValue.parsedTemplate);
2737
+ const requiredKeyTemplateValue = bestKeyPreservePlaceholder ? templateAttributeValue(bestKeyPreservePlaceholder) : null;
2738
+ if ((existing.templateExpressionCount ?? 0) !== 1 || !existingTemplateFragment) {
2241
2739
  throw new Error(
2242
2740
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2243
2741
  Component: ${args.componentName}
@@ -2247,20 +2745,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2247
2745
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2248
2746
  );
2249
2747
  }
2250
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2251
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2252
- if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
2748
+ const hasExact = requiredKeyTemplateValue ? templateFragmentContainsSingleExpression(existingTemplateValue.parsedTemplate, requiredKeyTemplateValue.parsedTemplate) : false;
2749
+ const hasVarAccess = getBestKeyAccessCandidates(bestKeyVariable).some((candidate) => existingTemplateFragment.expressionSource === candidate);
2750
+ if (!hasExact && !hasVarAccess && bestKeyPreservePlaceholder) {
2253
2751
  throw new Error(
2254
2752
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2255
2753
  Component: ${args.componentName}
2256
2754
  File: ${file}:${locationHint}
2257
2755
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2258
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2756
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2259
2757
 
2260
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2758
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2261
2759
  );
2262
2760
  }
2263
- dataTestId = templateAttributeValue(existing.template);
2761
+ dataTestId = existingTemplateValue;
2762
+ runtimeDataTestId = existingTemplateValue;
2264
2763
  fromExisting = true;
2265
2764
  } else {
2266
2765
  throw new Error(
@@ -2274,18 +2773,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2274
2773
  );
2275
2774
  }
2276
2775
  } else {
2277
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2776
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2278
2777
  throw new Error(
2279
2778
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2280
2779
  Component: ${args.componentName}
2281
2780
  File: ${file}:${locationHint}
2282
2781
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2283
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2782
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2284
2783
 
2285
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2784
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2286
2785
  );
2287
2786
  }
2288
2787
  dataTestId = staticAttributeValue(existing.value);
2788
+ runtimeDataTestId = staticAttributeValue(existing.value);
2289
2789
  fromExisting = true;
2290
2790
  }
2291
2791
  }
@@ -2315,8 +2815,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2315
2815
  };
2316
2816
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2317
2817
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2318
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2319
- const isKeyed = formattedDataTestIdForPom.includes("${key}");
2818
+ const formattedDataTestIdForPom = dataTestId.kind === "template" ? toPomKeyPattern(dataTestId) : dataTestId.value;
2819
+ const selectorPatternKind = dataTestId.kind === "template" ? "parameterized" : "static";
2820
+ const selectorPattern = createPomStringPattern(formattedDataTestIdForPom, selectorPatternKind);
2821
+ const selectorIsParameterized = selectorPatternKind === "parameterized";
2320
2822
  const deriveBaseMethodNameFromHint = (hint) => {
2321
2823
  const hintRaw = (hint ?? "").trim();
2322
2824
  const trimEdgeSeparators = (value) => {
@@ -2353,7 +2855,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2353
2855
  }
2354
2856
  return value.slice(0, idx) + value.slice(idx + "ByKey".length);
2355
2857
  };
2356
- const hasRoleSuffix = (baseName, roleSuffix) => {
2858
+ const hasRoleSuffix2 = (baseName, roleSuffix) => {
2357
2859
  if (baseName.endsWith(roleSuffix)) {
2358
2860
  return true;
2359
2861
  }
@@ -2363,14 +2865,14 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2363
2865
  const getPrimaryGetterName = (primaryMethodName) => {
2364
2866
  const roleSuffix = upperFirst(normalizedRole || "Element");
2365
2867
  const baseName = upperFirst(primaryMethodName);
2366
- const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2367
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2868
+ const propertyName = hasRoleSuffix2(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2869
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2368
2870
  };
2369
2871
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2370
2872
  const roleSuffix = upperFirst(normalizedRole || "Element");
2371
2873
  const baseName = upperFirst(primaryMethodName);
2372
- const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2373
- if (!isKeyed) {
2874
+ const propertyName = hasRoleSuffix2(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2875
+ if (!selectorIsParameterized) {
2374
2876
  return { primary: propertyName };
2375
2877
  }
2376
2878
  const stripped = removeByKeySegment2(propertyName);
@@ -2432,11 +2934,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2432
2934
  return false;
2433
2935
  }
2434
2936
  const existingSelectors = [
2435
- existingPom.formattedDataTestId,
2436
- ...existingPom.alternateFormattedDataTestIds ?? []
2937
+ existingPom.selector,
2938
+ ...existingPom.alternateSelectors ?? []
2437
2939
  ];
2438
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2439
- if (isKeyed && !sharesSelectorIdentity) {
2940
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2941
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2440
2942
  return false;
2441
2943
  }
2442
2944
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2451,12 +2953,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2451
2953
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2452
2954
  return false;
2453
2955
  }
2454
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2455
- existingPom.alternateFormattedDataTestIds ??= [];
2456
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2457
- existingPom.alternateFormattedDataTestIds.push(formattedDataTestIdForPom);
2956
+ if (!pomStringPatternEquals(existingPom.selector, selectorPattern)) {
2957
+ existingPom.alternateSelectors ??= [];
2958
+ if (!(existingPom.alternateSelectors ?? []).some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern))) {
2959
+ existingPom.alternateSelectors.push(selectorPattern);
2458
2960
  }
2459
2961
  }
2962
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
2963
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
2964
+ }
2460
2965
  return true;
2461
2966
  };
2462
2967
  let methodName = "";
@@ -2469,7 +2974,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2469
2974
  let suffix = 1;
2470
2975
  while (true) {
2471
2976
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2472
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2977
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2473
2978
  const actionName = getPrimaryActionMethodName(candidate);
2474
2979
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2475
2980
  let chosenGetterName = getterCandidates.primary;
@@ -2493,9 +2998,9 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2493
2998
  if (conflicts && nameCollisionBehavior === "error") {
2494
2999
  const roleSuffix = upperFirst(normalizedRole || "Element");
2495
3000
  const baseNameUpper = upperFirst(baseWithSuffix);
2496
- if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
3001
+ if (!hasRoleSuffix2(baseNameUpper, roleSuffix)) {
2497
3002
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2498
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
3003
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2499
3004
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2500
3005
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2501
3006
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2563,51 +3068,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2563
3068
  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".`
2564
3069
  );
2565
3070
  }
2566
- const params = {};
2567
- if (isKeyed) {
2568
- params.key = keyTypeFromValues;
2569
- }
3071
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2570
3072
  switch (normalizedRole) {
2571
3073
  case "input":
2572
- params.text = "string";
2573
- params.annotationText = 'string = ""';
2574
- if (!isKeyed) delete params.key;
3074
+ parameters = setPomParameter(parameters, "text", "string");
3075
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2575
3076
  break;
2576
3077
  case "select":
2577
- params.value = "string";
2578
- params.annotationText = 'string = ""';
2579
- if (!isKeyed) delete params.key;
3078
+ parameters = setPomParameter(parameters, "value", "string");
3079
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2580
3080
  break;
2581
3081
  case "vselect":
2582
- params.value = "string";
2583
- params.timeOut = "number = 500";
2584
- params.annotationText = 'string = ""';
2585
- if (!isKeyed) delete params.key;
3082
+ parameters = setPomParameter(parameters, "value", "string");
3083
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
3084
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2586
3085
  break;
2587
3086
  case "radio":
2588
- params.annotationText = 'string = ""';
3087
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2589
3088
  break;
2590
3089
  }
2591
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2592
- params.key = keyTypeFromValues;
2593
- }
3090
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2594
3091
  if (addHtmlAttribute && !fromExisting) {
2595
3092
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2596
3093
  }
2597
3094
  const childComponentName = args.element.tag;
2598
3095
  const dataTestIdEntry = {
2599
- value: getAttributeValueText(dataTestId),
2600
- templateLiteral: void 0,
2601
- ...entryOverrides
3096
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
3097
+ getAttributeValueText(dataTestId),
3098
+ dataTestId.kind === "template" ? "parameterized" : "static"
3099
+ ),
3100
+ templateLiteral: entryOverrides.templateLiteral,
3101
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2602
3102
  };
2603
3103
  dataTestIdEntry.pom = {
2604
3104
  nativeRole: normalizedRole,
2605
3105
  methodName,
2606
3106
  getterNameOverride,
2607
- formattedDataTestId: formattedDataTestIdForPom,
2608
- alternateFormattedDataTestIds: void 0,
3107
+ selector: selectorPattern,
3108
+ alternateSelectors: void 0,
2609
3109
  mergeKey: args.pomMergeKey,
2610
- params,
3110
+ parameters: normalizedParameters,
2611
3111
  keyValuesOverride: args.keyValuesOverride ?? null
2612
3112
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2613
3113
  };
@@ -2638,43 +3138,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2638
3138
  }
2639
3139
  };
2640
3140
  const getSignatureForGeneratedMethod = () => {
2641
- const role = normalizedRole;
2642
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2643
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2644
- const keyType = keyTypeFromValues;
2645
- if (isNavigation2) {
2646
- if (needsKey2) {
2647
- return { params: `key: ${keyType}`, argNames: ["key"] };
2648
- }
2649
- return { params: "", argNames: [] };
2650
- }
2651
- switch (role) {
2652
- case "input":
2653
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2654
- case "select":
2655
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2656
- case "vselect":
2657
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2658
- case "radio":
2659
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2660
- default:
2661
- if (needsKey2) {
2662
- return { params: `key: ${keyType}`, argNames: ["key"] };
2663
- }
2664
- return { params: "", argNames: [] };
2665
- }
3141
+ return createPomMethodSignature(normalizedParameters);
2666
3142
  };
2667
3143
  const registerPrimaryOnce = (pom) => {
2668
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2669
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3144
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2670
3145
  const key = JSON.stringify({
2671
3146
  kind: "primary",
2672
3147
  role: pom.nativeRole,
2673
3148
  methodName: pom.methodName,
2674
3149
  getterNameOverride: pom.getterNameOverride ?? null,
2675
- formattedDataTestId: pom.formattedDataTestId,
2676
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2677
- params: stableParams,
3150
+ selector: pom.selector,
3151
+ alternateSelectors: alternates.length ? alternates : void 0,
3152
+ parameters: pom.parameters,
2678
3153
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2679
3154
  emitPrimary: pom.emitPrimary ?? true
2680
3155
  });
@@ -2688,8 +3163,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2688
3163
  }
2689
3164
  };
2690
3165
  const addExtraClickMethod = (spec) => {
2691
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2692
- const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, params: stableParams });
3166
+ const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, parameters: spec.parameters });
2693
3167
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2694
3168
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2695
3169
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2712,7 +3186,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2712
3186
  if (prev === null) {
2713
3187
  return;
2714
3188
  }
2715
- if (signature === null || prev.params !== signature.params) {
3189
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2716
3190
  args.dependencies.generatedMethods.set(name, null);
2717
3191
  }
2718
3192
  };
@@ -2728,23 +3202,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2728
3202
  return candidate;
2729
3203
  };
2730
3204
  const tryGetDirectiveExpressionAst = (dir) => {
2731
- const exp = dir.exp;
2732
- if (!exp) {
2733
- return null;
2734
- }
2735
- if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
2736
- const simple = exp;
2737
- const ast = simple.ast;
2738
- if (ast && "type" in ast) {
2739
- return ast;
2740
- }
2741
- }
2742
- try {
2743
- const raw = args.context ? stringifyExpression(exp) : exp.loc.source;
2744
- return parseExpression(raw, { plugins: ["typescript"] });
2745
- } catch {
2746
- return null;
2747
- }
3205
+ return tryGetDirectiveBabelAst(dir, {
3206
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3207
+ plugins: ["typescript"]
3208
+ });
2748
3209
  };
2749
3210
  const tryGetStaticStringFromBabel = (node) => {
2750
3211
  if (!node) {
@@ -2841,17 +3302,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2841
3302
  name: generatedName2,
2842
3303
  selector: {
2843
3304
  kind: "withinTestIdByLabel",
2844
- rootFormattedDataTestId: wrapperTestId,
2845
- formattedLabel: label,
3305
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3306
+ label: createPomStringPattern(label, "static"),
2846
3307
  exact: true
2847
3308
  },
2848
- params: { annotationText: `string = ""` }
3309
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2849
3310
  });
2850
3311
  if (added2) {
2851
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3312
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2852
3313
  }
2853
3314
  }
2854
- return;
3315
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2855
3316
  }
2856
3317
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2857
3318
  if (dataTestIdEntry.pom) {
@@ -2863,19 +3324,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2863
3324
  name: generatedName,
2864
3325
  selector: {
2865
3326
  kind: "withinTestIdByLabel",
2866
- rootFormattedDataTestId: wrapperTestId,
2867
- formattedLabel: "${value}",
3327
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3328
+ label: createPomStringPattern("${value}", "parameterized"),
2868
3329
  exact: true
2869
3330
  },
2870
- params: { value: "string", annotationText: `string = ""` }
3331
+ parameters: [
3332
+ createPomParameterSpec("value", "string"),
3333
+ createPomParameterSpec("annotationText", `string = ""`)
3334
+ ]
2871
3335
  });
2872
3336
  if (added) {
2873
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3337
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3338
+ createPomParameterSpec("value", "string"),
3339
+ createPomParameterSpec("annotationText", `string = ""`)
3340
+ ]));
2874
3341
  }
2875
- return;
3342
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2876
3343
  }
2877
3344
  const staticKeyValues = args.keyValuesOverride ?? null;
2878
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3345
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2879
3346
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2880
3347
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2881
3348
  if (dataTestIdEntry.pom) {
@@ -2894,19 +3361,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2894
3361
  name: generatedName,
2895
3362
  selector: {
2896
3363
  kind: "testId",
2897
- formattedDataTestId: formattedDataTestIdForPom
3364
+ testId: selectorPattern
2898
3365
  },
2899
3366
  keyLiteral: rawValue,
2900
- params: { wait: "boolean = true", annotationText: 'string = ""' }
3367
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2901
3368
  });
2902
3369
  if (added) {
2903
- registerGeneratedMethodSignature(generatedName, {
2904
- params: `wait: boolean = true, annotationText: string = ""`,
2905
- argNames: ["wait", "annotationText"]
2906
- });
3370
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3371
+ createPomParameterSpec("wait", "boolean = true"),
3372
+ createPomParameterSpec("annotationText", 'string = ""')
3373
+ ]));
2907
3374
  }
2908
3375
  }
2909
- return;
3376
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2910
3377
  }
2911
3378
  if (dataTestIdEntry.pom) {
2912
3379
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2920,6 +3387,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2920
3387
  const generatedName = getGeneratedMethodName();
2921
3388
  registerGeneratedMethodSignature(generatedName, signature);
2922
3389
  }
3390
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2923
3391
  }
2924
3392
  function safeRealpath(value) {
2925
3393
  try {
@@ -3892,127 +4360,20 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
3892
4360
  debugLog("closing internal vite server");
3893
4361
  await server.close();
3894
4362
  }
3895
- } finally {
3896
- restoreDomShim();
3897
- }
3898
- });
3899
- }
3900
- const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
3901
- const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
3902
- const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
3903
- class VuePomGeneratorError extends Error {
3904
- constructor(message) {
3905
- const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
3906
- super(normalized);
3907
- this.name = "VuePomGeneratorError";
3908
- }
3909
- }
3910
- function splitParameterList(parameters) {
3911
- const parts = [];
3912
- let current = "";
3913
- let braceDepth = 0;
3914
- let bracketDepth = 0;
3915
- let parenDepth = 0;
3916
- let angleDepth = 0;
3917
- let inSingleQuote = false;
3918
- let inDoubleQuote = false;
3919
- let inTemplateString = false;
3920
- for (let index = 0; index < parameters.length; index += 1) {
3921
- const char = parameters[index];
3922
- const previous = index > 0 ? parameters[index - 1] : "";
3923
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3924
- inSingleQuote = !inSingleQuote;
3925
- current += char;
3926
- continue;
3927
- }
3928
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3929
- inDoubleQuote = !inDoubleQuote;
3930
- current += char;
3931
- continue;
3932
- }
3933
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3934
- inTemplateString = !inTemplateString;
3935
- current += char;
3936
- continue;
3937
- }
3938
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3939
- current += char;
3940
- continue;
3941
- }
3942
- switch (char) {
3943
- case "{":
3944
- braceDepth += 1;
3945
- break;
3946
- case "}":
3947
- braceDepth -= 1;
3948
- break;
3949
- case "[":
3950
- bracketDepth += 1;
3951
- break;
3952
- case "]":
3953
- bracketDepth -= 1;
3954
- break;
3955
- case "(":
3956
- parenDepth += 1;
3957
- break;
3958
- case ")":
3959
- parenDepth -= 1;
3960
- break;
3961
- case "<":
3962
- angleDepth += 1;
3963
- break;
3964
- case ">":
3965
- angleDepth -= 1;
3966
- break;
3967
- case ",":
3968
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
3969
- const trimmed2 = current.trim();
3970
- if (trimmed2) {
3971
- parts.push(trimmed2);
3972
- }
3973
- current = "";
3974
- continue;
3975
- }
3976
- break;
3977
- }
3978
- current += char;
3979
- }
3980
- const trimmed = current.trim();
3981
- if (trimmed) {
3982
- parts.push(trimmed);
3983
- }
3984
- return parts;
3985
- }
3986
- function parseParameterSignature(parameter) {
3987
- const colonIndex = parameter.indexOf(":");
3988
- if (colonIndex < 0) {
3989
- return { name: parameter.trim() };
3990
- }
3991
- const rawName = parameter.slice(0, colonIndex).trim();
3992
- const hasQuestionToken = rawName.endsWith("?");
3993
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
3994
- const remainder = parameter.slice(colonIndex + 1).trim();
3995
- const initializerIndex = remainder.lastIndexOf("=");
3996
- if (initializerIndex < 0) {
3997
- return {
3998
- name,
3999
- hasQuestionToken,
4000
- type: remainder || void 0
4001
- };
4002
- }
4003
- return {
4004
- name,
4005
- hasQuestionToken,
4006
- type: remainder.slice(0, initializerIndex).trim() || void 0,
4007
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
4008
- };
4363
+ } finally {
4364
+ restoreDomShim();
4365
+ }
4366
+ });
4009
4367
  }
4010
- function parseParameterSignatures(parameters) {
4011
- const trimmed = parameters.trim();
4012
- if (!trimmed) {
4013
- return [];
4368
+ const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
4369
+ const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
4370
+ const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
4371
+ class VuePomGeneratorError extends Error {
4372
+ constructor(message) {
4373
+ const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
4374
+ super(normalized);
4375
+ this.name = "VuePomGeneratorError";
4014
4376
  }
4015
- return splitParameterList(trimmed).map(parseParameterSignature);
4016
4377
  }
4017
4378
  function toPosixRelativePath(fromDir, toFile) {
4018
4379
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
@@ -4033,6 +4394,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
4033
4394
  const root = projectRoot ?? process.cwd();
4034
4395
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
4035
4396
  }
4397
+ function createMissingCustomPomDirectoryError(configuredDir, resolvedDir) {
4398
+ return new VuePomGeneratorError(
4399
+ `Custom POM directory "${configuredDir}" does not exist.
4400
+ Resolved path: ${resolvedDir}
4401
+ Create the directory, point generation.playwright.customPoms.dir at the correct location, or remove the customPoms configuration.`
4402
+ );
4403
+ }
4404
+ function createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir) {
4405
+ const renderedClassNames = missingClassNames.map((name) => `"${name}"`).join(", ");
4406
+ return new VuePomGeneratorError(
4407
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.
4408
+ Expected matching helper files/exports under "${configuredDir}".
4409
+ Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.`
4410
+ );
4411
+ }
4036
4412
  function createCustomPomImportCollisionError(exportName, requested) {
4037
4413
  return new VuePomGeneratorError(
4038
4414
  `Custom POM import name collision detected for "${exportName}".
@@ -4171,41 +4547,31 @@ function generateGoToSelfMethod(componentName) {
4171
4547
  })
4172
4548
  ];
4173
4549
  }
4174
- function formatMethodParams(params) {
4175
- if (!params)
4176
- return "";
4177
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4178
- const entries = Object.entries(params);
4179
- if (!entries.length)
4180
- return "";
4181
- const score = (name) => {
4182
- const idx = preferredOrder.indexOf(name);
4183
- return idx < 0 ? 999 : idx;
4184
- };
4185
- return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
4550
+ function getSelectorPatterns(selector) {
4551
+ return selector.kind === "testId" ? [selector.testId] : [selector.rootTestId, selector.label];
4186
4552
  }
4187
- function generateExtraClickMethodMembers(spec) {
4553
+ function generateExtraClickMethodMembers(spec, componentName) {
4188
4554
  if (spec.kind !== "click") {
4189
4555
  return [];
4190
4556
  }
4191
- const params = spec.params ?? {};
4192
- const signatureParams = formatMethodParams(params);
4193
- const parameters = parseParameterSignatures(signatureParams);
4194
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4195
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
4557
+ const selectorPatterns = getSelectorPatterns(spec.selector);
4558
+ const signatureSpecs = orderPomPatternParameters(
4559
+ spec.parameters,
4560
+ selectorPatterns,
4561
+ { omit: spec.keyLiteral !== void 0 ? ["key"] : [] }
4562
+ );
4563
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
4564
+ const hasAnnotationText = signatureSpecs.some((param) => param.name === "annotationText");
4565
+ const hasWait = signatureSpecs.some((param) => param.name === "wait");
4196
4566
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4197
4567
  const waitArg = hasWait ? "wait" : "true";
4568
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
4569
+ componentName,
4570
+ methodName: stripPomActionPrefix(spec.name),
4571
+ nativeRole: "button"
4572
+ }));
4198
4573
  if (spec.selector.kind === "testId") {
4199
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4200
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4201
- const clickArgs = [];
4202
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4203
- if (hasAnnotationText || hasWait) {
4204
- clickArgs.push(annotationArg);
4205
- }
4206
- if (hasWait) {
4207
- clickArgs.push(waitArg);
4208
- }
4574
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4209
4575
  return [
4210
4576
  createClassMethod({
4211
4577
  name: spec.name,
@@ -4215,20 +4581,16 @@ function generateExtraClickMethodMembers(spec) {
4215
4581
  if (spec.keyLiteral !== void 0) {
4216
4582
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4217
4583
  }
4218
- if (needsTemplate) {
4219
- writer.writeLine(`const testId = ${testIdExpr};`);
4584
+ for (const statement of testIdBinding.setupStatements) {
4585
+ writer.writeLine(statement);
4220
4586
  }
4221
- writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4587
+ writer.writeLine(`await this.clickByTestId(${testIdBinding.expression}, ${annotationArg}, ${waitArg}, ${locatorDescription});`);
4222
4588
  }
4223
4589
  })
4224
4590
  ];
4225
4591
  }
4226
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4227
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4228
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4229
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4230
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4231
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4592
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4593
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4232
4594
  return [
4233
4595
  createClassMethod({
4234
4596
  name: spec.name,
@@ -4238,45 +4600,45 @@ function generateExtraClickMethodMembers(spec) {
4238
4600
  if (spec.keyLiteral !== void 0) {
4239
4601
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4240
4602
  }
4241
- if (rootNeedsTemplate) {
4242
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4603
+ for (const statement of rootBinding.setupStatements) {
4604
+ writer.writeLine(statement);
4243
4605
  }
4244
- if (labelNeedsTemplate) {
4245
- writer.writeLine(`const label = ${labelExpr};`);
4606
+ for (const statement of labelBinding.setupStatements) {
4607
+ writer.writeLine(statement);
4246
4608
  }
4247
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4609
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg}, { description: ${locatorDescription} });`);
4248
4610
  }
4249
4611
  })
4250
4612
  ];
4251
4613
  }
4252
- function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4614
+ function generateMethodMembersFromPom(componentName, primary, targetPageObjectModelClass) {
4253
4615
  if (primary.emitPrimary === false) {
4254
4616
  return [];
4255
4617
  }
4256
4618
  return generateViewObjectModelMembers(
4619
+ componentName,
4257
4620
  targetPageObjectModelClass,
4258
4621
  primary.methodName,
4259
4622
  primary.nativeRole,
4260
- primary.formattedDataTestId,
4261
- primary.alternateFormattedDataTestIds,
4623
+ primary.selector,
4624
+ primary.alternateSelectors,
4262
4625
  primary.getterNameOverride,
4263
- primary.params ?? {}
4626
+ primary.parameters
4264
4627
  );
4265
4628
  }
4266
- function generateMethodsContentForDependencies(dependencies) {
4629
+ function generateMethodsContentForDependencies(componentName, dependencies) {
4267
4630
  const entries = Array.from(dependencies.dataTestIdSet ?? []);
4268
4631
  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));
4269
4632
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4270
4633
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4271
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4272
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4634
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4273
4635
  const key = JSON.stringify({
4274
4636
  role: pom.nativeRole,
4275
4637
  methodName: pom.methodName,
4276
4638
  getterNameOverride: pom.getterNameOverride ?? null,
4277
- testId: pom.formattedDataTestId,
4278
- alternateTestIds: alternates.length ? alternates : void 0,
4279
- params: stableParams,
4639
+ selector: pom.selector,
4640
+ alternateSelectors: alternates.length ? alternates : void 0,
4641
+ parameters: pom.parameters,
4280
4642
  target: target ?? null,
4281
4643
  emitPrimary: pom.emitPrimary ?? true
4282
4644
  });
@@ -4289,10 +4651,10 @@ function generateMethodsContentForDependencies(dependencies) {
4289
4651
  const extras = (dependencies.pomExtraMethods ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
4290
4652
  const members = [];
4291
4653
  for (const { pom, target } of primarySpecs) {
4292
- members.push(...generateMethodMembersFromPom(pom, target));
4654
+ members.push(...generateMethodMembersFromPom(componentName, pom, target));
4293
4655
  }
4294
4656
  for (const extra of extras) {
4295
- members.push(...generateExtraClickMethodMembers(extra));
4657
+ members.push(...generateExtraClickMethodMembers(extra, componentName));
4296
4658
  }
4297
4659
  return members;
4298
4660
  }
@@ -4303,6 +4665,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4303
4665
  customPomAttachments = [],
4304
4666
  projectRoot,
4305
4667
  customPomDir,
4668
+ requireCustomPomDir,
4306
4669
  customPomImportAliases,
4307
4670
  customPomImportNameCollisionBehavior = "error",
4308
4671
  testIdAttribute,
@@ -4335,6 +4698,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4335
4698
  customPomAttachments,
4336
4699
  projectRoot,
4337
4700
  customPomDir,
4701
+ requireCustomPomDir,
4338
4702
  customPomImportAliases,
4339
4703
  customPomImportNameCollisionBehavior,
4340
4704
  testIdAttribute,
@@ -4344,6 +4708,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4344
4708
  customPomAttachments,
4345
4709
  projectRoot,
4346
4710
  customPomDir,
4711
+ requireCustomPomDir,
4347
4712
  customPomImportAliases,
4348
4713
  customPomImportNameCollisionBehavior,
4349
4714
  testIdAttribute,
@@ -4399,9 +4764,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4399
4764
  }
4400
4765
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4401
4766
  customPomDir: options.customPomDir,
4767
+ requireCustomPomDir: options.requireCustomPomDir,
4402
4768
  customPomImportAliases: options.customPomImportAliases,
4403
4769
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4404
4770
  });
4771
+ assertCustomPomAttachmentsResolved(
4772
+ options.customPomAttachments ?? [],
4773
+ customPomImportResolution.classIdentifierMap,
4774
+ options.customPomDir ?? "tests/playwright/pom/custom"
4775
+ );
4405
4776
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4406
4777
  const files = [];
4407
4778
  for (const [name, deps] of entries) {
@@ -4560,21 +4931,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4560
4931
  return { filePath, content };
4561
4932
  });
4562
4933
  }
4563
- function toCSharpTestIdExpression(formattedDataTestId) {
4564
- const needsInterpolation = formattedDataTestId.includes("${");
4565
- if (!needsInterpolation) {
4566
- return JSON.stringify(formattedDataTestId);
4567
- }
4568
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4569
- const quoted = JSON.stringify(inner);
4570
- return `$${quoted}`;
4571
- }
4572
- function toCSharpParam(paramTypeExpr) {
4573
- const trimmed = (paramTypeExpr ?? "").trim();
4574
- const eqIdx = trimmed.indexOf("=");
4575
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4576
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4577
- const typePart = left.includes("|") ? "string" : left;
4934
+ function toCSharpParam(param) {
4935
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4578
4936
  let type = "string";
4579
4937
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4580
4938
  type = "bool";
@@ -4582,19 +4940,17 @@ function toCSharpParam(paramTypeExpr) {
4582
4940
  type = "string";
4583
4941
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4584
4942
  type = "int";
4585
- else if (/\d+/.test(typePart) && typePart === "")
4586
- type = "int";
4587
4943
  else if (/\btimeOut\b/i.test(typePart))
4588
4944
  type = "int";
4589
4945
  let defaultExpr;
4590
- if (right !== void 0) {
4946
+ if (param.initializer !== void 0) {
4591
4947
  if (type === "bool") {
4592
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : void 0;
4948
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : void 0;
4593
4949
  } else if (type === "int") {
4594
- const m = right.match(/\d+/);
4950
+ const m = param.initializer.match(/\d+/);
4595
4951
  defaultExpr = m ? m[0] : void 0;
4596
4952
  } else {
4597
- if (right === '""' || right === '""' || right === "''") {
4953
+ if (param.initializer === '""' || param.initializer === "''") {
4598
4954
  defaultExpr = '""';
4599
4955
  }
4600
4956
  }
@@ -4602,17 +4958,15 @@ function toCSharpParam(paramTypeExpr) {
4602
4958
  return { type, defaultExpr };
4603
4959
  }
4604
4960
  function formatCSharpParams(params) {
4605
- if (!params)
4606
- return { signature: "", argNames: [] };
4607
- const entries = Object.entries(params);
4608
- if (!entries.length)
4961
+ const normalizedParams = normalizePomParameters(params);
4962
+ if (!normalizedParams.length)
4609
4963
  return { signature: "", argNames: [] };
4610
4964
  const signatureParts = [];
4611
4965
  const argNames = [];
4612
- for (const [name, typeExpr] of entries) {
4613
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4614
- argNames.push(name);
4615
- signatureParts.push(defaultExpr !== void 0 ? `${type} ${name} = ${defaultExpr}` : `${type} ${name}`);
4966
+ for (const param of normalizedParams) {
4967
+ const { type, defaultExpr } = toCSharpParam(param);
4968
+ argNames.push(param.name);
4969
+ signatureParts.push(defaultExpr !== void 0 ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
4616
4970
  }
4617
4971
  return { signature: signatureParts.join(", "), argNames };
4618
4972
  }
@@ -4703,23 +5057,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4703
5057
  const baseMethodName = upperFirst(pom.methodName);
4704
5058
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4705
5059
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4706
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4707
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4708
- const templateVars = templateVarMatches.map((m) => m[1]);
4709
- const augmentedParams = { ...pom.params };
4710
- for (const v of templateVars) {
4711
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4712
- augmentedParams[v] = "string";
4713
- }
4714
- }
4715
- const orderedParams = Object.fromEntries([
4716
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4717
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4718
- ]);
5060
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
5061
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
5062
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4719
5063
  const { signature, argNames } = formatCSharpParams(orderedParams);
4720
5064
  const args = argNames.join(", ");
4721
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4722
- if (pom.formattedDataTestId.includes("${")) {
5065
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
5066
+ if (selectorIsParameterized) {
4723
5067
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4724
5068
  } else {
4725
5069
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4730,12 +5074,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4730
5074
  if (target) {
4731
5075
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4732
5076
  chunks.push(" {");
4733
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4734
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
5077
+ if (selectorIsParameterized || allTestIds.length <= 1) {
5078
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4735
5079
  chunks.push(` return new ${target}(Page);`);
4736
5080
  } else {
4737
5081
  chunks.push(" Exception? lastError = null;");
4738
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5082
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4739
5083
  chunks.push(" {");
4740
5084
  chunks.push(" try");
4741
5085
  chunks.push(" {");
@@ -4759,7 +5103,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4759
5103
  }
4760
5104
  chunks.push(` public async Task ${actionName}(${sig})`);
4761
5105
  chunks.push(" {");
4762
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5106
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4763
5107
  const emitActionCall = (locatorAccess) => {
4764
5108
  if (pom.nativeRole === "input") {
4765
5109
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4772,9 +5116,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4772
5116
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4773
5117
  }
4774
5118
  };
4775
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5119
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4776
5120
  chunks.push(" Exception? lastError = null;");
4777
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5121
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4778
5122
  chunks.push(" {");
4779
5123
  chunks.push(" try");
4780
5124
  chunks.push(" {");
@@ -4820,7 +5164,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4820
5164
  for (const extra of extras) {
4821
5165
  if (extra.kind !== "click")
4822
5166
  continue;
4823
- const { signature } = formatCSharpParams(extra.params);
5167
+ const extraParams = orderPomPatternParameters(
5168
+ extra.parameters,
5169
+ getSelectorPatterns(extra.selector),
5170
+ { omit: extra.keyLiteral !== void 0 ? ["key"] : [] }
5171
+ );
5172
+ const { signature } = formatCSharpParams(extraParams);
4824
5173
  const extraName = upperFirst(extra.name);
4825
5174
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4826
5175
  chunks.push(" {");
@@ -4828,29 +5177,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4828
5177
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4829
5178
  }
4830
5179
  if (extra.selector.kind === "testId") {
4831
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4832
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4833
- if (needsTemplate) {
4834
- chunks.push(` var testId = ${testIdExpr};`);
4835
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4836
- } else {
4837
- chunks.push(` await LocatorByTestId(${testIdExpr}).ClickAsync();`);
5180
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
5181
+ for (const statement of testIdBinding.setupStatements) {
5182
+ chunks.push(` ${statement}`);
4838
5183
  }
5184
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4839
5185
  } else {
4840
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4841
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4842
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4843
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5186
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5187
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4844
5188
  const exactArg = extra.selector.exact === false ? "false" : "true";
4845
- if (rootNeedsTemplate) {
4846
- chunks.push(` var rootTestId = ${rootExpr};`);
5189
+ for (const statement of rootBinding.setupStatements) {
5190
+ chunks.push(` ${statement}`);
4847
5191
  }
4848
- if (labelNeedsTemplate) {
4849
- chunks.push(` var label = ${labelExpr};`);
5192
+ for (const statement of labelBinding.setupStatements) {
5193
+ chunks.push(` ${statement}`);
4850
5194
  }
4851
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4852
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4853
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5195
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4854
5196
  }
4855
5197
  chunks.push(" }");
4856
5198
  chunks.push("");
@@ -5166,7 +5508,7 @@ function prepareViewObjectModelClass(componentName, dependencies, componentHiera
5166
5508
  members.push(...generateRouteProperty(routeMeta));
5167
5509
  members.push(...generateGoToSelfMethod(className));
5168
5510
  }
5169
- members.push(...generateMethodsContentForDependencies(dependencies));
5511
+ members.push(...generateMethodsContentForDependencies(componentName, dependencies));
5170
5512
  return {
5171
5513
  className,
5172
5514
  componentRefsForInstances,
@@ -5294,7 +5636,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5294
5636
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5295
5637
  continue;
5296
5638
  const list = methodToChildren.get(name) ?? [];
5297
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5639
+ list.push({ childProp, signature: sig });
5298
5640
  methodToChildren.set(name, list);
5299
5641
  }
5300
5642
  }
@@ -5304,12 +5646,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5304
5646
  return [];
5305
5647
  }
5306
5648
  return passthroughs.map(([methodName, candidates]) => {
5307
- const { childProp, params, argNames } = candidates[0];
5308
- const callArgs = argNames.join(", ");
5649
+ const { childProp, signature } = candidates[0];
5650
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5309
5651
  return createClassMethod({
5310
5652
  name: methodName,
5311
5653
  isAsync: true,
5312
- parameters: parseParameterSignatures(params),
5654
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5313
5655
  statements: [
5314
5656
  `return await this.${childProp}.${methodName}(${callArgs});`
5315
5657
  ]
@@ -5333,8 +5675,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5333
5675
  const list = methodToAttachments.get(methodName) ?? [];
5334
5676
  list.push({
5335
5677
  propertyName: attachment.propertyName,
5336
- params: signature.params,
5337
- argNames: signature.argNames
5678
+ signature
5338
5679
  });
5339
5680
  methodToAttachments.set(methodName, list);
5340
5681
  }
@@ -5345,12 +5686,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5345
5686
  return [];
5346
5687
  }
5347
5688
  return passthroughs.map(([methodName, candidates]) => {
5348
- const { propertyName, params, argNames } = candidates[0];
5349
- const callArgs = argNames.join(", ");
5689
+ const { propertyName, signature } = candidates[0];
5690
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5350
5691
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5351
5692
  return createClassMethod({
5352
5693
  name: methodName,
5353
- parameters: parseParameterSignatures(params),
5694
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5354
5695
  statements: [
5355
5696
  `return ${invocation};`
5356
5697
  ]
@@ -5364,15 +5705,44 @@ function sliceNodeSource(source, node) {
5364
5705
  const snippet = source.slice(node.start, node.end).trim();
5365
5706
  return snippet.length ? snippet : null;
5366
5707
  }
5367
- function getCustomPomCallArgumentName(param) {
5708
+ function getTypeAnnotationSource(source, node) {
5709
+ const rawTypeAnnotation = node.typeAnnotation;
5710
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
5711
+ return void 0;
5712
+ }
5713
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
5714
+ return typeAnnotation && typeof typeAnnotation === "object" ? sliceNodeSource(source, typeAnnotation) ?? void 0 : void 0;
5715
+ }
5716
+ function getCustomPomParameterSpec(source, param) {
5368
5717
  if (param.type === "Identifier") {
5369
- return param.name;
5718
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5719
+ hasQuestionToken: !!param.optional
5720
+ });
5370
5721
  }
5371
5722
  if (param.type === "AssignmentPattern") {
5372
- return param.left.type === "Identifier" ? param.left.name : null;
5723
+ if (param.left.type !== "Identifier") {
5724
+ return null;
5725
+ }
5726
+ const initializer = sliceNodeSource(source, param.right);
5727
+ if (!initializer) {
5728
+ return null;
5729
+ }
5730
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
5731
+ initializer,
5732
+ hasQuestionToken: !!param.left.optional
5733
+ });
5373
5734
  }
5374
5735
  if (param.type === "RestElement") {
5375
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
5736
+ if (param.argument.type !== "Identifier") {
5737
+ return null;
5738
+ }
5739
+ const typeExpression = getTypeAnnotationSource(
5740
+ source,
5741
+ param
5742
+ ) ?? getTypeAnnotationSource(source, param.argument);
5743
+ return createPomParameterSpec(param.argument.name, typeExpression, {
5744
+ isRestParameter: true
5745
+ });
5376
5746
  }
5377
5747
  return null;
5378
5748
  }
@@ -5405,29 +5775,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5405
5775
  if (member.key.type !== "Identifier") {
5406
5776
  continue;
5407
5777
  }
5408
- const params = [];
5409
- const argNames = [];
5778
+ const parameters = [];
5410
5779
  let supported = true;
5411
5780
  member.params.forEach((param) => {
5412
5781
  if (!supported) {
5413
5782
  return;
5414
5783
  }
5415
- const paramSource = sliceNodeSource(source, param);
5416
- const argName = getCustomPomCallArgumentName(param);
5417
- if (!paramSource || !argName) {
5784
+ const parameter = getCustomPomParameterSpec(source, param);
5785
+ if (!parameter) {
5418
5786
  supported = false;
5419
5787
  return;
5420
5788
  }
5421
- params.push(paramSource);
5422
- argNames.push(argName);
5789
+ parameters.push(parameter);
5423
5790
  });
5424
5791
  if (!supported) {
5425
5792
  continue;
5426
5793
  }
5427
- signatures.set(member.key.name, {
5428
- params: params.join(", "),
5429
- argNames
5430
- });
5794
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5431
5795
  }
5432
5796
  }
5433
5797
  return signatures;
@@ -5610,6 +5974,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5610
5974
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5611
5975
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5612
5976
  if (!fs.existsSync(customDirAbs)) {
5977
+ if (options.requireCustomPomDir) {
5978
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
5979
+ }
5613
5980
  return {
5614
5981
  classIdentifierMap,
5615
5982
  methodSignaturesByClass,
@@ -5652,6 +6019,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5652
6019
  importSpecifiersByClass
5653
6020
  };
5654
6021
  }
6022
+ function assertCustomPomAttachmentsResolved(attachments, classIdentifierMap, configuredDir) {
6023
+ const missingClassNames = Array.from(new Set(
6024
+ attachments.map((attachment) => attachment.className).filter((className) => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className))
6025
+ )).sort((left, right) => left.localeCompare(right));
6026
+ if (missingClassNames.length > 0) {
6027
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
6028
+ }
6029
+ }
5655
6030
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5656
6031
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5657
6032
  if (!filePath)
@@ -5680,7 +6055,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5680
6055
  if (!sig)
5681
6056
  continue;
5682
6057
  const list = methodToChildren.get(name) ?? [];
5683
- list.push({ child, params: sig.params, argNames: sig.argNames });
6058
+ list.push({ child, signature: sig });
5684
6059
  methodToChildren.set(name, list);
5685
6060
  }
5686
6061
  }
@@ -5688,12 +6063,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5688
6063
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5689
6064
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5690
6065
  continue;
5691
- const { child, params, argNames } = candidatesForMethod[0];
5692
- const callArgs = argNames.join(", ");
6066
+ const { child, signature } = candidatesForMethod[0];
6067
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5693
6068
  passthroughMembers.push(createClassMethod({
5694
6069
  name: methodName,
5695
6070
  isAsync: true,
5696
- parameters: parseParameterSignatures(params),
6071
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5697
6072
  statements: [
5698
6073
  `return await this.${child}.${methodName}(${callArgs});`
5699
6074
  ]
@@ -5742,9 +6117,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5742
6117
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5743
6118
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5744
6119
  customPomDir: options.customPomDir,
6120
+ requireCustomPomDir: options.requireCustomPomDir,
5745
6121
  customPomImportAliases: options.customPomImportAliases,
5746
6122
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5747
6123
  });
6124
+ assertCustomPomAttachmentsResolved(
6125
+ options.customPomAttachments ?? [],
6126
+ customPomImportResolution.classIdentifierMap,
6127
+ options.customPomDir ?? "tests/playwright/pom/custom"
6128
+ );
5748
6129
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5749
6130
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5750
6131
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5894,8 +6275,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5894
6275
  return candidate;
5895
6276
  };
5896
6277
  for (const dt of dataTestIdSet) {
5897
- const raw = dt.value;
5898
- if (raw.includes("${")) {
6278
+ const raw = dt.selectorValue.formatted;
6279
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5899
6280
  continue;
5900
6281
  }
5901
6282
  const toggleSuffix = "-toggle";
@@ -5985,7 +6366,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
5985
6366
  });
5986
6367
  }
5987
6368
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
5988
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
5989
6369
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
5990
6370
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
5991
6371
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6247,7 +6627,7 @@ function getConditionalDirectiveInfo(element) {
6247
6627
  if (directive.name === "else") {
6248
6628
  const exp2 = directive.exp;
6249
6629
  if (exp2 && (exp2.type === NodeTypes.SIMPLE_EXPRESSION || exp2.type === NodeTypes.COMPOUND_EXPRESSION)) {
6250
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6630
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6251
6631
  return { kind: "else-if", source: source2 };
6252
6632
  }
6253
6633
  return { kind: "else", source: "" };
@@ -6256,13 +6636,13 @@ function getConditionalDirectiveInfo(element) {
6256
6636
  const exp2 = directive.exp;
6257
6637
  if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION && exp2.type !== NodeTypes.COMPOUND_EXPRESSION)
6258
6638
  return null;
6259
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6639
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6260
6640
  return { kind: "else-if", source: source2 };
6261
6641
  }
6262
6642
  const exp = directive.exp;
6263
6643
  if (!exp || exp.type !== NodeTypes.SIMPLE_EXPRESSION && exp.type !== NodeTypes.COMPOUND_EXPRESSION)
6264
6644
  return null;
6265
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
6645
+ const source = getVueExpressionSource(exp, "content", "compiled");
6266
6646
  return { kind: directive.name, source };
6267
6647
  }
6268
6648
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6288,26 +6668,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6288
6668
  };
6289
6669
  try {
6290
6670
  const expr = parseExpression(src, { plugins: ["typescript"] });
6291
- const isNodeType = (n, type) => {
6671
+ const isNodeType2 = (n, type) => {
6292
6672
  return n !== null && n.type === type;
6293
6673
  };
6294
- const isStringLiteralNode = (n) => {
6295
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6674
+ const isStringLiteralNode2 = (n) => {
6675
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6296
6676
  };
6297
- const isIdentifierNode = (n) => {
6298
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6677
+ const isIdentifierNode2 = (n) => {
6678
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6299
6679
  };
6300
6680
  const results = [];
6301
6681
  const walk = (n) => {
6302
6682
  if (!n)
6303
6683
  return;
6304
- if (isStringLiteralNode(n)) {
6684
+ if (isStringLiteralNode2(n)) {
6305
6685
  const v = (n.value ?? "").trim();
6306
6686
  if (isIdentifierish(v)) {
6307
6687
  results.push(v);
6308
6688
  }
6309
6689
  }
6310
- if (isIdentifierNode(n)) {
6690
+ if (isIdentifierNode2(n)) {
6311
6691
  const v = (n.name ?? "").trim();
6312
6692
  if (isIdentifierish(v)) {
6313
6693
  results.push(v);
@@ -6457,34 +6837,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6457
6837
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6458
6838
  return null;
6459
6839
  }
6460
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6840
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6461
6841
  const jsStringLiteral = (value) => {
6462
6842
  return JSON.stringify(value);
6463
6843
  };
6464
6844
  const getTestIdExpressionForNode = () => {
6465
- const existing = findTestIdAttribute(element, testIdAttribute);
6466
- if (!existing) {
6467
- return "undefined";
6468
- }
6469
- if (existing.type === NodeTypes.ATTRIBUTE) {
6470
- const v = existing.value?.content;
6471
- if (!v) {
6472
- return "undefined";
6473
- }
6474
- return jsStringLiteral(v);
6475
- }
6476
- const directive = existing;
6477
- const exp2 = directive.exp;
6478
- if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION) {
6845
+ if (!resolvedRuntimeTestId) {
6479
6846
  return "undefined";
6480
6847
  }
6481
- const content = (exp2.content ?? "").trim();
6482
- if (!content) {
6483
- return "undefined";
6848
+ if (resolvedRuntimeTestId.kind === "static") {
6849
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6484
6850
  }
6485
- return `(${content})`;
6851
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6486
6852
  };
6487
- const testIdExpression2 = getTestIdExpressionForNode();
6853
+ const testIdExpression = getTestIdExpressionForNode();
6488
6854
  const clickDirective = tryGetClickDirective(element);
6489
6855
  if (!clickDirective)
6490
6856
  return;
@@ -6495,10 +6861,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6495
6861
  const exp = clickDirective.exp;
6496
6862
  if (!exp)
6497
6863
  return;
6498
- const existingSource = (exp.loc?.source ?? (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6864
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6499
6865
  if (existingSource.includes(CLICK_EVENT_NAME))
6500
6866
  return;
6501
- const originalExpression = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6867
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6502
6868
  if (!originalExpression)
6503
6869
  return;
6504
6870
  const isStatementBody = (() => {
@@ -6515,7 +6881,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6515
6881
  const statementWrappedHandler = `($event) => {
6516
6882
  const __win = ($event && $event.view) ? $event.view : undefined;
6517
6883
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6518
- const __testIdFromNode = ${testIdExpression2};
6884
+ const __testIdFromNode = ${testIdExpression};
6519
6885
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6520
6886
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6521
6887
  const __emit = (phase, err) => {
@@ -6526,16 +6892,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6526
6892
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6527
6893
  }
6528
6894
  } catch (e) {
6529
- // Instrumentation must never hide failures during e2e strict mode.
6530
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6531
- // Outside strict mode we log and continue so we don't break real user clicks.
6895
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6532
6896
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6533
6897
  if (__w && __w.console && typeof __w.console.error === 'function') {
6534
6898
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6535
6899
  }
6536
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6537
- throw e;
6538
- }
6900
+ throw e;
6539
6901
  }
6540
6902
  };
6541
6903
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6566,7 +6928,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6566
6928
  const expressionWrappedHandler = `($event) => {
6567
6929
  const __win = ($event && $event.view) ? $event.view : undefined;
6568
6930
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6569
- const __testIdFromNode = ${testIdExpression2};
6931
+ const __testIdFromNode = ${testIdExpression};
6570
6932
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6571
6933
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6572
6934
  const __emit = (phase, err) => {
@@ -6577,16 +6939,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6577
6939
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6578
6940
  }
6579
6941
  } catch (e) {
6580
- // Instrumentation must never hide failures during e2e strict mode.
6581
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6582
- // Outside strict mode we log and continue so we don't break real user clicks.
6942
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6583
6943
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6584
6944
  if (__w && __w.console && typeof __w.console.error === 'function') {
6585
6945
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6586
6946
  }
6587
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6588
- throw e;
6589
- }
6947
+ throw e;
6590
6948
  }
6591
6949
  };
6592
6950
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6624,10 +6982,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6624
6982
  let previousFileName = "";
6625
6983
  const hierarchyMap = /* @__PURE__ */ new Map();
6626
6984
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6627
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
6985
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6628
6986
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6629
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6630
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
6987
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6631
6988
  const warn = options.warn;
6632
6989
  const vueFilesPathMap = options.vueFilesPathMap;
6633
6990
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6703,7 +7060,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6703
7060
  conditionalHintByIfBranch.set(branch, hint);
6704
7061
  continue;
6705
7062
  }
6706
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
7063
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6707
7064
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6708
7065
  if (stable) {
6709
7066
  conditionalHintByIfBranch.set(branch, stable);
@@ -6765,17 +7122,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6765
7122
  nativeWrappers[element.tag] = { role: "grid" };
6766
7123
  }
6767
7124
  }
6768
- const getBestAvailableKeyValue = () => {
7125
+ const getBestAvailableKeyInfo = () => {
6769
7126
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6770
7127
  const isDirectVForChild = parentNode?.type === NodeTypes.FOR;
6771
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6772
- if (vForKey) return vForKey;
6773
- return getContainedInSlotDataKeyValue(element, hierarchyMap);
7128
+ const vForKeyInfo = (isDirectVForChild ? getKeyDirectiveInfo(element) : null) || getContainedInVForDirectiveKeyInfo(context, element, hierarchyMap);
7129
+ if (vForKeyInfo) {
7130
+ return vForKeyInfo;
7131
+ }
7132
+ return getContainedInSlotDataKeyInfo(element, hierarchyMap);
6774
7133
  };
6775
- const bestKeyInferred = getBestAvailableKeyValue();
6776
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6777
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6778
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7134
+ const bestKeyInfo = getBestAvailableKeyInfo();
7135
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7136
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6779
7137
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6780
7138
  const parentKey = context?.parent ? context.parent : null;
6781
7139
  const conditional = getConditionalDirectiveInfo(element);
@@ -6818,7 +7176,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6818
7176
  if (!cond) {
6819
7177
  conditionalHint = "else";
6820
7178
  } else {
6821
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
7179
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6822
7180
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6823
7181
  }
6824
7182
  }
@@ -6828,7 +7186,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6828
7186
  });
6829
7187
  if (showDirective?.exp && (showDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION)) {
6830
7188
  const exp = showDirective.exp;
6831
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
7189
+ const source = getVueExpressionSource(exp, "content", "compiled");
6832
7190
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6833
7191
  if (showHint) {
6834
7192
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6862,13 +7220,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6862
7220
  const tagSuffix = getTagSuffix();
6863
7221
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6864
7222
  };
7223
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7224
+ const tagSuffix = getTagSuffix();
7225
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7226
+ };
6865
7227
  const getSubmitDataTestId = (identifier) => {
6866
7228
  const tagSuffix = getTagSuffix();
6867
7229
  return `${componentName}-${identifier}${tagSuffix}`;
6868
7230
  };
6869
7231
  const applyResolvedDataTestIdForElement = (args) => {
6870
7232
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6871
- applyResolvedDataTestId({
7233
+ return applyResolvedDataTestId({
6872
7234
  element,
6873
7235
  componentName,
6874
7236
  parentComponentName,
@@ -6878,8 +7240,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6878
7240
  generatedMethodContentByComponent,
6879
7241
  nativeRole,
6880
7242
  preferredGeneratedValue: args.preferredGeneratedValue,
6881
- bestKeyPlaceholder,
6882
- bestKeyVariable,
7243
+ preferredRuntimeValue: args.preferredRuntimeValue,
7244
+ keyInfo: bestKeyInfo,
6883
7245
  keyValuesOverride,
6884
7246
  entryOverrides: args.entryOverrides,
6885
7247
  semanticNameHint: args.semanticNameHint,
@@ -6897,7 +7259,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6897
7259
  return p.type === NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6898
7260
  }) ?? null;
6899
7261
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6900
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7262
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6901
7263
  const loc = element.loc?.start;
6902
7264
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6903
7265
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6906,7 +7268,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6906
7268
  Element: <${element.tag}>
6907
7269
  Handler: ${handlerSource}
6908
7270
 
6909
- 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.`
7271
+ 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.`
6910
7272
  );
6911
7273
  }
6912
7274
  if (nativeWrappersValue) {
@@ -7033,20 +7395,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7033
7395
  contextFilename: context.filename
7034
7396
  });
7035
7397
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
7036
- const idOrName = getIdOrName(element) || void 0;
7398
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
7037
7399
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
7038
7400
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
7039
7401
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
7040
7402
  const testId = getClickDataTestId(clickSuffix);
7041
- applyResolvedDataTestIdForElement({
7403
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7404
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
7042
7405
  preferredGeneratedValue: testId,
7406
+ preferredRuntimeValue: runtimeTestId,
7043
7407
  semanticNameHint: semanticNameHint2,
7044
7408
  semanticNameHintAlternates,
7045
7409
  pomMergeKey
7046
7410
  });
7047
- {
7048
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7049
- }
7411
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7050
7412
  return;
7051
7413
  }
7052
7414
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7056,7 +7418,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7056
7418
  if (!isRecognizedInteractiveRole) {
7057
7419
  return;
7058
7420
  }
7059
- const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7421
+ const identifierHint = getStaticIdOrNameHint(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7060
7422
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7061
7423
  applyResolvedDataTestIdForElement({
7062
7424
  preferredGeneratedValue,
@@ -7066,7 +7428,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7066
7428
  }
7067
7429
  const isSubmit = element.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7068
7430
  if (isSubmit) {
7069
- const identifier = getIdOrName(element) || innerText;
7431
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7070
7432
  if (!identifier) {
7071
7433
  const loc = element.loc?.start;
7072
7434
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7118,29 +7480,32 @@ function createBuildProcessorPlugin(options) {
7118
7480
  getSourceDirs,
7119
7481
  basePageClassPath,
7120
7482
  normalizedBasePagePath,
7483
+ generation,
7484
+ projectRootRef,
7485
+ nativeWrappers,
7486
+ excludedComponents,
7487
+ getWrapperSearchRoots,
7488
+ getResolvedRouterEntry,
7489
+ loggerRef
7490
+ } = options;
7491
+ const {
7121
7492
  outDir,
7122
7493
  emitLanguages,
7123
7494
  typescriptOutputStructure,
7124
7495
  csharp,
7125
7496
  generateFixtures,
7126
7497
  customPomAttachments,
7127
- projectRootRef,
7128
7498
  customPomDir,
7499
+ requireCustomPomDir,
7129
7500
  customPomImportAliases,
7130
7501
  customPomImportNameCollisionBehavior,
7131
7502
  testIdAttribute,
7132
7503
  nameCollisionBehavior,
7133
- missingSemanticNameBehavior = "error",
7134
7504
  existingIdBehavior,
7135
- nativeWrappers,
7136
- excludedComponents,
7137
- getWrapperSearchRoots,
7138
7505
  routerAwarePoms,
7139
- getResolvedRouterEntry,
7140
7506
  routerType,
7141
- routerModuleShims,
7142
- loggerRef
7143
- } = options;
7507
+ routerModuleShims
7508
+ } = generation;
7144
7509
  let lastGeneratedMetrics = {
7145
7510
  entryCount: 0,
7146
7511
  interactiveComponentCount: 0,
@@ -7239,10 +7604,9 @@ function createBuildProcessorPlugin(options) {
7239
7604
  excludedComponents,
7240
7605
  getViewsDirAbs(),
7241
7606
  {
7242
- existingIdBehavior: existingIdBehavior ?? "preserve",
7607
+ existingIdBehavior: existingIdBehavior ?? "error",
7243
7608
  testIdAttribute,
7244
7609
  nameCollisionBehavior,
7245
- missingSemanticNameBehavior,
7246
7610
  warn: (message) => loggerRef.current.warn(message),
7247
7611
  vueFilesPathMap,
7248
7612
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7342,6 +7706,7 @@ function createBuildProcessorPlugin(options) {
7342
7706
  customPomAttachments,
7343
7707
  projectRoot: projectRootRef.current,
7344
7708
  customPomDir,
7709
+ requireCustomPomDir,
7345
7710
  customPomImportAliases,
7346
7711
  customPomImportNameCollisionBehavior,
7347
7712
  testIdAttribute,
@@ -7373,6 +7738,11 @@ function createDevProcessorPlugin(options) {
7373
7738
  projectRootRef,
7374
7739
  normalizedBasePagePath,
7375
7740
  basePageClassPath,
7741
+ generation,
7742
+ getResolvedRouterEntry,
7743
+ loggerRef
7744
+ } = options;
7745
+ const {
7376
7746
  outDir,
7377
7747
  emitLanguages,
7378
7748
  typescriptOutputStructure,
@@ -7380,18 +7750,16 @@ function createDevProcessorPlugin(options) {
7380
7750
  generateFixtures,
7381
7751
  customPomAttachments,
7382
7752
  customPomDir,
7753
+ requireCustomPomDir,
7383
7754
  customPomImportAliases,
7384
7755
  customPomImportNameCollisionBehavior,
7385
- nameCollisionBehavior = "suffix",
7386
- missingSemanticNameBehavior = "error",
7756
+ nameCollisionBehavior,
7387
7757
  existingIdBehavior,
7388
7758
  testIdAttribute,
7389
7759
  routerAwarePoms,
7390
- getResolvedRouterEntry,
7391
7760
  routerType,
7392
- routerModuleShims,
7393
- loggerRef
7394
- } = options;
7761
+ routerModuleShims
7762
+ } = generation;
7395
7763
  let scheduleVueFileRegen = null;
7396
7764
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7397
7765
  path.resolve(projectRootRef.current),
@@ -7578,9 +7946,8 @@ function createDevProcessorPlugin(options) {
7578
7946
  excludedComponents,
7579
7947
  getViewsDirAbs(),
7580
7948
  {
7581
- existingIdBehavior: existingIdBehavior ?? "preserve",
7949
+ existingIdBehavior: existingIdBehavior ?? "error",
7582
7950
  nameCollisionBehavior,
7583
- missingSemanticNameBehavior,
7584
7951
  testIdAttribute,
7585
7952
  warn: (message) => loggerRef.current.warn(message),
7586
7953
  vueFilesPathMap: provisionalVuePathMap,
@@ -7631,6 +7998,7 @@ function createDevProcessorPlugin(options) {
7631
7998
  customPomAttachments,
7632
7999
  projectRoot: projectRootRef.current,
7633
8000
  customPomDir,
8001
+ requireCustomPomDir,
7634
8002
  customPomImportAliases,
7635
8003
  customPomImportNameCollisionBehavior,
7636
8004
  pageDirs: getPageDirs(),
@@ -7805,8 +8173,119 @@ function createDevProcessorPlugin(options) {
7805
8173
  }
7806
8174
  };
7807
8175
  }
7808
- function generateTestIdsModule(componentTestIds) {
7809
- const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0]));
8176
+ function removeByKeySegment(value) {
8177
+ const idx = value.indexOf("ByKey");
8178
+ if (idx < 0) {
8179
+ return value;
8180
+ }
8181
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
8182
+ }
8183
+ function hasRoleSuffix(baseName, roleSuffix) {
8184
+ if (baseName.endsWith(roleSuffix)) {
8185
+ return true;
8186
+ }
8187
+ const re = new RegExp(`^${roleSuffix}\\d+$`);
8188
+ return re.test(baseName);
8189
+ }
8190
+ function getGeneratedPropertyName(pom) {
8191
+ if (pom.getterNameOverride) {
8192
+ return pom.getterNameOverride;
8193
+ }
8194
+ const roleSuffix = upperFirst(pom.nativeRole || "Element");
8195
+ const baseName = upperFirst(pom.methodName);
8196
+ const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
8197
+ return pom.selector.patternKind === "parameterized" ? removeByKeySegment(propertyName) : propertyName;
8198
+ }
8199
+ function getGeneratedActionName(entry, pom) {
8200
+ const methodNameUpper = upperFirst(pom.methodName);
8201
+ const radioMethodNameUpper = upperFirst(pom.methodName || "Radio");
8202
+ const isNavigation = !!entry.targetPageObjectModelClass;
8203
+ if (isNavigation) {
8204
+ return `goTo${methodNameUpper}`;
8205
+ }
8206
+ switch (pom.nativeRole) {
8207
+ case "input":
8208
+ return `type${methodNameUpper}`;
8209
+ case "select":
8210
+ case "vselect":
8211
+ return `select${methodNameUpper}`;
8212
+ case "radio":
8213
+ return `select${radioMethodNameUpper}`;
8214
+ default:
8215
+ return `click${methodNameUpper}`;
8216
+ }
8217
+ }
8218
+ function matchesPrimarySelector(extraMethod, pom) {
8219
+ if (extraMethod.selector.kind !== "testId") {
8220
+ return false;
8221
+ }
8222
+ return extraMethod.selector.testId.formatted === pom.selector.formatted && extraMethod.selector.testId.patternKind === pom.selector.patternKind;
8223
+ }
8224
+ function getManifestEntry(componentName, entry, componentMetadata, extraMethods) {
8225
+ const testId = entry.selectorValue.formatted;
8226
+ const metadata = componentMetadata?.get(testId);
8227
+ const pom = entry.pom;
8228
+ const generatedActionName = pom ? getGeneratedActionName(entry, pom) : null;
8229
+ const extraActionNames = pom ? extraMethods.filter((extraMethod) => matchesPrimarySelector(extraMethod, pom)).map((extraMethod) => extraMethod.name).sort((a, b) => a.localeCompare(b)) : [];
8230
+ const generatedActionNames = Array.from(/* @__PURE__ */ new Set([
8231
+ ...generatedActionName ? [generatedActionName] : [],
8232
+ ...extraActionNames.filter((name) => name !== generatedActionName)
8233
+ ]));
8234
+ return {
8235
+ testId,
8236
+ selectorPatternKind: entry.selectorValue.patternKind,
8237
+ semanticName: metadata?.semanticName ?? (pom ? humanizePomMethodName(pom.methodName) : testId),
8238
+ locatorDescription: pom ? buildPomLocatorDescription({
8239
+ componentName,
8240
+ methodName: pom.methodName,
8241
+ nativeRole: pom.nativeRole
8242
+ }) : componentName,
8243
+ inferredRole: pom?.nativeRole ?? null,
8244
+ generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
8245
+ generatedActionName,
8246
+ generatedActionNames,
8247
+ emitPrimary: pom?.emitPrimary !== false,
8248
+ ...entry.targetPageObjectModelClass ? { targetPageObjectModelClass: entry.targetPageObjectModelClass } : {},
8249
+ ...metadata?.tag ? { sourceTag: metadata.tag } : {},
8250
+ ...metadata ? { sourceTagType: metadata.tagType } : {},
8251
+ ...metadata?.patchFlag !== void 0 ? { patchFlag: metadata.patchFlag } : {},
8252
+ ...metadata?.dynamicProps?.length ? { dynamicProps: metadata.dynamicProps } : {},
8253
+ ...metadata?.hasClickHandler !== void 0 ? { hasClickHandler: metadata.hasClickHandler } : {},
8254
+ ...metadata?.hasDynamicClass !== void 0 ? { hasDynamicClass: metadata.hasDynamicClass } : {},
8255
+ ...metadata?.hasDynamicStyle !== void 0 ? { hasDynamicStyle: metadata.hasDynamicStyle } : {},
8256
+ ...metadata?.hasDynamicText !== void 0 ? { hasDynamicText: metadata.hasDynamicText } : {}
8257
+ };
8258
+ }
8259
+ function buildPomManifest(componentHierarchyMap, elementMetadata) {
8260
+ const manifestEntries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([componentName, dependencies]) => {
8261
+ 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 ?? []));
8262
+ if (!entries.length) {
8263
+ return null;
8264
+ }
8265
+ return [componentName, {
8266
+ componentName,
8267
+ className: componentName,
8268
+ sourceFile: dependencies.filePath,
8269
+ kind: dependencies.isView ? "view" : "component",
8270
+ testIds: Array.from(new Set(entries.map((entry) => entry.testId))),
8271
+ entries
8272
+ }];
8273
+ }).filter((entry) => entry !== null);
8274
+ return Object.fromEntries(manifestEntries);
8275
+ }
8276
+ function buildTestIdManifest(pomManifest) {
8277
+ return Object.fromEntries(
8278
+ Object.entries(pomManifest).map(([componentName, component]) => [componentName, Array.from(new Set(component.testIds)).sort((a, b) => a.localeCompare(b))])
8279
+ );
8280
+ }
8281
+ function writeConstJson(value) {
8282
+ return (writer) => {
8283
+ writer.write(`${JSON.stringify(value, null, 2)} as const`);
8284
+ };
8285
+ }
8286
+ function generateTestIdsModule(componentHierarchyMap, elementMetadata) {
8287
+ const pomManifest = buildPomManifest(componentHierarchyMap, elementMetadata);
8288
+ const testIdManifest = buildTestIdManifest(pomManifest);
7810
8289
  return renderSourceFile("virtual-testids.ts", (sourceFile) => {
7811
8290
  sourceFile.addStatements("// Virtual module: test id manifest");
7812
8291
  sourceFile.addVariableStatement({
@@ -7814,16 +8293,15 @@ function generateTestIdsModule(componentTestIds) {
7814
8293
  isExported: true,
7815
8294
  declarations: [{
7816
8295
  name: "testIdManifest",
7817
- initializer: (writer) => {
7818
- writer.write("{").newLine();
7819
- writer.indent(() => {
7820
- manifestEntries.forEach(([componentName, testIds], index) => {
7821
- const suffix = index === manifestEntries.length - 1 ? "" : ",";
7822
- writer.writeLine(`${JSON.stringify(componentName)}: ${JSON.stringify(Array.from(testIds).sort())}${suffix}`);
7823
- });
7824
- });
7825
- writer.write("} as const");
7826
- }
8296
+ initializer: writeConstJson(testIdManifest)
8297
+ }]
8298
+ });
8299
+ sourceFile.addVariableStatement({
8300
+ declarationKind: VariableDeclarationKind.Const,
8301
+ isExported: true,
8302
+ declarations: [{
8303
+ name: "pomManifest",
8304
+ initializer: writeConstJson(pomManifest)
7827
8305
  }]
7828
8306
  });
7829
8307
  sourceFile.addTypeAlias({
@@ -7836,27 +8314,67 @@ function generateTestIdsModule(componentTestIds) {
7836
8314
  name: "ComponentName",
7837
8315
  type: "keyof TestIdManifest"
7838
8316
  });
8317
+ sourceFile.addTypeAlias({
8318
+ isExported: true,
8319
+ name: "PomManifest",
8320
+ type: "typeof pomManifest"
8321
+ });
8322
+ sourceFile.addTypeAlias({
8323
+ isExported: true,
8324
+ name: "PomManifestComponentName",
8325
+ type: "keyof PomManifest"
8326
+ });
8327
+ });
8328
+ }
8329
+ function generatePomManifestModule(componentHierarchyMap, elementMetadata) {
8330
+ const pomManifest = buildPomManifest(componentHierarchyMap, elementMetadata);
8331
+ return renderSourceFile("virtual-pom-manifest.ts", (sourceFile) => {
8332
+ sourceFile.addStatements("// Virtual module: richer POM discoverability manifest");
8333
+ sourceFile.addVariableStatement({
8334
+ declarationKind: VariableDeclarationKind.Const,
8335
+ isExported: true,
8336
+ declarations: [{
8337
+ name: "pomManifest",
8338
+ initializer: writeConstJson(pomManifest)
8339
+ }]
8340
+ });
8341
+ sourceFile.addTypeAlias({
8342
+ isExported: true,
8343
+ name: "PomManifest",
8344
+ type: "typeof pomManifest"
8345
+ });
8346
+ sourceFile.addTypeAlias({
8347
+ isExported: true,
8348
+ name: "PomManifestComponentName",
8349
+ type: "keyof PomManifest"
8350
+ });
7839
8351
  });
7840
8352
  }
7841
- const VIRTUAL_ID = "virtual:testids";
7842
- const RESOLVED_ID = `\0${VIRTUAL_ID}`;
7843
- function createTestIdsVirtualModulesPlugin(componentTestIds) {
8353
+ const TEST_IDS_VIRTUAL_ID = "virtual:testids";
8354
+ const TEST_IDS_RESOLVED_ID = `\0${TEST_IDS_VIRTUAL_ID}`;
8355
+ const POM_MANIFEST_VIRTUAL_ID = "virtual:pom-manifest";
8356
+ const POM_MANIFEST_RESOLVED_ID = `\0${POM_MANIFEST_VIRTUAL_ID}`;
8357
+ function createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata) {
7844
8358
  return {
7845
8359
  name: "vue-pom-generator:virtual-testids",
7846
8360
  resolveId(id) {
7847
- if (id === VIRTUAL_ID)
7848
- return RESOLVED_ID;
8361
+ if (id === TEST_IDS_VIRTUAL_ID)
8362
+ return TEST_IDS_RESOLVED_ID;
8363
+ if (id === POM_MANIFEST_VIRTUAL_ID)
8364
+ return POM_MANIFEST_RESOLVED_ID;
7849
8365
  },
7850
8366
  load(id) {
7851
- if (id === RESOLVED_ID)
7852
- return generateTestIdsModule(componentTestIds);
8367
+ if (id === TEST_IDS_RESOLVED_ID)
8368
+ return generateTestIdsModule(componentHierarchyMap, elementMetadata);
8369
+ if (id === POM_MANIFEST_RESOLVED_ID)
8370
+ return generatePomManifestModule(componentHierarchyMap, elementMetadata);
7853
8371
  }
7854
8372
  };
7855
8373
  }
7856
8374
  function createSupportPlugins(options) {
7857
8375
  const {
7858
- componentTestIds,
7859
8376
  componentHierarchyMap,
8377
+ elementMetadata,
7860
8378
  vueFilesPathMap,
7861
8379
  nativeWrappers,
7862
8380
  excludedComponents,
@@ -7866,27 +8384,30 @@ function createSupportPlugins(options) {
7866
8384
  getViewsDir,
7867
8385
  getSourceDirs,
7868
8386
  getWrapperSearchRoots,
7869
- nameCollisionBehavior = "suffix",
7870
- missingSemanticNameBehavior = "error",
7871
- existingIdBehavior,
8387
+ generation,
8388
+ projectRootRef,
8389
+ basePageClassPath: basePageClassPathOverride,
8390
+ loggerRef
8391
+ } = options;
8392
+ const {
7872
8393
  outDir,
7873
8394
  emitLanguages,
7874
8395
  typescriptOutputStructure,
7875
8396
  csharp,
7876
- routerAwarePoms,
7877
- routerEntry,
7878
- routerType,
7879
- routerModuleShims,
7880
8397
  generateFixtures,
7881
8398
  customPomAttachments,
7882
- projectRootRef,
7883
- basePageClassPath: basePageClassPathOverride,
7884
8399
  customPomDir,
8400
+ requireCustomPomDir,
7885
8401
  customPomImportAliases,
7886
8402
  customPomImportNameCollisionBehavior,
8403
+ nameCollisionBehavior,
8404
+ existingIdBehavior,
7887
8405
  testIdAttribute,
7888
- loggerRef
7889
- } = options;
8406
+ routerAwarePoms,
8407
+ routerEntry,
8408
+ routerType,
8409
+ routerModuleShims
8410
+ } = generation;
7890
8411
  const resolveRouterEntry2 = () => {
7891
8412
  if (!routerAwarePoms)
7892
8413
  return void 0;
@@ -7915,27 +8436,12 @@ function createSupportPlugins(options) {
7915
8436
  getSourceDirs,
7916
8437
  basePageClassPath,
7917
8438
  normalizedBasePagePath,
7918
- outDir,
7919
- emitLanguages,
7920
- typescriptOutputStructure,
7921
- csharp,
7922
- generateFixtures,
7923
- customPomAttachments,
8439
+ generation,
7924
8440
  projectRootRef,
7925
- customPomDir,
7926
- customPomImportAliases,
7927
- customPomImportNameCollisionBehavior,
7928
- testIdAttribute,
7929
- nameCollisionBehavior,
7930
- missingSemanticNameBehavior,
7931
- existingIdBehavior,
7932
8441
  nativeWrappers,
7933
8442
  excludedComponents,
7934
8443
  getWrapperSearchRoots,
7935
- routerAwarePoms,
7936
- routerType,
7937
8444
  getResolvedRouterEntry: resolveRouterEntry2,
7938
- routerModuleShims,
7939
8445
  loggerRef
7940
8446
  });
7941
8447
  const devProcessor = createDevProcessorPlugin({
@@ -7950,26 +8456,11 @@ function createSupportPlugins(options) {
7950
8456
  projectRootRef,
7951
8457
  normalizedBasePagePath,
7952
8458
  basePageClassPath,
7953
- outDir,
7954
- emitLanguages,
7955
- typescriptOutputStructure,
7956
- csharp,
7957
- generateFixtures,
7958
- customPomAttachments,
7959
- customPomDir,
7960
- customPomImportAliases,
7961
- customPomImportNameCollisionBehavior,
7962
- nameCollisionBehavior,
7963
- missingSemanticNameBehavior,
7964
- existingIdBehavior,
7965
- testIdAttribute,
7966
- routerAwarePoms,
7967
- routerType,
8459
+ generation,
7968
8460
  getResolvedRouterEntry: resolveRouterEntry2,
7969
- routerModuleShims,
7970
8461
  loggerRef
7971
8462
  });
7972
- const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
8463
+ const virtualModules = createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata);
7973
8464
  return [tsProcessor, devProcessor, virtualModules];
7974
8465
  }
7975
8466
  function findDataTestIdProp(element, attributeName = "data-testid") {
@@ -8303,8 +8794,7 @@ function createVuePluginWithTestIds(options) {
8303
8794
  });
8304
8795
  const api = viteVuePlugin?.api;
8305
8796
  if (!api) {
8306
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8307
- return;
8797
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8308
8798
  }
8309
8799
  const currentOptions = api.options ?? {};
8310
8800
  const currentTemplate = currentOptions.template ?? {};
@@ -8370,33 +8860,6 @@ function assertOneOf(value, allowed, name) {
8370
8860
  }
8371
8861
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8372
8862
  }
8373
- function assertErrorBehavior(value, name) {
8374
- if (!value) {
8375
- return;
8376
- }
8377
- if (value === "ignore" || value === "error") {
8378
- return;
8379
- }
8380
- if (typeof value !== "object" || Array.isArray(value)) {
8381
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8382
- }
8383
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8384
- for (const key of Object.keys(value)) {
8385
- if (!supportedKeys.has(key)) {
8386
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8387
- }
8388
- }
8389
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8390
- }
8391
- function resolveMissingSemanticNameBehavior(value) {
8392
- if (!value) {
8393
- return "error";
8394
- }
8395
- if (value === "ignore" || value === "error") {
8396
- return value;
8397
- }
8398
- return value.missingSemanticNameBehavior ?? "error";
8399
- }
8400
8863
  function readPackageJson(projectRoot) {
8401
8864
  const packageJsonPath = path.join(projectRoot, "package.json");
8402
8865
  if (!fs.existsSync(packageJsonPath)) {
@@ -8487,7 +8950,6 @@ function getSharedGeneratorState(key) {
8487
8950
  let state = sharedGeneratorStateRegistry.get(key);
8488
8951
  if (!state) {
8489
8952
  state = {
8490
- componentTestIds: /* @__PURE__ */ new Map(),
8491
8953
  elementMetadata: /* @__PURE__ */ new Map(),
8492
8954
  semanticNameMap: /* @__PURE__ */ new Map(),
8493
8955
  componentHierarchyMap: /* @__PURE__ */ new Map(),
@@ -8505,7 +8967,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8505
8967
  '[vue-pom-generator] vuePluginOwnership="external" requires the resolved Vite Vue plugin, but none was found. Add vue() to your Vite plugins before spreading createVuePomGeneratorPlugins(...).'
8506
8968
  );
8507
8969
  }
8508
- throw new Error("[vue-pom-generator] Nuxt mode requires the resolved Vite Vue plugin, but none was found.");
8970
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8509
8971
  }
8510
8972
  const currentOptions = viteVuePlugin.api.options ?? {};
8511
8973
  const currentTemplate = currentOptions.template ?? {};
@@ -8562,18 +9024,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8562
9024
  const vueGenerationOptions = generationOptions;
8563
9025
  const verbosity = options.logging?.verbosity ?? "warn";
8564
9026
  const vueOptions = options.vueOptions;
8565
- const legacyVueOptions = options;
8566
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8567
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8568
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8569
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8570
- const nativeWrappers = injection.nativeWrappers ?? {};
8571
- const excludedComponents = injection.excludeComponents ?? [];
8572
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8573
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8574
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8575
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8576
- const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
9027
+ const resolvedInjectionOptionsRef = {
9028
+ current: resolveInjectionSupportOptions({
9029
+ isNuxt,
9030
+ viewsDir: injection.viewsDir,
9031
+ componentDirs: injection.componentDirs,
9032
+ layoutDirs: injection.layoutDirs,
9033
+ wrapperSearchRoots: injection.wrapperSearchRoots,
9034
+ nativeWrappers: injection.nativeWrappers,
9035
+ excludedComponents: injection.excludeComponents,
9036
+ existingIdBehavior: injection.existingIdBehavior,
9037
+ testIdAttribute: injection.attribute
9038
+ })
9039
+ };
9040
+ const resolvedInjectionOptions = resolvedInjectionOptionsRef.current;
9041
+ const nativeWrappers = resolvedInjectionOptions.nativeWrappers;
9042
+ const excludedComponents = resolvedInjectionOptions.excludedComponents;
9043
+ const testIdAttribute = resolvedInjectionOptions.testIdAttribute;
8577
9044
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8578
9045
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8579
9046
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8582,27 +9049,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8582
9049
  }
8583
9050
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8584
9051
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8585
- const csharp = generationOptions?.csharp;
8586
- const errorBehavior = options.errorBehavior;
8587
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8588
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8589
9052
  const generateFixtures = generationOptions?.playwright?.fixtures;
8590
9053
  const customPoms = generationOptions?.playwright?.customPoms;
8591
9054
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8592
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8593
9055
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8594
- const resolvedCustomPomImportCollisionBehavior = customPoms?.importNameCollisionBehavior ?? "error";
9056
+ const requireCustomPomDir = customPoms?.dir !== void 0 || resolvedCustomPomAttachments.length > 0 || Object.keys(resolvedCustomPomImportAliases ?? {}).length > 0;
9057
+ const resolvedGenerationOptions = resolveGenerationSupportOptions({
9058
+ outDir: generationOptions?.outDir,
9059
+ emitLanguages: generationOptions?.emit,
9060
+ typescriptOutputStructure: generationOptions?.playwright?.outputStructure,
9061
+ csharp: generationOptions?.csharp,
9062
+ generateFixtures,
9063
+ customPomAttachments: resolvedCustomPomAttachments,
9064
+ customPomDir: customPoms?.dir,
9065
+ requireCustomPomDir,
9066
+ customPomImportAliases: resolvedCustomPomImportAliases,
9067
+ customPomImportNameCollisionBehavior: customPoms?.importNameCollisionBehavior,
9068
+ nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
9069
+ existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
9070
+ testIdAttribute,
9071
+ routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
9072
+ routerEntry,
9073
+ routerType,
9074
+ routerModuleShims
9075
+ });
8595
9076
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8596
- const getPageDirs = () => pageDirsRef.current;
9077
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8597
9078
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8598
- const getComponentDirs = () => componentDirsRef.current;
8599
- const getLayoutDirs = () => layoutDirsRef.current;
9079
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
9080
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8600
9081
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8601
9082
  ...getPageDirs(),
8602
9083
  ...getComponentDirs(),
8603
9084
  ...getLayoutDirs()
8604
9085
  ]));
8605
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
9086
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8606
9087
  const sharedStateKey = JSON.stringify({
8607
9088
  cwd: process.cwd(),
8608
9089
  mode: isNuxt ? "nuxt" : "vue",
@@ -8610,9 +9091,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8610
9091
  componentDirs: isNuxt ? null : getComponentDirs(),
8611
9092
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8612
9093
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8613
- outDir,
9094
+ outDir: resolvedGenerationOptions.outDir,
8614
9095
  testIdAttribute,
8615
- routerType,
9096
+ routerType: resolvedGenerationOptions.routerType,
8616
9097
  vuePluginOwnership
8617
9098
  });
8618
9099
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8633,23 +9114,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8633
9114
  if (isNuxt) {
8634
9115
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8635
9116
  projectRootRef.current = nuxtDiscovery.rootDir;
8636
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8637
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8638
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8639
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
9117
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
9118
+ resolvedInjectionOptionsRef.current,
9119
+ nuxtDiscovery
9120
+ );
8640
9121
  }
8641
9122
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8642
9123
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8643
9124
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8644
9125
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8645
9126
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8646
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8647
9127
  if (generationEnabled) {
8648
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8649
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8650
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8651
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8652
- assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
9128
+ assertNonEmptyString(resolvedGenerationOptions.outDir, "[vue-pom-generator] generation.outDir");
9129
+ assertOneOf(resolvedGenerationOptions.typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
9130
+ assertRouterModuleShims(resolvedGenerationOptions.routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
9131
+ if (!isNuxt && vueGenerationOptions?.router && resolvedGenerationOptions.routerType === "vue-router") {
9132
+ assertNonEmptyString(resolvedGenerationOptions.routerEntry, "[vue-pom-generator] generation.router.entry");
8653
9133
  }
8654
9134
  }
8655
9135
  if (usesExternalVuePlugin) {
@@ -8668,11 +9148,11 @@ function createVuePomGeneratorPlugins(options = {}) {
8668
9148
  };
8669
9149
  const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, getViewsDir());
8670
9150
  const getWrapperSearchRootsAbs = () => getWrapperSearchRoots().map((root) => resolveFromProjectRoot(projectRootRef.current, root));
8671
- const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
9151
+ const { elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8672
9152
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8673
9153
  vueOptions,
8674
- existingIdBehavior,
8675
- nameCollisionBehavior,
9154
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
9155
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8676
9156
  nativeWrappers,
8677
9157
  elementMetadata,
8678
9158
  semanticNameMap,
@@ -8687,10 +9167,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8687
9167
  getProjectRoot: () => projectRootRef.current
8688
9168
  });
8689
9169
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8690
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8691
9170
  const supportPlugins = createSupportPlugins({
8692
- componentTestIds,
8693
9171
  componentHierarchyMap,
9172
+ elementMetadata,
8694
9173
  vueFilesPathMap,
8695
9174
  nativeWrappers,
8696
9175
  excludedComponents,
@@ -8700,26 +9179,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8700
9179
  getViewsDir,
8701
9180
  getSourceDirs,
8702
9181
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8703
- nameCollisionBehavior,
8704
- missingSemanticNameBehavior,
8705
- existingIdBehavior,
8706
- outDir,
8707
- emitLanguages,
8708
- typescriptOutputStructure,
8709
- csharp,
8710
- routerAwarePoms,
8711
- routerEntry,
8712
- generateFixtures,
9182
+ generation: resolvedGenerationOptions,
8713
9183
  projectRootRef,
8714
9184
  basePageClassPath: basePageClassPathOverride,
8715
- customPomAttachments: resolvedCustomPomAttachments,
8716
- customPomDir: resolvedCustomPomDir,
8717
- customPomImportAliases: resolvedCustomPomImportAliases,
8718
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8719
- testIdAttribute,
8720
- loggerRef,
8721
- routerType,
8722
- routerModuleShims
9185
+ loggerRef
8723
9186
  });
8724
9187
  if (isNuxt) {
8725
9188
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");
@@ -8733,7 +9196,7 @@ function createVuePomGeneratorPlugins(options = {}) {
8733
9196
  ...supportPlugins
8734
9197
  ];
8735
9198
  if (!generationEnabled) {
8736
- const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
9199
+ const virtualModules = createTestIdsVirtualModulesPlugin(componentHierarchyMap, elementMetadata);
8737
9200
  return [
8738
9201
  configPlugin,
8739
9202
  metadataCollectorPlugin,