@immense/vue-pom-generator 1.0.57 → 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 (52) hide show
  1. package/README.md +7 -19
  2. package/RELEASE_NOTES.md +76 -13
  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 +1283 -1019
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1285 -1021
  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/playwright.config.d.ts.map +1 -1
  22. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  23. package/dist/plugin/nuxt-discovery.d.ts.map +1 -1
  24. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  25. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  26. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  27. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  28. package/dist/plugin/support/build-plugin.d.ts +2 -29
  29. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  31. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  32. package/dist/plugin/support-plugins.d.ts +2 -32
  33. package/dist/plugin/support-plugins.d.ts.map +1 -1
  34. package/dist/plugin/types.d.ts +6 -23
  35. package/dist/plugin/types.d.ts.map +1 -1
  36. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  37. package/dist/pom-params.d.ts +40 -0
  38. package/dist/pom-params.d.ts.map +1 -0
  39. package/dist/pom-patterns.d.ts +31 -0
  40. package/dist/pom-patterns.d.ts.map +1 -0
  41. package/dist/routing/to-directive.d.ts +21 -0
  42. package/dist/routing/to-directive.d.ts.map +1 -1
  43. package/dist/tests/base-page.test.d.ts +2 -0
  44. package/dist/tests/base-page.test.d.ts.map +1 -0
  45. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  46. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  47. package/dist/transform.d.ts +0 -1
  48. package/dist/transform.d.ts.map +1 -1
  49. package/dist/utils.d.ts +129 -63
  50. package/dist/utils.d.ts.map +1 -1
  51. package/package.json +6 -4
  52. package/sequence-diagram.md +6 -6
package/dist/index.cjs CHANGED
@@ -104,6 +104,21 @@ function createLogger(options) {
104
104
  };
105
105
  }
106
106
  const requireFromModule = node_module.createRequire(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
107
+ function resolveNuxtKitEntry(cwd) {
108
+ const attemptResolvers = [
109
+ node_module.createRequire(path.resolve(cwd, "package.json")),
110
+ requireFromModule
111
+ ];
112
+ let lastError;
113
+ for (const resolver of attemptResolvers) {
114
+ try {
115
+ return resolver.resolve("@nuxt/kit");
116
+ } catch (error) {
117
+ lastError = error instanceof Error ? error : new Error(String(error));
118
+ }
119
+ }
120
+ throw lastError ?? new Error("Unknown module resolution error");
121
+ }
107
122
  function toUniqueResolvedPaths(paths) {
108
123
  return Array.from(new Set(paths.map((value) => path.resolve(value))));
109
124
  }
@@ -214,7 +229,7 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
214
229
  let loadNuxtConfig;
215
230
  let getLayerDirectories;
216
231
  try {
217
- const nuxtKitEntry = requireFromModule.resolve("@nuxt/kit");
232
+ const nuxtKitEntry = resolveNuxtKitEntry(cwd);
218
233
  ({ loadNuxtConfig, getLayerDirectories } = await import(node_url.pathToFileURL(nuxtKitEntry).href));
219
234
  } catch (error) {
220
235
  throw new TypeError(
@@ -230,6 +245,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
230
245
  const nuxtOptions = await loadNuxtConfig({ cwd });
231
246
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
232
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
+ }
233
291
  function createTypeScriptProject() {
234
292
  return new tsMorph.Project({
235
293
  useInMemoryFileSystem: true,
@@ -321,16 +379,7 @@ function createClassConstructor(constructorDeclaration) {
321
379
  ...constructorDeclaration
322
380
  };
323
381
  }
324
- function upperFirst$1(value) {
325
- if (!value) {
326
- return value;
327
- }
328
- return value.charAt(0).toUpperCase() + value.slice(1);
329
- }
330
- function hasParam(params, name) {
331
- return Object.prototype.hasOwnProperty.call(params, name);
332
- }
333
- function splitTypeAndInitializer(typeExpression) {
382
+ function splitPomParameterTypeExpression(typeExpression) {
334
383
  const trimmed = typeExpression.trim();
335
384
  const initializerIndex = trimmed.lastIndexOf("=");
336
385
  if (initializerIndex < 0) {
@@ -341,49 +390,237 @@ function splitTypeAndInitializer(typeExpression) {
341
390
  initializer: trimmed.slice(initializerIndex + 1).trim()
342
391
  };
343
392
  }
344
- function createParameter(name, typeExpression) {
345
- 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 };
346
396
  return {
347
397
  name,
348
- type: type || void 0,
349
- initializer
398
+ typeExpression: normalizedTypeExpression,
399
+ type,
400
+ initializer: options.initializer ?? initializer,
401
+ hasQuestionToken: options.hasQuestionToken,
402
+ isRestParameter: options.isRestParameter
350
403
  };
351
404
  }
352
- function createParameters(params) {
353
- 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
+ }));
354
414
  }
355
- 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) {
356
451
  return {
357
- name,
358
- type: options.type,
359
- initializer: options.initializer
452
+ parameters: normalizePomParameters(parameters)
360
453
  };
361
454
  }
362
- function removeByKeySegment(value) {
363
- const idx = value.lastIndexOf("ByKey");
364
- if (idx < 0) {
365
- 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;
366
463
  }
367
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
464
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
465
+ }
466
+ function pomMethodSignatureEquals(left, right) {
467
+ return pomParameterListEquals(left.parameters, right.parameters);
468
+ }
469
+ function isParameterizedPomPattern(kind) {
470
+ return kind === "parameterized";
471
+ }
472
+ function getTemplateVariables(formatted) {
473
+ const out = [];
474
+ const seen = /* @__PURE__ */ new Set();
475
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
476
+ for (const match of matches) {
477
+ const variableName = match[1];
478
+ if (seen.has(variableName)) {
479
+ continue;
480
+ }
481
+ seen.add(variableName);
482
+ out.push(variableName);
483
+ }
484
+ return out;
368
485
  }
369
- function uniqueAlternates(primary, alternates) {
486
+ function createPomStringPattern(formatted, patternKind) {
487
+ return {
488
+ formatted,
489
+ patternKind,
490
+ templateVariables: getTemplateVariables(formatted)
491
+ };
492
+ }
493
+ function getPomPatternVariables(patterns, options = {}) {
370
494
  const out = [];
371
495
  const seen = /* @__PURE__ */ new Set();
372
- seen.add(primary);
373
- for (const a of alternates ?? []) {
374
- if (!a) {
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);
375
518
  continue;
376
519
  }
377
- if (seen.has(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)) {
378
531
  continue;
379
532
  }
380
- seen.add(a);
381
- out.push(a);
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);
382
599
  }
383
600
  return out;
384
601
  }
385
- function testIdExpression(formattedDataTestId) {
386
- 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);
387
624
  }
388
625
  function createAsyncMethod(name, parameters, statements) {
389
626
  return createClassMethod({
@@ -393,17 +630,27 @@ function createAsyncMethod(name, parameters, statements) {
393
630
  statements
394
631
  });
395
632
  }
396
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
633
+ function generateClickMethod(methodName, selector, alternateSelectors, parameters) {
397
634
  const name = `click${methodName}`;
398
635
  const noWaitName = `${name}NoWait`;
399
- const baseParameters = createParameters(params);
400
- const argsForForward = Object.keys(params).join(", ");
401
- 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);
402
642
  if (alternates.length > 0) {
403
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
643
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
404
644
  const clickMethod = createAsyncMethod(
405
645
  name,
406
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })] : [createInlineParameter("wait", { type: "boolean", initializer: "true" })],
646
+ hasSelectorVariables ? [
647
+ ...baseParameters,
648
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
649
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
650
+ ] : [
651
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
652
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
653
+ ],
407
654
  (writer) => {
408
655
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
409
656
  writer.writeLine("let lastError: unknown;");
@@ -411,7 +658,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
411
658
  writer.writeLine("const locator = this.locatorByTestId(testId);");
412
659
  writer.write("try ").block(() => {
413
660
  writer.write("if (await locator.count()) ").block(() => {
414
- writer.writeLine('await this.clickLocator(locator, "", wait);');
661
+ writer.writeLine("await this.clickLocator(locator, annotationText, wait);");
415
662
  writer.writeLine("return;");
416
663
  });
417
664
  });
@@ -422,60 +669,77 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
422
669
  writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to click any candidate locator for ${name}.");`);
423
670
  }
424
671
  );
425
- const noWaitArgs = argsForForward ? `${argsForForward}, false` : "false";
672
+ const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
426
673
  const noWaitMethod = createAsyncMethod(
427
674
  noWaitName,
428
- hasParam(params, "key") ? baseParameters : [],
675
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
429
676
  (writer) => {
430
677
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
431
678
  }
432
679
  );
433
680
  return [clickMethod, noWaitMethod];
434
681
  }
435
- if (hasParam(params, "key")) {
682
+ if (hasSelectorVariables) {
436
683
  return [
437
- createAsyncMethod(name, [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
438
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, "", wait);`);
439
- }),
440
- createAsyncMethod(noWaitName, baseParameters, (writer) => {
441
- writer.writeLine(`await this.${name}(${argsForForward}, false);`);
442
- })
684
+ createAsyncMethod(
685
+ name,
686
+ [
687
+ ...baseParameters,
688
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
689
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
690
+ ],
691
+ (writer) => {
692
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
693
+ }
694
+ ),
695
+ createAsyncMethod(
696
+ noWaitName,
697
+ [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })],
698
+ (writer) => {
699
+ writer.writeLine(`await this.${name}(${argsForForward}, false, annotationText);`);
700
+ }
701
+ )
443
702
  ];
444
703
  }
445
704
  return [
446
- createAsyncMethod(name, [createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
447
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", "", wait);`);
448
- }),
449
- createAsyncMethod(noWaitName, [], (writer) => {
450
- writer.writeLine(`await this.${name}(false);`);
451
- })
705
+ createAsyncMethod(
706
+ name,
707
+ [
708
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
709
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
710
+ ],
711
+ (writer) => {
712
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
713
+ }
714
+ ),
715
+ createAsyncMethod(
716
+ noWaitName,
717
+ [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
718
+ (writer) => {
719
+ writer.writeLine(`await this.${name}(false, annotationText);`);
720
+ }
721
+ )
452
722
  ];
453
723
  }
