@immense/vue-pom-generator 1.0.58 → 1.0.59

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 (50) hide show
  1. package/README.md +6 -18
  2. package/RELEASE_NOTES.md +75 -29
  3. package/class-generation/base-page.ts +6 -13
  4. package/class-generation/index.ts +226 -317
  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 +1 -0
  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 +1216 -1008
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1218 -1010
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/method-generation.d.ts +4 -2
  20. package/dist/method-generation.d.ts.map +1 -1
  21. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  22. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  23. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  24. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  25. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  26. package/dist/plugin/support/build-plugin.d.ts +2 -29
  27. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  28. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  29. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support-plugins.d.ts +2 -32
  31. package/dist/plugin/support-plugins.d.ts.map +1 -1
  32. package/dist/plugin/types.d.ts +6 -23
  33. package/dist/plugin/types.d.ts.map +1 -1
  34. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  35. package/dist/pom-params.d.ts +40 -0
  36. package/dist/pom-params.d.ts.map +1 -0
  37. package/dist/pom-patterns.d.ts +31 -0
  38. package/dist/pom-patterns.d.ts.map +1 -0
  39. package/dist/routing/to-directive.d.ts +21 -0
  40. package/dist/routing/to-directive.d.ts.map +1 -1
  41. package/dist/tests/base-page.test.d.ts +2 -0
  42. package/dist/tests/base-page.test.d.ts.map +1 -0
  43. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  44. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  45. package/dist/transform.d.ts +0 -1
  46. package/dist/transform.d.ts.map +1 -1
  47. package/dist/utils.d.ts +129 -63
  48. package/dist/utils.d.ts.map +1 -1
  49. package/package.json +6 -4
  50. package/sequence-diagram.md +6 -6
package/dist/index.cjs CHANGED
@@ -245,6 +245,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
245
245
  const nuxtOptions = await loadNuxtConfig({ cwd });
246
246
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
247
247
  }