454
- function generateRadioMethod(methodName, formattedDataTestId) {
724
+ function generateRadioMethod(methodName, selector, parameters) {
455
725
  const name = `select${methodName}`;
456
- const hasKey = formattedDataTestId.includes("${key}");
457
- const parameters = hasKey ? [
458
- createInlineParameter("key", { type: "string" }),
459
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
460
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
461
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
726
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
727
+ const methodParameters = createParameters(selectorParams);
728
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
462
729
  return [
463
- createAsyncMethod(name, parameters, (writer) => {
730
+ createAsyncMethod(name, methodParameters, (writer) => {
464
731
  writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
465
732
  })
466
733
  ];
467
734
  }
468
- function generateSelectMethod(methodName, formattedDataTestId) {
735
+ function generateSelectMethod(methodName, selector, parameters) {
469
736
  const name = `select${methodName}`;
470
- const needsKey = formattedDataTestId.includes("${key}");
471
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
737
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
738
+ const selectorExpr = `this.selectorForTestId(${toTypeScriptPomPatternExpression(selector)})`;
472
739
  return [
473
740
  createAsyncMethod(
474
741
  name,
475
- [
476
- createInlineParameter("value", { type: "string" }),
477
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
478
- ],
742
+ createParameters(selectorParams),
479
743
  (writer) => {
480
744
  writer.writeLine(`const selector = ${selectorExpr};`);
481
745
  writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
@@ -484,33 +748,28 @@ function generateSelectMethod(methodName, formattedDataTestId) {
484
748
  )
485
749
  ];
486
750
  }
487
- function generateVSelectMethod(methodName, formattedDataTestId) {
751
+ function generateVSelectMethod(methodName, selector, parameters) {
488
752
  const name = `select${methodName}`;
753
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
489
754
  return [
490
755
  createAsyncMethod(
491
756
  name,
492
- [
493
- createInlineParameter("value", { type: "string" }),
494
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
495
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
496
- ],
757
+ createParameters(selectorParams),
497
758
  (writer) => {
498
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
759
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText);`);
499
760
  }
500
761
  )
501
762
  ];
502
763
  }
503
- function generateTypeMethod(methodName, formattedDataTestId) {
764
+ function generateTypeMethod(methodName, selector, parameters) {
504
765
  const name = `type${methodName}`;
766
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
505
767
  return [
506
768
  createAsyncMethod(
507
769
  name,
508
- [
509
- createInlineParameter("text", { type: "string" }),
510
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
511
- ],
770
+ createParameters(selectorParams),
512
771
  (writer) => {
513
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
772
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText);`);
514
773
  }
515
774
  )
516
775
  ];
@@ -525,30 +784,31 @@ function isAllDigits(value) {
525
784
  }
526
785
  return true;
527
786
  }
528
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
787
+ function generateGetElementByDataTestId(methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
529
788
  const roleSuffix = upperFirst$1(nativeRole || "Element");
530
789
  const baseName = upperFirst$1(methodName);
531
790
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
532
791
  const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
533
792
  const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
534
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
535
- if (needsKey) {
536
- 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";
537
797
  const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
538
798
  return [
539
799
  createClassGetter({
540
800
  name: keyedPropertyName,
541
801
  statements: [
542
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
802
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}));`
543
803
  ]
544
804
  })
545
805
  ];
546
806
  }
547
807
  const finalPropertyName = getterNameOverride ?? propertyName;
548
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
808
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
549
809
  if (alternates.length > 0) {
550
- const all = [formattedDataTestId, ...alternates];
551
- 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})`);
552
812
  return [
553
813
  createClassGetter({
554
814
  name: finalPropertyName,
@@ -559,21 +819,22 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
559
819
  return [
560
820
  createClassGetter({
561
821
  name: finalPropertyName,
562
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
822
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)});`]
563
823
  })
564
824
  ];
565
825
  }
566
826
  function generateNavigationMethod(args) {
567
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
827
+ const { targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
568
828
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
569
- const parameters = createParameters(params);
570
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
571
- 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(", ");
572
833
  if (alternates.length > 0) {
573
834
  return [
574
835
  createClassMethod({
575
836
  name: methodName,
576
- parameters,
837
+ parameters: methodParameters,
577
838
  returnType: `Fluent<${target}>`,
578
839
  statements: (writer) => {
579
840
  writer.write("return this.fluent(async () => ").block(() => {
@@ -601,11 +862,11 @@ function generateNavigationMethod(args) {
601
862
  return [
602
863
  createClassMethod({
603
864
  name: methodName,
604
- parameters,
865
+ parameters: methodParameters,
605
866
  returnType: `Fluent<${target}>`,
606
867
  statements: (writer) => {
607
868
  writer.write("return this.fluent(async () => ").block(() => {
608
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
869
+ writer.writeLine(`await this.clickByTestId(${toTypeScriptPomPatternExpression(selector)});`);
609
870
  writer.writeLine(`return new ${target}(this.page);`);
610
871
  });
611
872
  writer.writeLine(");");
@@ -613,15 +874,15 @@ function generateNavigationMethod(args) {
613
874
  })
614
875
  ];
615
876
  }
616
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
877
+ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
617
878
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
618
879
  const members = generateGetElementByDataTestId(
619
880
  baseMethodName,
620
881
  nativeRole,
621
- formattedDataTestId,
622
- alternateFormattedDataTestIds,
882
+ selector,
883
+ alternateSelectors,
623
884
  getterNameOverride,
624
- params
885
+ parameters
625
886
  );
626
887
  if (targetPageObjectModelClass) {
627
888
  return [
@@ -629,25 +890,25 @@ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName,
629
890
  ...generateNavigationMethod({
630
891
  targetPageObjectModelClass,
631
892
  baseMethodName,
632
- formattedDataTestId,
633
- alternateFormattedDataTestIds,
634
- params
893
+ selector,
894
+ alternateSelectors,
895
+ parameters
635
896
  })
636
897
  ];
637
898
  }
638
899
  if (nativeRole === "select") {
639
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
900
+ return [...members, ...generateSelectMethod(baseMethodName, selector, parameters)];
640
901
  }
641
902
  if (nativeRole === "vselect") {
642
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
903
+ return [...members, ...generateVSelectMethod(baseMethodName, selector, parameters)];
643
904
  }
644
905
  if (nativeRole === "input") {
645
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
906
+ return [...members, ...generateTypeMethod(baseMethodName, selector, parameters)];
646
907
  }
647
908
  if (nativeRole === "radio") {
648
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
909
+ return [...members, ...generateRadioMethod(baseMethodName || "Radio", selector, parameters)];
649
910
  }
650
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
911
+ return [...members, ...generateClickMethod(baseMethodName, selector, alternateSelectors, parameters)];
651
912
  }
652
913
  function isSimpleExpressionNode(value) {
653
914
  return value !== null && "type" in value && value.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION;
@@ -687,43 +948,72 @@ function buildPlaceholderParams(keys) {
687
948
  params[k] = "__placeholder__";
688
949
  return params;
689
950
  }
690
- function getRouteLocationLikeFromToDirective(toDirective) {
691
- if (!toDirective.exp)
692
- return null;
693
- const exp = toDirective.exp;
694
- 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();
695
991
  let expr;
696
992
  try {
697
993
  expr = parser.parseExpression(rawSource, { plugins: ["typescript"] });
698
- } catch {
699
- 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
+ };
700
1001
  }
701
- const isNodeType = (node, type) => {
702
- return node !== null && node.type === type;
703
- };
704
- const isStringLiteralNode = (node) => {
705
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
706
- };
707
- const isIdentifierNode = (node) => {
708
- return isNodeType(node, "Identifier") && typeof node.name === "string";
709
- };
710
- const isObjectPropertyNode = (node) => {
711
- if (!isNodeType(node, "ObjectProperty"))
712
- return false;
713
- const n = node;
714
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
715
- };
716
- const isObjectExpressionNode = (node) => {
717
- if (!isNodeType(node, "ObjectExpression"))
718
- return false;
719
- const n = node;
720
- return Array.isArray(n.properties);
721
- };
722
1002
  if (isStringLiteralNode(expr)) {
723
- return expr.value;
1003
+ return {
1004
+ kind: "resolved",
1005
+ rawSource,
1006
+ target: expr.value,
1007
+ routeNameKey: null,
1008
+ paramKeys: []
1009
+ };
724
1010
  }
725
1011
  if (!isObjectExpressionNode(expr)) {
726
- return null;
1012
+ return {
1013
+ kind: "unsupported",
1014
+ rawSource,
1015
+ reason: "dynamic-expression"
1016
+ };
727
1017
  }
728
1018
  const getStringField = (fieldName) => {
729
1019
  const prop = expr.properties.find((p) => {
@@ -744,7 +1034,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
744
1034
  const key = p.key;
745
1035
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
746
1036
  });
747
- let params;
1037
+ let paramKeys = [];
748
1038
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
749
1039
  const keys = [];
750
1040
  for (const prop of paramsProp.value.properties) {
@@ -756,47 +1046,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
756
1046
  else if (isStringLiteralNode(key))
757
1047
  keys.push(key.value);
758
1048
  }
759
- if (keys.length) {
760
- params = buildPlaceholderParams(Array.from(new Set(keys)));
761
- }
1049
+ paramKeys = Array.from(new Set(keys));
762
1050
  }
763
1051
  if (name) {
764
- 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
+ };
765
1067
  }
766
1068
  if (path2) {
767
- return { path: path2, params };
1069
+ return {
1070
+ kind: "resolved",
1071
+ rawSource,
1072
+ target: { path: path2 },
1073
+ routeNameKey: null,
1074
+ paramKeys
1075
+ };
768
1076
  }
769
- return null;
770
- }
771
- function toDirectiveObjectFieldNameValue$1(toDirective) {
772
- const to = getRouteLocationLikeFromToDirective(toDirective);
773
- if (!to || typeof to === "string")
774
- return null;
775
- const name = to.name;
776
- if (typeof name !== "string")
777
- return null;
778
- const trimmed = name.trim();
779
- if (!trimmed.length)
780
- return null;
781
- return toPascalCaseRouteKey(trimmed);
782
- }
783
- function getRouteNameKeyFromToDirective(toDirective) {
784
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
785
- if (objectName)
786
- return objectName;
787
- return null;
1077
+ return {
1078
+ kind: "unsupported",
1079
+ rawSource,
1080
+ reason: "missing-name-or-path"
1081
+ };
788
1082
  }
789
1083
  function tryResolveToDirectiveTargetComponentName(toDirective) {
790
- const to = getRouteLocationLikeFromToDirective(toDirective);
791
- if (to && resolveToComponentName) {
792
- const resolved = resolveToComponentName(to);
1084
+ const analysis = analyzeToDirectiveTarget(toDirective);
1085
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1086
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
793
1087
  if (resolved)
794
1088
  return resolved;
795
1089
  }
796
- const key = getRouteNameKeyFromToDirective(toDirective);
797
- if (!key || !routeNameToComponentName)
1090
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
798
1091
  return null;
799
- return routeNameToComponentName.get(key) ?? null;
1092
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
800
1093
  }
801
1094
  function getDataTestIdFromGroupOption(text) {
802
1095
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -831,11 +1124,81 @@ function staticAttributeValue(value) {
831
1124
  return { kind: "static", value };
832
1125
  }
833
1126
  function templateAttributeValue(template) {
834
- 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 };
835
1132
  }
836
1133
  function getAttributeValueText(value) {
837
1134
  return value.kind === "static" ? value.value : value.template;
838
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
+ }
839
1202
  function toPascalCase(str) {
840
1203
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
841
1204
  if (!cleaned) {
@@ -877,32 +1240,14 @@ function tryGetClickDirective(node) {
877
1240
  function nodeHasClickDirective(node) {
878
1241
  return tryGetClickDirective(node) !== void 0;
879
1242
  }
880
- function getTemplateSlotScope(node) {
1243
+ function findTemplateSlotScopeExpression(node) {
881
1244
  if (node.tag !== "template") {
882
1245
  return null;
883
1246
  }
884
1247
  const slotProp = node.props.find((prop) => {
885
1248
  return prop.type === compilerCore.NodeTypes.DIRECTIVE && prop.name === "slot";
886
1249
  });
887
- if (slotProp?.exp) {
888
- if (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
889
- return slotProp.exp.content;
890
- }
891
- if (slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) {
892
- return compilerCore.stringifyExpression(slotProp.exp);
893
- }
894
- }
895
- return null;
896
- }
897
- function isSimpleScopeIdentifier(value) {
898
- if (!value) {
899
- return false;
900
- }
901
- try {
902
- return types.isIdentifier(parser.parseExpression(value, { plugins: ["typescript"] }));
903
- } catch {
904
- return false;
905
- }
1250
+ return slotProp?.exp && (slotProp.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
906
1251
  }
907
1252
  function buildSlotScopeFallbackKeyExpression(identifier) {
908
1253
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -1004,43 +1349,64 @@ function tryGetSlotScopeKeyCandidate(node) {
1004
1349
  }
1005
1350
  return best;
1006
1351
  }
1007
- function tryGetTemplateSlotScopeKeyExpression(scope) {
1008
- const trimmed = scope.trim();
1009
- 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) {
1010
1362
  return null;
1011
1363
  }
1012
- if (isSimpleScopeIdentifier(trimmed)) {
1013
- return buildSlotScopeFallbackKeyExpression(trimmed);
1364
+ try {
1365
+ return parser.parseExpression(rawSource, { plugins: ["typescript"] });
1366
+ } catch {
1014
1367
  }
1015
1368
  try {
1016
- const parsed = parser.parse(`(${trimmed}) => {}`, {
1369
+ const parsed = parser.parse(`(${rawSource}) => {}`, {
1017
1370
  sourceType: "module",
1018
1371
  plugins: ["typescript"]
1019
1372
  });
1020
1373
  const statement = parsed.program.body[0];
1021
1374
  if (statement && types.isExpressionStatement(statement) && types.isArrowFunctionExpression(statement.expression)) {
1022
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1375
+ return statement.expression.params[0] ?? null;
1023
1376
  }
1024
1377
  } catch {
1378
+ return null;
1025
1379
  }
1026
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1027
- const inner = trimmed.slice(1, -1).trim();
1028
- let cutIdx = -1;
1029
- const commaIdx = inner.indexOf(",");
1030
- const colonIdx = inner.indexOf(":");
1031
- if (commaIdx !== -1 && colonIdx !== -1) {
1032
- cutIdx = Math.min(commaIdx, colonIdx);
1033
- } else if (commaIdx !== -1) {
1034
- cutIdx = commaIdx;
1035
- } else if (colonIdx !== -1) {
1036
- cutIdx = colonIdx;
1037
- }
1038
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
1039
- if (first && isSimpleScopeIdentifier(first)) {
1040
- return buildSlotScopeFallbackKeyExpression(first);
1041
- }
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;
1042
1399
  }
1043
- 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;
1044
1410
  }
1045
1411
  function nodeHasToDirective(node) {
1046
1412
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1049,51 +1415,160 @@ function nodeHasToDirective(node) {
1049
1415
  }
1050
1416
  return void 0;
1051
1417
  }
1052
- function nodeHasForDirective(node) {
1053
- return node.props.some(
1054
- (attr) => attr.type === compilerCore.NodeTypes.DIRECTIVE && attr.name === "for"
1055
- );
1056
- }
1057
1418
  function getKeyDirective(node) {
1058
1419
  return findDirectiveByName(node, "bind", "key") ?? null;
1059
1420
  }
1060
- function getKeyDirectiveValue(node, _context = null) {
1061
- const keyDirective = getKeyDirective(node);
1062
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1063
- if (rawSource) {
1064
- return `\${${rawSource}}`;
1421
+ function tryUnwrapTemplateLiteralSource(source) {
1422
+ const rawSource = source.trim();
1423
+ if (!rawSource) {
1424
+ return null;
1425
+ }
1426
+ let ast = null;
1427
+ try {
1428
+ ast = parser.parseExpression(rawSource, { plugins: ["typescript"] });
1429
+ } catch {
1430
+ return null;
1065
1431
  }
1066
- if (keyDirective?.exp) {
1067
- const value = compilerCore.stringifyExpression(keyDirective.exp);
1068
- if (value)
1069
- return `\${${value}}`;
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;
1070
1464
  }
1071
- 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);
1072
1551
  }
1073
1552
  function getModelBindingValues(node) {
1074
1553
  let vModel = "";
1075
1554
  const vModelDirective = findDirectiveByName(node, "model");
1076
- if (vModelDirective?.exp?.loc.source) {
1077
- 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"));
1078
1557
  }
1079
1558
  let modelValue = null;
1080
1559
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1081
- if (modelValueDirective?.exp?.ast) {
1082
- 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);
1083
1567
  modelValue = mv;
1084
1568
  }
1085
1569
  return { vModel, modelValue };
1086
1570
  }
1087
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1088
- if (node.isSelfClosing) {
1089
- const hasForDirective = nodeHasForDirective(node);
1090
- if (hasForDirective) {
1091
- return getKeyDirectiveValue(node);
1092
- }
1093
- }
1094
- return null;
1095
- }
1096
- function getIdOrName(node) {
1571
+ function getStaticIdOrNameHint(node) {
1097
1572
  let idAttr = findAttributeByKey(node, "id");
1098
1573
  if (!idAttr) {
1099
1574
  idAttr = findAttributeByKey(node, "name");
@@ -1101,8 +1576,9 @@ function getIdOrName(node) {
1101
1576
  let identifier = idAttr?.value?.content ?? "";
1102
1577
  if (!identifier) {
1103
1578
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1104
- if (dynamicIdAttr?.exp) {
1105
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1579
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1580
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1581
+ return "";
1106
1582
  }
1107
1583
  }
1108
1584
  if (identifier.includes("-")) {
@@ -1113,20 +1589,20 @@ function getIdOrName(node) {
1113
1589
  }
1114
1590
  return identifier;
1115
1591
  }
1116
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1592
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1117
1593
  let parent = getParent(hierarchyMap2, node);
1118
1594
  while (parent) {
1119
1595
  if (parent.type === compilerCore.NodeTypes.ELEMENT && parent.tag === "template") {
1120
- const scope = getTemplateSlotScope(parent);
1121
- if (scope) {
1122
- return tryGetTemplateSlotScopeKeyExpression(scope);
1596
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1597
+ if (slotScopeExpression) {
1598
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1123
1599
  }
1124
1600
  }
1125
1601
  parent = getParent(hierarchyMap2, parent);
1126
1602
  }
1127
1603
  return null;
1128
1604
  }
1129
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1605
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1130
1606
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1131
1607
  return null;
1132
1608
  }
@@ -1135,8 +1611,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1135
1611
  if (parent.type === compilerCore.NodeTypes.ELEMENT) {
1136
1612
  const forDirective = findDirectiveByName(parent, "for");
1137
1613
  if (forDirective) {
1138
- const keyValue = getKeyDirectiveValue(parent);
1139
- return keyValue;
1614
+ return getKeyDirectiveInfo(parent);
1140
1615
  }
1141
1616
  }
1142
1617
  parent = getParent(hierarchyMap2, parent);
@@ -1162,13 +1637,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1162
1637
  if (simpleSourceExp.constType === compilerCore.ConstantTypes.NOT_CONSTANT) {
1163
1638
  return null;
1164
1639
  }
1165
- const iterableRaw = (() => {
1166
- try {
1167
- return compilerCore.stringifyExpression(simpleSourceExp).trim();
1168
- } catch {
1169
- return (simpleSourceExp.loc?.source ?? "").trim();
1170
- }
1171
- })();
1640
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1172
1641
  if (!iterableRaw) {
1173
1642
  return null;
1174
1643
  }
@@ -1224,91 +1693,95 @@ function nodeHandlerAttributeInfo(node) {
1224
1693
  return null;
1225
1694
  }
1226
1695
  const exp = handlerDirective.exp;
1227
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
1696
+ const source = getVueExpressionSource(exp, "content", "compiled");
1228
1697
  if (!source) {
1229
1698
  return null;
1230
1699
  }
1231
1700
  const mergeKey = `handler:expr:${source}`;
1232
- let expr;
1233
- try {
1234
- expr = parser.parseExpression(source, { plugins: ["typescript", "jsx"] });
1235
- } 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) {
1236
1709
  return null;
1237
1710
  }
1238
- const isNodeType = (node2, type) => {
1711
+ const isNodeType2 = (node2, type) => {
1239
1712
  return node2 !== null && node2.type === type;
1240
1713
  };
1241
- const isIdentifierNode = (node2) => {
1242
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1714
+ const isIdentifierNode2 = (node2) => {
1715
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1243
1716
  };
1244
- const isStringLiteralNode = (node2) => {
1245
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1717
+ const isStringLiteralNode2 = (node2) => {
1718
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1246
1719
  };
1247
1720
  const isBooleanLiteralNode = (node2) => {
1248
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1721
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1249
1722
  };
1250
1723
  const isNumericLiteralNode = (node2) => {
1251
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1724
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1252
1725
  };
1253
1726
  const isNullLiteralNode = (node2) => {
1254
- return isNodeType(node2, "NullLiteral");
1727
+ return isNodeType2(node2, "NullLiteral");
1255
1728
  };
1256
1729
  const isMemberExpressionNode = (node2) => {
1257
- if (!isNodeType(node2, "MemberExpression"))
1730
+ if (!isNodeType2(node2, "MemberExpression"))
1258
1731
  return false;
1259
1732
  const n = node2;
1260
1733
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1261
1734
  };
1262
1735
  const isCallExpressionNode = (node2) => {
1263
- if (!isNodeType(node2, "CallExpression"))
1736
+ if (!isNodeType2(node2, "CallExpression"))
1264
1737
  return false;
1265
1738
  const n = node2;
1266
1739
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1267
1740
  };
1268
1741
  const isAwaitExpressionNode = (node2) => {
1269
- if (!isNodeType(node2, "AwaitExpression"))
1742
+ if (!isNodeType2(node2, "AwaitExpression"))
1270
1743
  return false;
1271
1744
  const n = node2;
1272
1745
  return typeof n.argument === "object" && n.argument !== null;
1273
1746
  };
1274
1747
  const isAssignmentExpressionNode = (node2) => {
1275
- if (!isNodeType(node2, "AssignmentExpression"))
1748
+ if (!isNodeType2(node2, "AssignmentExpression"))
1276
1749
  return false;
1277
1750
  const n = node2;
1278
1751
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1279
1752
  };
1280
1753
  const isArrowFunctionExpressionNode = (node2) => {
1281
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1754
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1282
1755
  return false;
1283
1756
  const n = node2;
1284
1757
  return typeof n.body === "object" && n.body !== null;
1285
1758
  };
1286
1759
  const isBlockStatementNode = (node2) => {
1287
- if (!isNodeType(node2, "BlockStatement"))
1760
+ if (!isNodeType2(node2, "BlockStatement"))
1288
1761
  return false;
1289
1762
  const n = node2;
1290
1763
  return Array.isArray(n.body);
1291
1764
  };
1292
1765
  const isExpressionStatementNode = (node2) => {
1293
- if (!isNodeType(node2, "ExpressionStatement"))
1766
+ if (!isNodeType2(node2, "ExpressionStatement"))
1294
1767
  return false;
1295
1768
  const n = node2;
1296
1769
  return typeof n.expression === "object" && n.expression !== null;
1297
1770
  };
1298
1771
  const isReturnStatementNode = (node2) => {
1299
- if (!isNodeType(node2, "ReturnStatement"))
1772
+ if (!isNodeType2(node2, "ReturnStatement"))
1300
1773
  return false;
1301
1774
  const n = node2;
1302
1775
  return typeof n.argument === "object" || n.argument === null;
1303
1776
  };
1304
- const isObjectExpressionNode = (node2) => {
1305
- if (!isNodeType(node2, "ObjectExpression"))
1777
+ const isObjectExpressionNode2 = (node2) => {
1778
+ if (!isNodeType2(node2, "ObjectExpression"))
1306
1779
  return false;
1307
1780
  const n = node2;
1308
1781
  return Array.isArray(n.properties);
1309
1782
  };
1310
- const isObjectPropertyNode = (node2) => {
1311
- if (!isNodeType(node2, "ObjectProperty"))
1783
+ const isObjectPropertyNode2 = (node2) => {
1784
+ if (!isNodeType2(node2, "ObjectProperty"))
1312
1785
  return false;
1313
1786
  const n = node2;
1314
1787
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1316,16 +1789,16 @@ function nodeHandlerAttributeInfo(node) {
1316
1789
  const getLastIdentifierFromMemberChain = (node2) => {
1317
1790
  if (!node2)
1318
1791
  return null;
1319
- if (isIdentifierNode(node2))
1792
+ if (isIdentifierNode2(node2))
1320
1793
  return node2.name;
1321
1794
  if (isMemberExpressionNode(node2)) {
1322
1795
  const prop = node2.property;
1323
1796
  if (node2.computed === false) {
1324
- if (isIdentifierNode(prop))
1797
+ if (isIdentifierNode2(prop))
1325
1798
  return prop.name;
1326
1799
  }
1327
1800
  if (node2.computed === true) {
1328
- if (isStringLiteralNode(prop))
1801
+ if (isStringLiteralNode2(prop))
1329
1802
  return prop.value;
1330
1803
  }
1331
1804
  }
@@ -1335,7 +1808,7 @@ function nodeHandlerAttributeInfo(node) {
1335
1808
  if (!node2) {
1336
1809
  return null;
1337
1810
  }
1338
- if (isIdentifierNode(node2)) {
1811
+ if (isIdentifierNode2(node2)) {
1339
1812
  return node2.name;
1340
1813
  }
1341
1814
  if (isMemberExpressionNode(node2)) {
@@ -1347,11 +1820,11 @@ function nodeHandlerAttributeInfo(node) {
1347
1820
  if (!lhs) {
1348
1821
  return null;
1349
1822
  }
1350
- if (isIdentifierNode(lhs)) {
1823
+ if (isIdentifierNode2(lhs)) {
1351
1824
  return lhs.name;
1352
1825
  }
1353
1826
  if (isMemberExpressionNode(lhs)) {
1354
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1827
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1355
1828
  return getLastIdentifierFromMemberChain(lhs.object);
1356
1829
  }
1357
1830
  return getLastIdentifierFromMemberChain(lhs);
@@ -1359,7 +1832,7 @@ function nodeHandlerAttributeInfo(node) {
1359
1832
  return null;
1360
1833
  };
1361
1834
  const isTemplateLiteralNode = (node2) => {
1362
- if (!isNodeType(node2, "TemplateLiteral")) {
1835
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1363
1836
  return false;
1364
1837
  }
1365
1838
  const n = node2;
@@ -1378,7 +1851,7 @@ function nodeHandlerAttributeInfo(node) {
1378
1851
  if (isNullLiteralNode(arg)) {
1379
1852
  return "Null";
1380
1853
  }
1381
- if (isStringLiteralNode(arg)) {
1854
+ if (isStringLiteralNode2(arg)) {
1382
1855
  const cleaned = (arg.value ?? "").trim();
1383
1856
  if (!cleaned) {
1384
1857
  return null;
@@ -1404,7 +1877,7 @@ function nodeHandlerAttributeInfo(node) {
1404
1877
  return toPascalCase(stableName.slice(0, 24));
1405
1878
  }
1406
1879
  }
1407
- if (isIdentifierNode(arg)) {
1880
+ if (isIdentifierNode2(arg)) {
1408
1881
  const firstChar = arg.name.charAt(0);
1409
1882
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1410
1883
  if (isUpperAlpha) {
@@ -1416,7 +1889,7 @@ function nodeHandlerAttributeInfo(node) {
1416
1889
  const getStableSuffixFromCall = (call) => {
1417
1890
  const args = call.arguments ?? [];
1418
1891
  const first = args.length > 0 ? args[0] : null;
1419
- if (!isObjectExpressionNode(first)) {
1892
+ if (!isObjectExpressionNode2(first)) {
1420
1893
  const parts2 = [];
1421
1894
  for (const arg of args.slice(0, 4)) {
1422
1895
  const w = stableWordFromValue(arg ?? null);
@@ -1435,20 +1908,20 @@ function nodeHandlerAttributeInfo(node) {
1435
1908
  }
1436
1909
  const parts = [];
1437
1910
  for (const prop of first.properties ?? []) {
1438
- if (!isObjectPropertyNode(prop)) {
1911
+ if (!isObjectPropertyNode2(prop)) {
1439
1912
  continue;
1440
1913
  }
1441
1914
  if (prop.computed) {
1442
1915
  continue;
1443
1916
  }
1444
- 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;
1445
1918
  if (!keyName) {
1446
1919
  continue;
1447
1920
  }
1448
1921
  let valueWord = null;
1449
1922
  if (isBooleanLiteralNode(prop.value)) {
1450
1923
  valueWord = prop.value.value ? "True" : "False";
1451
- } else if (isStringLiteralNode(prop.value)) {
1924
+ } else if (isStringLiteralNode2(prop.value)) {
1452
1925
  const cleaned = (prop.value.value ?? "").trim();
1453
1926
  if (cleaned) {
1454
1927
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1577,13 +2050,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1577
2050
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1578
2051
  }
1579
2052
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1580
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1581
- let value = attrDynamic.exp.loc.source;
1582
- 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)) {
1583
2061
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1584
2062
  }
1585
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1586
- value = compilerCore.stringifyExpression(attrDynamic.exp);
2063
+ if (types.isCallExpression(attrDynamicAst) || types.isOptionalCallExpression(attrDynamicAst)) {
2064
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1587
2065
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1588
2066
  }
1589
2067
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1591,16 +2069,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1591
2069
  return null;
1592
2070
  }
1593
2071
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1594
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1595
- if (key) {
1596
- 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)}`);
1597
2075
  } else {
1598
2076
  let name = toDirectiveObjectFieldNameValue(toDirective);
1599
2077
  if (!name) {
1600
2078
  if (toDirective.exp == null) {
1601
2079
  return null;
1602
2080
  }
1603
- const source = compilerCore.stringifyExpression(toDirective.exp);
2081
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1604
2082
  const toAst = toDirective.exp.ast;
1605
2083
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && types.isTemplateLiteral(toAst);
1606
2084
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1624,44 +2102,46 @@ function toDirectiveObjectFieldNameValue(node) {
1624
2102
  if (!node.exp || node.exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
1625
2103
  return null;
1626
2104
  }
1627
- const source = node.exp.loc.source.trim();
1628
- try {
1629
- const expr = parser.parseExpression(source, { plugins: ["typescript"] });
1630
- const isNodeType = (n, type) => {
1631
- return n !== null && n.type === type;
1632
- };
1633
- const isStringLiteralNode = (n) => {
1634
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1635
- };
1636
- const isIdentifierNode = (n) => {
1637
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1638
- };
1639
- const isObjectPropertyNode = (n) => {
1640
- if (!isNodeType(n, "ObjectProperty"))
1641
- return false;
1642
- const nn = n;
1643
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1644
- };
1645
- const isObjectExpressionNode = (n) => {
1646
- if (!isNodeType(n, "ObjectExpression"))
1647
- return false;
1648
- const nn = n;
1649
- return Array.isArray(nn.properties);
1650
- };
1651
- if (!isObjectExpressionNode(expr))
1652
- return null;
1653
- const nameProp = expr.properties.find((p) => {
1654
- if (!isObjectPropertyNode(p))
1655
- return false;
1656
- const key = p.key;
1657
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1658
- });
1659
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1660
- return null;
1661
- return toPascalCase(nameProp.value.value);
1662
- } catch {
2105
+ const expr = tryGetDirectiveBabelAst(node, {
2106
+ preferredViews: ["loc", "compiled"],
2107
+ plugins: ["typescript"],
2108
+ preferExistingAst: false
2109
+ });
2110
+ if (!expr) {
1663
2111
  return null;
1664
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);
1665
2145
  }
1666
2146
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1667
2147
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1671,7 +2151,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1671
2151
  let handlerName = "";
1672
2152
  if (click.exp) {
1673
2153
  const exp = click.exp;
1674
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
2154
+ const source = getVueExpressionSource(exp, "content", "compiled");
1675
2155
  if (source) {
1676
2156
  const parsed = tryParseBabelAstFromHandlerSource(source);
1677
2157
  if (parsed) {
@@ -1692,9 +2172,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1692
2172
  const trimmed = source.trim();
1693
2173
  if (!trimmed)
1694
2174
  return null;
1695
- try {
1696
- return parser.parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1697
- } catch {
2175
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2176
+ if (expressionAst) {
2177
+ return expressionAst;
1698
2178
  }
1699
2179
  try {
1700
2180
  return parser.parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -2033,23 +2513,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2033
2513
  return null;
2034
2514
  }
2035
2515
  const simpleExp = exp;
2036
- const ast = simpleExp.ast;
2037
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
2038
- const tl = ast;
2039
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2040
- const expressionCount = (tl.expressions ?? []).length;
2041
- const isStatic = expressionCount === 0;
2042
- const raw2 = (simpleExp.content ?? "").trim();
2043
- 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;
2044
2523
  if (isStatic) {
2045
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2524
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2046
2525
  }
2526
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2047
2527
  return {
2048
- value: unwrappedTemplate,
2528
+ value: templateValue.template,
2049
2529
  isDynamic: true,
2050
2530
  isStaticLiteral: false,
2051
- template: unwrappedTemplate,
2052
- templateExpressionCount: expressionCount
2531
+ template: templateValue.template,
2532
+ parsedTemplate: templateValue.parsedTemplate,
2533
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2053
2534
  };
2054
2535
  }
2055
2536
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2062,59 +2543,27 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2062
2543
  }
2063
2544
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2064
2545
  if (preservableReference) {
2546
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2065
2547
  return {
2066
2548
  value: preservableReference,
2067
- isDynamic: true,
2068
- isStaticLiteral: false,
2069
- template: `\${${preservableReference}}`,
2070
- templateExpressionCount: 1,
2071
- rawExpression: preservableReference
2072
- };
2073
- }
2074
- const raw = (simpleExp.content ?? "").trim();
2075
- if (!raw) {
2076
- return null;
2077
- }
2078
- try {
2079
- const ast2 = parser.parseExpression(raw, { plugins: ["typescript"] });
2080
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2081
- const tl = ast2;
2082
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2083
- const expressionCount = (tl.expressions ?? []).length;
2084
- const isStatic = expressionCount === 0;
2085
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2086
- if (isStatic) {
2087
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2088
- }
2089
- return {
2090
- value: unwrappedTemplate,
2091
- isDynamic: true,
2092
- isStaticLiteral: false,
2093
- template: unwrappedTemplate,
2094
- templateExpressionCount: expressionCount
2095
- };
2096
- }
2097
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2098
- const sl = ast2;
2099
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2100
- }
2101
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2102
- if (preservableReference2) {
2103
- return {
2104
- value: preservableReference2,
2105
- isDynamic: true,
2106
- isStaticLiteral: false,
2107
- template: `\${${preservableReference2}}`,
2108
- templateExpressionCount: 1,
2109
- rawExpression: preservableReference2
2110
- };
2111
- }
2112
- } 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;
2113
2560
  }
2114
2561
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2115
2562
  }
2116
2563
  function isTemplatePlaceholder(part) {
2117
- 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 === "";
2118
2567
  }
2119
2568
  function isAllCapsOrDigits(value) {
2120
2569
  if (value.length <= 1) {
@@ -2167,33 +2616,14 @@ function safeMethodNameFromParts(parts) {
2167
2616
  }
2168
2617
  return name;
2169
2618
  }
2170
- function replaceAllTemplateExpressionsWithKey(template) {
2619
+ function toPomKeyPattern(templateValue) {
2620
+ const { templateLiteral } = templateValue.parsedTemplate;
2171
2621
  let out = "";
2172
- let i = 0;
2173
- while (i < template.length) {
2174
- const start = template.indexOf("${", i);
2175
- if (start < 0) {
2176
- out += template.slice(i);
2177
- break;
2178
- }
2179
- out += template.slice(i, start);
2180
- let depth = 1;
2181
- let j = start + 2;
2182
- while (j < template.length && depth > 0) {
2183
- if (template[j] === "{") {
2184
- depth++;
2185
- } else if (template[j] === "}") {
2186
- depth--;
2187
- }
2188
- j++;
2189
- }
2190
- const end = depth === 0 ? j - 1 : -1;
2191
- if (end < 0) {
2192
- out += template.slice(start);
2193
- 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}";
2194
2626
  }
2195
- out += "${key}";
2196
- i = end + 1;
2197
2627
  }
2198
2628
  return out;
2199
2629
  }
@@ -2201,8 +2631,8 @@ function applyResolvedDataTestId(args) {
2201
2631
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2202
2632
  const entryOverrides = args.entryOverrides ?? {};
2203
2633
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2204
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2205
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2634
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2635
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2206
2636
  const warn = args.warn;
2207
2637
  const getBestKeyAccessCandidates = (expr) => {
2208
2638
  if (!expr) {
@@ -2211,7 +2641,10 @@ function applyResolvedDataTestId(args) {
2211
2641
  return splitNullishCoalescingExpression(expr);
2212
2642
  };
2213
2643
  let dataTestId = args.preferredGeneratedValue;
2644
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2214
2645
  let fromExisting = false;
2646
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2647
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2215
2648
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2216
2649
  if (existing) {
2217
2650
  const loc = args.element.loc?.start;
@@ -2232,8 +2665,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2232
2665
  if (existingIdBehavior === "preserve") {
2233
2666
  if (existing.isDynamic) {
2234
2667
  if (existing.template) {
2235
- const existingTemplate = existing.template;
2236
- 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) {
2237
2672
  throw new Error(
2238
2673
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2239
2674
  Component: ${args.componentName}
@@ -2243,20 +2678,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2243
2678
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2244
2679
  );
2245
2680
  }
2246
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2247
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2248
- 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) {
2249
2684
  throw new Error(
2250
2685
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2251
2686
  Component: ${args.componentName}
2252
2687
  File: ${file}:${locationHint}
2253
2688
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2254
- 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}"` : ""}
2255
2690
 
2256
- 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.`
2257
2692
  );
2258
2693
  }
2259
- dataTestId = templateAttributeValue(existing.template);
2694
+ dataTestId = existingTemplateValue;
2695
+ runtimeDataTestId = existingTemplateValue;
2260
2696
  fromExisting = true;
2261
2697
  } else {
2262
2698
  throw new Error(
@@ -2270,18 +2706,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2270
2706
  );
2271
2707
  }
2272
2708
  } else {
2273
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2709
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2274
2710
  throw new Error(
2275
2711
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2276
2712
  Component: ${args.componentName}
2277
2713
  File: ${file}:${locationHint}
2278
2714
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2279
- 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}"` : ""}
2280
2716
 
2281
- 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.`
2282
2718
  );
2283
2719
  }
2284
2720
  dataTestId = staticAttributeValue(existing.value);
2721
+ runtimeDataTestId = staticAttributeValue(existing.value);
2285
2722
  fromExisting = true;
2286
2723
  }
2287
2724
  }
@@ -2311,8 +2748,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2311
2748
  };
2312
2749
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2313
2750
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2314
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2315
- 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";
2316
2755
  const deriveBaseMethodNameFromHint = (hint) => {
2317
2756
  const hintRaw = (hint ?? "").trim();
2318
2757
  const trimEdgeSeparators = (value) => {
@@ -2360,13 +2799,13 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2360
2799
  const roleSuffix = upperFirst(normalizedRole || "Element");
2361
2800
  const baseName = upperFirst(primaryMethodName);
2362
2801
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2363
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2802
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2364
2803
  };
2365
2804
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2366
2805
  const roleSuffix = upperFirst(normalizedRole || "Element");
2367
2806
  const baseName = upperFirst(primaryMethodName);
2368
2807
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2369
- if (!isKeyed) {
2808
+ if (!selectorIsParameterized) {
2370
2809
  return { primary: propertyName };
2371
2810
  }
2372
2811
  const stripped = removeByKeySegment2(propertyName);
@@ -2428,11 +2867,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2428
2867
  return false;
2429
2868
  }
2430
2869
  const existingSelectors = [
2431
- existingPom.formattedDataTestId,
2432
- ...existingPom.alternateFormattedDataTestIds ?? []
2870
+ existingPom.selector,
2871
+ ...existingPom.alternateSelectors ?? []
2433
2872
  ];
2434
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2435
- if (isKeyed && !sharesSelectorIdentity) {
2873
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2874
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2436
2875
  return false;
2437
2876
  }
2438
2877
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2447,12 +2886,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2447
2886
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2448
2887
  return false;
2449
2888
  }
2450
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2451
- existingPom.alternateFormattedDataTestIds ??= [];
2452
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2453
- 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);
2454
2893
  }
2455
2894
  }
2895
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
2896
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
2897
+ }
2456
2898
  return true;
2457
2899
  };
2458
2900
  let methodName = "";
@@ -2465,7 +2907,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2465
2907
  let suffix = 1;
2466
2908
  while (true) {
2467
2909
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2468
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2910
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2469
2911
  const actionName = getPrimaryActionMethodName(candidate);
2470
2912
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2471
2913
  let chosenGetterName = getterCandidates.primary;
@@ -2491,7 +2933,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2491
2933
  const baseNameUpper = upperFirst(baseWithSuffix);
2492
2934
  if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
2493
2935
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2494
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2936
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2495
2937
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2496
2938
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2497
2939
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2559,51 +3001,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2559
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".`
2560
3002
  );
2561
3003
  }
2562
- const params = {};
2563
- if (isKeyed) {
2564
- params.key = keyTypeFromValues;
2565
- }
3004
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2566
3005
  switch (normalizedRole) {
2567
3006
  case "input":
2568
- params.text = "string";
2569
- params.annotationText = 'string = ""';
2570
- if (!isKeyed) delete params.key;
3007
+ parameters = setPomParameter(parameters, "text", "string");
3008
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2571
3009
  break;
2572
3010
  case "select":
2573
- params.value = "string";
2574
- params.annotationText = 'string = ""';
2575
- if (!isKeyed) delete params.key;
3011
+ parameters = setPomParameter(parameters, "value", "string");
3012
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2576
3013
  break;
2577
3014
  case "vselect":
2578
- params.value = "string";
2579
- params.timeOut = "number = 500";
2580
- params.annotationText = 'string = ""';
2581
- if (!isKeyed) delete params.key;
3015
+ parameters = setPomParameter(parameters, "value", "string");
3016
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
3017
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2582
3018
  break;
2583
3019
  case "radio":
2584
- params.annotationText = 'string = ""';
3020
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2585
3021
  break;
2586
3022
  }
2587
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2588
- params.key = keyTypeFromValues;
2589
- }
3023
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2590
3024
  if (addHtmlAttribute && !fromExisting) {
2591
3025
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2592
3026
  }
2593
3027
  const childComponentName = args.element.tag;
2594
3028
  const dataTestIdEntry = {
2595
- value: getAttributeValueText(dataTestId),
2596
- templateLiteral: void 0,
2597
- ...entryOverrides
3029
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
3030
+ getAttributeValueText(dataTestId),
3031
+ dataTestId.kind === "template" ? "parameterized" : "static"
3032
+ ),
3033
+ templateLiteral: entryOverrides.templateLiteral,
3034
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2598
3035
  };
2599
3036
  dataTestIdEntry.pom = {
2600
3037
  nativeRole: normalizedRole,
2601
3038
  methodName,
2602
3039
  getterNameOverride,
2603
- formattedDataTestId: formattedDataTestIdForPom,
2604
- alternateFormattedDataTestIds: void 0,
3040
+ selector: selectorPattern,
3041
+ alternateSelectors: void 0,
2605
3042
  mergeKey: args.pomMergeKey,
2606
- params,
3043
+ parameters: normalizedParameters,
2607
3044
  keyValuesOverride: args.keyValuesOverride ?? null
2608
3045
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2609
3046
  };
@@ -2634,43 +3071,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2634
3071
  }
2635
3072
  };
2636
3073
  const getSignatureForGeneratedMethod = () => {
2637
- const role = normalizedRole;
2638
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2639
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2640
- const keyType = keyTypeFromValues;
2641
- if (isNavigation2) {
2642
- if (needsKey2) {
2643
- return { params: `key: ${keyType}`, argNames: ["key"] };
2644
- }
2645
- return { params: "", argNames: [] };
2646
- }
2647
- switch (role) {
2648
- case "input":
2649
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2650
- case "select":
2651
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2652
- case "vselect":
2653
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2654
- case "radio":
2655
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2656
- default:
2657
- if (needsKey2) {
2658
- return { params: `key: ${keyType}`, argNames: ["key"] };
2659
- }
2660
- return { params: "", argNames: [] };
2661
- }
3074
+ return createPomMethodSignature(normalizedParameters);
2662
3075
  };
2663
3076
  const registerPrimaryOnce = (pom) => {
2664
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2665
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3077
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2666
3078
  const key = JSON.stringify({
2667
3079
  kind: "primary",
2668
3080
  role: pom.nativeRole,
2669
3081
  methodName: pom.methodName,
2670
3082
  getterNameOverride: pom.getterNameOverride ?? null,
2671
- formattedDataTestId: pom.formattedDataTestId,
2672
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2673
- params: stableParams,
3083
+ selector: pom.selector,
3084
+ alternateSelectors: alternates.length ? alternates : void 0,
3085
+ parameters: pom.parameters,
2674
3086
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2675
3087
  emitPrimary: pom.emitPrimary ?? true
2676
3088
  });
@@ -2684,8 +3096,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2684
3096
  }
2685
3097
  };
2686
3098
  const addExtraClickMethod = (spec) => {
2687
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2688
- 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 });
2689
3100
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2690
3101
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2691
3102
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2708,7 +3119,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2708
3119
  if (prev === null) {
2709
3120
  return;
2710
3121
  }
2711
- if (signature === null || prev.params !== signature.params) {
3122
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2712
3123
  args.dependencies.generatedMethods.set(name, null);
2713
3124
  }
2714
3125
  };
@@ -2724,23 +3135,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2724
3135
  return candidate;
2725
3136
  };
2726
3137
  const tryGetDirectiveExpressionAst = (dir) => {
2727
- const exp = dir.exp;
2728
- if (!exp) {
2729
- return null;
2730
- }
2731
- if (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
2732
- const simple = exp;
2733
- const ast = simple.ast;
2734
- if (ast && "type" in ast) {
2735
- return ast;
2736
- }
2737
- }
2738
- try {
2739
- const raw = args.context ? compilerCore.stringifyExpression(exp) : exp.loc.source;
2740
- return parser.parseExpression(raw, { plugins: ["typescript"] });
2741
- } catch {
2742
- return null;
2743
- }
3138
+ return tryGetDirectiveBabelAst(dir, {
3139
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3140
+ plugins: ["typescript"]
3141
+ });
2744
3142
  };
2745
3143
  const tryGetStaticStringFromBabel = (node) => {
2746
3144
  if (!node) {
@@ -2837,17 +3235,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2837
3235
  name: generatedName2,
2838
3236
  selector: {
2839
3237
  kind: "withinTestIdByLabel",
2840
- rootFormattedDataTestId: wrapperTestId,
2841
- formattedLabel: label,
3238
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3239
+ label: createPomStringPattern(label, "static"),
2842
3240
  exact: true
2843
3241
  },
2844
- params: { annotationText: `string = ""` }
3242
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2845
3243
  });
2846
3244
  if (added2) {
2847
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3245
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2848
3246
  }
2849
3247
  }
2850
- return;
3248
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2851
3249
  }
2852
3250
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2853
3251
  if (dataTestIdEntry.pom) {
@@ -2859,19 +3257,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2859
3257
  name: generatedName,
2860
3258
  selector: {
2861
3259
  kind: "withinTestIdByLabel",
2862
- rootFormattedDataTestId: wrapperTestId,
2863
- formattedLabel: "${value}",
3260
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3261
+ label: createPomStringPattern("${value}", "parameterized"),
2864
3262
  exact: true
2865
3263
  },
2866
- params: { value: "string", annotationText: `string = ""` }
3264
+ parameters: [
3265
+ createPomParameterSpec("value", "string"),
3266
+ createPomParameterSpec("annotationText", `string = ""`)
3267
+ ]
2867
3268
  });
2868
3269
  if (added) {
2869
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3270
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3271
+ createPomParameterSpec("value", "string"),
3272
+ createPomParameterSpec("annotationText", `string = ""`)
3273
+ ]));
2870
3274
  }
2871
- return;
3275
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2872
3276
  }
2873
3277
  const staticKeyValues = args.keyValuesOverride ?? null;
2874
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3278
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2875
3279
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2876
3280
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2877
3281
  if (dataTestIdEntry.pom) {
@@ -2890,16 +3294,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2890
3294
  name: generatedName,
2891
3295
  selector: {
2892
3296
  kind: "testId",
2893
- formattedDataTestId: formattedDataTestIdForPom
3297
+ testId: selectorPattern
2894
3298
  },
2895
3299
  keyLiteral: rawValue,
2896
- params: { wait: "boolean = true" }
3300
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2897
3301
  });
2898
3302
  if (added) {
2899
- registerGeneratedMethodSignature(generatedName, { params: `wait: boolean = true`, argNames: ["wait"] });
3303
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3304
+ createPomParameterSpec("wait", "boolean = true"),
3305
+ createPomParameterSpec("annotationText", 'string = ""')
3306
+ ]));
2900
3307
  }
2901
3308
  }
2902
- return;
3309
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2903
3310
  }
2904
3311
  if (dataTestIdEntry.pom) {
2905
3312
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2913,6 +3320,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2913
3320
  const generatedName = getGeneratedMethodName();
2914
3321
  registerGeneratedMethodSignature(generatedName, signature);
2915
3322
  }