248
+ function resolveGenerationSupportOptions(options) {
249
+ return {
250
+ outDir: (options.outDir ?? "tests/playwright/__generated__").trim(),
251
+ emitLanguages: options.emitLanguages?.length ? options.emitLanguages : ["ts"],
252
+ typescriptOutputStructure: options.typescriptOutputStructure ?? "aggregated",
253
+ csharp: options.csharp,
254
+ generateFixtures: options.generateFixtures,
255
+ customPomAttachments: options.customPomAttachments ?? [],
256
+ customPomDir: options.customPomDir ?? "tests/playwright/pom/custom",
257
+ requireCustomPomDir: options.requireCustomPomDir ?? false,
258
+ customPomImportAliases: options.customPomImportAliases,
259
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior ?? "error",
260
+ nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
261
+ existingIdBehavior: options.existingIdBehavior ?? "error",
262
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
263
+ routerAwarePoms: options.routerAwarePoms ?? false,
264
+ routerEntry: options.routerEntry,
265
+ routerType: options.routerType ?? "vue-router",
266
+ routerModuleShims: options.routerModuleShims
267
+ };
268
+ }
269
+ function resolveInjectionSupportOptions(options) {
270
+ const isNuxt = options.isNuxt ?? false;
271
+ return {
272
+ pageDirs: isNuxt ? ["app/pages"] : [options.viewsDir ?? "src/views"],
273
+ componentDirs: isNuxt ? ["app/components"] : options.componentDirs ?? ["src/components"],
274
+ layoutDirs: isNuxt ? ["app/layouts"] : options.layoutDirs ?? ["src/layouts"],
275
+ wrapperSearchRoots: isNuxt ? [] : options.wrapperSearchRoots ?? [],
276
+ nativeWrappers: options.nativeWrappers ?? {},
277
+ excludedComponents: options.excludedComponents ?? [],
278
+ existingIdBehavior: options.existingIdBehavior ?? "error",
279
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid"
280
+ };
281
+ }
282
+ function applyNuxtDiscoveryToInjectionOptions(options, discovery) {
283
+ return {
284
+ ...options,
285
+ pageDirs: discovery.pageDirs.length ? discovery.pageDirs : [path.resolve(discovery.srcDir, "pages")],
286
+ componentDirs: discovery.componentDirs,
287
+ layoutDirs: discovery.layoutDirs,
288
+ wrapperSearchRoots: discovery.wrapperSearchRoots
289
+ };
290
+ }
248
291
  function createTypeScriptProject() {
249
292
  return new tsMorph.Project({
250
293
  useInMemoryFileSystem: true,
@@ -336,16 +379,7 @@ function createClassConstructor(constructorDeclaration) {
336
379
  ...constructorDeclaration
337
380
  };
338
381
  }
339
- function upperFirst$1(value) {
340
- if (!value) {
341
- return value;
342
- }
343
- return value.charAt(0).toUpperCase() + value.slice(1);
344
- }
345
- function hasParam(params, name) {
346
- return Object.prototype.hasOwnProperty.call(params, name);
347
- }
348
- function splitTypeAndInitializer(typeExpression) {
382
+ function splitPomParameterTypeExpression(typeExpression) {
349
383
  const trimmed = typeExpression.trim();
350
384
  const initializerIndex = trimmed.lastIndexOf("=");
351
385
  if (initializerIndex < 0) {
@@ -356,49 +390,237 @@ function splitTypeAndInitializer(typeExpression) {
356
390
  initializer: trimmed.slice(initializerIndex + 1).trim()
357
391
  };
358
392
  }
359
- function createParameter(name, typeExpression) {
360
- const { type, initializer } = splitTypeAndInitializer(typeExpression);
393
+ function createPomParameterSpec(name, typeExpression, options = {}) {
394
+ const normalizedTypeExpression = typeExpression?.trim();
395
+ const { type, initializer } = normalizedTypeExpression ? splitPomParameterTypeExpression(normalizedTypeExpression) : { type: void 0, initializer: void 0 };
361
396
  return {
362
397
  name,
363
- type: type || void 0,
364
- initializer
398
+ typeExpression: normalizedTypeExpression,
399
+ type,
400
+ initializer: options.initializer ?? initializer,
401
+ hasQuestionToken: options.hasQuestionToken,
402
+ isRestParameter: options.isRestParameter
365
403
  };
366
404
  }
367
- function createParameters(params) {
368
- return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
405
+ function normalizePomParameters(params) {
406
+ if (!params) {
407
+ return [];
408
+ }
409
+ return params.map((param) => createPomParameterSpec(param.name, param.typeExpression ?? param.type, {
410
+ initializer: param.initializer,
411
+ hasQuestionToken: param.hasQuestionToken,
412
+ isRestParameter: param.isRestParameter
413
+ }));
369
414
  }
370
- function createInlineParameter(name, options = {}) {
415
+ function getPomParameterNames(params) {
416
+ return normalizePomParameters(params).map((param) => param.name);
417
+ }
418
+ function getPomParameter(params, name) {
419
+ return normalizePomParameters(params).find((param) => param.name === name);
420
+ }
421
+ function hasPomParameter(params, name) {
422
+ return !!getPomParameter(params, name);
423
+ }
424
+ function setPomParameter(params, name, typeExpression, options = {}) {
425
+ const nextParam = createPomParameterSpec(name, typeExpression, options);
426
+ const normalizedParams = normalizePomParameters(params);
427
+ const existingIndex = normalizedParams.findIndex((param) => param.name === name);
428
+ if (existingIndex < 0) {
429
+ return [...normalizedParams, nextParam];
430
+ }
431
+ const nextParams = normalizedParams.slice();
432
+ nextParams[existingIndex] = nextParam;
433
+ return nextParams;
434
+ }
435
+ function removePomParameter(params, name) {
436
+ return normalizePomParameters(params).filter((param) => param.name !== name);
437
+ }
438
+ function toTypeScriptPomParameterStructures(params) {
439
+ return normalizePomParameters(params).map((param) => ({
440
+ name: param.name,
441
+ type: param.type || void 0,
442
+ initializer: param.initializer,
443
+ hasQuestionToken: param.hasQuestionToken,
444
+ isRestParameter: param.isRestParameter
445
+ }));
446
+ }
447
+ function getPomParameterArgumentNames(params) {
448
+ return normalizePomParameters(params).map((param) => param.isRestParameter ? `...${param.name}` : param.name);
449
+ }
450
+ function createPomMethodSignature(parameters) {
371
451
  return {
372
- name,
373
- type: options.type,
374
- initializer: options.initializer
452
+ parameters: normalizePomParameters(parameters)
375
453
  };
376
454
  }
377
- function removeByKeySegment(value) {
378
- const idx = value.lastIndexOf("ByKey");
379
- if (idx < 0) {
380
- return value;
455
+ function pomParameterSpecEquals(left, right) {
456
+ return left.name === right.name && left.typeExpression === right.typeExpression && left.type === right.type && left.initializer === right.initializer && left.hasQuestionToken === right.hasQuestionToken && left.isRestParameter === right.isRestParameter;
457
+ }
458
+ function pomParameterListEquals(left, right) {
459
+ const leftParams = normalizePomParameters(left);
460
+ const rightParams = normalizePomParameters(right);
461
+ if (leftParams.length !== rightParams.length) {
462
+ return false;
381
463
  }
382
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
464
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
383
465
  }
384
- function uniqueAlternates(primary, alternates) {
466
+ function pomMethodSignatureEquals(left, right) {
467
+ return pomParameterListEquals(left.parameters, right.parameters);
468
+ }
469
+ function isParameterizedPomPattern(kind) {
470
+ return kind === "parameterized";
471
+ }
472
+ function getTemplateVariables(formatted) {
385
473
  const out = [];
386
474
  const seen = /* @__PURE__ */ new Set();
387
- seen.add(primary);
388
- for (const a of alternates ?? []) {
389
- if (!a) {
475
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
476
+ for (const match of matches) {
477
+ const variableName = match[1];
478
+ if (seen.has(variableName)) {
390
479
  continue;
391
480
  }
392
- if (seen.has(a)) {
481
+ seen.add(variableName);
482
+ out.push(variableName);
483
+ }
484
+ return out;
485
+ }
486
+ function createPomStringPattern(formatted, patternKind) {
487
+ return {
488
+ formatted,
489
+ patternKind,
490
+ templateVariables: getTemplateVariables(formatted)
491
+ };
492
+ }
493
+ function getPomPatternVariables(patterns, options = {}) {
494
+ const out = [];
495
+ const seen = /* @__PURE__ */ new Set();
496
+ const omitted = new Set(options.omit ?? []);
497
+ for (const pattern of patterns) {
498
+ for (const variableName of pattern.templateVariables) {
499
+ if (omitted.has(variableName) || seen.has(variableName)) {
500
+ continue;
501
+ }
502
+ seen.add(variableName);
503
+ out.push(variableName);
504
+ }
505
+ }
506
+ return out;
507
+ }
508
+ function orderPomPatternParameters(params, patterns, options = {}) {
509
+ const currentParams = normalizePomParameters(params);
510
+ const orderedParams = [];
511
+ const seen = /* @__PURE__ */ new Set();
512
+ const missingParams = [];
513
+ for (const variableName of getPomPatternVariables(patterns, options)) {
514
+ seen.add(variableName);
515
+ const existingParam = currentParams.find((param) => param.name === variableName);
516
+ if (!existingParam) {
517
+ missingParams.push(variableName);
393
518
  continue;
394
519
  }
395
- seen.add(a);
396
- out.push(a);
520
+ orderedParams.push(existingParam);
521
+ }
522
+ if (missingParams.length > 0) {
523
+ const availableParams = currentParams.map((param) => JSON.stringify(param.name)).join(", ") || "<none>";
524
+ const patternSummary = patterns.map((pattern) => JSON.stringify(pattern.formatted)).join(", ");
525
+ throw new Error(
526
+ `[vue-pom-generator] Missing selector parameter(s) ${missingParams.map((name) => JSON.stringify(name)).join(", ")} for parameterized pattern(s) ${patternSummary}. Available parameters: ${availableParams}.`
527
+ );
528
+ }
529
+ for (const param of currentParams) {
530
+ if (seen.has(param.name)) {
531
+ continue;
532
+ }
533
+ seen.add(param.name);
534
+ orderedParams.push(param);
535
+ }
536
+ return orderedParams;
537
+ }
538
+ function getIndexedPomPatternVariable(pattern) {
539
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
540
+ return null;
541
+ }
542
+ if (pattern.templateVariables.length !== 1) {
543
+ throw new Error(
544
+ `[vue-pom-generator] Parameterized locator getters require exactly one template variable; got ${pattern.templateVariables.length} in ${JSON.stringify(pattern.formatted)}.`
545
+ );
546
+ }
547
+ return pattern.templateVariables[0];
548
+ }
549
+ function hasPomPatternVariables(pattern) {
550
+ return pattern.templateVariables.length > 0;
551
+ }
552
+ function toTypeScriptPomPatternExpression(pattern) {
553
+ return isParameterizedPomPattern(pattern.patternKind) ? `\`${pattern.formatted}\`` : JSON.stringify(pattern.formatted);
554
+ }
555
+ function toCSharpPomPatternExpression(pattern) {
556
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
557
+ return JSON.stringify(pattern.formatted);
558
+ }
559
+ const inner = pattern.formatted.replace(/\$\{/g, "{");
560
+ return `$${JSON.stringify(inner)}`;
561
+ }
562
+ function bindTypeScriptPomPattern(pattern, variableName) {
563
+ const expression = toTypeScriptPomPatternExpression(pattern);
564
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
565
+ return { expression, setupStatements: [] };
566
+ }
567
+ return {
568
+ expression: variableName,
569
+ setupStatements: [`const ${variableName} = ${expression};`]
570
+ };
571
+ }
572
+ function bindCSharpPomPattern(pattern, variableName) {
573
+ const expression = toCSharpPomPatternExpression(pattern);
574
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
575
+ return { expression, setupStatements: [] };
576
+ }
577
+ return {
578
+ expression: variableName,
579
+ setupStatements: [`var ${variableName} = ${expression};`]
580
+ };
581
+ }
582
+ function pomStringPatternEquals(left, right) {
583
+ return left.formatted === right.formatted && left.patternKind === right.patternKind;
584
+ }
585
+ function uniquePomStringPatterns(primary, alternates) {
586
+ const out = [];
587
+ const seen = /* @__PURE__ */ new Set();
588
+ const add = (pattern) => {
589
+ const key = JSON.stringify(pattern);
590
+ if (seen.has(key)) {
591
+ return;
592
+ }
593
+ seen.add(key);
594
+ out.push(pattern);
595
+ };
596
+ add(primary);
597
+ for (const alternate of alternates ?? []) {
598
+ add(alternate);
397
599
  }
398
600
  return out;
399
601
  }
400
- function testIdExpression(formattedDataTestId) {
401
- return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
602
+ function upperFirst$1(value) {
603
+ if (!value) {
604
+ return value;
605
+ }
606
+ return value.charAt(0).toUpperCase() + value.slice(1);
607
+ }
608
+ function createParameters(params) {
609
+ return toTypeScriptPomParameterStructures(params);
610
+ }
611
+ function createInlineParameter(name, options = {}) {
612
+ return {
613
+ name,
614
+ type: options.type,
615
+ initializer: options.initializer
616
+ };
617
+ }
618
+ function removeByKeySegment(value) {
619
+ const idx = value.lastIndexOf("ByKey");
620
+ if (idx < 0) {
621
+ return value;
622
+ }
623
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
402
624
  }
403
625
  function createAsyncMethod(name, parameters, statements) {
404
626
  return createClassMethod({
@@ -408,17 +630,20 @@ function createAsyncMethod(name, parameters, statements) {
408
630
  statements
409
631
  });
410
632
  }
411
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
633
+ function generateClickMethod(methodName, selector, alternateSelectors, parameters) {
412
634
  const name = `click${methodName}`;
413
635
  const noWaitName = `${name}NoWait`;
414
- const baseParameters = createParameters(params);
415
- const argsForForward = Object.keys(params).join(", ");
416
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
636
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
637
+ const hasSelectorVariables = hasPomPatternVariables(selector);
638
+ const baseParameters = createParameters(selectorParams);
639
+ const argsForForward = getPomParameterNames(selectorParams).join(", ");
640
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
641
+ const primaryTestIdExpr = toTypeScriptPomPatternExpression(selector);
417
642
  if (alternates.length > 0) {
418
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
643
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
419
644
  const clickMethod = createAsyncMethod(
420
645
  name,
421
- hasParam(params, "key") ? [
646
+ hasSelectorVariables ? [
422
647
  ...baseParameters,
423
648
  createInlineParameter("wait", { type: "boolean", initializer: "true" }),
424
649
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
@@ -447,14 +672,14 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
447
672
  const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
448
673
  const noWaitMethod = createAsyncMethod(
449
674
  noWaitName,
450
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
675
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
451
676
  (writer) => {
452
677
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
453
678
  }
454
679
  );
455
680
  return [clickMethod, noWaitMethod];
456
681
  }
457
- if (hasParam(params, "key")) {
682
+ if (hasSelectorVariables) {
458
683
  return [
459
684
  createAsyncMethod(
460
685
  name,
@@ -464,7 +689,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
464
689
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
465
690
  ],
466
691
  (writer) => {
467
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, annotationText, wait);`);
692
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
468
693
  }
469
694
  ),
470
695
  createAsyncMethod(
@@ -484,7 +709,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
484
709
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
485
710
  ],
486
711
  (writer) => {
487
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", annotationText, wait);`);
712
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
488
713
  }
489
714
  ),
490
715
  createAsyncMethod(
@@ -496,31 +721,25 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
496
721
  )
497
722
  ];
498
723
  }
499
- function generateRadioMethod(methodName, formattedDataTestId) {
724
+ function generateRadioMethod(methodName, selector, parameters) {
500
725
  const name = `select${methodName}`;
501
- const hasKey = formattedDataTestId.includes("${key}");
502
- const parameters = hasKey ? [
503
- createInlineParameter("key", { type: "string" }),
504
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
505
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
506
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
726
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
727
+ const methodParameters = createParameters(selectorParams);
728
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
507
729
  return [
508
- createAsyncMethod(name, parameters, (writer) => {
730
+ createAsyncMethod(name, methodParameters, (writer) => {
509
731
  writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
510
732
  })
511
733
  ];
512
734
  }
513
- function generateSelectMethod(methodName, formattedDataTestId) {
735
+ function generateSelectMethod(methodName, selector, parameters) {
514
736
  const name = `select${methodName}`;
515
- const needsKey = formattedDataTestId.includes("${key}");
516
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
737
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
738
+ const selectorExpr = `this.selectorForTestId(${toTypeScriptPomPatternExpression(selector)})`;
517
739
  return [
518
740
  createAsyncMethod(
519
741
  name,
520
- [
521
- createInlineParameter("value", { type: "string" }),
522
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
523
- ],
742
+ createParameters(selectorParams),
524
743
  (writer) => {
525
744
  writer.writeLine(`const selector = ${selectorExpr};`);
526
745
  writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
@@ -529,33 +748,28 @@ function generateSelectMethod(methodName, formattedDataTestId) {
529
748
  )
530
749
  ];
531
750
  }
532
- function generateVSelectMethod(methodName, formattedDataTestId) {
751
+ function generateVSelectMethod(methodName, selector, parameters) {
533
752
  const name = `select${methodName}`;
753
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
534
754
  return [
535
755
  createAsyncMethod(
536
756
  name,
537
- [
538
- createInlineParameter("value", { type: "string" }),
539
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
540
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
541
- ],
757
+ createParameters(selectorParams),
542
758
  (writer) => {
543
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
759
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText);`);
544
760
  }
545
761
  )
546
762
  ];
547
763
  }
548
- function generateTypeMethod(methodName, formattedDataTestId) {
764
+ function generateTypeMethod(methodName, selector, parameters) {
549
765
  const name = `type${methodName}`;
766
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
550
767
  return [
551
768
  createAsyncMethod(
552
769
  name,
553
- [
554
- createInlineParameter("text", { type: "string" }),
555
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
556
- ],
770
+ createParameters(selectorParams),
557
771
  (writer) => {
558
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
772
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText);`);
559
773
  }
560
774
  )
561
775
  ];
@@ -570,30 +784,31 @@ function isAllDigits(value) {
570
784
  }
571
785
  return true;
572
786
  }
573
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
787
+ function generateGetElementByDataTestId(methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
574
788
  const roleSuffix = upperFirst$1(nativeRole || "Element");
575
789
  const baseName = upperFirst$1(methodName);
576
790
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
577
791
  const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
578
792
  const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
579
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
580
- if (needsKey) {
581
- const keyType = params.key || "string";
793
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
794
+ const indexedVariable = getIndexedPomPatternVariable(selector);
795
+ if (indexedVariable) {
796
+ const keyType = getPomParameter(selectorParams, indexedVariable)?.typeExpression || "string";
582
797
  const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
583
798
  return [
584
799
  createClassGetter({
585
800
  name: keyedPropertyName,
586
801
  statements: [
587
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
802
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}));`
588
803
  ]
589
804
  })
590
805
  ];
591
806
  }
592
807
  const finalPropertyName = getterNameOverride ?? propertyName;
593
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
808
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
594
809
  if (alternates.length > 0) {
595
- const all = [formattedDataTestId, ...alternates];
596
- const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
810
+ const all = [selector, ...alternates];
811
+ const locatorExpr = all.map((id) => `this.locatorByTestId(${toTypeScriptPomPatternExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
597
812
  return [
598
813
  createClassGetter({
599
814
  name: finalPropertyName,
@@ -604,21 +819,22 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
604
819
  return [
605
820
  createClassGetter({
606
821
  name: finalPropertyName,
607
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
822
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)});`]
608
823
  })
609
824
  ];
610
825
  }
611
826
  function generateNavigationMethod(args) {
612
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
827
+ const { targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
613
828
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
614
- const parameters = createParameters(params);
615
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
616
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
829
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
830
+ const methodParameters = createParameters(selectorParams);
831
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
832
+ const candidatesExpr = [toTypeScriptPomPatternExpression(selector), ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
617
833
  if (alternates.length > 0) {
618
834
  return [
619
835
  createClassMethod({
620
836
  name: methodName,
621
- parameters,
837
+ parameters: methodParameters,
622
838
  returnType: `Fluent<${target}>`,
623
839
  statements: (writer) => {
624
840
  writer.write("return this.fluent(async () => ").block(() => {
@@ -646,11 +862,11 @@ function generateNavigationMethod(args) {
646
862
  return [
647
863
  createClassMethod({
648
864
  name: methodName,
649
- parameters,
865
+ parameters: methodParameters,
650
866
  returnType: `Fluent<${target}>`,
651
867
  statements: (writer) => {
652
868
  writer.write("return this.fluent(async () => ").block(() => {
653
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
869
+ writer.writeLine(`await this.clickByTestId(${toTypeScriptPomPatternExpression(selector)});`);
654
870
  writer.writeLine(`return new ${target}(this.page);`);
655
871
  });
656
872
  writer.writeLine(");");
@@ -658,15 +874,15 @@ function generateNavigationMethod(args) {
658
874
  })
659
875
  ];
660
876
  }
661
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
877
+ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
662
878
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
663
879
  const members = generateGetElementByDataTestId(
664
880
  baseMethodName,
665
881
  nativeRole,
666
- formattedDataTestId,
667
- alternateFormattedDataTestIds,
882
+ selector,
883
+ alternateSelectors,
668
884
  getterNameOverride,
669
- params
885
+ parameters
670
886
  );
671
887
  if (targetPageObjectModelClass) {
672
888
  return [
@@ -674,25 +890,25 @@ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName,
674
890
  ...generateNavigationMethod({
675
891
  targetPageObjectModelClass,
676
892
  baseMethodName,
677
- formattedDataTestId,
678
- alternateFormattedDataTestIds,
679
- params
893
+ selector,
894
+ alternateSelectors,
895
+ parameters
680
896
  })
681
897
  ];
682
898
  }
683
899
  if (nativeRole === "select") {
684
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
900
+ return [...members, ...generateSelectMethod(baseMethodName, selector, parameters)];
685
901
  }
686
902
  if (nativeRole === "vselect") {
687
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
903
+ return [...members, ...generateVSelectMethod(baseMethodName, selector, parameters)];
688
904
  }
689
905
  if (nativeRole === "input") {
690
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
906
+ return [...members, ...generateTypeMethod(baseMethodName, selector, parameters)];
691
907
  }
692
908
  if (nativeRole === "radio") {
693
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
909
+ return [...members, ...generateRadioMethod(baseMethodName || "Radio", selector, parameters)];
694
910
  }
695
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
911
+ return [...members, ...generateClickMethod(baseMethodName, selector, alternateSelectors, parameters)];
696
912
  }
697
913
  function isSimpleExpressionNode(value) {
698
914
  return value !== null && "type" in value && value.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION;
@@ -732,43 +948,72 @@ function buildPlaceholderParams(keys) {
732
948
  params[k] = "__placeholder__";
733
949
  return params;
734
950
  }
735
- function getRouteLocationLikeFromToDirective(toDirective) {
736
- if (!toDirective.exp)
737
- return null;
738
- const exp = toDirective.exp;
739
- const rawSource = compilerCore.stringifyExpression(exp).trim();
951
+ const isNodeType = (node, type) => {
952
+ return node !== null && node.type === type;
953
+ };
954
+ const isStringLiteralNode = (node) => {
955
+ return isNodeType(node, "StringLiteral") && typeof node.value === "string";
956
+ };
957
+ const isIdentifierNode = (node) => {
958
+ return isNodeType(node, "Identifier") && typeof node.name === "string";
959
+ };
960
+ const isObjectPropertyNode = (node) => {
961
+ if (!isNodeType(node, "ObjectProperty"))
962
+ return false;
963
+ const n = node;
964
+ return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
965
+ };
966
+ const isObjectExpressionNode = (node) => {
967
+ if (!isNodeType(node, "ObjectExpression"))
968
+ return false;
969
+ const n = node;
970
+ return Array.isArray(n.properties);
971
+ };
972
+ function materializeResolvedRouteTarget(target, paramKeys) {
973
+ if (typeof target === "string")
974
+ return target;
975
+ if (!paramKeys.length)
976
+ return target;
977
+ return {
978
+ ...target,
979
+ params: buildPlaceholderParams(paramKeys)
980
+ };
981
+ }
982
+ function analyzeToDirectiveTarget(toDirective) {
983
+ if (!toDirective.exp) {
984
+ return {
985
+ kind: "unsupported",
986
+ rawSource: null,
987
+ reason: "missing-expression"
988
+ };
989
+ }
990
+ const rawSource = compilerCore.stringifyExpression(toDirective.exp).trim();
740
991
  let expr;
741
992
  try {
742
993
  expr = parser.parseExpression(rawSource, { plugins: ["typescript"] });
743
- } catch {
744
- return null;
994
+ } catch (error) {
995
+ return {
996
+ kind: "parse-error",
997
+ rawSource,
998
+ reason: "parse-error",
999
+ error: error instanceof Error ? error.message : String(error)
1000
+ };
745
1001
  }
746
- const isNodeType = (node, type) => {
747
- return node !== null && node.type === type;
748
- };
749
- const isStringLiteralNode = (node) => {
750
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
751
- };
752
- const isIdentifierNode = (node) => {
753
- return isNodeType(node, "Identifier") && typeof node.name === "string";
754
- };
755
- const isObjectPropertyNode = (node) => {
756
- if (!isNodeType(node, "ObjectProperty"))
757
- return false;
758
- const n = node;
759
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
760
- };
761
- const isObjectExpressionNode = (node) => {
762
- if (!isNodeType(node, "ObjectExpression"))
763
- return false;
764
- const n = node;
765
- return Array.isArray(n.properties);
766
- };
767
1002
  if (isStringLiteralNode(expr)) {
768
- return expr.value;
1003
+ return {
1004
+ kind: "resolved",
1005
+ rawSource,
1006
+ target: expr.value,
1007
+ routeNameKey: null,
1008
+ paramKeys: []
1009
+ };
769
1010
  }
770
1011
  if (!isObjectExpressionNode(expr)) {
771
- return null;
1012
+ return {
1013
+ kind: "unsupported",
1014
+ rawSource,
1015
+ reason: "dynamic-expression"
1016
+ };
772
1017
  }
773
1018
  const getStringField = (fieldName) => {
774
1019
  const prop = expr.properties.find((p) => {
@@ -789,7 +1034,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
789
1034
  const key = p.key;
790
1035
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
791
1036
  });
792
- let params;
1037
+ let paramKeys = [];
793
1038
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
794
1039
  const keys = [];
795
1040
  for (const prop of paramsProp.value.properties) {
@@ -801,47 +1046,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
801
1046
  else if (isStringLiteralNode(key))
802
1047
  keys.push(key.value);
803
1048
  }
804
- if (keys.length) {
805
- params = buildPlaceholderParams(Array.from(new Set(keys)));
806
- }
1049
+ paramKeys = Array.from(new Set(keys));
807
1050
  }
808
1051
  if (name) {
809
- return { name, params };
1052
+ const trimmed = name.trim();
1053
+ if (!trimmed.length) {
1054
+ return {
1055
+ kind: "unsupported",
1056
+ rawSource,
1057
+ reason: "missing-name-or-path"
1058
+ };
1059
+ }
1060
+ return {
1061
+ kind: "resolved",
1062
+ rawSource,
1063
+ target: { name },
1064
+ routeNameKey: toPascalCaseRouteKey(trimmed),
1065
+ paramKeys
1066
+ };
810
1067
  }
811
1068
  if (path2) {
812
- return { path: path2, params };
1069
+ return {
1070
+ kind: "resolved",
1071
+ rawSource,
1072
+ target: { path: path2 },
1073
+ routeNameKey: null,
1074
+ paramKeys
1075
+ };
813
1076
  }
814
- return null;
815
- }
816
- function toDirectiveObjectFieldNameValue$1(toDirective) {
817
- const to = getRouteLocationLikeFromToDirective(toDirective);
818
- if (!to || typeof to === "string")
819
- return null;
820
- const name = to.name;
821
- if (typeof name !== "string")
822
- return null;
823
- const trimmed = name.trim();
824
- if (!trimmed.length)
825
- return null;
826
- return toPascalCaseRouteKey(trimmed);
827
- }
828
- function getRouteNameKeyFromToDirective(toDirective) {
829
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
830
- if (objectName)
831
- return objectName;
832
- return null;
1077
+ return {
1078
+ kind: "unsupported",
1079
+ rawSource,
1080
+ reason: "missing-name-or-path"
1081
+ };
833
1082
  }
834
1083
  function tryResolveToDirectiveTargetComponentName(toDirective) {
835
- const to = getRouteLocationLikeFromToDirective(toDirective);
836
- if (to && resolveToComponentName) {
837
- const resolved = resolveToComponentName(to);
1084
+ const analysis = analyzeToDirectiveTarget(toDirective);
1085
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1086
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
838
1087
  if (resolved)
839
1088
  return resolved;
840
1089
  }
841
- const key = getRouteNameKeyFromToDirective(toDirective);
842
- if (!key || !routeNameToComponentName)
1090
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
843
1091
  return null;
844
- return routeNameToComponentName.get(key) ?? null;
1092
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
845
1093
  }
846
1094
  function getDataTestIdFromGroupOption(text) {
847
1095
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -876,11 +1124,81 @@ function staticAttributeValue(value) {
876
1124
  return { kind: "static", value };
877
1125
  }
878
1126
  function templateAttributeValue(template) {
879
- return { kind: "template", template };
1127
+ const parsedTemplate = tryParseTemplateFragment(template);
1128
+ if (!parsedTemplate) {
1129
+ throw new Error(`[vue-pom-generator] Failed to parse generated template fragment: ${template}`);
1130
+ }
1131
+ return { kind: "template", template, parsedTemplate };
880
1132
  }
881
1133
  function getAttributeValueText(value) {
882
1134
  return value.kind === "static" ? value.value : value.template;
883
1135
  }
1136
+ function getVueExpressionSource(expression, ...preferredViews) {
1137
+ if (!expression) {
1138
+ return "";
1139
+ }
1140
+ for (const view of preferredViews) {
1141
+ let value = "";
1142
+ switch (view) {
1143
+ case "content":
1144
+ value = expression.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? expression.content : "";
1145
+ break;
1146
+ case "loc":
1147
+ value = expression.loc?.source ?? "";
1148
+ break;
1149
+ case "compiled":
1150
+ try {
1151
+ value = compilerCore.stringifyExpression(expression);
1152
+ } catch {
1153
+ value = "";
1154
+ }
1155
+ break;
1156
+ default:
1157
+ value = "";
1158
+ break;
1159
+ }
1160
+ const trimmed = value.trim();
1161
+ if (trimmed) {
1162
+ return trimmed;
1163
+ }
1164
+ }
1165
+ return "";
1166
+ }
1167
+ function tryGetExistingVueExpressionAst(expression) {
1168
+ if (!expression) {
1169
+ return null;
1170
+ }
1171
+ const ast = "ast" in expression ? expression.ast : null;
1172
+ return ast && "type" in ast ? ast : null;
1173
+ }
1174
+ function tryParseBabelExpressionFromSource(source, plugins) {
1175
+ const trimmed = source.trim();
1176
+ if (!trimmed) {
1177
+ return null;
1178
+ }
1179
+ try {
1180
+ return parser.parseExpression(trimmed, { plugins });
1181
+ } catch {
1182
+ return null;
1183
+ }
1184
+ }
1185
+ function tryGetVueExpressionAst(expression, options) {
1186
+ if (!expression) {
1187
+ return null;
1188
+ }
1189
+ if (options?.preferExistingAst !== false) {
1190
+ const existingAst = tryGetExistingVueExpressionAst(expression);
1191
+ if (existingAst) {
1192
+ return existingAst;
1193
+ }
1194
+ }
1195
+ const source = getVueExpressionSource(expression, ...options?.preferredViews ?? ["content", "loc", "compiled"]);
1196
+ return source ? tryParseBabelExpressionFromSource(source, options?.plugins ?? ["typescript"]) : null;
1197
+ }
1198
+ function tryGetDirectiveBabelAst(directive, options) {
1199
+ const exp = directive.exp && (directive.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || directive.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? directive.exp : null;
1200
+ return tryGetVueExpressionAst(exp, options);
1201
+ }
884
1202
  function toPascalCase(str) {
885
1203
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
886
1204
  if (!cleaned) {
@@ -922,32 +1240,14 @@ function tryGetClickDirective(node) {
922
1240
  function nodeHasClickDirective(node) {
923
1241
  return tryGetClickDirective(node) !== void 0;
924
1242
  }
925
- function getTemplateSlotScope(node) {
1243
+ function findTemplateSlotScopeExpression(node) {
926
1244
  if (node.tag !== "template") {
927
1245
  return null;
928
1246
  }
929
1247
  const slotProp = node.props.find((prop) => {
930
1248
  return prop.type === compilerCore.NodeTypes.DIRECTIVE && prop.name === "slot";
931
1249
  });
932
- if (slotProp?.exp) {
933
- if (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
934
- return slotProp.exp.content;
935
- }
936
- if (slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) {
937
- return compilerCore.stringifyExpression(slotProp.exp);
938
- }
939
- }
940
- return null;
941
- }
942
- function isSimpleScopeIdentifier(value) {
943
- if (!value) {
944
- return false;
945
- }
946
- try {
947
- return types.isIdentifier(parser.parseExpression(value, { plugins: ["typescript"] }));
948
- } catch {
949
- return false;
950
- }
1250
+ return slotProp?.exp && (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
951
1251
  }
952
1252
  function buildSlotScopeFallbackKeyExpression(identifier) {
953
1253
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -1049,43 +1349,64 @@ function tryGetSlotScopeKeyCandidate(node) {
1049
1349
  }
1050
1350
  return best;
1051
1351
  }
1052
- function tryGetTemplateSlotScopeKeyExpression(scope) {
1053
- const trimmed = scope.trim();
1054
- if (!trimmed) {
1352
+ function tryGetTemplateSlotScopeBindingNode(expression) {
1353
+ const ast = tryGetExistingVueExpressionAst(expression);
1354
+ if (ast) {
1355
+ if (types.isArrowFunctionExpression(ast)) {
1356
+ return ast.params[0] ?? null;
1357
+ }
1358
+ return ast;
1359
+ }
1360
+ const rawSource = getVueExpressionSource(expression, "content", "loc", "compiled");
1361
+ if (!rawSource) {
1055
1362
  return null;
1056
1363
  }
1057
- if (isSimpleScopeIdentifier(trimmed)) {
1058
- return buildSlotScopeFallbackKeyExpression(trimmed);
1364
+ try {
1365
+ return parser.parseExpression(rawSource, { plugins: ["typescript"] });
1366
+ } catch {
1059
1367
  }
1060
1368
  try {
1061
- const parsed = parser.parse(`(${trimmed}) => {}`, {
1369
+ const parsed = parser.parse(`(${rawSource}) => {}`, {
1062
1370
  sourceType: "module",
1063
1371
  plugins: ["typescript"]
1064
1372
  });
1065
1373
  const statement = parsed.program.body[0];
1066
1374
  if (statement && types.isExpressionStatement(statement) && types.isArrowFunctionExpression(statement.expression)) {
1067
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1375
+ return statement.expression.params[0] ?? null;
1068
1376
  }
1069
1377
  } catch {
1378
+ return null;
1070
1379
  }
1071
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1072
- const inner = trimmed.slice(1, -1).trim();
1073
- let cutIdx = -1;
1074
- const commaIdx = inner.indexOf(",");
1075
- const colonIdx = inner.indexOf(":");
1076
- if (commaIdx !== -1 && colonIdx !== -1) {
1077
- cutIdx = Math.min(commaIdx, colonIdx);
1078
- } else if (commaIdx !== -1) {
1079
- cutIdx = commaIdx;
1080
- } else if (colonIdx !== -1) {
1081
- cutIdx = colonIdx;
1082
- }
1083
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
1084
- if (first && isSimpleScopeIdentifier(first)) {
1085
- return buildSlotScopeFallbackKeyExpression(first);
1086
- }
1380
+ return null;
1381
+ }
1382
+ function toResolvedTemplateFragment(source) {
1383
+ const templateLiteral = tryUnwrapTemplateLiteralSource(source);
1384
+ if (templateLiteral) {
1385
+ return {
1386
+ template: templateLiteral.template,
1387
+ rawExpression: null
1388
+ };
1389
+ }
1390
+ return toInterpolatedTemplateFragment(source);
1391
+ }
1392
+ function toResolvedKeyInfo(selectorSource, runtimeSource = selectorSource) {
1393
+ const selectorFragment = selectorSource ? toResolvedTemplateFragment(selectorSource) : null;
1394
+ const runtimeFragment = runtimeSource ? toResolvedTemplateFragment(runtimeSource) : null;
1395
+ const selectorTemplate = selectorFragment?.template ?? runtimeFragment?.template ?? null;
1396
+ const runtimeTemplate = runtimeFragment?.template ?? selectorFragment?.template ?? null;
1397
+ if (!selectorTemplate || !runtimeTemplate) {
1398
+ return null;
1087
1399
  }
1088
- return trimmed;
1400
+ return {
1401
+ selectorFragment: selectorTemplate,
1402
+ runtimeFragment: runtimeTemplate,
1403
+ rawExpression: runtimeFragment?.rawExpression ?? selectorFragment?.rawExpression ?? null
1404
+ };
1405
+ }
1406
+ function tryGetTemplateSlotScopeKeyInfo(expression) {
1407
+ const bindingNode = tryGetTemplateSlotScopeBindingNode(expression);
1408
+ const candidateExpression = bindingNode ? tryGetSlotScopeKeyCandidate(bindingNode)?.expression ?? null : null;
1409
+ return candidateExpression ? toResolvedKeyInfo(candidateExpression) : null;
1089
1410
  }
1090
1411
  function nodeHasToDirective(node) {
1091
1412
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1094,51 +1415,160 @@ function nodeHasToDirective(node) {
1094
1415
  }
1095
1416
  return void 0;
1096
1417
  }
1097
- function nodeHasForDirective(node) {
1098
- return node.props.some(
1099
- (attr) => attr.type === compilerCore.NodeTypes.DIRECTIVE && attr.name === "for"
1100
- );
1101
- }
1102
1418
  function getKeyDirective(node) {
1103
1419
  return findDirectiveByName(node, "bind", "key") ?? null;
1104
1420
  }
1105
- function getKeyDirectiveValue(node, _context = null) {
1106
- const keyDirective = getKeyDirective(node);
1107
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1108
- if (rawSource) {
1109
- return `\${${rawSource}}`;
1421
+ function tryUnwrapTemplateLiteralSource(source) {
1422
+ const rawSource = source.trim();
1423
+ if (!rawSource) {
1424
+ return null;
1110
1425
  }
1111
- if (keyDirective?.exp) {
1112
- const value = compilerCore.stringifyExpression(keyDirective.exp);
1113
- if (value)
1114
- return `\${${value}}`;
1426
+ let ast = null;
1427
+ try {
1428
+ ast = parser.parseExpression(rawSource, { plugins: ["typescript"] });
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ if (!ast || !types.isTemplateLiteral(ast)) {
1433
+ return null;
1434
+ }
1435
+ const cooked = ast.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
1436
+ try {
1437
+ const start = typeof ast.start === "number" ? ast.start + 1 : 1;
1438
+ const end = typeof ast.end === "number" ? ast.end - 1 : rawSource.length - 1;
1439
+ return {
1440
+ template: rawSource.slice(start, end) || cooked,
1441
+ expressionCount: ast.expressions.length
1442
+ };
1443
+ } catch {
1444
+ return {
1445
+ template: cooked,
1446
+ expressionCount: ast.expressions.length
1447
+ };
1448
+ }
1449
+ }
1450
+ function tryUnwrapTemplateLiteralExpressionSource(expression) {
1451
+ const rawSource = getVueExpressionSource(expression, "loc", "compiled");
1452
+ return rawSource ? tryUnwrapTemplateLiteralSource(rawSource) : null;
1453
+ }
1454
+ function tryParseTemplateFragment(fragment) {
1455
+ if (!fragment) {
1456
+ return null;
1457
+ }
1458
+ try {
1459
+ const source = `\`${fragment}\``;
1460
+ const ast = parser.parseExpression(source, { plugins: ["typescript"] });
1461
+ return types.isTemplateLiteral(ast) ? { source, templateLiteral: ast } : null;
1462
+ } catch {
1463
+ return null;
1115
1464
  }
1116
- return null;
1465
+ }
1466
+ function getTemplateExpressionSource(parsedTemplate, index) {
1467
+ const expression = parsedTemplate.templateLiteral.expressions[index];
1468
+ if (!expression) {
1469
+ return null;
1470
+ }
1471
+ const start = typeof expression.start === "number" ? expression.start : null;
1472
+ const end = typeof expression.end === "number" ? expression.end : null;
1473
+ if (start === null || end === null) {
1474
+ return null;
1475
+ }
1476
+ return parsedTemplate.source.slice(start, end);
1477
+ }
1478
+ function getSingleExpressionTemplateFragment(parsedTemplate) {
1479
+ const { templateLiteral } = parsedTemplate;
1480
+ if (templateLiteral.expressions.length !== 1 || templateLiteral.quasis.length !== 2) {
1481
+ return null;
1482
+ }
1483
+ const expressionSource = getTemplateExpressionSource(parsedTemplate, 0);
1484
+ if (expressionSource === null) {
1485
+ return null;
1486
+ }
1487
+ return {
1488
+ prefix: templateLiteral.quasis[0]?.value.raw ?? "",
1489
+ expressionSource,
1490
+ suffix: templateLiteral.quasis[1]?.value.raw ?? ""
1491
+ };
1492
+ }
1493
+ function templateFragmentContainsSingleExpression(container, candidate) {
1494
+ const containerFragment = getSingleExpressionTemplateFragment(container);
1495
+ const candidateFragment = getSingleExpressionTemplateFragment(candidate);
1496
+ if (!containerFragment || !candidateFragment) {
1497
+ return false;
1498
+ }
1499
+ return containerFragment.expressionSource === candidateFragment.expressionSource && containerFragment.prefix.endsWith(candidateFragment.prefix) && containerFragment.suffix.startsWith(candidateFragment.suffix);
1500
+ }
1501
+ function hasTemplateInterpolationExpressions(fragment) {
1502
+ return (tryParseTemplateFragment(fragment)?.templateLiteral.expressions.length ?? 0) > 0;
1503
+ }
1504
+ function toInterpolatedTemplateFragment(fragment) {
1505
+ if (!fragment) {
1506
+ return null;
1507
+ }
1508
+ if (hasTemplateInterpolationExpressions(fragment)) {
1509
+ return { template: fragment, rawExpression: null };
1510
+ }
1511
+ return {
1512
+ template: `\${${fragment}}`,
1513
+ rawExpression: fragment
1514
+ };
1515
+ }
1516
+ function renderTemplateLiteralExpression(templateValue) {
1517
+ const templateLiteralSource = templateValue.parsedTemplate.source;
1518
+ const templateLiteral = templateValue.parsedTemplate.templateLiteral;
1519
+ const writer = createTypeScriptWriter();
1520
+ writer.write("`");
1521
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
1522
+ writer.write(templateLiteral.quasis[i]?.value.raw ?? "");
1523
+ const interpolation = templateLiteral.expressions[i];
1524
+ if (!interpolation) {
1525
+ continue;
1526
+ }
1527
+ const start = typeof interpolation.start === "number" ? interpolation.start : null;
1528
+ const end = typeof interpolation.end === "number" ? interpolation.end : null;
1529
+ if (start === null || end === null) {
1530
+ return templateLiteralSource;
1531
+ }
1532
+ writer.write("${");
1533
+ writer.write(templateLiteralSource.slice(start, end));
1534
+ writer.write("}");
1535
+ }
1536
+ writer.write("`");
1537
+ return writer.toString();
1538
+ }
1539
+ function getKeyDirectiveExpression(node) {
1540
+ const keyDirective = getKeyDirective(node);
1541
+ return keyDirective?.exp && (keyDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || keyDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? keyDirective.exp : null;
1542
+ }
1543
+ function getKeyDirectiveInfo(node) {
1544
+ const keyExpression = getKeyDirectiveExpression(node);
1545
+ if (!keyExpression) {
1546
+ return null;
1547
+ }
1548
+ const selectorSource = getVueExpressionSource(keyExpression, "compiled", "loc");
1549
+ const runtimeSource = getVueExpressionSource(keyExpression, "loc", "compiled");
1550
+ return toResolvedKeyInfo(selectorSource, runtimeSource);
1117
1551
  }
1118
1552
  function getModelBindingValues(node) {
1119
1553
  let vModel = "";
1120
1554
  const vModelDirective = findDirectiveByName(node, "model");
1121
- if (vModelDirective?.exp?.loc.source) {
1122
- vModel = toPascalCase(vModelDirective.exp.loc.source);
1555
+ if (vModelDirective?.exp && (vModelDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || vModelDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
1556
+ vModel = toPascalCase(getVueExpressionSource(vModelDirective.exp, "loc", "content"));
1123
1557
  }
1124
1558
  let modelValue = null;
1125
1559
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1126
- if (modelValueDirective?.exp?.ast) {
1127
- const { name: mv } = getClickHandlerNameFromAst(modelValueDirective.exp.ast);
1560
+ const modelValueAst = modelValueDirective ? tryGetDirectiveBabelAst(modelValueDirective, {
1561
+ preferredViews: ["loc", "compiled"],
1562
+ plugins: ["typescript"],
1563
+ preferExistingAst: false
1564
+ }) : null;
1565
+ if (modelValueAst) {
1566
+ const { name: mv } = getClickHandlerNameFromAst(modelValueAst);
1128
1567
  modelValue = mv;
1129
1568
  }
1130
1569
  return { vModel, modelValue };
1131
1570
  }
1132
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1133
- if (node.isSelfClosing) {
1134
- const hasForDirective = nodeHasForDirective(node);
1135
- if (hasForDirective) {
1136
- return getKeyDirectiveValue(node);
1137
- }
1138
- }
1139
- return null;
1140
- }
1141
- function getIdOrName(node) {
1571
+ function getStaticIdOrNameHint(node) {
1142
1572
  let idAttr = findAttributeByKey(node, "id");
1143
1573
  if (!idAttr) {
1144
1574
  idAttr = findAttributeByKey(node, "name");
@@ -1146,8 +1576,9 @@ function getIdOrName(node) {
1146
1576
  let identifier = idAttr?.value?.content ?? "";
1147
1577
  if (!identifier) {
1148
1578
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1149
- if (dynamicIdAttr?.exp) {
1150
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1579
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1580
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1581
+ return "";
1151
1582
  }
1152
1583
  }
1153
1584
  if (identifier.includes("-")) {
@@ -1158,20 +1589,20 @@ function getIdOrName(node) {
1158
1589
  }
1159
1590
  return identifier;
1160
1591
  }
1161
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1592
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1162
1593
  let parent = getParent(hierarchyMap2, node);
1163
1594
  while (parent) {
1164
1595
  if (parent.type === compilerCore.NodeTypes.ELEMENT && parent.tag === "template") {
1165
- const scope = getTemplateSlotScope(parent);
1166
- if (scope) {
1167
- return tryGetTemplateSlotScopeKeyExpression(scope);
1596
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1597
+ if (slotScopeExpression) {
1598
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1168
1599
  }
1169
1600
  }
1170
1601
  parent = getParent(hierarchyMap2, parent);
1171
1602
  }
1172
1603
  return null;
1173
1604
  }
1174
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1605
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1175
1606
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1176
1607
  return null;
1177
1608
  }
@@ -1180,8 +1611,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1180
1611
  if (parent.type === compilerCore.NodeTypes.ELEMENT) {
1181
1612
  const forDirective = findDirectiveByName(parent, "for");
1182
1613
  if (forDirective) {
1183
- const keyValue = getKeyDirectiveValue(parent);
1184
- return keyValue;
1614
+ return getKeyDirectiveInfo(parent);
1185
1615
  }
1186
1616
  }
1187
1617
  parent = getParent(hierarchyMap2, parent);
@@ -1207,13 +1637,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1207
1637
  if (simpleSourceExp.constType === compilerCore.ConstantTypes.NOT_CONSTANT) {
1208
1638
  return null;
1209
1639
  }
1210
- const iterableRaw = (() => {
1211
- try {
1212
- return compilerCore.stringifyExpression(simpleSourceExp).trim();
1213
- } catch {
1214
- return (simpleSourceExp.loc?.source ?? "").trim();
1215
- }
1216
- })();
1640
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1217
1641
  if (!iterableRaw) {
1218
1642
  return null;
1219
1643
  }
@@ -1269,91 +1693,95 @@ function nodeHandlerAttributeInfo(node) {
1269
1693
  return null;
1270
1694
  }
1271
1695
  const exp = handlerDirective.exp;
1272
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
1696
+ const source = getVueExpressionSource(exp, "content", "compiled");
1273
1697
  if (!source) {
1274
1698
  return null;
1275
1699
  }
1276
1700
  const mergeKey = `handler:expr:${source}`;
1277
- let expr;
1278
- try {
1279
- expr = parser.parseExpression(source, { plugins: ["typescript", "jsx"] });
1280
- } catch {
1701
+ const expr = tryGetDirectiveBabelAst(handlerDirective, {
1702
+ preferredViews: ["content", "compiled"],
1703
+ plugins: ["typescript", "jsx"],
1704
+ // Vue's compiler AST can encode `_ctx.foo` as an Identifier name instead of a MemberExpression.
1705
+ // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1706
+ preferExistingAst: false
1707
+ });
1708
+ if (!expr) {
1281
1709
  return null;
1282
1710
  }
1283
- const isNodeType = (node2, type) => {
1711
+ const isNodeType2 = (node2, type) => {
1284
1712
  return node2 !== null && node2.type === type;
1285
1713
  };
1286
- const isIdentifierNode = (node2) => {
1287
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1714
+ const isIdentifierNode2 = (node2) => {
1715
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1288
1716
  };
1289
- const isStringLiteralNode = (node2) => {
1290
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1717
+ const isStringLiteralNode2 = (node2) => {
1718
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1291
1719
  };
1292
1720
  const isBooleanLiteralNode = (node2) => {
1293
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1721
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1294
1722
  };
1295
1723
  const isNumericLiteralNode = (node2) => {
1296
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1724
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1297
1725
  };
1298
1726
  const isNullLiteralNode = (node2) => {
1299
- return isNodeType(node2, "NullLiteral");
1727
+ return isNodeType2(node2, "NullLiteral");
1300
1728
  };
1301
1729
  const isMemberExpressionNode = (node2) => {
1302
- if (!isNodeType(node2, "MemberExpression"))
1730
+ if (!isNodeType2(node2, "MemberExpression"))
1303
1731
  return false;
1304
1732
  const n = node2;
1305
1733
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1306
1734
  };
1307
1735
  const isCallExpressionNode = (node2) => {
1308
- if (!isNodeType(node2, "CallExpression"))
1736
+ if (!isNodeType2(node2, "CallExpression"))
1309
1737
  return false;
1310
1738
  const n = node2;
1311
1739
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1312
1740
  };
1313
1741
  const isAwaitExpressionNode = (node2) => {
1314
- if (!isNodeType(node2, "AwaitExpression"))
1742
+ if (!isNodeType2(node2, "AwaitExpression"))
1315
1743
  return false;
1316
1744
  const n = node2;
1317
1745
  return typeof n.argument === "object" && n.argument !== null;
1318
1746
  };
1319
1747
  const isAssignmentExpressionNode = (node2) => {
1320
- if (!isNodeType(node2, "AssignmentExpression"))
1748
+ if (!isNodeType2(node2, "AssignmentExpression"))
1321
1749
  return false;
1322
1750
  const n = node2;
1323
1751
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1324
1752
  };
1325
1753
  const isArrowFunctionExpressionNode = (node2) => {
1326
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1754
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1327
1755
  return false;
1328
1756
  const n = node2;
1329
1757
  return typeof n.body === "object" && n.body !== null;
1330
1758
  };
1331
1759
  const isBlockStatementNode = (node2) => {
1332
- if (!isNodeType(node2, "BlockStatement"))
1760
+ if (!isNodeType2(node2, "BlockStatement"))
1333
1761
  return false;
1334
1762
  const n = node2;
1335
1763
  return Array.isArray(n.body);
1336
1764
  };
1337
1765
  const isExpressionStatementNode = (node2) => {
1338
- if (!isNodeType(node2, "ExpressionStatement"))
1766
+ if (!isNodeType2(node2, "ExpressionStatement"))
1339
1767
  return false;
1340
1768
  const n = node2;
1341
1769
  return typeof n.expression === "object" && n.expression !== null;
1342
1770
  };
1343
1771
  const isReturnStatementNode = (node2) => {
1344
- if (!isNodeType(node2, "ReturnStatement"))
1772
+ if (!isNodeType2(node2, "ReturnStatement"))
1345
1773
  return false;
1346
1774
  const n = node2;
1347
1775
  return typeof n.argument === "object" || n.argument === null;
1348
1776
  };
1349
- const isObjectExpressionNode = (node2) => {
1350
- if (!isNodeType(node2, "ObjectExpression"))
1777
+ const isObjectExpressionNode2 = (node2) => {
1778
+ if (!isNodeType2(node2, "ObjectExpression"))
1351
1779
  return false;
1352
1780
  const n = node2;
1353
1781
  return Array.isArray(n.properties);
1354
1782
  };
1355
- const isObjectPropertyNode = (node2) => {
1356
- if (!isNodeType(node2, "ObjectProperty"))
1783
+ const isObjectPropertyNode2 = (node2) => {
1784
+ if (!isNodeType2(node2, "ObjectProperty"))
1357
1785
  return false;
1358
1786
  const n = node2;
1359
1787
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1361,16 +1789,16 @@ function nodeHandlerAttributeInfo(node) {
1361
1789
  const getLastIdentifierFromMemberChain = (node2) => {
1362
1790
  if (!node2)
1363
1791
  return null;
1364
- if (isIdentifierNode(node2))
1792
+ if (isIdentifierNode2(node2))
1365
1793
  return node2.name;
1366
1794
  if (isMemberExpressionNode(node2)) {
1367
1795
  const prop = node2.property;
1368
1796
  if (node2.computed === false) {
1369
- if (isIdentifierNode(prop))
1797
+ if (isIdentifierNode2(prop))
1370
1798
  return prop.name;
1371
1799
  }
1372
1800
  if (node2.computed === true) {
1373
- if (isStringLiteralNode(prop))
1801
+ if (isStringLiteralNode2(prop))
1374
1802
  return prop.value;
1375
1803
  }
1376
1804
  }
@@ -1380,7 +1808,7 @@ function nodeHandlerAttributeInfo(node) {
1380
1808
  if (!node2) {
1381
1809
  return null;
1382
1810
  }
1383
- if (isIdentifierNode(node2)) {
1811
+ if (isIdentifierNode2(node2)) {
1384
1812
  return node2.name;
1385
1813
  }
1386
1814
  if (isMemberExpressionNode(node2)) {
@@ -1392,11 +1820,11 @@ function nodeHandlerAttributeInfo(node) {
1392
1820
  if (!lhs) {
1393
1821
  return null;
1394
1822
  }
1395
- if (isIdentifierNode(lhs)) {
1823
+ if (isIdentifierNode2(lhs)) {
1396
1824
  return lhs.name;
1397
1825
  }
1398
1826
  if (isMemberExpressionNode(lhs)) {
1399
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1827
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1400
1828
  return getLastIdentifierFromMemberChain(lhs.object);
1401
1829
  }
1402
1830
  return getLastIdentifierFromMemberChain(lhs);
@@ -1404,7 +1832,7 @@ function nodeHandlerAttributeInfo(node) {
1404
1832
  return null;
1405
1833
  };
1406
1834
  const isTemplateLiteralNode = (node2) => {
1407
- if (!isNodeType(node2, "TemplateLiteral")) {
1835
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1408
1836
  return false;
1409
1837
  }
1410
1838
  const n = node2;
@@ -1423,7 +1851,7 @@ function nodeHandlerAttributeInfo(node) {
1423
1851
  if (isNullLiteralNode(arg)) {
1424
1852
  return "Null";
1425
1853
  }
1426
- if (isStringLiteralNode(arg)) {
1854
+ if (isStringLiteralNode2(arg)) {
1427
1855
  const cleaned = (arg.value ?? "").trim();
1428
1856
  if (!cleaned) {
1429
1857
  return null;
@@ -1449,7 +1877,7 @@ function nodeHandlerAttributeInfo(node) {
1449
1877
  return toPascalCase(stableName.slice(0, 24));
1450
1878
  }
1451
1879
  }
1452
- if (isIdentifierNode(arg)) {
1880
+ if (isIdentifierNode2(arg)) {
1453
1881
  const firstChar = arg.name.charAt(0);
1454
1882
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1455
1883
  if (isUpperAlpha) {
@@ -1461,7 +1889,7 @@ function nodeHandlerAttributeInfo(node) {
1461
1889
  const getStableSuffixFromCall = (call) => {
1462
1890
  const args = call.arguments ?? [];
1463
1891
  const first = args.length > 0 ? args[0] : null;
1464
- if (!isObjectExpressionNode(first)) {
1892
+ if (!isObjectExpressionNode2(first)) {
1465
1893
  const parts2 = [];
1466
1894
  for (const arg of args.slice(0, 4)) {
1467
1895
  const w = stableWordFromValue(arg ?? null);
@@ -1480,20 +1908,20 @@ function nodeHandlerAttributeInfo(node) {
1480
1908
  }
1481
1909
  const parts = [];
1482
1910
  for (const prop of first.properties ?? []) {
1483
- if (!isObjectPropertyNode(prop)) {
1911
+ if (!isObjectPropertyNode2(prop)) {
1484
1912
  continue;
1485
1913
  }
1486
1914
  if (prop.computed) {
1487
1915
  continue;
1488
1916
  }
1489
- const keyName = isIdentifierNode(prop.key) ? prop.key.name : isStringLiteralNode(prop.key) ? prop.key.value : null;
1917
+ const keyName = isIdentifierNode2(prop.key) ? prop.key.name : isStringLiteralNode2(prop.key) ? prop.key.value : null;
1490
1918
  if (!keyName) {
1491
1919
  continue;
1492
1920
  }
1493
1921
  let valueWord = null;
1494
1922
  if (isBooleanLiteralNode(prop.value)) {
1495
1923
  valueWord = prop.value.value ? "True" : "False";
1496
- } else if (isStringLiteralNode(prop.value)) {
1924
+ } else if (isStringLiteralNode2(prop.value)) {
1497
1925
  const cleaned = (prop.value.value ?? "").trim();
1498
1926
  if (cleaned) {
1499
1927
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1622,13 +2050,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1622
2050
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1623
2051
  }
1624
2052
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1625
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1626
- let value = attrDynamic.exp.loc.source;
1627
- if (attrDynamic.exp.ast?.type === "MemberExpression") {
2053
+ const attrDynamicAst = attrDynamic ? tryGetDirectiveBabelAst(attrDynamic, {
2054
+ preferredViews: ["loc", "compiled"],
2055
+ plugins: ["typescript"],
2056
+ preferExistingAst: false
2057
+ }) : null;
2058
+ if (attrDynamic?.exp && attrDynamicAst) {
2059
+ let value = getVueExpressionSource(attrDynamic.exp, "loc", "compiled");
2060
+ if (types.isMemberExpression(attrDynamicAst) || types.isOptionalMemberExpression(attrDynamicAst)) {
1628
2061
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1629
2062
  }
1630
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1631
- value = compilerCore.stringifyExpression(attrDynamic.exp);
2063
+ if (types.isCallExpression(attrDynamicAst) || types.isOptionalCallExpression(attrDynamicAst)) {
2064
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1632
2065
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1633
2066
  }
1634
2067
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1636,16 +2069,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1636
2069
  return null;
1637
2070
  }
1638
2071
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1639
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1640
- if (key) {
1641
- return templateAttributeValue(`${componentName}-${key}-${formatTagName(node, nativeWrappers)}`);
2072
+ const keyInfo = getKeyDirectiveInfo(node) || getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2);
2073
+ if (keyInfo) {
2074
+ return templateAttributeValue(`${componentName}-${keyInfo.selectorFragment}-${formatTagName(node, nativeWrappers)}`);
1642
2075
  } else {
1643
2076
  let name = toDirectiveObjectFieldNameValue(toDirective);
1644
2077
  if (!name) {
1645
2078
  if (toDirective.exp == null) {
1646
2079
  return null;
1647
2080
  }
1648
- const source = compilerCore.stringifyExpression(toDirective.exp);
2081
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1649
2082
  const toAst = toDirective.exp.ast;
1650
2083
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && types.isTemplateLiteral(toAst);
1651
2084
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1669,44 +2102,46 @@ function toDirectiveObjectFieldNameValue(node) {
1669
2102
  if (!node.exp || node.exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
1670
2103
  return null;
1671
2104
  }
1672
- const source = node.exp.loc.source.trim();
1673
- try {
1674
- const expr = parser.parseExpression(source, { plugins: ["typescript"] });
1675
- const isNodeType = (n, type) => {
1676
- return n !== null && n.type === type;
1677
- };
1678
- const isStringLiteralNode = (n) => {
1679
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1680
- };
1681
- const isIdentifierNode = (n) => {
1682
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1683
- };
1684
- const isObjectPropertyNode = (n) => {
1685
- if (!isNodeType(n, "ObjectProperty"))
1686
- return false;
1687
- const nn = n;
1688
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1689
- };
1690
- const isObjectExpressionNode = (n) => {
1691
- if (!isNodeType(n, "ObjectExpression"))
1692
- return false;
1693
- const nn = n;
1694
- return Array.isArray(nn.properties);
1695
- };
1696
- if (!isObjectExpressionNode(expr))
1697
- return null;
1698
- const nameProp = expr.properties.find((p) => {
1699
- if (!isObjectPropertyNode(p))
1700
- return false;
1701
- const key = p.key;
1702
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1703
- });
1704
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1705
- return null;
1706
- return toPascalCase(nameProp.value.value);
1707
- } catch {
2105
+ const expr = tryGetDirectiveBabelAst(node, {
2106
+ preferredViews: ["loc", "compiled"],
2107
+ plugins: ["typescript"],
2108
+ preferExistingAst: false
2109
+ });
2110
+ if (!expr) {
1708
2111
  return null;
1709
2112
  }
2113
+ const isNodeType2 = (n, type) => {
2114
+ return n !== null && n.type === type;
2115
+ };
2116
+ const isStringLiteralNode2 = (n) => {
2117
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
2118
+ };
2119
+ const isIdentifierNode2 = (n) => {
2120
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
2121
+ };
2122
+ const isObjectPropertyNode2 = (n) => {
2123
+ if (!isNodeType2(n, "ObjectProperty"))
2124
+ return false;
2125
+ const nn = n;
2126
+ return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
2127
+ };
2128
+ const isObjectExpressionNode2 = (n) => {
2129
+ if (!isNodeType2(n, "ObjectExpression"))
2130
+ return false;
2131
+ const nn = n;
2132
+ return Array.isArray(nn.properties);
2133
+ };
2134
+ if (!isObjectExpressionNode2(expr))
2135
+ return null;
2136
+ const nameProp = expr.properties.find((p) => {
2137
+ if (!isObjectPropertyNode2(p))
2138
+ return false;
2139
+ const key = p.key;
2140
+ return isIdentifierNode2(key) && key.name === "name" || isStringLiteralNode2(key) && key.value === "name";
2141
+ });
2142
+ if (!nameProp || !isObjectPropertyNode2(nameProp) || !isStringLiteralNode2(nameProp.value))
2143
+ return null;
2144
+ return toPascalCase(nameProp.value.value);
1710
2145
  }
1711
2146
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1712
2147
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1716,7 +2151,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1716
2151
  let handlerName = "";
1717
2152
  if (click.exp) {
1718
2153
  const exp = click.exp;
1719
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
2154
+ const source = getVueExpressionSource(exp, "content", "compiled");
1720
2155
  if (source) {
1721
2156
  const parsed = tryParseBabelAstFromHandlerSource(source);
1722
2157
  if (parsed) {
@@ -1737,9 +2172,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1737
2172
  const trimmed = source.trim();
1738
2173
  if (!trimmed)
1739
2174
  return null;
1740
- try {
1741
- return parser.parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1742
- } catch {
2175
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2176
+ if (expressionAst) {
2177
+ return expressionAst;
1743
2178
  }
1744
2179
  try {
1745
2180
  return parser.parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -2078,23 +2513,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2078
2513
  return null;
2079
2514
  }
2080
2515
  const simpleExp = exp;
2081
- const ast = simpleExp.ast;
2082
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
2083
- const tl = ast;
2084
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2085
- const expressionCount = (tl.expressions ?? []).length;
2086
- const isStatic = expressionCount === 0;
2087
- const raw2 = (simpleExp.content ?? "").trim();
2088
- const unwrappedTemplate = raw2.startsWith("`") && raw2.endsWith("`") && raw2.length >= 2 ? raw2.slice(1, -1) : cooked;
2516
+ const ast = tryGetVueExpressionAst(simpleExp, {
2517
+ preferredViews: ["content", "loc", "compiled"],
2518
+ plugins: ["typescript"]
2519
+ });
2520
+ const unwrappedTemplateLiteral = tryUnwrapTemplateLiteralExpressionSource(simpleExp);
2521
+ if (unwrappedTemplateLiteral) {
2522
+ const isStatic = unwrappedTemplateLiteral.expressionCount === 0;
2089
2523
  if (isStatic) {
2090
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2524
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2091
2525
  }
2526
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2092
2527
  return {
2093
- value: unwrappedTemplate,
2528
+ value: templateValue.template,
2094
2529
  isDynamic: true,
2095
2530
  isStaticLiteral: false,
2096
- template: unwrappedTemplate,
2097
- templateExpressionCount: expressionCount
2531
+ template: templateValue.template,
2532
+ parsedTemplate: templateValue.parsedTemplate,
2533
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2098
2534
  };
2099
2535
  }
2100
2536
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2107,59 +2543,27 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2107
2543
  }
2108
2544
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2109
2545
  if (preservableReference) {
2546
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2110
2547
  return {
2111
2548
  value: preservableReference,
2112
- isDynamic: true,
2113
- isStaticLiteral: false,
2114
- template: `\${${preservableReference}}`,
2115
- templateExpressionCount: 1,
2116
- rawExpression: preservableReference
2117
- };
2118
- }
2119
- const raw = (simpleExp.content ?? "").trim();
2120
- if (!raw) {
2121
- return null;
2122
- }
2123
- try {
2124
- const ast2 = parser.parseExpression(raw, { plugins: ["typescript"] });
2125
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2126
- const tl = ast2;
2127
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2128
- const expressionCount = (tl.expressions ?? []).length;
2129
- const isStatic = expressionCount === 0;
2130
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2131
- if (isStatic) {
2132
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2133
- }
2134
- return {
2135
- value: unwrappedTemplate,
2136
- isDynamic: true,
2137
- isStaticLiteral: false,
2138
- template: unwrappedTemplate,
2139
- templateExpressionCount: expressionCount
2140
- };
2141
- }
2142
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2143
- const sl = ast2;
2144
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2145
- }
2146
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2147
- if (preservableReference2) {
2148
- return {
2149
- value: preservableReference2,
2150
- isDynamic: true,
2151
- isStaticLiteral: false,
2152
- template: `\${${preservableReference2}}`,
2153
- templateExpressionCount: 1,
2154
- rawExpression: preservableReference2
2155
- };
2156
- }
2157
- } catch {
2549
+ isDynamic: true,
2550
+ isStaticLiteral: false,
2551
+ template: templateValue.template,
2552
+ parsedTemplate: templateValue.parsedTemplate,
2553
+ templateExpressionCount: 1,
2554
+ rawExpression: preservableReference
2555
+ };
2556
+ }
2557
+ const raw = (simpleExp.content ?? "").trim();
2558
+ if (!raw) {
2559
+ return null;
2158
2560
  }
2159
2561
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2160
2562
  }
2161
2563
  function isTemplatePlaceholder(part) {
2162
- return part.startsWith("${") && part.endsWith("}") && part.length >= 3;
2564
+ const parsedTemplate = tryParseTemplateFragment(part);
2565
+ const templateFragment = parsedTemplate ? getSingleExpressionTemplateFragment(parsedTemplate) : null;
2566
+ return !!templateFragment && templateFragment.prefix === "" && templateFragment.suffix === "";
2163
2567
  }
2164
2568
  function isAllCapsOrDigits(value) {
2165
2569
  if (value.length <= 1) {
@@ -2212,33 +2616,14 @@ function safeMethodNameFromParts(parts) {
2212
2616
  }
2213
2617
  return name;
2214
2618
  }
2215
- function replaceAllTemplateExpressionsWithKey(template) {
2619
+ function toPomKeyPattern(templateValue) {
2620
+ const { templateLiteral } = templateValue.parsedTemplate;
2216
2621
  let out = "";
2217
- let i = 0;
2218
- while (i < template.length) {
2219
- const start = template.indexOf("${", i);
2220
- if (start < 0) {
2221
- out += template.slice(i);
2222
- break;
2223
- }
2224
- out += template.slice(i, start);
2225
- let depth = 1;
2226
- let j = start + 2;
2227
- while (j < template.length && depth > 0) {
2228
- if (template[j] === "{") {
2229
- depth++;
2230
- } else if (template[j] === "}") {
2231
- depth--;
2232
- }
2233
- j++;
2234
- }
2235
- const end = depth === 0 ? j - 1 : -1;
2236
- if (end < 0) {
2237
- out += template.slice(start);
2238
- break;
2622
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
2623
+ out += templateLiteral.quasis[i]?.value.raw ?? "";
2624
+ if (templateLiteral.expressions[i]) {
2625
+ out += "${key}";
2239
2626
  }
2240
- out += "${key}";
2241
- i = end + 1;
2242
2627
  }
2243
2628
  return out;
2244
2629
  }
@@ -2246,8 +2631,8 @@ function applyResolvedDataTestId(args) {
2246
2631
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2247
2632
  const entryOverrides = args.entryOverrides ?? {};
2248
2633
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2249
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2250
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2634
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2635
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2251
2636
  const warn = args.warn;
2252
2637
  const getBestKeyAccessCandidates = (expr) => {
2253
2638
  if (!expr) {
@@ -2256,7 +2641,10 @@ function applyResolvedDataTestId(args) {
2256
2641
  return splitNullishCoalescingExpression(expr);
2257
2642
  };
2258
2643
  let dataTestId = args.preferredGeneratedValue;
2644
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2259
2645
  let fromExisting = false;
2646
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2647
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2260
2648
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2261
2649
  if (existing) {
2262
2650
  const loc = args.element.loc?.start;
@@ -2277,8 +2665,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2277
2665
  if (existingIdBehavior === "preserve") {
2278
2666
  if (existing.isDynamic) {
2279
2667
  if (existing.template) {
2280
- const existingTemplate = existing.template;
2281
- if ((existing.templateExpressionCount ?? 0) !== 1) {
2668
+ const existingTemplateValue = existing.parsedTemplate ? { kind: "template", template: existing.template, parsedTemplate: existing.parsedTemplate } : templateAttributeValue(existing.template);
2669
+ const existingTemplateFragment = getSingleExpressionTemplateFragment(existingTemplateValue.parsedTemplate);
2670
+ const requiredKeyTemplateValue = bestKeyPreservePlaceholder ? templateAttributeValue(bestKeyPreservePlaceholder) : null;
2671
+ if ((existing.templateExpressionCount ?? 0) !== 1 || !existingTemplateFragment) {
2282
2672
  throw new Error(
2283
2673
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2284
2674
  Component: ${args.componentName}
@@ -2288,20 +2678,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2288
2678
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2289
2679
  );
2290
2680
  }
2291
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2292
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2293
- if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
2681
+ const hasExact = requiredKeyTemplateValue ? templateFragmentContainsSingleExpression(existingTemplateValue.parsedTemplate, requiredKeyTemplateValue.parsedTemplate) : false;
2682
+ const hasVarAccess = getBestKeyAccessCandidates(bestKeyVariable).some((candidate) => existingTemplateFragment.expressionSource === candidate);
2683
+ if (!hasExact && !hasVarAccess && bestKeyPreservePlaceholder) {
2294
2684
  throw new Error(
2295
2685
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2296
2686
  Component: ${args.componentName}
2297
2687
  File: ${file}:${locationHint}
2298
2688
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2299
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2689
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2300
2690
 
2301
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2691
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2302
2692
  );
2303
2693
  }
2304
- dataTestId = templateAttributeValue(existing.template);
2694
+ dataTestId = existingTemplateValue;
2695
+ runtimeDataTestId = existingTemplateValue;
2305
2696
  fromExisting = true;
2306
2697
  } else {
2307
2698
  throw new Error(
@@ -2315,18 +2706,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2315
2706
  );
2316
2707
  }
2317
2708
  } else {
2318
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2709
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2319
2710
  throw new Error(
2320
2711
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2321
2712
  Component: ${args.componentName}
2322
2713
  File: ${file}:${locationHint}
2323
2714
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2324
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2715
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2325
2716
 
2326
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2717
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2327
2718
  );
2328
2719
  }
2329
2720
  dataTestId = staticAttributeValue(existing.value);
2721
+ runtimeDataTestId = staticAttributeValue(existing.value);
2330
2722
  fromExisting = true;
2331
2723
  }
2332
2724
  }
@@ -2356,8 +2748,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2356
2748
  };
2357
2749
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2358
2750
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2359
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2360
- const isKeyed = formattedDataTestIdForPom.includes("${key}");
2751
+ const formattedDataTestIdForPom = dataTestId.kind === "template" ? toPomKeyPattern(dataTestId) : dataTestId.value;
2752
+ const selectorPatternKind = dataTestId.kind === "template" ? "parameterized" : "static";
2753
+ const selectorPattern = createPomStringPattern(formattedDataTestIdForPom, selectorPatternKind);
2754
+ const selectorIsParameterized = selectorPatternKind === "parameterized";
2361
2755
  const deriveBaseMethodNameFromHint = (hint) => {
2362
2756
  const hintRaw = (hint ?? "").trim();
2363
2757
  const trimEdgeSeparators = (value) => {
@@ -2405,13 +2799,13 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2405
2799
  const roleSuffix = upperFirst(normalizedRole || "Element");
2406
2800
  const baseName = upperFirst(primaryMethodName);
2407
2801
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2408
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2802
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2409
2803
  };
2410
2804
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2411
2805
  const roleSuffix = upperFirst(normalizedRole || "Element");
2412
2806
  const baseName = upperFirst(primaryMethodName);
2413
2807
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2414
- if (!isKeyed) {
2808
+ if (!selectorIsParameterized) {
2415
2809
  return { primary: propertyName };
2416
2810
  }
2417
2811
  const stripped = removeByKeySegment2(propertyName);
@@ -2473,11 +2867,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2473
2867
  return false;
2474
2868
  }
2475
2869
  const existingSelectors = [
2476
- existingPom.formattedDataTestId,
2477
- ...existingPom.alternateFormattedDataTestIds ?? []
2870
+ existingPom.selector,
2871
+ ...existingPom.alternateSelectors ?? []
2478
2872
  ];
2479
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2480
- if (isKeyed && !sharesSelectorIdentity) {
2873
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2874
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2481
2875
  return false;
2482
2876
  }
2483
2877
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2492,12 +2886,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2492
2886
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2493
2887
  return false;
2494
2888
  }
2495
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2496
- existingPom.alternateFormattedDataTestIds ??= [];
2497
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2498
- existingPom.alternateFormattedDataTestIds.push(formattedDataTestIdForPom);
2889
+ if (!pomStringPatternEquals(existingPom.selector, selectorPattern)) {
2890
+ existingPom.alternateSelectors ??= [];
2891
+ if (!(existingPom.alternateSelectors ?? []).some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern))) {
2892
+ existingPom.alternateSelectors.push(selectorPattern);
2499
2893
  }
2500
2894
  }
2895
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
2896
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
2897
+ }
2501
2898
  return true;
2502
2899
  };
2503
2900
  let methodName = "";
@@ -2510,7 +2907,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2510
2907
  let suffix = 1;
2511
2908
  while (true) {
2512
2909
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2513
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2910
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2514
2911
  const actionName = getPrimaryActionMethodName(candidate);
2515
2912
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2516
2913
  let chosenGetterName = getterCandidates.primary;
@@ -2536,7 +2933,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2536
2933
  const baseNameUpper = upperFirst(baseWithSuffix);
2537
2934
  if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
2538
2935
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2539
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2936
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2540
2937
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2541
2938
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2542
2939
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2604,51 +3001,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2604
3001
  Fix: make the element identifiable (e.g. add id/name/inner text or use a more specific click handler name), or switch generation.nameCollisionBehavior to "warn"/"suffix".`
2605
3002
  );
2606
3003
  }
2607
- const params = {};
2608
- if (isKeyed) {
2609
- params.key = keyTypeFromValues;
2610
- }
3004
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2611
3005
  switch (normalizedRole) {
2612
3006
  case "input":
2613
- params.text = "string";
2614
- params.annotationText = 'string = ""';
2615
- if (!isKeyed) delete params.key;
3007
+ parameters = setPomParameter(parameters, "text", "string");
3008
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2616
3009
  break;
2617
3010
  case "select":
2618
- params.value = "string";
2619
- params.annotationText = 'string = ""';
2620
- if (!isKeyed) delete params.key;
3011
+ parameters = setPomParameter(parameters, "value", "string");
3012
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2621
3013
  break;
2622
3014
  case "vselect":
2623
- params.value = "string";
2624
- params.timeOut = "number = 500";
2625
- params.annotationText = 'string = ""';
2626
- if (!isKeyed) delete params.key;
3015
+ parameters = setPomParameter(parameters, "value", "string");
3016
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
3017
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2627
3018
  break;
2628
3019
  case "radio":
2629
- params.annotationText = 'string = ""';
3020
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2630
3021
  break;
2631
3022
  }
2632
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2633
- params.key = keyTypeFromValues;
2634
- }
3023
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2635
3024
  if (addHtmlAttribute && !fromExisting) {
2636
3025
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2637
3026
  }
2638
3027
  const childComponentName = args.element.tag;
2639
3028
  const dataTestIdEntry = {
2640
- value: getAttributeValueText(dataTestId),
2641
- templateLiteral: void 0,
2642
- ...entryOverrides
3029
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
3030
+ getAttributeValueText(dataTestId),
3031
+ dataTestId.kind === "template" ? "parameterized" : "static"
3032
+ ),
3033
+ templateLiteral: entryOverrides.templateLiteral,
3034
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2643
3035
  };
2644
3036
  dataTestIdEntry.pom = {
2645
3037
  nativeRole: normalizedRole,
2646
3038
  methodName,
2647
3039
  getterNameOverride,
2648
- formattedDataTestId: formattedDataTestIdForPom,
2649
- alternateFormattedDataTestIds: void 0,
3040
+ selector: selectorPattern,
3041
+ alternateSelectors: void 0,
2650
3042
  mergeKey: args.pomMergeKey,
2651
- params,
3043
+ parameters: normalizedParameters,
2652
3044
  keyValuesOverride: args.keyValuesOverride ?? null
2653
3045
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2654
3046
  };
@@ -2679,43 +3071,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2679
3071
  }
2680
3072
  };
2681
3073
  const getSignatureForGeneratedMethod = () => {
2682
- const role = normalizedRole;
2683
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2684
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2685
- const keyType = keyTypeFromValues;
2686
- if (isNavigation2) {
2687
- if (needsKey2) {
2688
- return { params: `key: ${keyType}`, argNames: ["key"] };
2689
- }
2690
- return { params: "", argNames: [] };
2691
- }
2692
- switch (role) {
2693
- case "input":
2694
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2695
- case "select":
2696
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2697
- case "vselect":
2698
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2699
- case "radio":
2700
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2701
- default:
2702
- if (needsKey2) {
2703
- return { params: `key: ${keyType}`, argNames: ["key"] };
2704
- }
2705
- return { params: "", argNames: [] };
2706
- }
3074
+ return createPomMethodSignature(normalizedParameters);
2707
3075
  };
2708
3076
  const registerPrimaryOnce = (pom) => {
2709
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2710
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3077
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2711
3078
  const key = JSON.stringify({
2712
3079
  kind: "primary",
2713
3080
  role: pom.nativeRole,
2714
3081
  methodName: pom.methodName,
2715
3082
  getterNameOverride: pom.getterNameOverride ?? null,
2716
- formattedDataTestId: pom.formattedDataTestId,
2717
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2718
- params: stableParams,
3083
+ selector: pom.selector,
3084
+ alternateSelectors: alternates.length ? alternates : void 0,
3085
+ parameters: pom.parameters,
2719
3086
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2720
3087
  emitPrimary: pom.emitPrimary ?? true
2721
3088
  });
@@ -2729,8 +3096,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2729
3096
  }
2730
3097
  };
2731
3098
  const addExtraClickMethod = (spec) => {
2732
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2733
- const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, params: stableParams });
3099
+ const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, parameters: spec.parameters });
2734
3100
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2735
3101
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2736
3102
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2753,7 +3119,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2753
3119
  if (prev === null) {
2754
3120
  return;
2755
3121
  }
2756
- if (signature === null || prev.params !== signature.params) {
3122
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2757
3123
  args.dependencies.generatedMethods.set(name, null);
2758
3124
  }
2759
3125
  };
@@ -2769,23 +3135,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2769
3135
  return candidate;
2770
3136
  };
2771
3137
  const tryGetDirectiveExpressionAst = (dir) => {
2772
- const exp = dir.exp;
2773
- if (!exp) {
2774
- return null;
2775
- }
2776
- if (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
2777
- const simple = exp;
2778
- const ast = simple.ast;
2779
- if (ast && "type" in ast) {
2780
- return ast;
2781
- }
2782
- }
2783
- try {
2784
- const raw = args.context ? compilerCore.stringifyExpression(exp) : exp.loc.source;
2785
- return parser.parseExpression(raw, { plugins: ["typescript"] });
2786
- } catch {
2787
- return null;
2788
- }
3138
+ return tryGetDirectiveBabelAst(dir, {
3139
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3140
+ plugins: ["typescript"]
3141
+ });
2789
3142
  };
2790
3143
  const tryGetStaticStringFromBabel = (node) => {
2791
3144
  if (!node) {
@@ -2882,17 +3235,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2882
3235
  name: generatedName2,
2883
3236
  selector: {
2884
3237
  kind: "withinTestIdByLabel",
2885
- rootFormattedDataTestId: wrapperTestId,
2886
- formattedLabel: label,
3238
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3239
+ label: createPomStringPattern(label, "static"),
2887
3240
  exact: true
2888
3241
  },
2889
- params: { annotationText: `string = ""` }
3242
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2890
3243
  });
2891
3244
  if (added2) {
2892
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3245
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2893
3246
  }
2894
3247
  }
2895
- return;
3248
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2896
3249
  }
2897
3250
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2898
3251
  if (dataTestIdEntry.pom) {
@@ -2904,19 +3257,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2904
3257
  name: generatedName,
2905
3258
  selector: {
2906
3259
  kind: "withinTestIdByLabel",
2907
- rootFormattedDataTestId: wrapperTestId,
2908
- formattedLabel: "${value}",
3260
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3261
+ label: createPomStringPattern("${value}", "parameterized"),
2909
3262
  exact: true
2910
3263
  },
2911
- params: { value: "string", annotationText: `string = ""` }
3264
+ parameters: [
3265
+ createPomParameterSpec("value", "string"),
3266
+ createPomParameterSpec("annotationText", `string = ""`)
3267
+ ]
2912
3268
  });
2913
3269
  if (added) {
2914
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3270
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3271
+ createPomParameterSpec("value", "string"),
3272
+ createPomParameterSpec("annotationText", `string = ""`)
3273
+ ]));
2915
3274
  }
2916
- return;
3275
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2917
3276
  }
2918
3277
  const staticKeyValues = args.keyValuesOverride ?? null;
2919
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3278
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2920
3279
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2921
3280
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2922
3281
  if (dataTestIdEntry.pom) {
@@ -2935,19 +3294,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2935
3294
  name: generatedName,
2936
3295
  selector: {
2937
3296
  kind: "testId",
2938
- formattedDataTestId: formattedDataTestIdForPom
3297
+ testId: selectorPattern
2939
3298
  },
2940
3299
  keyLiteral: rawValue,
2941
- params: { wait: "boolean = true", annotationText: 'string = ""' }
3300
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2942
3301
  });
2943
3302
  if (added) {
2944
- registerGeneratedMethodSignature(generatedName, {
2945
- params: `wait: boolean = true, annotationText: string = ""`,
2946
- argNames: ["wait", "annotationText"]
2947
- });
3303
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3304
+ createPomParameterSpec("wait", "boolean = true"),
3305
+ createPomParameterSpec("annotationText", 'string = ""')
3306
+ ]));
2948
3307
  }
2949
3308
  }
2950
- return;
3309
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2951
3310
  }
2952
3311
  if (dataTestIdEntry.pom) {
2953
3312
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2961,6 +3320,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2961
3320
  const generatedName = getGeneratedMethodName();
2962
3321
  registerGeneratedMethodSignature(generatedName, signature);
2963
3322
  }
3323
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2964
3324
  }
2965
3325
  function safeRealpath(value) {
2966
3326
  try {
@@ -3948,113 +4308,6 @@ class VuePomGeneratorError extends Error {
3948
4308
  this.name = "VuePomGeneratorError";
3949
4309
  }
3950
4310
  }
3951
- function splitParameterList(parameters) {
3952
- const parts = [];
3953
- let current = "";
3954
- let braceDepth = 0;
3955
- let bracketDepth = 0;
3956
- let parenDepth = 0;
3957
- let angleDepth = 0;
3958
- let inSingleQuote = false;
3959
- let inDoubleQuote = false;
3960
- let inTemplateString = false;
3961
- for (let index = 0; index < parameters.length; index += 1) {
3962
- const char = parameters[index];
3963
- const previous = index > 0 ? parameters[index - 1] : "";
3964
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3965
- inSingleQuote = !inSingleQuote;
3966
- current += char;
3967
- continue;
3968
- }
3969
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3970
- inDoubleQuote = !inDoubleQuote;
3971
- current += char;
3972
- continue;
3973
- }
3974
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3975
- inTemplateString = !inTemplateString;
3976
- current += char;
3977
- continue;
3978
- }
3979
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3980
- current += char;
3981
- continue;
3982
- }
3983
- switch (char) {
3984
- case "{":
3985
- braceDepth += 1;
3986
- break;
3987
- case "}":
3988
- braceDepth -= 1;
3989
- break;
3990
- case "[":
3991
- bracketDepth += 1;
3992
- break;
3993
- case "]":
3994
- bracketDepth -= 1;
3995
- break;
3996
- case "(":
3997
- parenDepth += 1;
3998
- break;
3999
- case ")":
4000
- parenDepth -= 1;
4001
- break;
4002
- case "<":
4003
- angleDepth += 1;
4004
- break;
4005
- case ">":
4006
- angleDepth -= 1;
4007
- break;
4008
- case ",":
4009
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
4010
- const trimmed2 = current.trim();
4011
- if (trimmed2) {
4012
- parts.push(trimmed2);
4013
- }
4014
- current = "";
4015
- continue;
4016
- }
4017
- break;
4018
- }
4019
- current += char;
4020
- }
4021
- const trimmed = current.trim();
4022
- if (trimmed) {
4023
- parts.push(trimmed);
4024
- }
4025
- return parts;
4026
- }
4027
- function parseParameterSignature(parameter) {
4028
- const colonIndex = parameter.indexOf(":");
4029
- if (colonIndex < 0) {
4030
- return { name: parameter.trim() };
4031
- }
4032
- const rawName = parameter.slice(0, colonIndex).trim();
4033
- const hasQuestionToken = rawName.endsWith("?");
4034
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
4035
- const remainder = parameter.slice(colonIndex + 1).trim();
4036
- const initializerIndex = remainder.lastIndexOf("=");
4037
- if (initializerIndex < 0) {
4038
- return {
4039
- name,
4040
- hasQuestionToken,
4041
- type: remainder || void 0
4042
- };
4043
- }
4044
- return {
4045
- name,
4046
- hasQuestionToken,
4047
- type: remainder.slice(0, initializerIndex).trim() || void 0,
4048
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
4049
- };
4050
- }
4051
- function parseParameterSignatures(parameters) {
4052
- const trimmed = parameters.trim();
4053
- if (!trimmed) {
4054
- return [];
4055
- }
4056
- return splitParameterList(trimmed).map(parseParameterSignature);
4057
- }
4058
4311
  function toPosixRelativePath(fromDir, toFile) {
4059
4312
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
4060
4313
  if (!rel.startsWith(".")) {
@@ -4074,6 +4327,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
4074
4327
  const root = projectRoot ?? process.cwd();
4075
4328
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
4076
4329
  }
4330
+ function createMissingCustomPomDirectoryError(configuredDir, resolvedDir) {
4331
+ return new VuePomGeneratorError(
4332
+ `Custom POM directory "${configuredDir}" does not exist.
4333
+ Resolved path: ${resolvedDir}
4334
+ Create the directory, point generation.playwright.customPoms.dir at the correct location, or remove the customPoms configuration.`
4335
+ );
4336
+ }
4337
+ function createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir) {
4338
+ const renderedClassNames = missingClassNames.map((name) => `"${name}"`).join(", ");
4339
+ return new VuePomGeneratorError(
4340
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.
4341
+ Expected matching helper files/exports under "${configuredDir}".
4342
+ Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.`
4343
+ );
4344
+ }
4077
4345
  function createCustomPomImportCollisionError(exportName, requested) {
4078
4346
  return new VuePomGeneratorError(
4079
4347
  `Custom POM import name collision detected for "${exportName}".
@@ -4212,35 +4480,28 @@ function generateGoToSelfMethod(componentName) {
4212
4480
  })
4213
4481
  ];
4214
4482
  }
4215
- function formatMethodParams(params) {
4216
- if (!params)
4217
- return "";
4218
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4219
- const entries = Object.entries(params);
4220
- if (!entries.length)
4221
- return "";
4222
- const score = (name) => {
4223
- const idx = preferredOrder.indexOf(name);
4224
- return idx < 0 ? 999 : idx;
4225
- };
4226
- return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
4483
+ function getSelectorPatterns(selector) {
4484
+ return selector.kind === "testId" ? [selector.testId] : [selector.rootTestId, selector.label];
4227
4485
  }
4228
4486
  function generateExtraClickMethodMembers(spec) {
4229
4487
  if (spec.kind !== "click") {
4230
4488
  return [];
4231
4489
  }
4232
- const params = spec.params ?? {};
4233
- const signatureParams = formatMethodParams(params);
4234
- const parameters = parseParameterSignatures(signatureParams);
4235
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4236
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
4490
+ const selectorPatterns = getSelectorPatterns(spec.selector);
4491
+ const signatureSpecs = orderPomPatternParameters(
4492
+ spec.parameters,
4493
+ selectorPatterns,
4494
+ { omit: spec.keyLiteral !== void 0 ? ["key"] : [] }
4495
+ );
4496
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
4497
+ const hasAnnotationText = signatureSpecs.some((param) => param.name === "annotationText");
4498
+ const hasWait = signatureSpecs.some((param) => param.name === "wait");
4237
4499
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4238
4500
  const waitArg = hasWait ? "wait" : "true";
4239
4501
  if (spec.selector.kind === "testId") {
4240
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4241
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4502
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4242
4503
  const clickArgs = [];
4243
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4504
+ clickArgs.push(testIdBinding.expression);
4244
4505
  if (hasAnnotationText || hasWait) {
4245
4506
  clickArgs.push(annotationArg);
4246
4507
  }
@@ -4256,20 +4517,16 @@ function generateExtraClickMethodMembers(spec) {
4256
4517
  if (spec.keyLiteral !== void 0) {
4257
4518
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4258
4519
  }
4259
- if (needsTemplate) {
4260
- writer.writeLine(`const testId = ${testIdExpr};`);
4520
+ for (const statement of testIdBinding.setupStatements) {
4521
+ writer.writeLine(statement);
4261
4522
  }
4262
4523
  writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4263
4524
  }
4264
4525
  })
4265
4526
  ];
4266
4527
  }
4267
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4268
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4269
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4270
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4271
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4272
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4528
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4529
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4273
4530
  return [
4274
4531
  createClassMethod({
4275
4532
  name: spec.name,
@@ -4279,13 +4536,13 @@ function generateExtraClickMethodMembers(spec) {
4279
4536
  if (spec.keyLiteral !== void 0) {
4280
4537
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4281
4538
  }
4282
- if (rootNeedsTemplate) {
4283
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4539
+ for (const statement of rootBinding.setupStatements) {
4540
+ writer.writeLine(statement);
4284
4541
  }
4285
- if (labelNeedsTemplate) {
4286
- writer.writeLine(`const label = ${labelExpr};`);
4542
+ for (const statement of labelBinding.setupStatements) {
4543
+ writer.writeLine(statement);
4287
4544
  }
4288
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4545
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg});`);
4289
4546
  }
4290
4547
  })
4291
4548
  ];
@@ -4298,10 +4555,10 @@ function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4298
4555
  targetPageObjectModelClass,
4299
4556
  primary.methodName,
4300
4557
  primary.nativeRole,
4301
- primary.formattedDataTestId,
4302
- primary.alternateFormattedDataTestIds,
4558
+ primary.selector,
4559
+ primary.alternateSelectors,
4303
4560
  primary.getterNameOverride,
4304
- primary.params ?? {}
4561
+ primary.parameters
4305
4562
  );
4306
4563
  }
4307
4564
  function generateMethodsContentForDependencies(dependencies) {
@@ -4309,15 +4566,14 @@ function generateMethodsContentForDependencies(dependencies) {
4309
4566
  const primarySpecsAll = entries.map((e) => ({ pom: e.pom, target: e.targetPageObjectModelClass })).filter((x) => !!x.pom).sort((a, b) => a.pom.methodName.localeCompare(b.pom.methodName));
4310
4567
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4311
4568
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4312
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4313
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4569
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4314
4570
  const key = JSON.stringify({
4315
4571
  role: pom.nativeRole,
4316
4572
  methodName: pom.methodName,
4317
4573
  getterNameOverride: pom.getterNameOverride ?? null,
4318
- testId: pom.formattedDataTestId,
4319
- alternateTestIds: alternates.length ? alternates : void 0,
4320
- params: stableParams,
4574
+ selector: pom.selector,
4575
+ alternateSelectors: alternates.length ? alternates : void 0,
4576
+ parameters: pom.parameters,
4321
4577
  target: target ?? null,
4322
4578
  emitPrimary: pom.emitPrimary ?? true
4323
4579
  });
@@ -4344,6 +4600,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4344
4600
  customPomAttachments = [],
4345
4601
  projectRoot,
4346
4602
  customPomDir,
4603
+ requireCustomPomDir,
4347
4604
  customPomImportAliases,
4348
4605
  customPomImportNameCollisionBehavior = "error",
4349
4606
  testIdAttribute,
@@ -4376,6 +4633,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4376
4633
  customPomAttachments,
4377
4634
  projectRoot,
4378
4635
  customPomDir,
4636
+ requireCustomPomDir,
4379
4637
  customPomImportAliases,
4380
4638
  customPomImportNameCollisionBehavior,
4381
4639
  testIdAttribute,
@@ -4385,6 +4643,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4385
4643
  customPomAttachments,
4386
4644
  projectRoot,
4387
4645
  customPomDir,
4646
+ requireCustomPomDir,
4388
4647
  customPomImportAliases,
4389
4648
  customPomImportNameCollisionBehavior,
4390
4649
  testIdAttribute,
@@ -4440,9 +4699,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4440
4699
  }
4441
4700
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4442
4701
  customPomDir: options.customPomDir,
4702
+ requireCustomPomDir: options.requireCustomPomDir,
4443
4703
  customPomImportAliases: options.customPomImportAliases,
4444
4704
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4445
4705
  });
4706
+ assertCustomPomAttachmentsResolved(
4707
+ options.customPomAttachments ?? [],
4708
+ customPomImportResolution.classIdentifierMap,
4709
+ options.customPomDir ?? "tests/playwright/pom/custom"
4710
+ );
4446
4711
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4447
4712
  const files = [];
4448
4713
  for (const [name, deps] of entries) {
@@ -4601,21 +4866,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4601
4866
  return { filePath, content };
4602
4867
  });
4603
4868
  }
4604
- function toCSharpTestIdExpression(formattedDataTestId) {
4605
- const needsInterpolation = formattedDataTestId.includes("${");
4606
- if (!needsInterpolation) {
4607
- return JSON.stringify(formattedDataTestId);
4608
- }
4609
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4610
- const quoted = JSON.stringify(inner);
4611
- return `$${quoted}`;
4612
- }
4613
- function toCSharpParam(paramTypeExpr) {
4614
- const trimmed = (paramTypeExpr ?? "").trim();
4615
- const eqIdx = trimmed.indexOf("=");
4616
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4617
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4618
- const typePart = left.includes("|") ? "string" : left;
4869
+ function toCSharpParam(param) {
4870
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4619
4871
  let type = "string";
4620
4872
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4621
4873
  type = "bool";
@@ -4623,19 +4875,17 @@ function toCSharpParam(paramTypeExpr) {
4623
4875
  type = "string";
4624
4876
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4625
4877
  type = "int";
4626
- else if (/\d+/.test(typePart) && typePart === "")
4627
- type = "int";
4628
4878
  else if (/\btimeOut\b/i.test(typePart))
4629
4879
  type = "int";
4630
4880
  let defaultExpr;
4631
- if (right !== void 0) {
4881
+ if (param.initializer !== void 0) {
4632
4882
  if (type === "bool") {
4633
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : void 0;
4883
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : void 0;
4634
4884
  } else if (type === "int") {
4635
- const m = right.match(/\d+/);
4885
+ const m = param.initializer.match(/\d+/);
4636
4886
  defaultExpr = m ? m[0] : void 0;
4637
4887
  } else {
4638
- if (right === '""' || right === '""' || right === "''") {
4888
+ if (param.initializer === '""' || param.initializer === "''") {
4639
4889
  defaultExpr = '""';
4640
4890
  }
4641
4891
  }
@@ -4643,17 +4893,15 @@ function toCSharpParam(paramTypeExpr) {
4643
4893
  return { type, defaultExpr };
4644
4894
  }
4645
4895
  function formatCSharpParams(params) {
4646
- if (!params)
4647
- return { signature: "", argNames: [] };
4648
- const entries = Object.entries(params);
4649
- if (!entries.length)
4896
+ const normalizedParams = normalizePomParameters(params);
4897
+ if (!normalizedParams.length)
4650
4898
  return { signature: "", argNames: [] };
4651
4899
  const signatureParts = [];
4652
4900
  const argNames = [];
4653
- for (const [name, typeExpr] of entries) {
4654
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4655
- argNames.push(name);
4656
- signatureParts.push(defaultExpr !== void 0 ? `${type} ${name} = ${defaultExpr}` : `${type} ${name}`);
4901
+ for (const param of normalizedParams) {
4902
+ const { type, defaultExpr } = toCSharpParam(param);
4903
+ argNames.push(param.name);
4904
+ signatureParts.push(defaultExpr !== void 0 ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
4657
4905
  }
4658
4906
  return { signature: signatureParts.join(", "), argNames };
4659
4907
  }
@@ -4744,23 +4992,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4744
4992
  const baseMethodName = upperFirst(pom.methodName);
4745
4993
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4746
4994
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4747
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4748
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4749
- const templateVars = templateVarMatches.map((m) => m[1]);
4750
- const augmentedParams = { ...pom.params };
4751
- for (const v of templateVars) {
4752
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4753
- augmentedParams[v] = "string";
4754
- }
4755
- }
4756
- const orderedParams = Object.fromEntries([
4757
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4758
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4759
- ]);
4995
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
4996
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
4997
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4760
4998
  const { signature, argNames } = formatCSharpParams(orderedParams);
4761
4999
  const args = argNames.join(", ");
4762
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4763
- if (pom.formattedDataTestId.includes("${")) {
5000
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
5001
+ if (selectorIsParameterized) {
4764
5002
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4765
5003
  } else {
4766
5004
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4771,12 +5009,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4771
5009
  if (target) {
4772
5010
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4773
5011
  chunks.push(" {");
4774
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4775
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
5012
+ if (selectorIsParameterized || allTestIds.length <= 1) {
5013
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4776
5014
  chunks.push(` return new ${target}(Page);`);
4777
5015
  } else {
4778
5016
  chunks.push(" Exception? lastError = null;");
4779
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5017
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4780
5018
  chunks.push(" {");
4781
5019
  chunks.push(" try");
4782
5020
  chunks.push(" {");
@@ -4800,7 +5038,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4800
5038
  }
4801
5039
  chunks.push(` public async Task ${actionName}(${sig})`);
4802
5040
  chunks.push(" {");
4803
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5041
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4804
5042
  const emitActionCall = (locatorAccess) => {
4805
5043
  if (pom.nativeRole === "input") {
4806
5044
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4813,9 +5051,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4813
5051
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4814
5052
  }
4815
5053
  };
4816
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5054
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4817
5055
  chunks.push(" Exception? lastError = null;");
4818
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5056
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4819
5057
  chunks.push(" {");
4820
5058
  chunks.push(" try");
4821
5059
  chunks.push(" {");
@@ -4861,7 +5099,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4861
5099
  for (const extra of extras) {
4862
5100
  if (extra.kind !== "click")
4863
5101
  continue;
4864
- const { signature } = formatCSharpParams(extra.params);
5102
+ const extraParams = orderPomPatternParameters(
5103
+ extra.parameters,
5104
+ getSelectorPatterns(extra.selector),
5105
+ { omit: extra.keyLiteral !== void 0 ? ["key"] : [] }
5106
+ );
5107
+ const { signature } = formatCSharpParams(extraParams);
4865
5108
  const extraName = upperFirst(extra.name);
4866
5109
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4867
5110
  chunks.push(" {");
@@ -4869,29 +5112,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4869
5112
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4870
5113
  }
4871
5114
  if (extra.selector.kind === "testId") {
4872
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4873
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4874
- if (needsTemplate) {
4875
- chunks.push(` var testId = ${testIdExpr};`);
4876
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4877
- } else {
4878
- chunks.push(` await LocatorByTestId(${testIdExpr}).ClickAsync();`);
5115
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
5116
+ for (const statement of testIdBinding.setupStatements) {
5117
+ chunks.push(` ${statement}`);
4879
5118
  }
5119
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4880
5120
  } else {
4881
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4882
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4883
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4884
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5121
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5122
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4885
5123
  const exactArg = extra.selector.exact === false ? "false" : "true";
4886
- if (rootNeedsTemplate) {
4887
- chunks.push(` var rootTestId = ${rootExpr};`);
5124
+ for (const statement of rootBinding.setupStatements) {
5125
+ chunks.push(` ${statement}`);
4888
5126
  }
4889
- if (labelNeedsTemplate) {
4890
- chunks.push(` var label = ${labelExpr};`);
5127
+ for (const statement of labelBinding.setupStatements) {
5128
+ chunks.push(` ${statement}`);
4891
5129
  }
4892
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4893
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4894
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5130
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4895
5131
  }
4896
5132
  chunks.push(" }");
4897
5133
  chunks.push("");
@@ -5335,7 +5571,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5335
5571
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5336
5572
  continue;
5337
5573
  const list = methodToChildren.get(name) ?? [];
5338
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5574
+ list.push({ childProp, signature: sig });
5339
5575
  methodToChildren.set(name, list);
5340
5576
  }
5341
5577
  }
@@ -5345,12 +5581,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5345
5581
  return [];
5346
5582
  }
5347
5583
  return passthroughs.map(([methodName, candidates]) => {
5348
- const { childProp, params, argNames } = candidates[0];
5349
- const callArgs = argNames.join(", ");
5584
+ const { childProp, signature } = candidates[0];
5585
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5350
5586
  return createClassMethod({
5351
5587
  name: methodName,
5352
5588
  isAsync: true,
5353
- parameters: parseParameterSignatures(params),
5589
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5354
5590
  statements: [
5355
5591
  `return await this.${childProp}.${methodName}(${callArgs});`
5356
5592
  ]
@@ -5374,8 +5610,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5374
5610
  const list = methodToAttachments.get(methodName) ?? [];
5375
5611
  list.push({
5376
5612
  propertyName: attachment.propertyName,
5377
- params: signature.params,
5378
- argNames: signature.argNames
5613
+ signature
5379
5614
  });
5380
5615
  methodToAttachments.set(methodName, list);
5381
5616
  }
@@ -5386,12 +5621,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5386
5621
  return [];
5387
5622
  }
5388
5623
  return passthroughs.map(([methodName, candidates]) => {
5389
- const { propertyName, params, argNames } = candidates[0];
5390
- const callArgs = argNames.join(", ");
5624
+ const { propertyName, signature } = candidates[0];
5625
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5391
5626
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5392
5627
  return createClassMethod({
5393
5628
  name: methodName,
5394
- parameters: parseParameterSignatures(params),
5629
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5395
5630
  statements: [
5396
5631
  `return ${invocation};`
5397
5632
  ]
@@ -5405,15 +5640,44 @@ function sliceNodeSource(source, node) {
5405
5640
  const snippet = source.slice(node.start, node.end).trim();
5406
5641
  return snippet.length ? snippet : null;
5407
5642
  }
5408
- function getCustomPomCallArgumentName(param) {
5643
+ function getTypeAnnotationSource(source, node) {
5644
+ const rawTypeAnnotation = node.typeAnnotation;
5645
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
5646
+ return void 0;
5647
+ }
5648
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
5649
+ return typeAnnotation && typeof typeAnnotation === "object" ? sliceNodeSource(source, typeAnnotation) ?? void 0 : void 0;
5650
+ }
5651
+ function getCustomPomParameterSpec(source, param) {
5409
5652
  if (param.type === "Identifier") {
5410
- return param.name;
5653
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5654
+ hasQuestionToken: !!param.optional
5655
+ });
5411
5656
  }
5412
5657
  if (param.type === "AssignmentPattern") {
5413
- return param.left.type === "Identifier" ? param.left.name : null;
5658
+ if (param.left.type !== "Identifier") {
5659
+ return null;
5660
+ }
5661
+ const initializer = sliceNodeSource(source, param.right);
5662
+ if (!initializer) {
5663
+ return null;
5664
+ }
5665
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
5666
+ initializer,
5667
+ hasQuestionToken: !!param.left.optional
5668
+ });
5414
5669
  }
5415
5670
  if (param.type === "RestElement") {
5416
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
5671
+ if (param.argument.type !== "Identifier") {
5672
+ return null;
5673
+ }
5674
+ const typeExpression = getTypeAnnotationSource(
5675
+ source,
5676
+ param
5677
+ ) ?? getTypeAnnotationSource(source, param.argument);
5678
+ return createPomParameterSpec(param.argument.name, typeExpression, {
5679
+ isRestParameter: true
5680
+ });
5417
5681
  }
5418
5682
  return null;
5419
5683
  }
@@ -5446,29 +5710,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5446
5710
  if (member.key.type !== "Identifier") {
5447
5711
  continue;
5448
5712
  }
5449
- const params = [];
5450
- const argNames = [];
5713
+ const parameters = [];
5451
5714
  let supported = true;
5452
5715
  member.params.forEach((param) => {
5453
5716
  if (!supported) {
5454
5717
  return;
5455
5718
  }
5456
- const paramSource = sliceNodeSource(source, param);
5457
- const argName = getCustomPomCallArgumentName(param);
5458
- if (!paramSource || !argName) {
5719
+ const parameter = getCustomPomParameterSpec(source, param);
5720
+ if (!parameter) {
5459
5721
  supported = false;
5460
5722
  return;
5461
5723
  }
5462
- params.push(paramSource);
5463
- argNames.push(argName);
5724
+ parameters.push(parameter);
5464
5725
  });
5465
5726
  if (!supported) {
5466
5727
  continue;
5467
5728
  }
5468
- signatures.set(member.key.name, {
5469
- params: params.join(", "),
5470
- argNames
5471
- });
5729
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5472
5730
  }
5473
5731
  }
5474
5732
  return signatures;
@@ -5651,6 +5909,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5651
5909
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5652
5910
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5653
5911
  if (!fs.existsSync(customDirAbs)) {
5912
+ if (options.requireCustomPomDir) {
5913
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
5914
+ }
5654
5915
  return {
5655
5916
  classIdentifierMap,
5656
5917
  methodSignaturesByClass,
@@ -5693,6 +5954,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5693
5954
  importSpecifiersByClass
5694
5955
  };
5695
5956
  }
5957
+ function assertCustomPomAttachmentsResolved(attachments, classIdentifierMap, configuredDir) {
5958
+ const missingClassNames = Array.from(new Set(
5959
+ attachments.map((attachment) => attachment.className).filter((className) => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className))
5960
+ )).sort((left, right) => left.localeCompare(right));
5961
+ if (missingClassNames.length > 0) {
5962
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
5963
+ }
5964
+ }
5696
5965
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5697
5966
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5698
5967
  if (!filePath)
@@ -5721,7 +5990,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5721
5990
  if (!sig)
5722
5991
  continue;
5723
5992
  const list = methodToChildren.get(name) ?? [];
5724
- list.push({ child, params: sig.params, argNames: sig.argNames });
5993
+ list.push({ child, signature: sig });
5725
5994
  methodToChildren.set(name, list);
5726
5995
  }
5727
5996
  }
@@ -5729,12 +5998,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5729
5998
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5730
5999
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5731
6000
  continue;
5732
- const { child, params, argNames } = candidatesForMethod[0];
5733
- const callArgs = argNames.join(", ");
6001
+ const { child, signature } = candidatesForMethod[0];
6002
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5734
6003
  passthroughMembers.push(createClassMethod({
5735
6004
  name: methodName,
5736
6005
  isAsync: true,
5737
- parameters: parseParameterSignatures(params),
6006
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5738
6007
  statements: [
5739
6008
  `return await this.${child}.${methodName}(${callArgs});`
5740
6009
  ]
@@ -5783,9 +6052,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5783
6052
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5784
6053
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5785
6054
  customPomDir: options.customPomDir,
6055
+ requireCustomPomDir: options.requireCustomPomDir,
5786
6056
  customPomImportAliases: options.customPomImportAliases,
5787
6057
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5788
6058
  });
6059
+ assertCustomPomAttachmentsResolved(
6060
+ options.customPomAttachments ?? [],
6061
+ customPomImportResolution.classIdentifierMap,
6062
+ options.customPomDir ?? "tests/playwright/pom/custom"
6063
+ );
5789
6064
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5790
6065
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5791
6066
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5935,8 +6210,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5935
6210
  return candidate;
5936
6211
  };
5937
6212
  for (const dt of dataTestIdSet) {
5938
- const raw = dt.value;
5939
- if (raw.includes("${")) {
6213
+ const raw = dt.selectorValue.formatted;
6214
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5940
6215
  continue;
5941
6216
  }
5942
6217
  const toggleSuffix = "-toggle";
@@ -6026,7 +6301,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
6026
6301
  });
6027
6302
  }
6028
6303
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
6029
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
6030
6304
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
6031
6305
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
6032
6306
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6288,7 +6562,7 @@ function getConditionalDirectiveInfo(element) {
6288
6562
  if (directive.name === "else") {
6289
6563
  const exp2 = directive.exp;
6290
6564
  if (exp2 && (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || exp2.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6291
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6565
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6292
6566
  return { kind: "else-if", source: source2 };
6293
6567
  }
6294
6568
  return { kind: "else", source: "" };
@@ -6297,13 +6571,13 @@ function getConditionalDirectiveInfo(element) {
6297
6571
  const exp2 = directive.exp;
6298
6572
  if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp2.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6299
6573
  return null;
6300
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6574
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6301
6575
  return { kind: "else-if", source: source2 };
6302
6576
  }
6303
6577
  const exp = directive.exp;
6304
6578
  if (!exp || exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6305
6579
  return null;
6306
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
6580
+ const source = getVueExpressionSource(exp, "content", "compiled");
6307
6581
  return { kind: directive.name, source };
6308
6582
  }
6309
6583
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6329,26 +6603,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6329
6603
  };
6330
6604
  try {
6331
6605
  const expr = parser.parseExpression(src, { plugins: ["typescript"] });
6332
- const isNodeType = (n, type) => {
6606
+ const isNodeType2 = (n, type) => {
6333
6607
  return n !== null && n.type === type;
6334
6608
  };
6335
- const isStringLiteralNode = (n) => {
6336
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6609
+ const isStringLiteralNode2 = (n) => {
6610
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6337
6611
  };
6338
- const isIdentifierNode = (n) => {
6339
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6612
+ const isIdentifierNode2 = (n) => {
6613
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6340
6614
  };
6341
6615
  const results = [];
6342
6616
  const walk = (n) => {
6343
6617
  if (!n)
6344
6618
  return;
6345
- if (isStringLiteralNode(n)) {
6619
+ if (isStringLiteralNode2(n)) {
6346
6620
  const v = (n.value ?? "").trim();
6347
6621
  if (isIdentifierish(v)) {
6348
6622
  results.push(v);
6349
6623
  }
6350
6624
  }
6351
- if (isIdentifierNode(n)) {
6625
+ if (isIdentifierNode2(n)) {
6352
6626
  const v = (n.name ?? "").trim();
6353
6627
  if (isIdentifierish(v)) {
6354
6628
  results.push(v);
@@ -6498,34 +6772,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6498
6772
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6499
6773
  return null;
6500
6774
  }
6501
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6775
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6502
6776
  const jsStringLiteral = (value) => {
6503
6777
  return JSON.stringify(value);
6504
6778
  };
6505
6779
  const getTestIdExpressionForNode = () => {
6506
- const existing = findTestIdAttribute(element, testIdAttribute);
6507
- if (!existing) {
6508
- return "undefined";
6509
- }
6510
- if (existing.type === compilerCore.NodeTypes.ATTRIBUTE) {
6511
- const v = existing.value?.content;
6512
- if (!v) {
6513
- return "undefined";
6514
- }
6515
- return jsStringLiteral(v);
6516
- }
6517
- const directive = existing;
6518
- const exp2 = directive.exp;
6519
- if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
6780
+ if (!resolvedRuntimeTestId) {
6520
6781
  return "undefined";
6521
6782
  }
6522
- const content = (exp2.content ?? "").trim();
6523
- if (!content) {
6524
- return "undefined";
6783
+ if (resolvedRuntimeTestId.kind === "static") {
6784
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6525
6785
  }
6526
- return `(${content})`;
6786
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6527
6787
  };
6528
- const testIdExpression2 = getTestIdExpressionForNode();
6788
+ const testIdExpression = getTestIdExpressionForNode();
6529
6789
  const clickDirective = tryGetClickDirective(element);
6530
6790
  if (!clickDirective)
6531
6791
  return;
@@ -6536,10 +6796,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6536
6796
  const exp = clickDirective.exp;
6537
6797
  if (!exp)
6538
6798
  return;
6539
- const existingSource = (exp.loc?.source ?? (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6799
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6540
6800
  if (existingSource.includes(CLICK_EVENT_NAME))
6541
6801
  return;
6542
- const originalExpression = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6802
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6543
6803
  if (!originalExpression)
6544
6804
  return;
6545
6805
  const isStatementBody = (() => {
@@ -6556,7 +6816,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6556
6816
  const statementWrappedHandler = `($event) => {
6557
6817
  const __win = ($event && $event.view) ? $event.view : undefined;
6558
6818
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6559
- const __testIdFromNode = ${testIdExpression2};
6819
+ const __testIdFromNode = ${testIdExpression};
6560
6820
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6561
6821
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6562
6822
  const __emit = (phase, err) => {
@@ -6567,16 +6827,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6567
6827
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6568
6828
  }
6569
6829
  } catch (e) {
6570
- // Instrumentation must never hide failures during e2e strict mode.
6571
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6572
- // Outside strict mode we log and continue so we don't break real user clicks.
6830
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6573
6831
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6574
6832
  if (__w && __w.console && typeof __w.console.error === 'function') {
6575
6833
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6576
6834
  }
6577
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6578
- throw e;
6579
- }
6835
+ throw e;
6580
6836
  }
6581
6837
  };
6582
6838
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6607,7 +6863,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6607
6863
  const expressionWrappedHandler = `($event) => {
6608
6864
  const __win = ($event && $event.view) ? $event.view : undefined;
6609
6865
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6610
- const __testIdFromNode = ${testIdExpression2};
6866
+ const __testIdFromNode = ${testIdExpression};
6611
6867
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6612
6868
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6613
6869
  const __emit = (phase, err) => {
@@ -6618,16 +6874,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6618
6874
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6619
6875
  }
6620
6876
  } catch (e) {
6621
- // Instrumentation must never hide failures during e2e strict mode.
6622
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6623
- // Outside strict mode we log and continue so we don't break real user clicks.
6877
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6624
6878
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6625
6879
  if (__w && __w.console && typeof __w.console.error === 'function') {
6626
6880
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6627
6881
  }
6628
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6629
- throw e;
6630
- }
6882
+ throw e;
6631
6883
  }
6632
6884
  };
6633
6885
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6665,10 +6917,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6665
6917
  let previousFileName = "";
6666
6918
  const hierarchyMap = /* @__PURE__ */ new Map();
6667
6919
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6668
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
6920
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6669
6921
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6670
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6671
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
6922
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6672
6923
  const warn = options.warn;
6673
6924
  const vueFilesPathMap = options.vueFilesPathMap;
6674
6925
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6744,7 +6995,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6744
6995
  conditionalHintByIfBranch.set(branch, hint);
6745
6996
  continue;
6746
6997
  }
6747
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
6998
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6748
6999
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6749
7000
  if (stable) {
6750
7001
  conditionalHintByIfBranch.set(branch, stable);
@@ -6806,17 +7057,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6806
7057
  nativeWrappers[element.tag] = { role: "grid" };
6807
7058
  }
6808
7059
  }
6809
- const getBestAvailableKeyValue = () => {
7060
+ const getBestAvailableKeyInfo = () => {
6810
7061
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6811
7062
  const isDirectVForChild = parentNode?.type === compilerCore.NodeTypes.FOR;
6812
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6813
- if (vForKey) return vForKey;
6814
- return getContainedInSlotDataKeyValue(element, hierarchyMap);
7063
+ const vForKeyInfo = (isDirectVForChild ? getKeyDirectiveInfo(element) : null) || getContainedInVForDirectiveKeyInfo(context, element, hierarchyMap);
7064
+ if (vForKeyInfo) {
7065
+ return vForKeyInfo;
7066
+ }
7067
+ return getContainedInSlotDataKeyInfo(element, hierarchyMap);
6815
7068
  };
6816
- const bestKeyInferred = getBestAvailableKeyValue();
6817
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6818
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6819
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7069
+ const bestKeyInfo = getBestAvailableKeyInfo();
7070
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7071
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6820
7072
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6821
7073
  const parentKey = context?.parent ? context.parent : null;
6822
7074
  const conditional = getConditionalDirectiveInfo(element);
@@ -6859,7 +7111,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6859
7111
  if (!cond) {
6860
7112
  conditionalHint = "else";
6861
7113
  } else {
6862
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
7114
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6863
7115
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6864
7116
  }
6865
7117
  }
@@ -6869,7 +7121,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6869
7121
  });
6870
7122
  if (showDirective?.exp && (showDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6871
7123
  const exp = showDirective.exp;
6872
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
7124
+ const source = getVueExpressionSource(exp, "content", "compiled");
6873
7125
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6874
7126
  if (showHint) {
6875
7127
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6903,13 +7155,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6903
7155
  const tagSuffix = getTagSuffix();
6904
7156
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6905
7157
  };
7158
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7159
+ const tagSuffix = getTagSuffix();
7160
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7161
+ };
6906
7162
  const getSubmitDataTestId = (identifier) => {
6907
7163
  const tagSuffix = getTagSuffix();
6908
7164
  return `${componentName}-${identifier}${tagSuffix}`;
6909
7165
  };
6910
7166
  const applyResolvedDataTestIdForElement = (args) => {
6911
7167
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6912
- applyResolvedDataTestId({
7168
+ return applyResolvedDataTestId({
6913
7169
  element,
6914
7170
  componentName,
6915
7171
  parentComponentName,
@@ -6919,8 +7175,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6919
7175
  generatedMethodContentByComponent,
6920
7176
  nativeRole,
6921
7177
  preferredGeneratedValue: args.preferredGeneratedValue,
6922
- bestKeyPlaceholder,
6923
- bestKeyVariable,
7178
+ preferredRuntimeValue: args.preferredRuntimeValue,
7179
+ keyInfo: bestKeyInfo,
6924
7180
  keyValuesOverride,
6925
7181
  entryOverrides: args.entryOverrides,
6926
7182
  semanticNameHint: args.semanticNameHint,
@@ -6938,7 +7194,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6938
7194
  return p.type === compilerCore.NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6939
7195
  }) ?? null;
6940
7196
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6941
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7197
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6942
7198
  const loc = element.loc?.start;
6943
7199
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6944
7200
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6947,7 +7203,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6947
7203
  Element: <${element.tag}>
6948
7204
  Handler: ${handlerSource}
6949
7205
 
6950
- Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name. You can also set errorBehavior = "ignore" to keep generic fallback behavior.`
7206
+ Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name.`
6951
7207
  );
6952
7208
  }
6953
7209
  if (nativeWrappersValue) {
@@ -7074,20 +7330,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7074
7330
  contextFilename: context.filename
7075
7331
  });
7076
7332
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
7077
- const idOrName = getIdOrName(element) || void 0;
7333
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
7078
7334
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
7079
7335
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
7080
7336
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
7081
7337
  const testId = getClickDataTestId(clickSuffix);
7082
- applyResolvedDataTestIdForElement({
7338
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7339
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
7083
7340
  preferredGeneratedValue: testId,
7341
+ preferredRuntimeValue: runtimeTestId,
7084
7342
  semanticNameHint: semanticNameHint2,
7085
7343
  semanticNameHintAlternates,
7086
7344
  pomMergeKey
7087
7345
  });
7088
- {
7089
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7090
- }
7346
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7091
7347
  return;
7092
7348
  }
7093
7349
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7097,7 +7353,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7097
7353
  if (!isRecognizedInteractiveRole) {
7098
7354
  return;
7099
7355
  }
7100
- const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7356
+ const identifierHint = getStaticIdOrNameHint(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7101
7357
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7102
7358
  applyResolvedDataTestIdForElement({
7103
7359
  preferredGeneratedValue,
@@ -7107,7 +7363,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7107
7363
  }
7108
7364
  const isSubmit = element.props.find((p) => p.type === compilerCore.NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7109
7365
  if (isSubmit) {
7110
- const identifier = getIdOrName(element) || innerText;
7366
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7111
7367
  if (!identifier) {
7112
7368
  const loc = element.loc?.start;
7113
7369
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7159,29 +7415,32 @@ function createBuildProcessorPlugin(options) {
7159
7415
  getSourceDirs,
7160
7416
  basePageClassPath,
7161
7417
  normalizedBasePagePath,
7418
+ generation,
7419
+ projectRootRef,
7420
+ nativeWrappers,
7421
+ excludedComponents,
7422
+ getWrapperSearchRoots,
7423
+ getResolvedRouterEntry,
7424
+ loggerRef
7425
+ } = options;
7426
+ const {
7162
7427
  outDir,
7163
7428
  emitLanguages,
7164
7429
  typescriptOutputStructure,
7165
7430
  csharp,
7166
7431
  generateFixtures,
7167
7432
  customPomAttachments,
7168
- projectRootRef,
7169
7433
  customPomDir,
7434
+ requireCustomPomDir,
7170
7435
  customPomImportAliases,
7171
7436
  customPomImportNameCollisionBehavior,
7172
7437
  testIdAttribute,
7173
7438
  nameCollisionBehavior,
7174
- missingSemanticNameBehavior = "error",
7175
7439
  existingIdBehavior,
7176
- nativeWrappers,
7177
- excludedComponents,
7178
- getWrapperSearchRoots,
7179
7440
  routerAwarePoms,
7180
- getResolvedRouterEntry,
7181
7441
  routerType,
7182
- routerModuleShims,
7183
- loggerRef
7184
- } = options;
7442
+ routerModuleShims
7443
+ } = generation;
7185
7444
  let lastGeneratedMetrics = {
7186
7445
  entryCount: 0,
7187
7446
  interactiveComponentCount: 0,
@@ -7280,10 +7539,9 @@ function createBuildProcessorPlugin(options) {
7280
7539
  excludedComponents,
7281
7540
  getViewsDirAbs(),
7282
7541
  {
7283
- existingIdBehavior: existingIdBehavior ?? "preserve",
7542
+ existingIdBehavior: existingIdBehavior ?? "error",
7284
7543
  testIdAttribute,
7285
7544
  nameCollisionBehavior,
7286
- missingSemanticNameBehavior,
7287
7545
  warn: (message) => loggerRef.current.warn(message),
7288
7546
  vueFilesPathMap,
7289
7547
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7383,6 +7641,7 @@ function createBuildProcessorPlugin(options) {
7383
7641
  customPomAttachments,
7384
7642
  projectRoot: projectRootRef.current,
7385
7643
  customPomDir,
7644
+ requireCustomPomDir,
7386
7645
  customPomImportAliases,
7387
7646
  customPomImportNameCollisionBehavior,
7388
7647
  testIdAttribute,
@@ -7414,6 +7673,11 @@ function createDevProcessorPlugin(options) {
7414
7673
  projectRootRef,
7415
7674
  normalizedBasePagePath,
7416
7675
  basePageClassPath,
7676
+ generation,
7677
+ getResolvedRouterEntry,
7678
+ loggerRef
7679
+ } = options;
7680
+ const {
7417
7681
  outDir,
7418
7682
  emitLanguages,
7419
7683
  typescriptOutputStructure,
@@ -7421,18 +7685,16 @@ function createDevProcessorPlugin(options) {
7421
7685
  generateFixtures,
7422
7686
  customPomAttachments,
7423
7687
  customPomDir,
7688
+ requireCustomPomDir,
7424
7689
  customPomImportAliases,
7425
7690
  customPomImportNameCollisionBehavior,
7426
- nameCollisionBehavior = "suffix",
7427
- missingSemanticNameBehavior = "error",
7691
+ nameCollisionBehavior,
7428
7692
  existingIdBehavior,
7429
7693
  testIdAttribute,
7430
7694
  routerAwarePoms,
7431
- getResolvedRouterEntry,
7432
7695
  routerType,
7433
- routerModuleShims,
7434
- loggerRef
7435
- } = options;
7696
+ routerModuleShims
7697
+ } = generation;
7436
7698
  let scheduleVueFileRegen = null;
7437
7699
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7438
7700
  path.resolve(projectRootRef.current),
@@ -7619,9 +7881,8 @@ function createDevProcessorPlugin(options) {
7619
7881
  excludedComponents,
7620
7882
  getViewsDirAbs(),
7621
7883
  {
7622
- existingIdBehavior: existingIdBehavior ?? "preserve",
7884
+ existingIdBehavior: existingIdBehavior ?? "error",
7623
7885
  nameCollisionBehavior,
7624
- missingSemanticNameBehavior,
7625
7886
  testIdAttribute,
7626
7887
  warn: (message) => loggerRef.current.warn(message),
7627
7888
  vueFilesPathMap: provisionalVuePathMap,
@@ -7672,6 +7933,7 @@ function createDevProcessorPlugin(options) {
7672
7933
  customPomAttachments,
7673
7934
  projectRoot: projectRootRef.current,
7674
7935
  customPomDir,
7936
+ requireCustomPomDir,
7675
7937
  customPomImportAliases,
7676
7938
  customPomImportNameCollisionBehavior,
7677
7939
  pageDirs: getPageDirs(),
@@ -7907,27 +8169,30 @@ function createSupportPlugins(options) {
7907
8169
  getViewsDir,
7908
8170
  getSourceDirs,
7909
8171
  getWrapperSearchRoots,
7910
- nameCollisionBehavior = "suffix",
7911
- missingSemanticNameBehavior = "error",
7912
- existingIdBehavior,
8172
+ generation,
8173
+ projectRootRef,
8174
+ basePageClassPath: basePageClassPathOverride,
8175
+ loggerRef
8176
+ } = options;
8177
+ const {
7913
8178
  outDir,
7914
8179
  emitLanguages,
7915
8180
  typescriptOutputStructure,
7916
8181
  csharp,
7917
- routerAwarePoms,
7918
- routerEntry,
7919
- routerType,
7920
- routerModuleShims,
7921
8182
  generateFixtures,
7922
8183
  customPomAttachments,
7923
- projectRootRef,
7924
- basePageClassPath: basePageClassPathOverride,
7925
8184
  customPomDir,
8185
+ requireCustomPomDir,
7926
8186
  customPomImportAliases,
7927
8187
  customPomImportNameCollisionBehavior,
8188
+ nameCollisionBehavior,
8189
+ existingIdBehavior,
7928
8190
  testIdAttribute,
7929
- loggerRef
7930
- } = options;
8191
+ routerAwarePoms,
8192
+ routerEntry,
8193
+ routerType,
8194
+ routerModuleShims
8195
+ } = generation;
7931
8196
  const resolveRouterEntry2 = () => {
7932
8197
  if (!routerAwarePoms)
7933
8198
  return void 0;
@@ -7956,27 +8221,12 @@ function createSupportPlugins(options) {
7956
8221
  getSourceDirs,
7957
8222
  basePageClassPath,
7958
8223
  normalizedBasePagePath,
7959
- outDir,
7960
- emitLanguages,
7961
- typescriptOutputStructure,
7962
- csharp,
7963
- generateFixtures,
7964
- customPomAttachments,
8224
+ generation,
7965
8225
  projectRootRef,
7966
- customPomDir,
7967
- customPomImportAliases,
7968
- customPomImportNameCollisionBehavior,
7969
- testIdAttribute,
7970
- nameCollisionBehavior,
7971
- missingSemanticNameBehavior,
7972
- existingIdBehavior,
7973
8226
  nativeWrappers,
7974
8227
  excludedComponents,
7975
8228
  getWrapperSearchRoots,
7976
- routerAwarePoms,
7977
- routerType,
7978
8229
  getResolvedRouterEntry: resolveRouterEntry2,
7979
- routerModuleShims,
7980
8230
  loggerRef
7981
8231
  });
7982
8232
  const devProcessor = createDevProcessorPlugin({
@@ -7991,23 +8241,8 @@ function createSupportPlugins(options) {
7991
8241
  projectRootRef,
7992
8242
  normalizedBasePagePath,
7993
8243
  basePageClassPath,
7994
- outDir,
7995
- emitLanguages,
7996
- typescriptOutputStructure,
7997
- csharp,
7998
- generateFixtures,
7999
- customPomAttachments,
8000
- customPomDir,
8001
- customPomImportAliases,
8002
- customPomImportNameCollisionBehavior,
8003
- nameCollisionBehavior,
8004
- missingSemanticNameBehavior,
8005
- existingIdBehavior,
8006
- testIdAttribute,
8007
- routerAwarePoms,
8008
- routerType,
8244
+ generation,
8009
8245
  getResolvedRouterEntry: resolveRouterEntry2,
8010
- routerModuleShims,
8011
8246
  loggerRef
8012
8247
  });
8013
8248
  const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
@@ -8344,8 +8579,7 @@ function createVuePluginWithTestIds(options) {
8344
8579
  });
8345
8580
  const api = viteVuePlugin?.api;
8346
8581
  if (!api) {
8347
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8348
- return;
8582
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8349
8583
  }
8350
8584
  const currentOptions = api.options ?? {};
8351
8585
  const currentTemplate = currentOptions.template ?? {};
@@ -8411,33 +8645,6 @@ function assertOneOf(value, allowed, name) {
8411
8645
  }
8412
8646
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8413
8647
  }
8414
- function assertErrorBehavior(value, name) {
8415
- if (!value) {
8416
- return;
8417
- }
8418
- if (value === "ignore" || value === "error") {
8419
- return;
8420
- }
8421
- if (typeof value !== "object" || Array.isArray(value)) {
8422
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8423
- }
8424
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8425
- for (const key of Object.keys(value)) {
8426
- if (!supportedKeys.has(key)) {
8427
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8428
- }
8429
- }
8430
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8431
- }
8432
- function resolveMissingSemanticNameBehavior(value) {
8433
- if (!value) {
8434
- return "error";
8435
- }
8436
- if (value === "ignore" || value === "error") {
8437
- return value;
8438
- }
8439
- return value.missingSemanticNameBehavior ?? "error";
8440
- }
8441
8648
  function readPackageJson(projectRoot) {
8442
8649
  const packageJsonPath = path.join(projectRoot, "package.json");
8443
8650
  if (!fs.existsSync(packageJsonPath)) {
@@ -8546,7 +8753,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8546
8753
  '[vue-pom-generator] vuePluginOwnership="external" requires the resolved Vite Vue plugin, but none was found. Add vue() to your Vite plugins before spreading createVuePomGeneratorPlugins(...).'
8547
8754
  );
8548
8755
  }
8549
- throw new Error("[vue-pom-generator] Nuxt mode requires the resolved Vite Vue plugin, but none was found.");
8756
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8550
8757
  }
8551
8758
  const currentOptions = viteVuePlugin.api.options ?? {};
8552
8759
  const currentTemplate = currentOptions.template ?? {};
@@ -8603,18 +8810,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8603
8810
  const vueGenerationOptions = generationOptions;
8604
8811
  const verbosity = options.logging?.verbosity ?? "warn";
8605
8812
  const vueOptions = options.vueOptions;
8606
- const legacyVueOptions = options;
8607
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8608
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8609
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8610
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8611
- const nativeWrappers = injection.nativeWrappers ?? {};
8612
- const excludedComponents = injection.excludeComponents ?? [];
8613
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8614
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8615
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8616
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8617
- const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
8813
+ const resolvedInjectionOptionsRef = {
8814
+ current: resolveInjectionSupportOptions({
8815
+ isNuxt,
8816
+ viewsDir: injection.viewsDir,
8817
+ componentDirs: injection.componentDirs,
8818
+ layoutDirs: injection.layoutDirs,
8819
+ wrapperSearchRoots: injection.wrapperSearchRoots,
8820
+ nativeWrappers: injection.nativeWrappers,
8821
+ excludedComponents: injection.excludeComponents,
8822
+ existingIdBehavior: injection.existingIdBehavior,
8823
+ testIdAttribute: injection.attribute
8824
+ })
8825
+ };
8826
+ const resolvedInjectionOptions = resolvedInjectionOptionsRef.current;
8827
+ const nativeWrappers = resolvedInjectionOptions.nativeWrappers;
8828
+ const excludedComponents = resolvedInjectionOptions.excludedComponents;
8829
+ const testIdAttribute = resolvedInjectionOptions.testIdAttribute;
8618
8830
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8619
8831
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8620
8832
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8623,27 +8835,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8623
8835
  }
8624
8836
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8625
8837
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8626
- const csharp = generationOptions?.csharp;
8627
- const errorBehavior = options.errorBehavior;
8628
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8629
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8630
8838
  const generateFixtures = generationOptions?.playwright?.fixtures;
8631
8839
  const customPoms = generationOptions?.playwright?.customPoms;
8632
8840
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8633
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8634
8841
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8635
- const resolvedCustomPomImportCollisionBehavior = customPoms?.importNameCollisionBehavior ?? "error";
8842
+ const requireCustomPomDir = customPoms?.dir !== void 0 || resolvedCustomPomAttachments.length > 0 || Object.keys(resolvedCustomPomImportAliases ?? {}).length > 0;
8843
+ const resolvedGenerationOptions = resolveGenerationSupportOptions({
8844
+ outDir: generationOptions?.outDir,
8845
+ emitLanguages: generationOptions?.emit,
8846
+ typescriptOutputStructure: generationOptions?.playwright?.outputStructure,
8847
+ csharp: generationOptions?.csharp,
8848
+ generateFixtures,
8849
+ customPomAttachments: resolvedCustomPomAttachments,
8850
+ customPomDir: customPoms?.dir,
8851
+ requireCustomPomDir,
8852
+ customPomImportAliases: resolvedCustomPomImportAliases,
8853
+ customPomImportNameCollisionBehavior: customPoms?.importNameCollisionBehavior,
8854
+ nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
8855
+ existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
8856
+ testIdAttribute,
8857
+ routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
8858
+ routerEntry,
8859
+ routerType,
8860
+ routerModuleShims
8861
+ });
8636
8862
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8637
- const getPageDirs = () => pageDirsRef.current;
8863
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8638
8864
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8639
- const getComponentDirs = () => componentDirsRef.current;
8640
- const getLayoutDirs = () => layoutDirsRef.current;
8865
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
8866
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8641
8867
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8642
8868
  ...getPageDirs(),
8643
8869
  ...getComponentDirs(),
8644
8870
  ...getLayoutDirs()
8645
8871
  ]));
8646
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
8872
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8647
8873
  const sharedStateKey = JSON.stringify({
8648
8874
  cwd: process.cwd(),
8649
8875
  mode: isNuxt ? "nuxt" : "vue",
@@ -8651,9 +8877,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8651
8877
  componentDirs: isNuxt ? null : getComponentDirs(),
8652
8878
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8653
8879
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8654
- outDir,
8880
+ outDir: resolvedGenerationOptions.outDir,
8655
8881
  testIdAttribute,
8656
- routerType,
8882
+ routerType: resolvedGenerationOptions.routerType,
8657
8883
  vuePluginOwnership
8658
8884
  });
8659
8885
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8674,23 +8900,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8674
8900
  if (isNuxt) {
8675
8901
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8676
8902
  projectRootRef.current = nuxtDiscovery.rootDir;
8677
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8678
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8679
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8680
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
8903
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
8904
+ resolvedInjectionOptionsRef.current,
8905
+ nuxtDiscovery
8906
+ );
8681
8907
  }
8682
8908
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8683
8909
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8684
8910
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8685
8911
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8686
8912
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8687
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8688
8913
  if (generationEnabled) {
8689
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8690
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8691
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8692
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8693
- assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
8914
+ assertNonEmptyString(resolvedGenerationOptions.outDir, "[vue-pom-generator] generation.outDir");
8915
+ assertOneOf(resolvedGenerationOptions.typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8916
+ assertRouterModuleShims(resolvedGenerationOptions.routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8917
+ if (!isNuxt && vueGenerationOptions?.router && resolvedGenerationOptions.routerType === "vue-router") {
8918
+ assertNonEmptyString(resolvedGenerationOptions.routerEntry, "[vue-pom-generator] generation.router.entry");
8694
8919
  }
8695
8920
  }
8696
8921
  if (usesExternalVuePlugin) {
@@ -8712,8 +8937,8 @@ function createVuePomGeneratorPlugins(options = {}) {
8712
8937
  const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8713
8938
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8714
8939
  vueOptions,
8715
- existingIdBehavior,
8716
- nameCollisionBehavior,
8940
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
8941
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8717
8942
  nativeWrappers,
8718
8943
  elementMetadata,
8719
8944
  semanticNameMap,
@@ -8728,7 +8953,6 @@ function createVuePomGeneratorPlugins(options = {}) {
8728
8953
  getProjectRoot: () => projectRootRef.current
8729
8954
  });
8730
8955
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8731
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8732
8956
  const supportPlugins = createSupportPlugins({
8733
8957
  componentTestIds,
8734
8958
  componentHierarchyMap,
@@ -8741,26 +8965,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8741
8965
  getViewsDir,
8742
8966
  getSourceDirs,
8743
8967
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8744
- nameCollisionBehavior,
8745
- missingSemanticNameBehavior,
8746
- existingIdBehavior,
8747
- outDir,
8748
- emitLanguages,
8749
- typescriptOutputStructure,
8750
- csharp,
8751
- routerAwarePoms,
8752
- routerEntry,
8753
- generateFixtures,
8968
+ generation: resolvedGenerationOptions,
8754
8969
  projectRootRef,
8755
8970
  basePageClassPath: basePageClassPathOverride,
8756
- customPomAttachments: resolvedCustomPomAttachments,
8757
- customPomDir: resolvedCustomPomDir,
8758
- customPomImportAliases: resolvedCustomPomImportAliases,
8759
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8760
- testIdAttribute,
8761
- loggerRef,
8762
- routerType,
8763
- routerModuleShims
8971
+ loggerRef
8764
8972
  });
8765
8973
  if (isNuxt) {
8766
8974
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");