3323
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2916
3324
  }
2917
3325
  function safeRealpath(value) {
2918
3326
  try {
@@ -3900,113 +4308,6 @@ class VuePomGeneratorError extends Error {
3900
4308
  this.name = "VuePomGeneratorError";
3901
4309
  }
3902
4310
  }
3903
- function splitParameterList(parameters) {
3904
- const parts = [];
3905
- let current = "";
3906
- let braceDepth = 0;
3907
- let bracketDepth = 0;
3908
- let parenDepth = 0;
3909
- let angleDepth = 0;
3910
- let inSingleQuote = false;
3911
- let inDoubleQuote = false;
3912
- let inTemplateString = false;
3913
- for (let index = 0; index < parameters.length; index += 1) {
3914
- const char = parameters[index];
3915
- const previous = index > 0 ? parameters[index - 1] : "";
3916
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3917
- inSingleQuote = !inSingleQuote;
3918
- current += char;
3919
- continue;
3920
- }
3921
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3922
- inDoubleQuote = !inDoubleQuote;
3923
- current += char;
3924
- continue;
3925
- }
3926
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3927
- inTemplateString = !inTemplateString;
3928
- current += char;
3929
- continue;
3930
- }
3931
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3932
- current += char;
3933
- continue;
3934
- }
3935
- switch (char) {
3936
- case "{":
3937
- braceDepth += 1;
3938
- break;
3939
- case "}":
3940
- braceDepth -= 1;
3941
- break;
3942
- case "[":
3943
- bracketDepth += 1;
3944
- break;
3945
- case "]":
3946
- bracketDepth -= 1;
3947
- break;
3948
- case "(":
3949
- parenDepth += 1;
3950
- break;
3951
- case ")":
3952
- parenDepth -= 1;
3953
- break;
3954
- case "<":
3955
- angleDepth += 1;
3956
- break;
3957
- case ">":
3958
- angleDepth -= 1;
3959
- break;
3960
- case ",":
3961
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
3962
- const trimmed2 = current.trim();
3963
- if (trimmed2) {
3964
- parts.push(trimmed2);
3965
- }
3966
- current = "";
3967
- continue;
3968
- }
3969
- break;
3970
- }
3971
- current += char;
3972
- }
3973
- const trimmed = current.trim();
3974
- if (trimmed) {
3975
- parts.push(trimmed);
3976
- }
3977
- return parts;
3978
- }
3979
- function parseParameterSignature(parameter) {
3980
- const colonIndex = parameter.indexOf(":");
3981
- if (colonIndex < 0) {
3982
- return { name: parameter.trim() };
3983
- }
3984
- const rawName = parameter.slice(0, colonIndex).trim();
3985
- const hasQuestionToken = rawName.endsWith("?");
3986
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
3987
- const remainder = parameter.slice(colonIndex + 1).trim();
3988
- const initializerIndex = remainder.lastIndexOf("=");
3989
- if (initializerIndex < 0) {
3990
- return {
3991
- name,
3992
- hasQuestionToken,
3993
- type: remainder || void 0
3994
- };
3995
- }
3996
- return {
3997
- name,
3998
- hasQuestionToken,
3999
- type: remainder.slice(0, initializerIndex).trim() || void 0,
4000
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
4001
- };
4002
- }
4003
- function parseParameterSignatures(parameters) {
4004
- const trimmed = parameters.trim();
4005
- if (!trimmed) {
4006
- return [];
4007
- }
4008
- return splitParameterList(trimmed).map(parseParameterSignature);
4009
- }
4010
4311
  function toPosixRelativePath(fromDir, toFile) {
4011
4312
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
4012
4313
  if (!rel.startsWith(".")) {
@@ -4026,6 +4327,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
4026
4327
  const root = projectRoot ?? process.cwd();
4027
4328
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
4028
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
+ }
4029
4345
  function createCustomPomImportCollisionError(exportName, requested) {
4030
4346
  return new VuePomGeneratorError(
4031
4347
  `Custom POM import name collision detected for "${exportName}".
@@ -4164,35 +4480,28 @@ function generateGoToSelfMethod(componentName) {
4164
4480
  })
4165
4481
  ];
4166
4482
  }
4167
- function formatMethodParams(params) {
4168
- if (!params)
4169
- return "";
4170
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4171
- const entries = Object.entries(params);
4172
- if (!entries.length)
4173
- return "";
4174
- const score = (name) => {
4175
- const idx = preferredOrder.indexOf(name);
4176
- return idx < 0 ? 999 : idx;
4177
- };
4178
- 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];
4179
4485
  }
4180
4486
  function generateExtraClickMethodMembers(spec) {
4181
4487
  if (spec.kind !== "click") {
4182
4488
  return [];
4183
4489
  }
4184
- const params = spec.params ?? {};
4185
- const signatureParams = formatMethodParams(params);
4186
- const parameters = parseParameterSignatures(signatureParams);
4187
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4188
- 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");
4189
4499
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4190
4500
  const waitArg = hasWait ? "wait" : "true";
4191
4501
  if (spec.selector.kind === "testId") {
4192
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4193
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4502
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4194
4503
  const clickArgs = [];
4195
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4504
+ clickArgs.push(testIdBinding.expression);
4196
4505
  if (hasAnnotationText || hasWait) {
4197
4506
  clickArgs.push(annotationArg);
4198
4507
  }
@@ -4208,20 +4517,16 @@ function generateExtraClickMethodMembers(spec) {
4208
4517
  if (spec.keyLiteral !== void 0) {
4209
4518
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4210
4519
  }
4211
- if (needsTemplate) {
4212
- writer.writeLine(`const testId = ${testIdExpr};`);
4520
+ for (const statement of testIdBinding.setupStatements) {
4521
+ writer.writeLine(statement);
4213
4522
  }
4214
4523
  writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4215
4524
  }
4216
4525
  })
4217
4526
  ];
4218
4527
  }
4219
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4220
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4221
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4222
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4223
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4224
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4528
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4529
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4225
4530
  return [
4226
4531
  createClassMethod({
4227
4532
  name: spec.name,
@@ -4231,13 +4536,13 @@ function generateExtraClickMethodMembers(spec) {
4231
4536
  if (spec.keyLiteral !== void 0) {
4232
4537
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4233
4538
  }
4234
- if (rootNeedsTemplate) {
4235
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4539
+ for (const statement of rootBinding.setupStatements) {
4540
+ writer.writeLine(statement);
4236
4541
  }
4237
- if (labelNeedsTemplate) {
4238
- writer.writeLine(`const label = ${labelExpr};`);
4542
+ for (const statement of labelBinding.setupStatements) {
4543
+ writer.writeLine(statement);
4239
4544
  }
4240
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4545
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg});`);
4241
4546
  }
4242
4547
  })
4243
4548
  ];
@@ -4250,10 +4555,10 @@ function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4250
4555
  targetPageObjectModelClass,
4251
4556
  primary.methodName,
4252
4557
  primary.nativeRole,
4253
- primary.formattedDataTestId,
4254
- primary.alternateFormattedDataTestIds,
4558
+ primary.selector,
4559
+ primary.alternateSelectors,
4255
4560
  primary.getterNameOverride,
4256
- primary.params ?? {}
4561
+ primary.parameters
4257
4562
  );
4258
4563
  }
4259
4564
  function generateMethodsContentForDependencies(dependencies) {
@@ -4261,15 +4566,14 @@ function generateMethodsContentForDependencies(dependencies) {
4261
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));
4262
4567
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4263
4568
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4264
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4265
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4569
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4266
4570
  const key = JSON.stringify({
4267
4571
  role: pom.nativeRole,
4268
4572
  methodName: pom.methodName,
4269
4573
  getterNameOverride: pom.getterNameOverride ?? null,
4270
- testId: pom.formattedDataTestId,
4271
- alternateTestIds: alternates.length ? alternates : void 0,
4272
- params: stableParams,
4574
+ selector: pom.selector,
4575
+ alternateSelectors: alternates.length ? alternates : void 0,
4576
+ parameters: pom.parameters,
4273
4577
  target: target ?? null,
4274
4578
  emitPrimary: pom.emitPrimary ?? true
4275
4579
  });
@@ -4296,6 +4600,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4296
4600
  customPomAttachments = [],
4297
4601
  projectRoot,
4298
4602
  customPomDir,
4603
+ requireCustomPomDir,
4299
4604
  customPomImportAliases,
4300
4605
  customPomImportNameCollisionBehavior = "error",
4301
4606
  testIdAttribute,
@@ -4328,6 +4633,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4328
4633
  customPomAttachments,
4329
4634
  projectRoot,
4330
4635
  customPomDir,
4636
+ requireCustomPomDir,
4331
4637
  customPomImportAliases,
4332
4638
  customPomImportNameCollisionBehavior,
4333
4639
  testIdAttribute,
@@ -4337,6 +4643,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4337
4643
  customPomAttachments,
4338
4644
  projectRoot,
4339
4645
  customPomDir,
4646
+ requireCustomPomDir,
4340
4647
  customPomImportAliases,
4341
4648
  customPomImportNameCollisionBehavior,
4342
4649
  testIdAttribute,
@@ -4392,9 +4699,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4392
4699
  }
4393
4700
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4394
4701
  customPomDir: options.customPomDir,
4702
+ requireCustomPomDir: options.requireCustomPomDir,
4395
4703
  customPomImportAliases: options.customPomImportAliases,
4396
4704
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4397
4705
  });
4706
+ assertCustomPomAttachmentsResolved(
4707
+ options.customPomAttachments ?? [],
4708
+ customPomImportResolution.classIdentifierMap,
4709
+ options.customPomDir ?? "tests/playwright/pom/custom"
4710
+ );
4398
4711
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4399
4712
  const files = [];
4400
4713
  for (const [name, deps] of entries) {
@@ -4553,21 +4866,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4553
4866
  return { filePath, content };
4554
4867
  });
4555
4868
  }
4556
- function toCSharpTestIdExpression(formattedDataTestId) {
4557
- const needsInterpolation = formattedDataTestId.includes("${");
4558
- if (!needsInterpolation) {
4559
- return JSON.stringify(formattedDataTestId);
4560
- }
4561
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4562
- const quoted = JSON.stringify(inner);
4563
- return `$${quoted}`;
4564
- }
4565
- function toCSharpParam(paramTypeExpr) {
4566
- const trimmed = (paramTypeExpr ?? "").trim();
4567
- const eqIdx = trimmed.indexOf("=");
4568
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4569
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4570
- const typePart = left.includes("|") ? "string" : left;
4869
+ function toCSharpParam(param) {
4870
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4571
4871
  let type = "string";
4572
4872
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4573
4873
  type = "bool";
@@ -4575,19 +4875,17 @@ function toCSharpParam(paramTypeExpr) {
4575
4875
  type = "string";
4576
4876
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4577
4877
  type = "int";
4578
- else if (/\d+/.test(typePart) && typePart === "")
4579
- type = "int";
4580
4878
  else if (/\btimeOut\b/i.test(typePart))
4581
4879
  type = "int";
4582
4880
  let defaultExpr;
4583
- if (right !== void 0) {
4881
+ if (param.initializer !== void 0) {
4584
4882
  if (type === "bool") {
4585
- 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;
4586
4884
  } else if (type === "int") {
4587
- const m = right.match(/\d+/);
4885
+ const m = param.initializer.match(/\d+/);
4588
4886
  defaultExpr = m ? m[0] : void 0;
4589
4887
  } else {
4590
- if (right === '""' || right === '""' || right === "''") {
4888
+ if (param.initializer === '""' || param.initializer === "''") {
4591
4889
  defaultExpr = '""';
4592
4890
  }
4593
4891
  }
@@ -4595,17 +4893,15 @@ function toCSharpParam(paramTypeExpr) {
4595
4893
  return { type, defaultExpr };
4596
4894
  }
4597
4895
  function formatCSharpParams(params) {
4598
- if (!params)
4599
- return { signature: "", argNames: [] };
4600
- const entries = Object.entries(params);
4601
- if (!entries.length)
4896
+ const normalizedParams = normalizePomParameters(params);
4897
+ if (!normalizedParams.length)
4602
4898
  return { signature: "", argNames: [] };
4603
4899
  const signatureParts = [];
4604
4900
  const argNames = [];
4605
- for (const [name, typeExpr] of entries) {
4606
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4607
- argNames.push(name);
4608
- 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}`);
4609
4905
  }
4610
4906
  return { signature: signatureParts.join(", "), argNames };
4611
4907
  }
@@ -4696,23 +4992,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4696
4992
  const baseMethodName = upperFirst(pom.methodName);
4697
4993
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4698
4994
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4699
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4700
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4701
- const templateVars = templateVarMatches.map((m) => m[1]);
4702
- const augmentedParams = { ...pom.params };
4703
- for (const v of templateVars) {
4704
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4705
- augmentedParams[v] = "string";
4706
- }
4707
- }
4708
- const orderedParams = Object.fromEntries([
4709
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4710
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4711
- ]);
4995
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
4996
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
4997
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4712
4998
  const { signature, argNames } = formatCSharpParams(orderedParams);
4713
4999
  const args = argNames.join(", ");
4714
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4715
- if (pom.formattedDataTestId.includes("${")) {
5000
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
5001
+ if (selectorIsParameterized) {
4716
5002
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4717
5003
  } else {
4718
5004
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4723,12 +5009,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4723
5009
  if (target) {
4724
5010
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4725
5011
  chunks.push(" {");
4726
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4727
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
5012
+ if (selectorIsParameterized || allTestIds.length <= 1) {
5013
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4728
5014
  chunks.push(` return new ${target}(Page);`);
4729
5015
  } else {
4730
5016
  chunks.push(" Exception? lastError = null;");
4731
- 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(", ")} })`);
4732
5018
  chunks.push(" {");
4733
5019
  chunks.push(" try");
4734
5020
  chunks.push(" {");
@@ -4752,7 +5038,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4752
5038
  }
4753
5039
  chunks.push(` public async Task ${actionName}(${sig})`);
4754
5040
  chunks.push(" {");
4755
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5041
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4756
5042
  const emitActionCall = (locatorAccess) => {
4757
5043
  if (pom.nativeRole === "input") {
4758
5044
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4765,9 +5051,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4765
5051
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4766
5052
  }
4767
5053
  };
4768
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5054
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4769
5055
  chunks.push(" Exception? lastError = null;");
4770
- 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(", ")} })`);
4771
5057
  chunks.push(" {");
4772
5058
  chunks.push(" try");
4773
5059
  chunks.push(" {");
@@ -4813,7 +5099,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4813
5099
  for (const extra of extras) {
4814
5100
  if (extra.kind !== "click")
4815
5101
  continue;
4816
- 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);
4817
5108
  const extraName = upperFirst(extra.name);
4818
5109
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4819
5110
  chunks.push(" {");
@@ -4821,29 +5112,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4821
5112
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4822
5113
  }
4823
5114
  if (extra.selector.kind === "testId") {
4824
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4825
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4826
- if (needsTemplate) {
4827
- chunks.push(` var testId = ${testIdExpr};`);
4828
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4829
- } else {
4830
- 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}`);
4831
5118
  }
5119
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4832
5120
  } else {
4833
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4834
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4835
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4836
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5121
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5122
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4837
5123
  const exactArg = extra.selector.exact === false ? "false" : "true";
4838
- if (rootNeedsTemplate) {
4839
- chunks.push(` var rootTestId = ${rootExpr};`);
5124
+ for (const statement of rootBinding.setupStatements) {
5125
+ chunks.push(` ${statement}`);
4840
5126
  }
4841
- if (labelNeedsTemplate) {
4842
- chunks.push(` var label = ${labelExpr};`);
5127
+ for (const statement of labelBinding.setupStatements) {
5128
+ chunks.push(` ${statement}`);
4843
5129
  }
4844
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4845
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4846
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5130
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4847
5131
  }
4848
5132
  chunks.push(" }");
4849
5133
  chunks.push("");
@@ -5287,7 +5571,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5287
5571
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5288
5572
  continue;
5289
5573
  const list = methodToChildren.get(name) ?? [];
5290
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5574
+ list.push({ childProp, signature: sig });
5291
5575
  methodToChildren.set(name, list);
5292
5576
  }
5293
5577
  }
@@ -5297,12 +5581,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5297
5581
  return [];
5298
5582
  }
5299
5583
  return passthroughs.map(([methodName, candidates]) => {
5300
- const { childProp, params, argNames } = candidates[0];
5301
- const callArgs = argNames.join(", ");
5584
+ const { childProp, signature } = candidates[0];
5585
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5302
5586
  return createClassMethod({
5303
5587
  name: methodName,
5304
5588
  isAsync: true,
5305
- parameters: parseParameterSignatures(params),
5589
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5306
5590
  statements: [
5307
5591
  `return await this.${childProp}.${methodName}(${callArgs});`
5308
5592
  ]
@@ -5326,8 +5610,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5326
5610
  const list = methodToAttachments.get(methodName) ?? [];
5327
5611
  list.push({
5328
5612
  propertyName: attachment.propertyName,
5329
- params: signature.params,
5330
- argNames: signature.argNames
5613
+ signature
5331
5614
  });
5332
5615
  methodToAttachments.set(methodName, list);
5333
5616
  }
@@ -5338,12 +5621,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5338
5621
  return [];
5339
5622
  }
5340
5623
  return passthroughs.map(([methodName, candidates]) => {
5341
- const { propertyName, params, argNames } = candidates[0];
5342
- const callArgs = argNames.join(", ");
5624
+ const { propertyName, signature } = candidates[0];
5625
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5343
5626
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5344
5627
  return createClassMethod({
5345
5628
  name: methodName,
5346
- parameters: parseParameterSignatures(params),
5629
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5347
5630
  statements: [
5348
5631
  `return ${invocation};`
5349
5632
  ]
@@ -5357,15 +5640,44 @@ function sliceNodeSource(source, node) {
5357
5640
  const snippet = source.slice(node.start, node.end).trim();
5358
5641
  return snippet.length ? snippet : null;
5359
5642
  }
5360
- 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) {
5361
5652
  if (param.type === "Identifier") {
5362
- return param.name;
5653
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5654
+ hasQuestionToken: !!param.optional
5655
+ });
5363
5656
  }
5364
5657
  if (param.type === "AssignmentPattern") {
5365
- 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
+ });
5366
5669
  }
5367
5670
  if (param.type === "RestElement") {
5368
- 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
+ });
5369
5681
  }
5370
5682
  return null;
5371
5683
  }
@@ -5398,29 +5710,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5398
5710
  if (member.key.type !== "Identifier") {
5399
5711
  continue;
5400
5712
  }
5401
- const params = [];
5402
- const argNames = [];
5713
+ const parameters = [];
5403
5714
  let supported = true;
5404
5715
  member.params.forEach((param) => {
5405
5716
  if (!supported) {
5406
5717
  return;
5407
5718
  }
5408
- const paramSource = sliceNodeSource(source, param);
5409
- const argName = getCustomPomCallArgumentName(param);
5410
- if (!paramSource || !argName) {
5719
+ const parameter = getCustomPomParameterSpec(source, param);
5720
+ if (!parameter) {
5411
5721
  supported = false;
5412
5722
  return;
5413
5723
  }
5414
- params.push(paramSource);
5415
- argNames.push(argName);
5724
+ parameters.push(parameter);
5416
5725
  });
5417
5726
  if (!supported) {
5418
5727
  continue;
5419
5728
  }
5420
- signatures.set(member.key.name, {
5421
- params: params.join(", "),
5422
- argNames
5423
- });
5729
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5424
5730
  }
5425
5731
  }
5426
5732
  return signatures;
@@ -5603,6 +5909,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5603
5909
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5604
5910
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5605
5911
  if (!fs.existsSync(customDirAbs)) {
5912
+ if (options.requireCustomPomDir) {
5913
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
5914
+ }
5606
5915
  return {
5607
5916
  classIdentifierMap,
5608
5917
  methodSignaturesByClass,
@@ -5645,6 +5954,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5645
5954
  importSpecifiersByClass
5646
5955
  };
5647
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
+ }
5648
5965
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5649
5966
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5650
5967
  if (!filePath)
@@ -5673,7 +5990,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5673
5990
  if (!sig)
5674
5991
  continue;
5675
5992
  const list = methodToChildren.get(name) ?? [];
5676
- list.push({ child, params: sig.params, argNames: sig.argNames });
5993
+ list.push({ child, signature: sig });
5677
5994
  methodToChildren.set(name, list);
5678
5995
  }
5679
5996
  }
@@ -5681,12 +5998,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5681
5998
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5682
5999
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5683
6000
  continue;
5684
- const { child, params, argNames } = candidatesForMethod[0];
5685
- const callArgs = argNames.join(", ");
6001
+ const { child, signature } = candidatesForMethod[0];
6002
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5686
6003
  passthroughMembers.push(createClassMethod({
5687
6004
  name: methodName,
5688
6005
  isAsync: true,
5689
- parameters: parseParameterSignatures(params),
6006
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5690
6007
  statements: [
5691
6008
  `return await this.${child}.${methodName}(${callArgs});`
5692
6009
  ]
@@ -5735,9 +6052,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5735
6052
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5736
6053
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5737
6054
  customPomDir: options.customPomDir,
6055
+ requireCustomPomDir: options.requireCustomPomDir,
5738
6056
  customPomImportAliases: options.customPomImportAliases,
5739
6057
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5740
6058
  });
6059
+ assertCustomPomAttachmentsResolved(
6060
+ options.customPomAttachments ?? [],
6061
+ customPomImportResolution.classIdentifierMap,
6062
+ options.customPomDir ?? "tests/playwright/pom/custom"
6063
+ );
5741
6064
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5742
6065
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5743
6066
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5887,8 +6210,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5887
6210
  return candidate;
5888
6211
  };
5889
6212
  for (const dt of dataTestIdSet) {
5890
- const raw = dt.value;
5891
- if (raw.includes("${")) {
6213
+ const raw = dt.selectorValue.formatted;
6214
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5892
6215
  continue;
5893
6216
  }
5894
6217
  const toggleSuffix = "-toggle";
@@ -5978,7 +6301,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
5978
6301
  });
5979
6302
  }
5980
6303
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
5981
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
5982
6304
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
5983
6305
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
5984
6306
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6240,7 +6562,7 @@ function getConditionalDirectiveInfo(element) {
6240
6562
  if (directive.name === "else") {
6241
6563
  const exp2 = directive.exp;
6242
6564
  if (exp2 && (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || exp2.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6243
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6565
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6244
6566
  return { kind: "else-if", source: source2 };
6245
6567
  }
6246
6568
  return { kind: "else", source: "" };
@@ -6249,13 +6571,13 @@ function getConditionalDirectiveInfo(element) {
6249
6571
  const exp2 = directive.exp;
6250
6572
  if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp2.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6251
6573
  return null;
6252
- const source2 = (exp2.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp2.content : compilerCore.stringifyExpression(exp2)).trim();
6574
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6253
6575
  return { kind: "else-if", source: source2 };
6254
6576
  }
6255
6577
  const exp = directive.exp;
6256
6578
  if (!exp || exp.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION && exp.type !== compilerCore.NodeTypes.COMPOUND_EXPRESSION)
6257
6579
  return null;
6258
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
6580
+ const source = getVueExpressionSource(exp, "content", "compiled");
6259
6581
  return { kind: directive.name, source };
6260
6582
  }
6261
6583
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6281,26 +6603,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6281
6603
  };
6282
6604
  try {
6283
6605
  const expr = parser.parseExpression(src, { plugins: ["typescript"] });
6284
- const isNodeType = (n, type) => {
6606
+ const isNodeType2 = (n, type) => {
6285
6607
  return n !== null && n.type === type;
6286
6608
  };
6287
- const isStringLiteralNode = (n) => {
6288
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6609
+ const isStringLiteralNode2 = (n) => {
6610
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6289
6611
  };
6290
- const isIdentifierNode = (n) => {
6291
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6612
+ const isIdentifierNode2 = (n) => {
6613
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6292
6614
  };
6293
6615
  const results = [];
6294
6616
  const walk = (n) => {
6295
6617
  if (!n)
6296
6618
  return;
6297
- if (isStringLiteralNode(n)) {
6619
+ if (isStringLiteralNode2(n)) {
6298
6620
  const v = (n.value ?? "").trim();
6299
6621
  if (isIdentifierish(v)) {
6300
6622
  results.push(v);
6301
6623
  }
6302
6624
  }
6303
- if (isIdentifierNode(n)) {
6625
+ if (isIdentifierNode2(n)) {
6304
6626
  const v = (n.name ?? "").trim();
6305
6627
  if (isIdentifierish(v)) {
6306
6628
  results.push(v);
@@ -6450,34 +6772,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6450
6772
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6451
6773
  return null;
6452
6774
  }
6453
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6775
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6454
6776
  const jsStringLiteral = (value) => {
6455
6777
  return JSON.stringify(value);
6456
6778
  };
6457
6779
  const getTestIdExpressionForNode = () => {
6458
- const existing = findTestIdAttribute(element, testIdAttribute);
6459
- if (!existing) {
6460
- return "undefined";
6461
- }
6462
- if (existing.type === compilerCore.NodeTypes.ATTRIBUTE) {
6463
- const v = existing.value?.content;
6464
- if (!v) {
6465
- return "undefined";
6466
- }
6467
- return jsStringLiteral(v);
6468
- }
6469
- const directive = existing;
6470
- const exp2 = directive.exp;
6471
- if (!exp2 || exp2.type !== compilerCore.NodeTypes.SIMPLE_EXPRESSION) {
6780
+ if (!resolvedRuntimeTestId) {
6472
6781
  return "undefined";
6473
6782
  }
6474
- const content = (exp2.content ?? "").trim();
6475
- if (!content) {
6476
- return "undefined";
6783
+ if (resolvedRuntimeTestId.kind === "static") {
6784
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6477
6785
  }
6478
- return `(${content})`;
6786
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6479
6787
  };
6480
- const testIdExpression2 = getTestIdExpressionForNode();
6788
+ const testIdExpression = getTestIdExpressionForNode();
6481
6789
  const clickDirective = tryGetClickDirective(element);
6482
6790
  if (!clickDirective)
6483
6791
  return;
@@ -6488,10 +6796,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6488
6796
  const exp = clickDirective.exp;
6489
6797
  if (!exp)
6490
6798
  return;
6491
- const existingSource = (exp.loc?.source ?? (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6799
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6492
6800
  if (existingSource.includes(CLICK_EVENT_NAME))
6493
6801
  return;
6494
- const originalExpression = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6802
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6495
6803
  if (!originalExpression)
6496
6804
  return;
6497
6805
  const isStatementBody = (() => {
@@ -6508,7 +6816,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6508
6816
  const statementWrappedHandler = `($event) => {
6509
6817
  const __win = ($event && $event.view) ? $event.view : undefined;
6510
6818
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6511
- const __testIdFromNode = ${testIdExpression2};
6819
+ const __testIdFromNode = ${testIdExpression};
6512
6820
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6513
6821
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6514
6822
  const __emit = (phase, err) => {
@@ -6519,16 +6827,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6519
6827
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6520
6828
  }
6521
6829
  } catch (e) {
6522
- // Instrumentation must never hide failures during e2e strict mode.
6523
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6524
- // 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.
6525
6831
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6526
6832
  if (__w && __w.console && typeof __w.console.error === 'function') {
6527
6833
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6528
6834
  }
6529
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6530
- throw e;
6531
- }
6835
+ throw e;
6532
6836
  }
6533
6837
  };
6534
6838
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6559,7 +6863,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6559
6863
  const expressionWrappedHandler = `($event) => {
6560
6864
  const __win = ($event && $event.view) ? $event.view : undefined;
6561
6865
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6562
- const __testIdFromNode = ${testIdExpression2};
6866
+ const __testIdFromNode = ${testIdExpression};
6563
6867
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6564
6868
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6565
6869
  const __emit = (phase, err) => {
@@ -6570,16 +6874,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6570
6874
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6571
6875
  }
6572
6876
  } catch (e) {
6573
- // Instrumentation must never hide failures during e2e strict mode.
6574
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6575
- // 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.
6576
6878
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6577
6879
  if (__w && __w.console && typeof __w.console.error === 'function') {
6578
6880
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6579
6881
  }
6580
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6581
- throw e;
6582
- }
6882
+ throw e;
6583
6883
  }
6584
6884
  };
6585
6885
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6617,10 +6917,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6617
6917
  let previousFileName = "";
6618
6918
  const hierarchyMap = /* @__PURE__ */ new Map();
6619
6919
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6620
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
6920
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6621
6921
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6622
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6623
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
6922
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6624
6923
  const warn = options.warn;
6625
6924
  const vueFilesPathMap = options.vueFilesPathMap;
6626
6925
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6696,7 +6995,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6696
6995
  conditionalHintByIfBranch.set(branch, hint);
6697
6996
  continue;
6698
6997
  }
6699
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
6998
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6700
6999
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6701
7000
  if (stable) {
6702
7001
  conditionalHintByIfBranch.set(branch, stable);
@@ -6758,17 +7057,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6758
7057
  nativeWrappers[element.tag] = { role: "grid" };
6759
7058
  }
6760
7059
  }
6761
- const getBestAvailableKeyValue = () => {
7060
+ const getBestAvailableKeyInfo = () => {
6762
7061
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6763
7062
  const isDirectVForChild = parentNode?.type === compilerCore.NodeTypes.FOR;
6764
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6765
- if (vForKey) return vForKey;
6766
- 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);
6767
7068
  };
6768
- const bestKeyInferred = getBestAvailableKeyValue();
6769
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6770
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6771
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7069
+ const bestKeyInfo = getBestAvailableKeyInfo();
7070
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7071
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6772
7072
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6773
7073
  const parentKey = context?.parent ? context.parent : null;
6774
7074
  const conditional = getConditionalDirectiveInfo(element);
@@ -6811,7 +7111,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6811
7111
  if (!cond) {
6812
7112
  conditionalHint = "else";
6813
7113
  } else {
6814
- const condSource = (cond.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? cond.content : compilerCore.stringifyExpression(cond)).trim();
7114
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6815
7115
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6816
7116
  }
6817
7117
  }
@@ -6821,7 +7121,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6821
7121
  });
6822
7122
  if (showDirective?.exp && (showDirective.exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === compilerCore.NodeTypes.COMPOUND_EXPRESSION)) {
6823
7123
  const exp = showDirective.exp;
6824
- const source = (exp.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION ? exp.content : compilerCore.stringifyExpression(exp)).trim();
7124
+ const source = getVueExpressionSource(exp, "content", "compiled");
6825
7125
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6826
7126
  if (showHint) {
6827
7127
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6855,13 +7155,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6855
7155
  const tagSuffix = getTagSuffix();
6856
7156
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6857
7157
  };
7158
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7159
+ const tagSuffix = getTagSuffix();
7160
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7161
+ };
6858
7162
  const getSubmitDataTestId = (identifier) => {
6859
7163
  const tagSuffix = getTagSuffix();
6860
7164
  return `${componentName}-${identifier}${tagSuffix}`;
6861
7165
  };
6862
7166
  const applyResolvedDataTestIdForElement = (args) => {
6863
7167
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6864
- applyResolvedDataTestId({
7168
+ return applyResolvedDataTestId({
6865
7169
  element,
6866
7170
  componentName,
6867
7171
  parentComponentName,
@@ -6871,8 +7175,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6871
7175
  generatedMethodContentByComponent,
6872
7176
  nativeRole,
6873
7177
  preferredGeneratedValue: args.preferredGeneratedValue,
6874
- bestKeyPlaceholder,
6875
- bestKeyVariable,
7178
+ preferredRuntimeValue: args.preferredRuntimeValue,
7179
+ keyInfo: bestKeyInfo,
6876
7180
  keyValuesOverride,
6877
7181
  entryOverrides: args.entryOverrides,
6878
7182
  semanticNameHint: args.semanticNameHint,
@@ -6890,7 +7194,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6890
7194
  return p.type === compilerCore.NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6891
7195
  }) ?? null;
6892
7196
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6893
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7197
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6894
7198
  const loc = element.loc?.start;
6895
7199
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6896
7200
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6899,7 +7203,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6899
7203
  Element: <${element.tag}>
6900
7204
  Handler: ${handlerSource}
6901
7205
 
6902
- 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.`
6903
7207
  );
6904
7208
  }
6905
7209
  if (nativeWrappersValue) {
@@ -7026,20 +7330,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7026
7330
  contextFilename: context.filename
7027
7331
  });
7028
7332
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
7029
- const idOrName = getIdOrName(element) || void 0;
7333
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
7030
7334
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
7031
7335
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
7032
7336
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
7033
7337
  const testId = getClickDataTestId(clickSuffix);
7034
- applyResolvedDataTestIdForElement({
7338
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7339
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
7035
7340
  preferredGeneratedValue: testId,
7341
+ preferredRuntimeValue: runtimeTestId,
7036
7342
  semanticNameHint: semanticNameHint2,
7037
7343
  semanticNameHintAlternates,
7038
7344
  pomMergeKey
7039
7345
  });
7040
- {
7041
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7042
- }
7346
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7043
7347
  return;
7044
7348
  }
7045
7349
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7049,7 +7353,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7049
7353
  if (!isRecognizedInteractiveRole) {
7050
7354
  return;
7051
7355
  }
7052
- 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;
7053
7357
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7054
7358
  applyResolvedDataTestIdForElement({
7055
7359
  preferredGeneratedValue,
@@ -7059,7 +7363,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7059
7363
  }
7060
7364
  const isSubmit = element.props.find((p) => p.type === compilerCore.NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7061
7365
  if (isSubmit) {
7062
- const identifier = getIdOrName(element) || innerText;
7366
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7063
7367
  if (!identifier) {
7064
7368
  const loc = element.loc?.start;
7065
7369
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7111,29 +7415,32 @@ function createBuildProcessorPlugin(options) {
7111
7415
  getSourceDirs,
7112
7416
  basePageClassPath,
7113
7417
  normalizedBasePagePath,
7418
+ generation,
7419
+ projectRootRef,
7420
+ nativeWrappers,
7421
+ excludedComponents,
7422
+ getWrapperSearchRoots,
7423
+ getResolvedRouterEntry,
7424
+ loggerRef
7425
+ } = options;
7426
+ const {
7114
7427
  outDir,
7115
7428
  emitLanguages,
7116
7429
  typescriptOutputStructure,
7117
7430
  csharp,
7118
7431
  generateFixtures,
7119
7432
  customPomAttachments,
7120
- projectRootRef,
7121
7433
  customPomDir,
7434
+ requireCustomPomDir,
7122
7435
  customPomImportAliases,
7123
7436
  customPomImportNameCollisionBehavior,
7124
7437
  testIdAttribute,
7125
7438
  nameCollisionBehavior,
7126
- missingSemanticNameBehavior = "error",
7127
7439
  existingIdBehavior,
7128
- nativeWrappers,
7129
- excludedComponents,
7130
- getWrapperSearchRoots,
7131
7440
  routerAwarePoms,
7132
- getResolvedRouterEntry,
7133
7441
  routerType,
7134
- routerModuleShims,
7135
- loggerRef
7136
- } = options;
7442
+ routerModuleShims
7443
+ } = generation;
7137
7444
  let lastGeneratedMetrics = {
7138
7445
  entryCount: 0,
7139
7446
  interactiveComponentCount: 0,
@@ -7232,10 +7539,9 @@ function createBuildProcessorPlugin(options) {
7232
7539
  excludedComponents,
7233
7540
  getViewsDirAbs(),
7234
7541
  {
7235
- existingIdBehavior: existingIdBehavior ?? "preserve",
7542
+ existingIdBehavior: existingIdBehavior ?? "error",
7236
7543
  testIdAttribute,
7237
7544
  nameCollisionBehavior,
7238
- missingSemanticNameBehavior,
7239
7545
  warn: (message) => loggerRef.current.warn(message),
7240
7546
  vueFilesPathMap,
7241
7547
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7335,6 +7641,7 @@ function createBuildProcessorPlugin(options) {
7335
7641
  customPomAttachments,
7336
7642
  projectRoot: projectRootRef.current,
7337
7643
  customPomDir,
7644
+ requireCustomPomDir,
7338
7645
  customPomImportAliases,
7339
7646
  customPomImportNameCollisionBehavior,
7340
7647
  testIdAttribute,
@@ -7366,6 +7673,11 @@ function createDevProcessorPlugin(options) {
7366
7673
  projectRootRef,
7367
7674
  normalizedBasePagePath,
7368
7675
  basePageClassPath,
7676
+ generation,
7677
+ getResolvedRouterEntry,
7678
+ loggerRef
7679
+ } = options;
7680
+ const {
7369
7681
  outDir,
7370
7682
  emitLanguages,
7371
7683
  typescriptOutputStructure,
@@ -7373,18 +7685,16 @@ function createDevProcessorPlugin(options) {
7373
7685
  generateFixtures,
7374
7686
  customPomAttachments,
7375
7687
  customPomDir,
7688
+ requireCustomPomDir,
7376
7689
  customPomImportAliases,
7377
7690
  customPomImportNameCollisionBehavior,
7378
- nameCollisionBehavior = "suffix",
7379
- missingSemanticNameBehavior = "error",
7691
+ nameCollisionBehavior,
7380
7692
  existingIdBehavior,
7381
7693
  testIdAttribute,
7382
7694
  routerAwarePoms,
7383
- getResolvedRouterEntry,
7384
7695
  routerType,
7385
- routerModuleShims,
7386
- loggerRef
7387
- } = options;
7696
+ routerModuleShims
7697
+ } = generation;
7388
7698
  let scheduleVueFileRegen = null;
7389
7699
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7390
7700
  path.resolve(projectRootRef.current),
@@ -7571,9 +7881,8 @@ function createDevProcessorPlugin(options) {
7571
7881
  excludedComponents,
7572
7882
  getViewsDirAbs(),
7573
7883
  {
7574
- existingIdBehavior: existingIdBehavior ?? "preserve",
7884
+ existingIdBehavior: existingIdBehavior ?? "error",
7575
7885
  nameCollisionBehavior,
7576
- missingSemanticNameBehavior,
7577
7886
  testIdAttribute,
7578
7887
  warn: (message) => loggerRef.current.warn(message),
7579
7888
  vueFilesPathMap: provisionalVuePathMap,
@@ -7624,6 +7933,7 @@ function createDevProcessorPlugin(options) {
7624
7933
  customPomAttachments,
7625
7934
  projectRoot: projectRootRef.current,
7626
7935
  customPomDir,
7936
+ requireCustomPomDir,
7627
7937
  customPomImportAliases,
7628
7938
  customPomImportNameCollisionBehavior,
7629
7939
  pageDirs: getPageDirs(),
@@ -7859,27 +8169,30 @@ function createSupportPlugins(options) {
7859
8169
  getViewsDir,
7860
8170
  getSourceDirs,
7861
8171
  getWrapperSearchRoots,
7862
- nameCollisionBehavior = "suffix",
7863
- missingSemanticNameBehavior = "error",
7864
- existingIdBehavior,
8172
+ generation,
8173
+ projectRootRef,
8174
+ basePageClassPath: basePageClassPathOverride,
8175
+ loggerRef
8176
+ } = options;
8177
+ const {
7865
8178
  outDir,
7866
8179
  emitLanguages,
7867
8180
  typescriptOutputStructure,
7868
8181
  csharp,
7869
- routerAwarePoms,
7870
- routerEntry,
7871
- routerType,
7872
- routerModuleShims,
7873
8182
  generateFixtures,
7874
8183
  customPomAttachments,
7875
- projectRootRef,
7876
- basePageClassPath: basePageClassPathOverride,
7877
8184
  customPomDir,
8185
+ requireCustomPomDir,
7878
8186
  customPomImportAliases,
7879
8187
  customPomImportNameCollisionBehavior,
8188
+ nameCollisionBehavior,
8189
+ existingIdBehavior,
7880
8190
  testIdAttribute,
7881
- loggerRef
7882
- } = options;
8191
+ routerAwarePoms,
8192
+ routerEntry,
8193
+ routerType,
8194
+ routerModuleShims
8195
+ } = generation;
7883
8196
  const resolveRouterEntry2 = () => {
7884
8197
  if (!routerAwarePoms)
7885
8198
  return void 0;
@@ -7908,27 +8221,12 @@ function createSupportPlugins(options) {
7908
8221
  getSourceDirs,
7909
8222
  basePageClassPath,
7910
8223
  normalizedBasePagePath,
7911
- outDir,
7912
- emitLanguages,
7913
- typescriptOutputStructure,
7914
- csharp,
7915
- generateFixtures,
7916
- customPomAttachments,
8224
+ generation,
7917
8225
  projectRootRef,
7918
- customPomDir,
7919
- customPomImportAliases,
7920
- customPomImportNameCollisionBehavior,
7921
- testIdAttribute,
7922
- nameCollisionBehavior,
7923
- missingSemanticNameBehavior,
7924
- existingIdBehavior,
7925
8226
  nativeWrappers,
7926
8227
  excludedComponents,
7927
8228
  getWrapperSearchRoots,
7928
- routerAwarePoms,
7929
- routerType,
7930
8229
  getResolvedRouterEntry: resolveRouterEntry2,
7931
- routerModuleShims,
7932
8230
  loggerRef
7933
8231
  });
7934
8232
  const devProcessor = createDevProcessorPlugin({
@@ -7943,23 +8241,8 @@ function createSupportPlugins(options) {
7943
8241
  projectRootRef,
7944
8242
  normalizedBasePagePath,
7945
8243
  basePageClassPath,
7946
- outDir,
7947
- emitLanguages,
7948
- typescriptOutputStructure,
7949
- csharp,
7950
- generateFixtures,
7951
- customPomAttachments,
7952
- customPomDir,
7953
- customPomImportAliases,
7954
- customPomImportNameCollisionBehavior,
7955
- nameCollisionBehavior,
7956
- missingSemanticNameBehavior,
7957
- existingIdBehavior,
7958
- testIdAttribute,
7959
- routerAwarePoms,
7960
- routerType,
8244
+ generation,
7961
8245
  getResolvedRouterEntry: resolveRouterEntry2,
7962
- routerModuleShims,
7963
8246
  loggerRef
7964
8247
  });
7965
8248
  const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
@@ -8060,6 +8343,13 @@ function tryCreateElementMetadata(args) {
8060
8343
  };
8061
8344
  return metadata;
8062
8345
  }
8346
+ function resolveCompilerSfcParse(compilerSfc2) {
8347
+ const parse = compilerSfc2.parse ?? compilerSfc2.default?.parse;
8348
+ if (typeof parse !== "function") {
8349
+ throw new TypeError("[vue-pom-generator] Failed to resolve @vue/compiler-sfc.parse.");
8350
+ }
8351
+ return parse;
8352
+ }
8063
8353
  function extractMetadataAfterTransform(ast, componentName, elementMetadata, semanticNameMap, testIdAttribute) {
8064
8354
  const componentMetadata = /* @__PURE__ */ new Map();
8065
8355
  function traverseNode(node) {
@@ -8256,7 +8546,8 @@ function createVuePluginWithTestIds(options) {
8256
8546
  }
8257
8547
  const componentName = getComponentNameFromPath(cleanPath);
8258
8548
  loggerRef.current.debug(`Collecting metadata for ${cleanPath} (component: ${componentName})`);
8259
- const { parse } = await import("@vue/compiler-sfc");
8549
+ const compilerSfc2 = await import("@vue/compiler-sfc");
8550
+ const parse = resolveCompilerSfcParse(compilerSfc2);
8260
8551
  const compilerDom2 = await import("@vue/compiler-dom");
8261
8552
  const compile = compilerDom2.compile;
8262
8553
  const { descriptor } = parse(code, { filename: cleanPath });
@@ -8288,8 +8579,7 @@ function createVuePluginWithTestIds(options) {
8288
8579
  });
8289
8580
  const api = viteVuePlugin?.api;
8290
8581
  if (!api) {
8291
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8292
- return;
8582
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8293
8583
  }
8294
8584
  const currentOptions = api.options ?? {};
8295
8585
  const currentTemplate = currentOptions.template ?? {};
@@ -8355,33 +8645,6 @@ function assertOneOf(value, allowed, name) {
8355
8645
  }
8356
8646
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8357
8647
  }
8358
- function assertErrorBehavior(value, name) {
8359
- if (!value) {
8360
- return;
8361
- }
8362
- if (value === "ignore" || value === "error") {
8363
- return;
8364
- }
8365
- if (typeof value !== "object" || Array.isArray(value)) {
8366
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8367
- }
8368
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8369
- for (const key of Object.keys(value)) {
8370
- if (!supportedKeys.has(key)) {
8371
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8372
- }
8373
- }
8374
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8375
- }
8376
- function resolveMissingSemanticNameBehavior(value) {
8377
- if (!value) {
8378
- return "error";
8379
- }
8380
- if (value === "ignore" || value === "error") {
8381
- return value;
8382
- }
8383
- return value.missingSemanticNameBehavior ?? "error";
8384
- }
8385
8648
  function readPackageJson(projectRoot) {
8386
8649
  const packageJsonPath = path.join(projectRoot, "package.json");
8387
8650
  if (!fs.existsSync(packageJsonPath)) {
@@ -8490,7 +8753,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8490
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(...).'
8491
8754
  );
8492
8755
  }
8493
- 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.");
8494
8757
  }
8495
8758
  const currentOptions = viteVuePlugin.api.options ?? {};
8496
8759
  const currentTemplate = currentOptions.template ?? {};
@@ -8547,18 +8810,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8547
8810
  const vueGenerationOptions = generationOptions;
8548
8811
  const verbosity = options.logging?.verbosity ?? "warn";
8549
8812
  const vueOptions = options.vueOptions;
8550
- const legacyVueOptions = options;
8551
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8552
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8553
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8554
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8555
- const nativeWrappers = injection.nativeWrappers ?? {};
8556
- const excludedComponents = injection.excludeComponents ?? [];
8557
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8558
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8559
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8560
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8561
- 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;
8562
8830
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8563
8831
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8564
8832
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8567,27 +8835,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8567
8835
  }
8568
8836
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8569
8837
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8570
- const csharp = generationOptions?.csharp;
8571
- const errorBehavior = options.errorBehavior;
8572
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8573
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8574
8838
  const generateFixtures = generationOptions?.playwright?.fixtures;
8575
8839
  const customPoms = generationOptions?.playwright?.customPoms;
8576
8840
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8577
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8578
8841
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8579
- 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
+ });
8580
8862
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8581
- const getPageDirs = () => pageDirsRef.current;
8863
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8582
8864
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8583
- const getComponentDirs = () => componentDirsRef.current;
8584
- const getLayoutDirs = () => layoutDirsRef.current;
8865
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
8866
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8585
8867
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8586
8868
  ...getPageDirs(),
8587
8869
  ...getComponentDirs(),
8588
8870
  ...getLayoutDirs()
8589
8871
  ]));
8590
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
8872
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8591
8873
  const sharedStateKey = JSON.stringify({
8592
8874
  cwd: process.cwd(),
8593
8875
  mode: isNuxt ? "nuxt" : "vue",
@@ -8595,9 +8877,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8595
8877
  componentDirs: isNuxt ? null : getComponentDirs(),
8596
8878
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8597
8879
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8598
- outDir,
8880
+ outDir: resolvedGenerationOptions.outDir,
8599
8881
  testIdAttribute,
8600
- routerType,
8882
+ routerType: resolvedGenerationOptions.routerType,
8601
8883
  vuePluginOwnership
8602
8884
  });
8603
8885
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8618,23 +8900,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8618
8900
  if (isNuxt) {
8619
8901
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8620
8902
  projectRootRef.current = nuxtDiscovery.rootDir;
8621
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8622
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8623
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8624
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
8903
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
8904
+ resolvedInjectionOptionsRef.current,
8905
+ nuxtDiscovery
8906
+ );
8625
8907
  }
8626
8908
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8627
8909
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8628
8910
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8629
8911
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8630
8912
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8631
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8632
8913
  if (generationEnabled) {
8633
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8634
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8635
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8636
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8637
- 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");
8638
8919
  }
8639
8920
  }
8640
8921
  if (usesExternalVuePlugin) {
@@ -8656,8 +8937,8 @@ function createVuePomGeneratorPlugins(options = {}) {
8656
8937
  const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8657
8938
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8658
8939
  vueOptions,
8659
- existingIdBehavior,
8660
- nameCollisionBehavior,
8940
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
8941
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8661
8942
  nativeWrappers,
8662
8943
  elementMetadata,
8663
8944
  semanticNameMap,
@@ -8672,7 +8953,6 @@ function createVuePomGeneratorPlugins(options = {}) {
8672
8953
  getProjectRoot: () => projectRootRef.current
8673
8954
  });
8674
8955
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8675
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8676
8956
  const supportPlugins = createSupportPlugins({
8677
8957
  componentTestIds,
8678
8958
  componentHierarchyMap,
@@ -8685,26 +8965,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8685
8965
  getViewsDir,
8686
8966
  getSourceDirs,
8687
8967
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8688
- nameCollisionBehavior,
8689
- missingSemanticNameBehavior,
8690
- existingIdBehavior,
8691
- outDir,
8692
- emitLanguages,
8693
- typescriptOutputStructure,
8694
- csharp,
8695
- routerAwarePoms,
8696
- routerEntry,
8697
- generateFixtures,
8968
+ generation: resolvedGenerationOptions,
8698
8969
  projectRootRef,
8699
8970
  basePageClassPath: basePageClassPathOverride,
8700
- customPomAttachments: resolvedCustomPomAttachments,
8701
- customPomDir: resolvedCustomPomDir,
8702
- customPomImportAliases: resolvedCustomPomImportAliases,
8703
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8704
- testIdAttribute,
8705
- loggerRef,
8706
- routerType,
8707
- routerModuleShims
8971
+ loggerRef
8708
8972
  });
8709
8973
  if (isNuxt) {
8710
8974
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");