@immense/vue-pom-generator 1.0.58 → 1.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +6 -18
  2. package/RELEASE_NOTES.md +75 -29
  3. package/class-generation/base-page.ts +6 -13
  4. package/class-generation/index.ts +226 -317
  5. package/class-generation/playwright-types.ts +1 -1
  6. package/click-instrumentation.ts +0 -4
  7. package/dist/class-generation/base-page.d.ts +1 -0
  8. package/dist/class-generation/base-page.d.ts.map +1 -1
  9. package/dist/class-generation/index.d.ts +2 -0
  10. package/dist/class-generation/index.d.ts.map +1 -1
  11. package/dist/class-generation/playwright-types.d.ts +1 -1
  12. package/dist/class-generation/playwright-types.d.ts.map +1 -1
  13. package/dist/click-instrumentation.d.ts +0 -1
  14. package/dist/click-instrumentation.d.ts.map +1 -1
  15. package/dist/index.cjs +1216 -1008
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1218 -1010
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/method-generation.d.ts +4 -2
  20. package/dist/method-generation.d.ts.map +1 -1
  21. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  22. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  23. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  24. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  25. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  26. package/dist/plugin/support/build-plugin.d.ts +2 -29
  27. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  28. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  29. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support-plugins.d.ts +2 -32
  31. package/dist/plugin/support-plugins.d.ts.map +1 -1
  32. package/dist/plugin/types.d.ts +6 -23
  33. package/dist/plugin/types.d.ts.map +1 -1
  34. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  35. package/dist/pom-params.d.ts +40 -0
  36. package/dist/pom-params.d.ts.map +1 -0
  37. package/dist/pom-patterns.d.ts +31 -0
  38. package/dist/pom-patterns.d.ts.map +1 -0
  39. package/dist/routing/to-directive.d.ts +21 -0
  40. package/dist/routing/to-directive.d.ts.map +1 -1
  41. package/dist/tests/base-page.test.d.ts +2 -0
  42. package/dist/tests/base-page.test.d.ts.map +1 -0
  43. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  44. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  45. package/dist/transform.d.ts +0 -1
  46. package/dist/transform.d.ts.map +1 -1
  47. package/dist/utils.d.ts +129 -63
  48. package/dist/utils.d.ts.map +1 -1
  49. package/package.json +6 -4
  50. package/sequence-diagram.md +6 -6
package/dist/index.mjs CHANGED
@@ -8,8 +8,8 @@ import { parse as parse$2, NodeTypes as NodeTypes$1 } from "@vue/compiler-dom";
8
8
  import { parse as parse$1, compileScript } from "@vue/compiler-sfc";
9
9
  import { parseExpression, parse } from "@babel/parser";
10
10
  import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression, ElementTypes } from "@vue/compiler-core";
11
- import { Project, QuoteKind, NewLineKind, IndentationText, StructureKind, CodeBlockWriter, VariableDeclarationKind } from "ts-morph";
12
- import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isArrowFunctionExpression, isOptionalMemberExpression, isObjectExpression, isFile, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
11
+ import { Project, QuoteKind, NewLineKind, IndentationText, CodeBlockWriter, StructureKind, VariableDeclarationKind } from "ts-morph";
12
+ import { isArrayExpression, isStringLiteral, isTemplateLiteral, isMemberExpression, isOptionalMemberExpression, isCallExpression, isOptionalCallExpression, isAssignmentExpression, isIdentifier, isExpressionStatement, isObjectExpression, isFile, isArrowFunctionExpression, isBlockStatement, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
13
13
  import { performance } from "node:perf_hooks";
14
14
  import vue from "@vitejs/plugin-vue";
15
15
  const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
@@ -204,6 +204,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
204
204
  const nuxtOptions = await loadNuxtConfig({ cwd });
205
205
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
206
206
  }
207
+ function resolveGenerationSupportOptions(options) {
208
+ return {
209
+ outDir: (options.outDir ?? "tests/playwright/__generated__").trim(),
210
+ emitLanguages: options.emitLanguages?.length ? options.emitLanguages : ["ts"],
211
+ typescriptOutputStructure: options.typescriptOutputStructure ?? "aggregated",
212
+ csharp: options.csharp,
213
+ generateFixtures: options.generateFixtures,
214
+ customPomAttachments: options.customPomAttachments ?? [],
215
+ customPomDir: options.customPomDir ?? "tests/playwright/pom/custom",
216
+ requireCustomPomDir: options.requireCustomPomDir ?? false,
217
+ customPomImportAliases: options.customPomImportAliases,
218
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior ?? "error",
219
+ nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
220
+ existingIdBehavior: options.existingIdBehavior ?? "error",
221
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
222
+ routerAwarePoms: options.routerAwarePoms ?? false,
223
+ routerEntry: options.routerEntry,
224
+ routerType: options.routerType ?? "vue-router",
225
+ routerModuleShims: options.routerModuleShims
226
+ };
227
+ }
228
+ function resolveInjectionSupportOptions(options) {
229
+ const isNuxt = options.isNuxt ?? false;
230
+ return {
231
+ pageDirs: isNuxt ? ["app/pages"] : [options.viewsDir ?? "src/views"],
232
+ componentDirs: isNuxt ? ["app/components"] : options.componentDirs ?? ["src/components"],
233
+ layoutDirs: isNuxt ? ["app/layouts"] : options.layoutDirs ?? ["src/layouts"],
234
+ wrapperSearchRoots: isNuxt ? [] : options.wrapperSearchRoots ?? [],
235
+ nativeWrappers: options.nativeWrappers ?? {},
236
+ excludedComponents: options.excludedComponents ?? [],
237
+ existingIdBehavior: options.existingIdBehavior ?? "error",
238
+ testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid"
239
+ };
240
+ }
241
+ function applyNuxtDiscoveryToInjectionOptions(options, discovery) {
242
+ return {
243
+ ...options,
244
+ pageDirs: discovery.pageDirs.length ? discovery.pageDirs : [path.resolve(discovery.srcDir, "pages")],
245
+ componentDirs: discovery.componentDirs,
246
+ layoutDirs: discovery.layoutDirs,
247
+ wrapperSearchRoots: discovery.wrapperSearchRoots
248
+ };
249
+ }
207
250
  function createTypeScriptProject() {
208
251
  return new Project({
209
252
  useInMemoryFileSystem: true,
@@ -295,16 +338,7 @@ function createClassConstructor(constructorDeclaration) {
295
338
  ...constructorDeclaration
296
339
  };
297
340
  }
298
- function upperFirst$1(value) {
299
- if (!value) {
300
- return value;
301
- }
302
- return value.charAt(0).toUpperCase() + value.slice(1);
303
- }
304
- function hasParam(params, name) {
305
- return Object.prototype.hasOwnProperty.call(params, name);
306
- }
307
- function splitTypeAndInitializer(typeExpression) {
341
+ function splitPomParameterTypeExpression(typeExpression) {
308
342
  const trimmed = typeExpression.trim();
309
343
  const initializerIndex = trimmed.lastIndexOf("=");
310
344
  if (initializerIndex < 0) {
@@ -315,49 +349,237 @@ function splitTypeAndInitializer(typeExpression) {
315
349
  initializer: trimmed.slice(initializerIndex + 1).trim()
316
350
  };
317
351
  }
318
- function createParameter(name, typeExpression) {
319
- const { type, initializer } = splitTypeAndInitializer(typeExpression);
352
+ function createPomParameterSpec(name, typeExpression, options = {}) {
353
+ const normalizedTypeExpression = typeExpression?.trim();
354
+ const { type, initializer } = normalizedTypeExpression ? splitPomParameterTypeExpression(normalizedTypeExpression) : { type: void 0, initializer: void 0 };
320
355
  return {
321
356
  name,
322
- type: type || void 0,
323
- initializer
357
+ typeExpression: normalizedTypeExpression,
358
+ type,
359
+ initializer: options.initializer ?? initializer,
360
+ hasQuestionToken: options.hasQuestionToken,
361
+ isRestParameter: options.isRestParameter
324
362
  };
325
363
  }
326
- function createParameters(params) {
327
- return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
364
+ function normalizePomParameters(params) {
365
+ if (!params) {
366
+ return [];
367
+ }
368
+ return params.map((param) => createPomParameterSpec(param.name, param.typeExpression ?? param.type, {
369
+ initializer: param.initializer,
370
+ hasQuestionToken: param.hasQuestionToken,
371
+ isRestParameter: param.isRestParameter
372
+ }));
328
373
  }
329
- function createInlineParameter(name, options = {}) {
374
+ function getPomParameterNames(params) {
375
+ return normalizePomParameters(params).map((param) => param.name);
376
+ }
377
+ function getPomParameter(params, name) {
378
+ return normalizePomParameters(params).find((param) => param.name === name);
379
+ }
380
+ function hasPomParameter(params, name) {
381
+ return !!getPomParameter(params, name);
382
+ }
383
+ function setPomParameter(params, name, typeExpression, options = {}) {
384
+ const nextParam = createPomParameterSpec(name, typeExpression, options);
385
+ const normalizedParams = normalizePomParameters(params);
386
+ const existingIndex = normalizedParams.findIndex((param) => param.name === name);
387
+ if (existingIndex < 0) {
388
+ return [...normalizedParams, nextParam];
389
+ }
390
+ const nextParams = normalizedParams.slice();
391
+ nextParams[existingIndex] = nextParam;
392
+ return nextParams;
393
+ }
394
+ function removePomParameter(params, name) {
395
+ return normalizePomParameters(params).filter((param) => param.name !== name);
396
+ }
397
+ function toTypeScriptPomParameterStructures(params) {
398
+ return normalizePomParameters(params).map((param) => ({
399
+ name: param.name,
400
+ type: param.type || void 0,
401
+ initializer: param.initializer,
402
+ hasQuestionToken: param.hasQuestionToken,
403
+ isRestParameter: param.isRestParameter
404
+ }));
405
+ }
406
+ function getPomParameterArgumentNames(params) {
407
+ return normalizePomParameters(params).map((param) => param.isRestParameter ? `...${param.name}` : param.name);
408
+ }
409
+ function createPomMethodSignature(parameters) {
330
410
  return {
331
- name,
332
- type: options.type,
333
- initializer: options.initializer
411
+ parameters: normalizePomParameters(parameters)
334
412
  };
335
413
  }
336
- function removeByKeySegment(value) {
337
- const idx = value.lastIndexOf("ByKey");
338
- if (idx < 0) {
339
- return value;
414
+ function pomParameterSpecEquals(left, right) {
415
+ return left.name === right.name && left.typeExpression === right.typeExpression && left.type === right.type && left.initializer === right.initializer && left.hasQuestionToken === right.hasQuestionToken && left.isRestParameter === right.isRestParameter;
416
+ }
417
+ function pomParameterListEquals(left, right) {
418
+ const leftParams = normalizePomParameters(left);
419
+ const rightParams = normalizePomParameters(right);
420
+ if (leftParams.length !== rightParams.length) {
421
+ return false;
340
422
  }
341
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
423
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
342
424
  }
343
- function uniqueAlternates(primary, alternates) {
425
+ function pomMethodSignatureEquals(left, right) {
426
+ return pomParameterListEquals(left.parameters, right.parameters);
427
+ }
428
+ function isParameterizedPomPattern(kind) {
429
+ return kind === "parameterized";
430
+ }
431
+ function getTemplateVariables(formatted) {
344
432
  const out = [];
345
433
  const seen = /* @__PURE__ */ new Set();
346
- seen.add(primary);
347
- for (const a of alternates ?? []) {
348
- if (!a) {
434
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
435
+ for (const match of matches) {
436
+ const variableName = match[1];
437
+ if (seen.has(variableName)) {
349
438
  continue;
350
439
  }
351
- if (seen.has(a)) {
440
+ seen.add(variableName);
441
+ out.push(variableName);
442
+ }
443
+ return out;
444
+ }
445
+ function createPomStringPattern(formatted, patternKind) {
446
+ return {
447
+ formatted,
448
+ patternKind,
449
+ templateVariables: getTemplateVariables(formatted)
450
+ };
451
+ }
452
+ function getPomPatternVariables(patterns, options = {}) {
453
+ const out = [];
454
+ const seen = /* @__PURE__ */ new Set();
455
+ const omitted = new Set(options.omit ?? []);
456
+ for (const pattern of patterns) {
457
+ for (const variableName of pattern.templateVariables) {
458
+ if (omitted.has(variableName) || seen.has(variableName)) {
459
+ continue;
460
+ }
461
+ seen.add(variableName);
462
+ out.push(variableName);
463
+ }
464
+ }
465
+ return out;
466
+ }
467
+ function orderPomPatternParameters(params, patterns, options = {}) {
468
+ const currentParams = normalizePomParameters(params);
469
+ const orderedParams = [];
470
+ const seen = /* @__PURE__ */ new Set();
471
+ const missingParams = [];
472
+ for (const variableName of getPomPatternVariables(patterns, options)) {
473
+ seen.add(variableName);
474
+ const existingParam = currentParams.find((param) => param.name === variableName);
475
+ if (!existingParam) {
476
+ missingParams.push(variableName);
352
477
  continue;
353
478
  }
354
- seen.add(a);
355
- out.push(a);
479
+ orderedParams.push(existingParam);
480
+ }
481
+ if (missingParams.length > 0) {
482
+ const availableParams = currentParams.map((param) => JSON.stringify(param.name)).join(", ") || "<none>";
483
+ const patternSummary = patterns.map((pattern) => JSON.stringify(pattern.formatted)).join(", ");
484
+ throw new Error(
485
+ `[vue-pom-generator] Missing selector parameter(s) ${missingParams.map((name) => JSON.stringify(name)).join(", ")} for parameterized pattern(s) ${patternSummary}. Available parameters: ${availableParams}.`
486
+ );
487
+ }
488
+ for (const param of currentParams) {
489
+ if (seen.has(param.name)) {
490
+ continue;
491
+ }
492
+ seen.add(param.name);
493
+ orderedParams.push(param);
494
+ }
495
+ return orderedParams;
496
+ }
497
+ function getIndexedPomPatternVariable(pattern) {
498
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
499
+ return null;
500
+ }
501
+ if (pattern.templateVariables.length !== 1) {
502
+ throw new Error(
503
+ `[vue-pom-generator] Parameterized locator getters require exactly one template variable; got ${pattern.templateVariables.length} in ${JSON.stringify(pattern.formatted)}.`
504
+ );
505
+ }
506
+ return pattern.templateVariables[0];
507
+ }
508
+ function hasPomPatternVariables(pattern) {
509
+ return pattern.templateVariables.length > 0;
510
+ }
511
+ function toTypeScriptPomPatternExpression(pattern) {
512
+ return isParameterizedPomPattern(pattern.patternKind) ? `\`${pattern.formatted}\`` : JSON.stringify(pattern.formatted);
513
+ }
514
+ function toCSharpPomPatternExpression(pattern) {
515
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
516
+ return JSON.stringify(pattern.formatted);
517
+ }
518
+ const inner = pattern.formatted.replace(/\$\{/g, "{");
519
+ return `$${JSON.stringify(inner)}`;
520
+ }
521
+ function bindTypeScriptPomPattern(pattern, variableName) {
522
+ const expression = toTypeScriptPomPatternExpression(pattern);
523
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
524
+ return { expression, setupStatements: [] };
525
+ }
526
+ return {
527
+ expression: variableName,
528
+ setupStatements: [`const ${variableName} = ${expression};`]
529
+ };
530
+ }
531
+ function bindCSharpPomPattern(pattern, variableName) {
532
+ const expression = toCSharpPomPatternExpression(pattern);
533
+ if (!isParameterizedPomPattern(pattern.patternKind)) {
534
+ return { expression, setupStatements: [] };
535
+ }
536
+ return {
537
+ expression: variableName,
538
+ setupStatements: [`var ${variableName} = ${expression};`]
539
+ };
540
+ }
541
+ function pomStringPatternEquals(left, right) {
542
+ return left.formatted === right.formatted && left.patternKind === right.patternKind;
543
+ }
544
+ function uniquePomStringPatterns(primary, alternates) {
545
+ const out = [];
546
+ const seen = /* @__PURE__ */ new Set();
547
+ const add = (pattern) => {
548
+ const key = JSON.stringify(pattern);
549
+ if (seen.has(key)) {
550
+ return;
551
+ }
552
+ seen.add(key);
553
+ out.push(pattern);
554
+ };
555
+ add(primary);
556
+ for (const alternate of alternates ?? []) {
557
+ add(alternate);
356
558
  }
357
559
  return out;
358
560
  }
359
- function testIdExpression(formattedDataTestId) {
360
- return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
561
+ function upperFirst$1(value) {
562
+ if (!value) {
563
+ return value;
564
+ }
565
+ return value.charAt(0).toUpperCase() + value.slice(1);
566
+ }
567
+ function createParameters(params) {
568
+ return toTypeScriptPomParameterStructures(params);
569
+ }
570
+ function createInlineParameter(name, options = {}) {
571
+ return {
572
+ name,
573
+ type: options.type,
574
+ initializer: options.initializer
575
+ };
576
+ }
577
+ function removeByKeySegment(value) {
578
+ const idx = value.lastIndexOf("ByKey");
579
+ if (idx < 0) {
580
+ return value;
581
+ }
582
+ return value.slice(0, idx) + value.slice(idx + "ByKey".length);
361
583
  }
362
584
  function createAsyncMethod(name, parameters, statements) {
363
585
  return createClassMethod({
@@ -367,17 +589,20 @@ function createAsyncMethod(name, parameters, statements) {
367
589
  statements
368
590
  });
369
591
  }
370
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
592
+ function generateClickMethod(methodName, selector, alternateSelectors, parameters) {
371
593
  const name = `click${methodName}`;
372
594
  const noWaitName = `${name}NoWait`;
373
- const baseParameters = createParameters(params);
374
- const argsForForward = Object.keys(params).join(", ");
375
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
595
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
596
+ const hasSelectorVariables = hasPomPatternVariables(selector);
597
+ const baseParameters = createParameters(selectorParams);
598
+ const argsForForward = getPomParameterNames(selectorParams).join(", ");
599
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
600
+ const primaryTestIdExpr = toTypeScriptPomPatternExpression(selector);
376
601
  if (alternates.length > 0) {
377
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
602
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
378
603
  const clickMethod = createAsyncMethod(
379
604
  name,
380
- hasParam(params, "key") ? [
605
+ hasSelectorVariables ? [
381
606
  ...baseParameters,
382
607
  createInlineParameter("wait", { type: "boolean", initializer: "true" }),
383
608
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
@@ -406,14 +631,14 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
406
631
  const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
407
632
  const noWaitMethod = createAsyncMethod(
408
633
  noWaitName,
409
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
634
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
410
635
  (writer) => {
411
636
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
412
637
  }
413
638
  );
414
639
  return [clickMethod, noWaitMethod];
415
640
  }
416
- if (hasParam(params, "key")) {
641
+ if (hasSelectorVariables) {
417
642
  return [
418
643
  createAsyncMethod(
419
644
  name,
@@ -423,7 +648,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
423
648
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
424
649
  ],
425
650
  (writer) => {
426
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, annotationText, wait);`);
651
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
427
652
  }
428
653
  ),
429
654
  createAsyncMethod(
@@ -443,7 +668,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
443
668
  createInlineParameter("annotationText", { type: "string", initializer: '""' })
444
669
  ],
445
670
  (writer) => {
446
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", annotationText, wait);`);
671
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
447
672
  }
448
673
  ),
449
674
  createAsyncMethod(
@@ -455,31 +680,25 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
455
680
  )
456
681
  ];
457
682
  }
458
- function generateRadioMethod(methodName, formattedDataTestId) {
683
+ function generateRadioMethod(methodName, selector, parameters) {
459
684
  const name = `select${methodName}`;
460
- const hasKey = formattedDataTestId.includes("${key}");
461
- const parameters = hasKey ? [
462
- createInlineParameter("key", { type: "string" }),
463
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
464
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
465
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
685
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
686
+ const methodParameters = createParameters(selectorParams);
687
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
466
688
  return [
467
- createAsyncMethod(name, parameters, (writer) => {
689
+ createAsyncMethod(name, methodParameters, (writer) => {
468
690
  writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
469
691
  })
470
692
  ];
471
693
  }
472
- function generateSelectMethod(methodName, formattedDataTestId) {
694
+ function generateSelectMethod(methodName, selector, parameters) {
473
695
  const name = `select${methodName}`;
474
- const needsKey = formattedDataTestId.includes("${key}");
475
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
696
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
697
+ const selectorExpr = `this.selectorForTestId(${toTypeScriptPomPatternExpression(selector)})`;
476
698
  return [
477
699
  createAsyncMethod(
478
700
  name,
479
- [
480
- createInlineParameter("value", { type: "string" }),
481
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
482
- ],
701
+ createParameters(selectorParams),
483
702
  (writer) => {
484
703
  writer.writeLine(`const selector = ${selectorExpr};`);
485
704
  writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
@@ -488,33 +707,28 @@ function generateSelectMethod(methodName, formattedDataTestId) {
488
707
  )
489
708
  ];
490
709
  }
491
- function generateVSelectMethod(methodName, formattedDataTestId) {
710
+ function generateVSelectMethod(methodName, selector, parameters) {
492
711
  const name = `select${methodName}`;
712
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
493
713
  return [
494
714
  createAsyncMethod(
495
715
  name,
496
- [
497
- createInlineParameter("value", { type: "string" }),
498
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
499
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
500
- ],
716
+ createParameters(selectorParams),
501
717
  (writer) => {
502
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
718
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText);`);
503
719
  }
504
720
  )
505
721
  ];
506
722
  }
507
- function generateTypeMethod(methodName, formattedDataTestId) {
723
+ function generateTypeMethod(methodName, selector, parameters) {
508
724
  const name = `type${methodName}`;
725
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
509
726
  return [
510
727
  createAsyncMethod(
511
728
  name,
512
- [
513
- createInlineParameter("text", { type: "string" }),
514
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
515
- ],
729
+ createParameters(selectorParams),
516
730
  (writer) => {
517
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
731
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText);`);
518
732
  }
519
733
  )
520
734
  ];
@@ -529,30 +743,31 @@ function isAllDigits(value) {
529
743
  }
530
744
  return true;
531
745
  }
532
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
746
+ function generateGetElementByDataTestId(methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
533
747
  const roleSuffix = upperFirst$1(nativeRole || "Element");
534
748
  const baseName = upperFirst$1(methodName);
535
749
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
536
750
  const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
537
751
  const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
538
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
539
- if (needsKey) {
540
- const keyType = params.key || "string";
752
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
753
+ const indexedVariable = getIndexedPomPatternVariable(selector);
754
+ if (indexedVariable) {
755
+ const keyType = getPomParameter(selectorParams, indexedVariable)?.typeExpression || "string";
541
756
  const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
542
757
  return [
543
758
  createClassGetter({
544
759
  name: keyedPropertyName,
545
760
  statements: [
546
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
761
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}));`
547
762
  ]
548
763
  })
549
764
  ];
550
765
  }
551
766
  const finalPropertyName = getterNameOverride ?? propertyName;
552
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
767
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
553
768
  if (alternates.length > 0) {
554
- const all = [formattedDataTestId, ...alternates];
555
- const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
769
+ const all = [selector, ...alternates];
770
+ const locatorExpr = all.map((id) => `this.locatorByTestId(${toTypeScriptPomPatternExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
556
771
  return [
557
772
  createClassGetter({
558
773
  name: finalPropertyName,
@@ -563,21 +778,22 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
563
778
  return [
564
779
  createClassGetter({
565
780
  name: finalPropertyName,
566
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
781
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)});`]
567
782
  })
568
783
  ];
569
784
  }
570
785
  function generateNavigationMethod(args) {
571
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
786
+ const { targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
572
787
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
573
- const parameters = createParameters(params);
574
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
575
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
788
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
789
+ const methodParameters = createParameters(selectorParams);
790
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
791
+ const candidatesExpr = [toTypeScriptPomPatternExpression(selector), ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
576
792
  if (alternates.length > 0) {
577
793
  return [
578
794
  createClassMethod({
579
795
  name: methodName,
580
- parameters,
796
+ parameters: methodParameters,
581
797
  returnType: `Fluent<${target}>`,
582
798
  statements: (writer) => {
583
799
  writer.write("return this.fluent(async () => ").block(() => {
@@ -605,11 +821,11 @@ function generateNavigationMethod(args) {
605
821
  return [
606
822
  createClassMethod({
607
823
  name: methodName,
608
- parameters,
824
+ parameters: methodParameters,
609
825
  returnType: `Fluent<${target}>`,
610
826
  statements: (writer) => {
611
827
  writer.write("return this.fluent(async () => ").block(() => {
612
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
828
+ writer.writeLine(`await this.clickByTestId(${toTypeScriptPomPatternExpression(selector)});`);
613
829
  writer.writeLine(`return new ${target}(this.page);`);
614
830
  });
615
831
  writer.writeLine(");");
@@ -617,15 +833,15 @@ function generateNavigationMethod(args) {
617
833
  })
618
834
  ];
619
835
  }
620
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
836
+ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
621
837
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
622
838
  const members = generateGetElementByDataTestId(
623
839
  baseMethodName,
624
840
  nativeRole,
625
- formattedDataTestId,
626
- alternateFormattedDataTestIds,
841
+ selector,
842
+ alternateSelectors,
627
843
  getterNameOverride,
628
- params
844
+ parameters
629
845
  );
630
846
  if (targetPageObjectModelClass) {
631
847
  return [
@@ -633,25 +849,25 @@ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName,
633
849
  ...generateNavigationMethod({
634
850
  targetPageObjectModelClass,
635
851
  baseMethodName,
636
- formattedDataTestId,
637
- alternateFormattedDataTestIds,
638
- params
852
+ selector,
853
+ alternateSelectors,
854
+ parameters
639
855
  })
640
856
  ];
641
857
  }
642
858
  if (nativeRole === "select") {
643
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
859
+ return [...members, ...generateSelectMethod(baseMethodName, selector, parameters)];
644
860
  }
645
861
  if (nativeRole === "vselect") {
646
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
862
+ return [...members, ...generateVSelectMethod(baseMethodName, selector, parameters)];
647
863
  }
648
864
  if (nativeRole === "input") {
649
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
865
+ return [...members, ...generateTypeMethod(baseMethodName, selector, parameters)];
650
866
  }
651
867
  if (nativeRole === "radio") {
652
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
868
+ return [...members, ...generateRadioMethod(baseMethodName || "Radio", selector, parameters)];
653
869
  }
654
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
870
+ return [...members, ...generateClickMethod(baseMethodName, selector, alternateSelectors, parameters)];
655
871
  }
656
872
  function isSimpleExpressionNode(value) {
657
873
  return value !== null && "type" in value && value.type === NodeTypes.SIMPLE_EXPRESSION;
@@ -691,43 +907,72 @@ function buildPlaceholderParams(keys) {
691
907
  params[k] = "__placeholder__";
692
908
  return params;
693
909
  }
694
- function getRouteLocationLikeFromToDirective(toDirective) {
695
- if (!toDirective.exp)
696
- return null;
697
- const exp = toDirective.exp;
698
- const rawSource = stringifyExpression(exp).trim();
910
+ const isNodeType = (node, type) => {
911
+ return node !== null && node.type === type;
912
+ };
913
+ const isStringLiteralNode = (node) => {
914
+ return isNodeType(node, "StringLiteral") && typeof node.value === "string";
915
+ };
916
+ const isIdentifierNode = (node) => {
917
+ return isNodeType(node, "Identifier") && typeof node.name === "string";
918
+ };
919
+ const isObjectPropertyNode = (node) => {
920
+ if (!isNodeType(node, "ObjectProperty"))
921
+ return false;
922
+ const n = node;
923
+ return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
924
+ };
925
+ const isObjectExpressionNode = (node) => {
926
+ if (!isNodeType(node, "ObjectExpression"))
927
+ return false;
928
+ const n = node;
929
+ return Array.isArray(n.properties);
930
+ };
931
+ function materializeResolvedRouteTarget(target, paramKeys) {
932
+ if (typeof target === "string")
933
+ return target;
934
+ if (!paramKeys.length)
935
+ return target;
936
+ return {
937
+ ...target,
938
+ params: buildPlaceholderParams(paramKeys)
939
+ };
940
+ }
941
+ function analyzeToDirectiveTarget(toDirective) {
942
+ if (!toDirective.exp) {
943
+ return {
944
+ kind: "unsupported",
945
+ rawSource: null,
946
+ reason: "missing-expression"
947
+ };
948
+ }
949
+ const rawSource = stringifyExpression(toDirective.exp).trim();
699
950
  let expr;
700
951
  try {
701
952
  expr = parseExpression(rawSource, { plugins: ["typescript"] });
702
- } catch {
703
- return null;
953
+ } catch (error) {
954
+ return {
955
+ kind: "parse-error",
956
+ rawSource,
957
+ reason: "parse-error",
958
+ error: error instanceof Error ? error.message : String(error)
959
+ };
704
960
  }
705
- const isNodeType = (node, type) => {
706
- return node !== null && node.type === type;
707
- };
708
- const isStringLiteralNode = (node) => {
709
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
710
- };
711
- const isIdentifierNode = (node) => {
712
- return isNodeType(node, "Identifier") && typeof node.name === "string";
713
- };
714
- const isObjectPropertyNode = (node) => {
715
- if (!isNodeType(node, "ObjectProperty"))
716
- return false;
717
- const n = node;
718
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
719
- };
720
- const isObjectExpressionNode = (node) => {
721
- if (!isNodeType(node, "ObjectExpression"))
722
- return false;
723
- const n = node;
724
- return Array.isArray(n.properties);
725
- };
726
961
  if (isStringLiteralNode(expr)) {
727
- return expr.value;
962
+ return {
963
+ kind: "resolved",
964
+ rawSource,
965
+ target: expr.value,
966
+ routeNameKey: null,
967
+ paramKeys: []
968
+ };
728
969
  }
729
970
  if (!isObjectExpressionNode(expr)) {
730
- return null;
971
+ return {
972
+ kind: "unsupported",
973
+ rawSource,
974
+ reason: "dynamic-expression"
975
+ };
731
976
  }
732
977
  const getStringField = (fieldName) => {
733
978
  const prop = expr.properties.find((p) => {
@@ -748,7 +993,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
748
993
  const key = p.key;
749
994
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
750
995
  });
751
- let params;
996
+ let paramKeys = [];
752
997
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
753
998
  const keys = [];
754
999
  for (const prop of paramsProp.value.properties) {
@@ -760,47 +1005,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
760
1005
  else if (isStringLiteralNode(key))
761
1006
  keys.push(key.value);
762
1007
  }
763
- if (keys.length) {
764
- params = buildPlaceholderParams(Array.from(new Set(keys)));
765
- }
1008
+ paramKeys = Array.from(new Set(keys));
766
1009
  }
767
1010
  if (name) {
768
- return { name, params };
1011
+ const trimmed = name.trim();
1012
+ if (!trimmed.length) {
1013
+ return {
1014
+ kind: "unsupported",
1015
+ rawSource,
1016
+ reason: "missing-name-or-path"
1017
+ };
1018
+ }
1019
+ return {
1020
+ kind: "resolved",
1021
+ rawSource,
1022
+ target: { name },
1023
+ routeNameKey: toPascalCaseRouteKey(trimmed),
1024
+ paramKeys
1025
+ };
769
1026
  }
770
1027
  if (path2) {
771
- return { path: path2, params };
1028
+ return {
1029
+ kind: "resolved",
1030
+ rawSource,
1031
+ target: { path: path2 },
1032
+ routeNameKey: null,
1033
+ paramKeys
1034
+ };
772
1035
  }
773
- return null;
774
- }
775
- function toDirectiveObjectFieldNameValue$1(toDirective) {
776
- const to = getRouteLocationLikeFromToDirective(toDirective);
777
- if (!to || typeof to === "string")
778
- return null;
779
- const name = to.name;
780
- if (typeof name !== "string")
781
- return null;
782
- const trimmed = name.trim();
783
- if (!trimmed.length)
784
- return null;
785
- return toPascalCaseRouteKey(trimmed);
786
- }
787
- function getRouteNameKeyFromToDirective(toDirective) {
788
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
789
- if (objectName)
790
- return objectName;
791
- return null;
1036
+ return {
1037
+ kind: "unsupported",
1038
+ rawSource,
1039
+ reason: "missing-name-or-path"
1040
+ };
792
1041
  }
793
1042
  function tryResolveToDirectiveTargetComponentName(toDirective) {
794
- const to = getRouteLocationLikeFromToDirective(toDirective);
795
- if (to && resolveToComponentName) {
796
- const resolved = resolveToComponentName(to);
1043
+ const analysis = analyzeToDirectiveTarget(toDirective);
1044
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1045
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
797
1046
  if (resolved)
798
1047
  return resolved;
799
1048
  }
800
- const key = getRouteNameKeyFromToDirective(toDirective);
801
- if (!key || !routeNameToComponentName)
1049
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
802
1050
  return null;
803
- return routeNameToComponentName.get(key) ?? null;
1051
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
804
1052
  }
805
1053
  function getDataTestIdFromGroupOption(text) {
806
1054
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -835,11 +1083,81 @@ function staticAttributeValue(value) {
835
1083
  return { kind: "static", value };
836
1084
  }
837
1085
  function templateAttributeValue(template) {
838
- return { kind: "template", template };
1086
+ const parsedTemplate = tryParseTemplateFragment(template);
1087
+ if (!parsedTemplate) {
1088
+ throw new Error(`[vue-pom-generator] Failed to parse generated template fragment: ${template}`);
1089
+ }
1090
+ return { kind: "template", template, parsedTemplate };
839
1091
  }
840
1092
  function getAttributeValueText(value) {
841
1093
  return value.kind === "static" ? value.value : value.template;
842
1094
  }
1095
+ function getVueExpressionSource(expression, ...preferredViews) {
1096
+ if (!expression) {
1097
+ return "";
1098
+ }
1099
+ for (const view of preferredViews) {
1100
+ let value = "";
1101
+ switch (view) {
1102
+ case "content":
1103
+ value = expression.type === NodeTypes.SIMPLE_EXPRESSION ? expression.content : "";
1104
+ break;
1105
+ case "loc":
1106
+ value = expression.loc?.source ?? "";
1107
+ break;
1108
+ case "compiled":
1109
+ try {
1110
+ value = stringifyExpression(expression);
1111
+ } catch {
1112
+ value = "";
1113
+ }
1114
+ break;
1115
+ default:
1116
+ value = "";
1117
+ break;
1118
+ }
1119
+ const trimmed = value.trim();
1120
+ if (trimmed) {
1121
+ return trimmed;
1122
+ }
1123
+ }
1124
+ return "";
1125
+ }
1126
+ function tryGetExistingVueExpressionAst(expression) {
1127
+ if (!expression) {
1128
+ return null;
1129
+ }
1130
+ const ast = "ast" in expression ? expression.ast : null;
1131
+ return ast && "type" in ast ? ast : null;
1132
+ }
1133
+ function tryParseBabelExpressionFromSource(source, plugins) {
1134
+ const trimmed = source.trim();
1135
+ if (!trimmed) {
1136
+ return null;
1137
+ }
1138
+ try {
1139
+ return parseExpression(trimmed, { plugins });
1140
+ } catch {
1141
+ return null;
1142
+ }
1143
+ }
1144
+ function tryGetVueExpressionAst(expression, options) {
1145
+ if (!expression) {
1146
+ return null;
1147
+ }
1148
+ if (options?.preferExistingAst !== false) {
1149
+ const existingAst = tryGetExistingVueExpressionAst(expression);
1150
+ if (existingAst) {
1151
+ return existingAst;
1152
+ }
1153
+ }
1154
+ const source = getVueExpressionSource(expression, ...options?.preferredViews ?? ["content", "loc", "compiled"]);
1155
+ return source ? tryParseBabelExpressionFromSource(source, options?.plugins ?? ["typescript"]) : null;
1156
+ }
1157
+ function tryGetDirectiveBabelAst(directive, options) {
1158
+ const exp = directive.exp && (directive.exp.type === NodeTypes.SIMPLE_EXPRESSION || directive.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? directive.exp : null;
1159
+ return tryGetVueExpressionAst(exp, options);
1160
+ }
843
1161
  function toPascalCase(str) {
844
1162
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
845
1163
  if (!cleaned) {
@@ -881,32 +1199,14 @@ function tryGetClickDirective(node) {
881
1199
  function nodeHasClickDirective(node) {
882
1200
  return tryGetClickDirective(node) !== void 0;
883
1201
  }
884
- function getTemplateSlotScope(node) {
1202
+ function findTemplateSlotScopeExpression(node) {
885
1203
  if (node.tag !== "template") {
886
1204
  return null;
887
1205
  }
888
1206
  const slotProp = node.props.find((prop) => {
889
1207
  return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot";
890
1208
  });
891
- if (slotProp?.exp) {
892
- if (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
893
- return slotProp.exp.content;
894
- }
895
- if (slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) {
896
- return stringifyExpression(slotProp.exp);
897
- }
898
- }
899
- return null;
900
- }
901
- function isSimpleScopeIdentifier(value) {
902
- if (!value) {
903
- return false;
904
- }
905
- try {
906
- return isIdentifier(parseExpression(value, { plugins: ["typescript"] }));
907
- } catch {
908
- return false;
909
- }
1209
+ return slotProp?.exp && (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
910
1210
  }
911
1211
  function buildSlotScopeFallbackKeyExpression(identifier) {
912
1212
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -1008,43 +1308,64 @@ function tryGetSlotScopeKeyCandidate(node) {
1008
1308
  }
1009
1309
  return best;
1010
1310
  }
1011
- function tryGetTemplateSlotScopeKeyExpression(scope) {
1012
- const trimmed = scope.trim();
1013
- if (!trimmed) {
1311
+ function tryGetTemplateSlotScopeBindingNode(expression) {
1312
+ const ast = tryGetExistingVueExpressionAst(expression);
1313
+ if (ast) {
1314
+ if (isArrowFunctionExpression(ast)) {
1315
+ return ast.params[0] ?? null;
1316
+ }
1317
+ return ast;
1318
+ }
1319
+ const rawSource = getVueExpressionSource(expression, "content", "loc", "compiled");
1320
+ if (!rawSource) {
1014
1321
  return null;
1015
1322
  }
1016
- if (isSimpleScopeIdentifier(trimmed)) {
1017
- return buildSlotScopeFallbackKeyExpression(trimmed);
1323
+ try {
1324
+ return parseExpression(rawSource, { plugins: ["typescript"] });
1325
+ } catch {
1018
1326
  }
1019
1327
  try {
1020
- const parsed = parse(`(${trimmed}) => {}`, {
1328
+ const parsed = parse(`(${rawSource}) => {}`, {
1021
1329
  sourceType: "module",
1022
1330
  plugins: ["typescript"]
1023
1331
  });
1024
1332
  const statement = parsed.program.body[0];
1025
1333
  if (statement && isExpressionStatement(statement) && isArrowFunctionExpression(statement.expression)) {
1026
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1334
+ return statement.expression.params[0] ?? null;
1027
1335
  }
1028
1336
  } catch {
1337
+ return null;
1029
1338
  }
1030
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1031
- const inner = trimmed.slice(1, -1).trim();
1032
- let cutIdx = -1;
1033
- const commaIdx = inner.indexOf(",");
1034
- const colonIdx = inner.indexOf(":");
1035
- if (commaIdx !== -1 && colonIdx !== -1) {
1036
- cutIdx = Math.min(commaIdx, colonIdx);
1037
- } else if (commaIdx !== -1) {
1038
- cutIdx = commaIdx;
1039
- } else if (colonIdx !== -1) {
1040
- cutIdx = colonIdx;
1041
- }
1042
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
1043
- if (first && isSimpleScopeIdentifier(first)) {
1044
- return buildSlotScopeFallbackKeyExpression(first);
1045
- }
1339
+ return null;
1340
+ }
1341
+ function toResolvedTemplateFragment(source) {
1342
+ const templateLiteral = tryUnwrapTemplateLiteralSource(source);
1343
+ if (templateLiteral) {
1344
+ return {
1345
+ template: templateLiteral.template,
1346
+ rawExpression: null
1347
+ };
1348
+ }
1349
+ return toInterpolatedTemplateFragment(source);
1350
+ }
1351
+ function toResolvedKeyInfo(selectorSource, runtimeSource = selectorSource) {
1352
+ const selectorFragment = selectorSource ? toResolvedTemplateFragment(selectorSource) : null;
1353
+ const runtimeFragment = runtimeSource ? toResolvedTemplateFragment(runtimeSource) : null;
1354
+ const selectorTemplate = selectorFragment?.template ?? runtimeFragment?.template ?? null;
1355
+ const runtimeTemplate = runtimeFragment?.template ?? selectorFragment?.template ?? null;
1356
+ if (!selectorTemplate || !runtimeTemplate) {
1357
+ return null;
1046
1358
  }
1047
- return trimmed;
1359
+ return {
1360
+ selectorFragment: selectorTemplate,
1361
+ runtimeFragment: runtimeTemplate,
1362
+ rawExpression: runtimeFragment?.rawExpression ?? selectorFragment?.rawExpression ?? null
1363
+ };
1364
+ }
1365
+ function tryGetTemplateSlotScopeKeyInfo(expression) {
1366
+ const bindingNode = tryGetTemplateSlotScopeBindingNode(expression);
1367
+ const candidateExpression = bindingNode ? tryGetSlotScopeKeyCandidate(bindingNode)?.expression ?? null : null;
1368
+ return candidateExpression ? toResolvedKeyInfo(candidateExpression) : null;
1048
1369
  }
1049
1370
  function nodeHasToDirective(node) {
1050
1371
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1053,51 +1374,160 @@ function nodeHasToDirective(node) {
1053
1374
  }
1054
1375
  return void 0;
1055
1376
  }
1056
- function nodeHasForDirective(node) {
1057
- return node.props.some(
1058
- (attr) => attr.type === NodeTypes.DIRECTIVE && attr.name === "for"
1059
- );
1060
- }
1061
1377
  function getKeyDirective(node) {
1062
1378
  return findDirectiveByName(node, "bind", "key") ?? null;
1063
1379
  }
1064
- function getKeyDirectiveValue(node, _context = null) {
1065
- const keyDirective = getKeyDirective(node);
1066
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1067
- if (rawSource) {
1068
- return `\${${rawSource}}`;
1380
+ function tryUnwrapTemplateLiteralSource(source) {
1381
+ const rawSource = source.trim();
1382
+ if (!rawSource) {
1383
+ return null;
1069
1384
  }
1070
- if (keyDirective?.exp) {
1071
- const value = stringifyExpression(keyDirective.exp);
1072
- if (value)
1073
- return `\${${value}}`;
1385
+ let ast = null;
1386
+ try {
1387
+ ast = parseExpression(rawSource, { plugins: ["typescript"] });
1388
+ } catch {
1389
+ return null;
1390
+ }
1391
+ if (!ast || !isTemplateLiteral(ast)) {
1392
+ return null;
1393
+ }
1394
+ const cooked = ast.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
1395
+ try {
1396
+ const start = typeof ast.start === "number" ? ast.start + 1 : 1;
1397
+ const end = typeof ast.end === "number" ? ast.end - 1 : rawSource.length - 1;
1398
+ return {
1399
+ template: rawSource.slice(start, end) || cooked,
1400
+ expressionCount: ast.expressions.length
1401
+ };
1402
+ } catch {
1403
+ return {
1404
+ template: cooked,
1405
+ expressionCount: ast.expressions.length
1406
+ };
1407
+ }
1408
+ }
1409
+ function tryUnwrapTemplateLiteralExpressionSource(expression) {
1410
+ const rawSource = getVueExpressionSource(expression, "loc", "compiled");
1411
+ return rawSource ? tryUnwrapTemplateLiteralSource(rawSource) : null;
1412
+ }
1413
+ function tryParseTemplateFragment(fragment) {
1414
+ if (!fragment) {
1415
+ return null;
1416
+ }
1417
+ try {
1418
+ const source = `\`${fragment}\``;
1419
+ const ast = parseExpression(source, { plugins: ["typescript"] });
1420
+ return isTemplateLiteral(ast) ? { source, templateLiteral: ast } : null;
1421
+ } catch {
1422
+ return null;
1074
1423
  }
1075
- return null;
1424
+ }
1425
+ function getTemplateExpressionSource(parsedTemplate, index) {
1426
+ const expression = parsedTemplate.templateLiteral.expressions[index];
1427
+ if (!expression) {
1428
+ return null;
1429
+ }
1430
+ const start = typeof expression.start === "number" ? expression.start : null;
1431
+ const end = typeof expression.end === "number" ? expression.end : null;
1432
+ if (start === null || end === null) {
1433
+ return null;
1434
+ }
1435
+ return parsedTemplate.source.slice(start, end);
1436
+ }
1437
+ function getSingleExpressionTemplateFragment(parsedTemplate) {
1438
+ const { templateLiteral } = parsedTemplate;
1439
+ if (templateLiteral.expressions.length !== 1 || templateLiteral.quasis.length !== 2) {
1440
+ return null;
1441
+ }
1442
+ const expressionSource = getTemplateExpressionSource(parsedTemplate, 0);
1443
+ if (expressionSource === null) {
1444
+ return null;
1445
+ }
1446
+ return {
1447
+ prefix: templateLiteral.quasis[0]?.value.raw ?? "",
1448
+ expressionSource,
1449
+ suffix: templateLiteral.quasis[1]?.value.raw ?? ""
1450
+ };
1451
+ }
1452
+ function templateFragmentContainsSingleExpression(container, candidate) {
1453
+ const containerFragment = getSingleExpressionTemplateFragment(container);
1454
+ const candidateFragment = getSingleExpressionTemplateFragment(candidate);
1455
+ if (!containerFragment || !candidateFragment) {
1456
+ return false;
1457
+ }
1458
+ return containerFragment.expressionSource === candidateFragment.expressionSource && containerFragment.prefix.endsWith(candidateFragment.prefix) && containerFragment.suffix.startsWith(candidateFragment.suffix);
1459
+ }
1460
+ function hasTemplateInterpolationExpressions(fragment) {
1461
+ return (tryParseTemplateFragment(fragment)?.templateLiteral.expressions.length ?? 0) > 0;
1462
+ }
1463
+ function toInterpolatedTemplateFragment(fragment) {
1464
+ if (!fragment) {
1465
+ return null;
1466
+ }
1467
+ if (hasTemplateInterpolationExpressions(fragment)) {
1468
+ return { template: fragment, rawExpression: null };
1469
+ }
1470
+ return {
1471
+ template: `\${${fragment}}`,
1472
+ rawExpression: fragment
1473
+ };
1474
+ }
1475
+ function renderTemplateLiteralExpression(templateValue) {
1476
+ const templateLiteralSource = templateValue.parsedTemplate.source;
1477
+ const templateLiteral = templateValue.parsedTemplate.templateLiteral;
1478
+ const writer = createTypeScriptWriter();
1479
+ writer.write("`");
1480
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
1481
+ writer.write(templateLiteral.quasis[i]?.value.raw ?? "");
1482
+ const interpolation = templateLiteral.expressions[i];
1483
+ if (!interpolation) {
1484
+ continue;
1485
+ }
1486
+ const start = typeof interpolation.start === "number" ? interpolation.start : null;
1487
+ const end = typeof interpolation.end === "number" ? interpolation.end : null;
1488
+ if (start === null || end === null) {
1489
+ return templateLiteralSource;
1490
+ }
1491
+ writer.write("${");
1492
+ writer.write(templateLiteralSource.slice(start, end));
1493
+ writer.write("}");
1494
+ }
1495
+ writer.write("`");
1496
+ return writer.toString();
1497
+ }
1498
+ function getKeyDirectiveExpression(node) {
1499
+ const keyDirective = getKeyDirective(node);
1500
+ return keyDirective?.exp && (keyDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || keyDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? keyDirective.exp : null;
1501
+ }
1502
+ function getKeyDirectiveInfo(node) {
1503
+ const keyExpression = getKeyDirectiveExpression(node);
1504
+ if (!keyExpression) {
1505
+ return null;
1506
+ }
1507
+ const selectorSource = getVueExpressionSource(keyExpression, "compiled", "loc");
1508
+ const runtimeSource = getVueExpressionSource(keyExpression, "loc", "compiled");
1509
+ return toResolvedKeyInfo(selectorSource, runtimeSource);
1076
1510
  }
1077
1511
  function getModelBindingValues(node) {
1078
1512
  let vModel = "";
1079
1513
  const vModelDirective = findDirectiveByName(node, "model");
1080
- if (vModelDirective?.exp?.loc.source) {
1081
- vModel = toPascalCase(vModelDirective.exp.loc.source);
1514
+ if (vModelDirective?.exp && (vModelDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || vModelDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION)) {
1515
+ vModel = toPascalCase(getVueExpressionSource(vModelDirective.exp, "loc", "content"));
1082
1516
  }
1083
1517
  let modelValue = null;
1084
1518
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1085
- if (modelValueDirective?.exp?.ast) {
1086
- const { name: mv } = getClickHandlerNameFromAst(modelValueDirective.exp.ast);
1519
+ const modelValueAst = modelValueDirective ? tryGetDirectiveBabelAst(modelValueDirective, {
1520
+ preferredViews: ["loc", "compiled"],
1521
+ plugins: ["typescript"],
1522
+ preferExistingAst: false
1523
+ }) : null;
1524
+ if (modelValueAst) {
1525
+ const { name: mv } = getClickHandlerNameFromAst(modelValueAst);
1087
1526
  modelValue = mv;
1088
1527
  }
1089
1528
  return { vModel, modelValue };
1090
1529
  }
1091
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1092
- if (node.isSelfClosing) {
1093
- const hasForDirective = nodeHasForDirective(node);
1094
- if (hasForDirective) {
1095
- return getKeyDirectiveValue(node);
1096
- }
1097
- }
1098
- return null;
1099
- }
1100
- function getIdOrName(node) {
1530
+ function getStaticIdOrNameHint(node) {
1101
1531
  let idAttr = findAttributeByKey(node, "id");
1102
1532
  if (!idAttr) {
1103
1533
  idAttr = findAttributeByKey(node, "name");
@@ -1105,8 +1535,9 @@ function getIdOrName(node) {
1105
1535
  let identifier = idAttr?.value?.content ?? "";
1106
1536
  if (!identifier) {
1107
1537
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1108
- if (dynamicIdAttr?.exp) {
1109
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1538
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1539
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1540
+ return "";
1110
1541
  }
1111
1542
  }
1112
1543
  if (identifier.includes("-")) {
@@ -1117,20 +1548,20 @@ function getIdOrName(node) {
1117
1548
  }
1118
1549
  return identifier;
1119
1550
  }
1120
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1551
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1121
1552
  let parent = getParent(hierarchyMap2, node);
1122
1553
  while (parent) {
1123
1554
  if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
1124
- const scope = getTemplateSlotScope(parent);
1125
- if (scope) {
1126
- return tryGetTemplateSlotScopeKeyExpression(scope);
1555
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1556
+ if (slotScopeExpression) {
1557
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1127
1558
  }
1128
1559
  }
1129
1560
  parent = getParent(hierarchyMap2, parent);
1130
1561
  }
1131
1562
  return null;
1132
1563
  }
1133
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1564
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1134
1565
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1135
1566
  return null;
1136
1567
  }
@@ -1139,8 +1570,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1139
1570
  if (parent.type === NodeTypes.ELEMENT) {
1140
1571
  const forDirective = findDirectiveByName(parent, "for");
1141
1572
  if (forDirective) {
1142
- const keyValue = getKeyDirectiveValue(parent);
1143
- return keyValue;
1573
+ return getKeyDirectiveInfo(parent);
1144
1574
  }
1145
1575
  }
1146
1576
  parent = getParent(hierarchyMap2, parent);
@@ -1166,13 +1596,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1166
1596
  if (simpleSourceExp.constType === ConstantTypes.NOT_CONSTANT) {
1167
1597
  return null;
1168
1598
  }
1169
- const iterableRaw = (() => {
1170
- try {
1171
- return stringifyExpression(simpleSourceExp).trim();
1172
- } catch {
1173
- return (simpleSourceExp.loc?.source ?? "").trim();
1174
- }
1175
- })();
1599
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1176
1600
  if (!iterableRaw) {
1177
1601
  return null;
1178
1602
  }
@@ -1228,91 +1652,95 @@ function nodeHandlerAttributeInfo(node) {
1228
1652
  return null;
1229
1653
  }
1230
1654
  const exp = handlerDirective.exp;
1231
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
1655
+ const source = getVueExpressionSource(exp, "content", "compiled");
1232
1656
  if (!source) {
1233
1657
  return null;
1234
1658
  }
1235
1659
  const mergeKey = `handler:expr:${source}`;
1236
- let expr;
1237
- try {
1238
- expr = parseExpression(source, { plugins: ["typescript", "jsx"] });
1239
- } catch {
1660
+ const expr = tryGetDirectiveBabelAst(handlerDirective, {
1661
+ preferredViews: ["content", "compiled"],
1662
+ plugins: ["typescript", "jsx"],
1663
+ // Vue's compiler AST can encode `_ctx.foo` as an Identifier name instead of a MemberExpression.
1664
+ // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1665
+ preferExistingAst: false
1666
+ });
1667
+ if (!expr) {
1240
1668
  return null;
1241
1669
  }
1242
- const isNodeType = (node2, type) => {
1670
+ const isNodeType2 = (node2, type) => {
1243
1671
  return node2 !== null && node2.type === type;
1244
1672
  };
1245
- const isIdentifierNode = (node2) => {
1246
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1673
+ const isIdentifierNode2 = (node2) => {
1674
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1247
1675
  };
1248
- const isStringLiteralNode = (node2) => {
1249
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1676
+ const isStringLiteralNode2 = (node2) => {
1677
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1250
1678
  };
1251
1679
  const isBooleanLiteralNode = (node2) => {
1252
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1680
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1253
1681
  };
1254
1682
  const isNumericLiteralNode = (node2) => {
1255
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1683
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1256
1684
  };
1257
1685
  const isNullLiteralNode = (node2) => {
1258
- return isNodeType(node2, "NullLiteral");
1686
+ return isNodeType2(node2, "NullLiteral");
1259
1687
  };
1260
1688
  const isMemberExpressionNode = (node2) => {
1261
- if (!isNodeType(node2, "MemberExpression"))
1689
+ if (!isNodeType2(node2, "MemberExpression"))
1262
1690
  return false;
1263
1691
  const n = node2;
1264
1692
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1265
1693
  };
1266
1694
  const isCallExpressionNode = (node2) => {
1267
- if (!isNodeType(node2, "CallExpression"))
1695
+ if (!isNodeType2(node2, "CallExpression"))
1268
1696
  return false;
1269
1697
  const n = node2;
1270
1698
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1271
1699
  };
1272
1700
  const isAwaitExpressionNode = (node2) => {
1273
- if (!isNodeType(node2, "AwaitExpression"))
1701
+ if (!isNodeType2(node2, "AwaitExpression"))
1274
1702
  return false;
1275
1703
  const n = node2;
1276
1704
  return typeof n.argument === "object" && n.argument !== null;
1277
1705
  };
1278
1706
  const isAssignmentExpressionNode = (node2) => {
1279
- if (!isNodeType(node2, "AssignmentExpression"))
1707
+ if (!isNodeType2(node2, "AssignmentExpression"))
1280
1708
  return false;
1281
1709
  const n = node2;
1282
1710
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1283
1711
  };
1284
1712
  const isArrowFunctionExpressionNode = (node2) => {
1285
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1713
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1286
1714
  return false;
1287
1715
  const n = node2;
1288
1716
  return typeof n.body === "object" && n.body !== null;
1289
1717
  };
1290
1718
  const isBlockStatementNode = (node2) => {
1291
- if (!isNodeType(node2, "BlockStatement"))
1719
+ if (!isNodeType2(node2, "BlockStatement"))
1292
1720
  return false;
1293
1721
  const n = node2;
1294
1722
  return Array.isArray(n.body);
1295
1723
  };
1296
1724
  const isExpressionStatementNode = (node2) => {
1297
- if (!isNodeType(node2, "ExpressionStatement"))
1725
+ if (!isNodeType2(node2, "ExpressionStatement"))
1298
1726
  return false;
1299
1727
  const n = node2;
1300
1728
  return typeof n.expression === "object" && n.expression !== null;
1301
1729
  };
1302
1730
  const isReturnStatementNode = (node2) => {
1303
- if (!isNodeType(node2, "ReturnStatement"))
1731
+ if (!isNodeType2(node2, "ReturnStatement"))
1304
1732
  return false;
1305
1733
  const n = node2;
1306
1734
  return typeof n.argument === "object" || n.argument === null;
1307
1735
  };
1308
- const isObjectExpressionNode = (node2) => {
1309
- if (!isNodeType(node2, "ObjectExpression"))
1736
+ const isObjectExpressionNode2 = (node2) => {
1737
+ if (!isNodeType2(node2, "ObjectExpression"))
1310
1738
  return false;
1311
1739
  const n = node2;
1312
1740
  return Array.isArray(n.properties);
1313
1741
  };
1314
- const isObjectPropertyNode = (node2) => {
1315
- if (!isNodeType(node2, "ObjectProperty"))
1742
+ const isObjectPropertyNode2 = (node2) => {
1743
+ if (!isNodeType2(node2, "ObjectProperty"))
1316
1744
  return false;
1317
1745
  const n = node2;
1318
1746
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1320,16 +1748,16 @@ function nodeHandlerAttributeInfo(node) {
1320
1748
  const getLastIdentifierFromMemberChain = (node2) => {
1321
1749
  if (!node2)
1322
1750
  return null;
1323
- if (isIdentifierNode(node2))
1751
+ if (isIdentifierNode2(node2))
1324
1752
  return node2.name;
1325
1753
  if (isMemberExpressionNode(node2)) {
1326
1754
  const prop = node2.property;
1327
1755
  if (node2.computed === false) {
1328
- if (isIdentifierNode(prop))
1756
+ if (isIdentifierNode2(prop))
1329
1757
  return prop.name;
1330
1758
  }
1331
1759
  if (node2.computed === true) {
1332
- if (isStringLiteralNode(prop))
1760
+ if (isStringLiteralNode2(prop))
1333
1761
  return prop.value;
1334
1762
  }
1335
1763
  }
@@ -1339,7 +1767,7 @@ function nodeHandlerAttributeInfo(node) {
1339
1767
  if (!node2) {
1340
1768
  return null;
1341
1769
  }
1342
- if (isIdentifierNode(node2)) {
1770
+ if (isIdentifierNode2(node2)) {
1343
1771
  return node2.name;
1344
1772
  }
1345
1773
  if (isMemberExpressionNode(node2)) {
@@ -1351,11 +1779,11 @@ function nodeHandlerAttributeInfo(node) {
1351
1779
  if (!lhs) {
1352
1780
  return null;
1353
1781
  }
1354
- if (isIdentifierNode(lhs)) {
1782
+ if (isIdentifierNode2(lhs)) {
1355
1783
  return lhs.name;
1356
1784
  }
1357
1785
  if (isMemberExpressionNode(lhs)) {
1358
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1786
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1359
1787
  return getLastIdentifierFromMemberChain(lhs.object);
1360
1788
  }
1361
1789
  return getLastIdentifierFromMemberChain(lhs);
@@ -1363,7 +1791,7 @@ function nodeHandlerAttributeInfo(node) {
1363
1791
  return null;
1364
1792
  };
1365
1793
  const isTemplateLiteralNode = (node2) => {
1366
- if (!isNodeType(node2, "TemplateLiteral")) {
1794
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1367
1795
  return false;
1368
1796
  }
1369
1797
  const n = node2;
@@ -1382,7 +1810,7 @@ function nodeHandlerAttributeInfo(node) {
1382
1810
  if (isNullLiteralNode(arg)) {
1383
1811
  return "Null";
1384
1812
  }
1385
- if (isStringLiteralNode(arg)) {
1813
+ if (isStringLiteralNode2(arg)) {
1386
1814
  const cleaned = (arg.value ?? "").trim();
1387
1815
  if (!cleaned) {
1388
1816
  return null;
@@ -1408,7 +1836,7 @@ function nodeHandlerAttributeInfo(node) {
1408
1836
  return toPascalCase(stableName.slice(0, 24));
1409
1837
  }
1410
1838
  }
1411
- if (isIdentifierNode(arg)) {
1839
+ if (isIdentifierNode2(arg)) {
1412
1840
  const firstChar = arg.name.charAt(0);
1413
1841
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1414
1842
  if (isUpperAlpha) {
@@ -1420,7 +1848,7 @@ function nodeHandlerAttributeInfo(node) {
1420
1848
  const getStableSuffixFromCall = (call) => {
1421
1849
  const args = call.arguments ?? [];
1422
1850
  const first = args.length > 0 ? args[0] : null;
1423
- if (!isObjectExpressionNode(first)) {
1851
+ if (!isObjectExpressionNode2(first)) {
1424
1852
  const parts2 = [];
1425
1853
  for (const arg of args.slice(0, 4)) {
1426
1854
  const w = stableWordFromValue(arg ?? null);
@@ -1439,20 +1867,20 @@ function nodeHandlerAttributeInfo(node) {
1439
1867
  }
1440
1868
  const parts = [];
1441
1869
  for (const prop of first.properties ?? []) {
1442
- if (!isObjectPropertyNode(prop)) {
1870
+ if (!isObjectPropertyNode2(prop)) {
1443
1871
  continue;
1444
1872
  }
1445
1873
  if (prop.computed) {
1446
1874
  continue;
1447
1875
  }
1448
- const keyName = isIdentifierNode(prop.key) ? prop.key.name : isStringLiteralNode(prop.key) ? prop.key.value : null;
1876
+ const keyName = isIdentifierNode2(prop.key) ? prop.key.name : isStringLiteralNode2(prop.key) ? prop.key.value : null;
1449
1877
  if (!keyName) {
1450
1878
  continue;
1451
1879
  }
1452
1880
  let valueWord = null;
1453
1881
  if (isBooleanLiteralNode(prop.value)) {
1454
1882
  valueWord = prop.value.value ? "True" : "False";
1455
- } else if (isStringLiteralNode(prop.value)) {
1883
+ } else if (isStringLiteralNode2(prop.value)) {
1456
1884
  const cleaned = (prop.value.value ?? "").trim();
1457
1885
  if (cleaned) {
1458
1886
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1581,13 +2009,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1581
2009
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1582
2010
  }
1583
2011
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1584
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1585
- let value = attrDynamic.exp.loc.source;
1586
- if (attrDynamic.exp.ast?.type === "MemberExpression") {
2012
+ const attrDynamicAst = attrDynamic ? tryGetDirectiveBabelAst(attrDynamic, {
2013
+ preferredViews: ["loc", "compiled"],
2014
+ plugins: ["typescript"],
2015
+ preferExistingAst: false
2016
+ }) : null;
2017
+ if (attrDynamic?.exp && attrDynamicAst) {
2018
+ let value = getVueExpressionSource(attrDynamic.exp, "loc", "compiled");
2019
+ if (isMemberExpression(attrDynamicAst) || isOptionalMemberExpression(attrDynamicAst)) {
1587
2020
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1588
2021
  }
1589
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1590
- value = stringifyExpression(attrDynamic.exp);
2022
+ if (isCallExpression(attrDynamicAst) || isOptionalCallExpression(attrDynamicAst)) {
2023
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1591
2024
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1592
2025
  }
1593
2026
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1595,16 +2028,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1595
2028
  return null;
1596
2029
  }
1597
2030
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1598
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1599
- if (key) {
1600
- return templateAttributeValue(`${componentName}-${key}-${formatTagName(node, nativeWrappers)}`);
2031
+ const keyInfo = getKeyDirectiveInfo(node) || getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2);
2032
+ if (keyInfo) {
2033
+ return templateAttributeValue(`${componentName}-${keyInfo.selectorFragment}-${formatTagName(node, nativeWrappers)}`);
1601
2034
  } else {
1602
2035
  let name = toDirectiveObjectFieldNameValue(toDirective);
1603
2036
  if (!name) {
1604
2037
  if (toDirective.exp == null) {
1605
2038
  return null;
1606
2039
  }
1607
- const source = stringifyExpression(toDirective.exp);
2040
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1608
2041
  const toAst = toDirective.exp.ast;
1609
2042
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && isTemplateLiteral(toAst);
1610
2043
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1628,44 +2061,46 @@ function toDirectiveObjectFieldNameValue(node) {
1628
2061
  if (!node.exp || node.exp.type !== NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== NodeTypes.SIMPLE_EXPRESSION) {
1629
2062
  return null;
1630
2063
  }
1631
- const source = node.exp.loc.source.trim();
1632
- try {
1633
- const expr = parseExpression(source, { plugins: ["typescript"] });
1634
- const isNodeType = (n, type) => {
1635
- return n !== null && n.type === type;
1636
- };
1637
- const isStringLiteralNode = (n) => {
1638
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1639
- };
1640
- const isIdentifierNode = (n) => {
1641
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1642
- };
1643
- const isObjectPropertyNode = (n) => {
1644
- if (!isNodeType(n, "ObjectProperty"))
1645
- return false;
1646
- const nn = n;
1647
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1648
- };
1649
- const isObjectExpressionNode = (n) => {
1650
- if (!isNodeType(n, "ObjectExpression"))
1651
- return false;
1652
- const nn = n;
1653
- return Array.isArray(nn.properties);
1654
- };
1655
- if (!isObjectExpressionNode(expr))
1656
- return null;
1657
- const nameProp = expr.properties.find((p) => {
1658
- if (!isObjectPropertyNode(p))
1659
- return false;
1660
- const key = p.key;
1661
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1662
- });
1663
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1664
- return null;
1665
- return toPascalCase(nameProp.value.value);
1666
- } catch {
2064
+ const expr = tryGetDirectiveBabelAst(node, {
2065
+ preferredViews: ["loc", "compiled"],
2066
+ plugins: ["typescript"],
2067
+ preferExistingAst: false
2068
+ });
2069
+ if (!expr) {
1667
2070
  return null;
1668
2071
  }
2072
+ const isNodeType2 = (n, type) => {
2073
+ return n !== null && n.type === type;
2074
+ };
2075
+ const isStringLiteralNode2 = (n) => {
2076
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
2077
+ };
2078
+ const isIdentifierNode2 = (n) => {
2079
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
2080
+ };
2081
+ const isObjectPropertyNode2 = (n) => {
2082
+ if (!isNodeType2(n, "ObjectProperty"))
2083
+ return false;
2084
+ const nn = n;
2085
+ return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
2086
+ };
2087
+ const isObjectExpressionNode2 = (n) => {
2088
+ if (!isNodeType2(n, "ObjectExpression"))
2089
+ return false;
2090
+ const nn = n;
2091
+ return Array.isArray(nn.properties);
2092
+ };
2093
+ if (!isObjectExpressionNode2(expr))
2094
+ return null;
2095
+ const nameProp = expr.properties.find((p) => {
2096
+ if (!isObjectPropertyNode2(p))
2097
+ return false;
2098
+ const key = p.key;
2099
+ return isIdentifierNode2(key) && key.name === "name" || isStringLiteralNode2(key) && key.value === "name";
2100
+ });
2101
+ if (!nameProp || !isObjectPropertyNode2(nameProp) || !isStringLiteralNode2(nameProp.value))
2102
+ return null;
2103
+ return toPascalCase(nameProp.value.value);
1669
2104
  }
1670
2105
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1671
2106
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1675,7 +2110,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1675
2110
  let handlerName = "";
1676
2111
  if (click.exp) {
1677
2112
  const exp = click.exp;
1678
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
2113
+ const source = getVueExpressionSource(exp, "content", "compiled");
1679
2114
  if (source) {
1680
2115
  const parsed = tryParseBabelAstFromHandlerSource(source);
1681
2116
  if (parsed) {
@@ -1696,9 +2131,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1696
2131
  const trimmed = source.trim();
1697
2132
  if (!trimmed)
1698
2133
  return null;
1699
- try {
1700
- return parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1701
- } catch {
2134
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2135
+ if (expressionAst) {
2136
+ return expressionAst;
1702
2137
  }
1703
2138
  try {
1704
2139
  return parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -2037,23 +2472,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2037
2472
  return null;
2038
2473
  }
2039
2474
  const simpleExp = exp;
2040
- const ast = simpleExp.ast;
2041
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
2042
- const tl = ast;
2043
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2044
- const expressionCount = (tl.expressions ?? []).length;
2045
- const isStatic = expressionCount === 0;
2046
- const raw2 = (simpleExp.content ?? "").trim();
2047
- const unwrappedTemplate = raw2.startsWith("`") && raw2.endsWith("`") && raw2.length >= 2 ? raw2.slice(1, -1) : cooked;
2475
+ const ast = tryGetVueExpressionAst(simpleExp, {
2476
+ preferredViews: ["content", "loc", "compiled"],
2477
+ plugins: ["typescript"]
2478
+ });
2479
+ const unwrappedTemplateLiteral = tryUnwrapTemplateLiteralExpressionSource(simpleExp);
2480
+ if (unwrappedTemplateLiteral) {
2481
+ const isStatic = unwrappedTemplateLiteral.expressionCount === 0;
2048
2482
  if (isStatic) {
2049
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2483
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2050
2484
  }
2485
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2051
2486
  return {
2052
- value: unwrappedTemplate,
2487
+ value: templateValue.template,
2053
2488
  isDynamic: true,
2054
2489
  isStaticLiteral: false,
2055
- template: unwrappedTemplate,
2056
- templateExpressionCount: expressionCount
2490
+ template: templateValue.template,
2491
+ parsedTemplate: templateValue.parsedTemplate,
2492
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2057
2493
  };
2058
2494
  }
2059
2495
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2066,59 +2502,27 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2066
2502
  }
2067
2503
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2068
2504
  if (preservableReference) {
2505
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2069
2506
  return {
2070
2507
  value: preservableReference,
2071
- isDynamic: true,
2072
- isStaticLiteral: false,
2073
- template: `\${${preservableReference}}`,
2074
- templateExpressionCount: 1,
2075
- rawExpression: preservableReference
2076
- };
2077
- }
2078
- const raw = (simpleExp.content ?? "").trim();
2079
- if (!raw) {
2080
- return null;
2081
- }
2082
- try {
2083
- const ast2 = parseExpression(raw, { plugins: ["typescript"] });
2084
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2085
- const tl = ast2;
2086
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2087
- const expressionCount = (tl.expressions ?? []).length;
2088
- const isStatic = expressionCount === 0;
2089
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2090
- if (isStatic) {
2091
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2092
- }
2093
- return {
2094
- value: unwrappedTemplate,
2095
- isDynamic: true,
2096
- isStaticLiteral: false,
2097
- template: unwrappedTemplate,
2098
- templateExpressionCount: expressionCount
2099
- };
2100
- }
2101
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2102
- const sl = ast2;
2103
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2104
- }
2105
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2106
- if (preservableReference2) {
2107
- return {
2108
- value: preservableReference2,
2109
- isDynamic: true,
2110
- isStaticLiteral: false,
2111
- template: `\${${preservableReference2}}`,
2112
- templateExpressionCount: 1,
2113
- rawExpression: preservableReference2
2114
- };
2115
- }
2116
- } catch {
2508
+ isDynamic: true,
2509
+ isStaticLiteral: false,
2510
+ template: templateValue.template,
2511
+ parsedTemplate: templateValue.parsedTemplate,
2512
+ templateExpressionCount: 1,
2513
+ rawExpression: preservableReference
2514
+ };
2515
+ }
2516
+ const raw = (simpleExp.content ?? "").trim();
2517
+ if (!raw) {
2518
+ return null;
2117
2519
  }
2118
2520
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2119
2521
  }
2120
2522
  function isTemplatePlaceholder(part) {
2121
- return part.startsWith("${") && part.endsWith("}") && part.length >= 3;
2523
+ const parsedTemplate = tryParseTemplateFragment(part);
2524
+ const templateFragment = parsedTemplate ? getSingleExpressionTemplateFragment(parsedTemplate) : null;
2525
+ return !!templateFragment && templateFragment.prefix === "" && templateFragment.suffix === "";
2122
2526
  }
2123
2527
  function isAllCapsOrDigits(value) {
2124
2528
  if (value.length <= 1) {
@@ -2171,33 +2575,14 @@ function safeMethodNameFromParts(parts) {
2171
2575
  }
2172
2576
  return name;
2173
2577
  }
2174
- function replaceAllTemplateExpressionsWithKey(template) {
2578
+ function toPomKeyPattern(templateValue) {
2579
+ const { templateLiteral } = templateValue.parsedTemplate;
2175
2580
  let out = "";
2176
- let i = 0;
2177
- while (i < template.length) {
2178
- const start = template.indexOf("${", i);
2179
- if (start < 0) {
2180
- out += template.slice(i);
2181
- break;
2182
- }
2183
- out += template.slice(i, start);
2184
- let depth = 1;
2185
- let j = start + 2;
2186
- while (j < template.length && depth > 0) {
2187
- if (template[j] === "{") {
2188
- depth++;
2189
- } else if (template[j] === "}") {
2190
- depth--;
2191
- }
2192
- j++;
2193
- }
2194
- const end = depth === 0 ? j - 1 : -1;
2195
- if (end < 0) {
2196
- out += template.slice(start);
2197
- break;
2581
+ for (let i = 0; i < templateLiteral.quasis.length; i += 1) {
2582
+ out += templateLiteral.quasis[i]?.value.raw ?? "";
2583
+ if (templateLiteral.expressions[i]) {
2584
+ out += "${key}";
2198
2585
  }
2199
- out += "${key}";
2200
- i = end + 1;
2201
2586
  }
2202
2587
  return out;
2203
2588
  }
@@ -2205,8 +2590,8 @@ function applyResolvedDataTestId(args) {
2205
2590
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2206
2591
  const entryOverrides = args.entryOverrides ?? {};
2207
2592
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2208
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2209
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2593
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2594
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2210
2595
  const warn = args.warn;
2211
2596
  const getBestKeyAccessCandidates = (expr) => {
2212
2597
  if (!expr) {
@@ -2215,7 +2600,10 @@ function applyResolvedDataTestId(args) {
2215
2600
  return splitNullishCoalescingExpression(expr);
2216
2601
  };
2217
2602
  let dataTestId = args.preferredGeneratedValue;
2603
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2218
2604
  let fromExisting = false;
2605
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2606
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2219
2607
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2220
2608
  if (existing) {
2221
2609
  const loc = args.element.loc?.start;
@@ -2236,8 +2624,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2236
2624
  if (existingIdBehavior === "preserve") {
2237
2625
  if (existing.isDynamic) {
2238
2626
  if (existing.template) {
2239
- const existingTemplate = existing.template;
2240
- if ((existing.templateExpressionCount ?? 0) !== 1) {
2627
+ const existingTemplateValue = existing.parsedTemplate ? { kind: "template", template: existing.template, parsedTemplate: existing.parsedTemplate } : templateAttributeValue(existing.template);
2628
+ const existingTemplateFragment = getSingleExpressionTemplateFragment(existingTemplateValue.parsedTemplate);
2629
+ const requiredKeyTemplateValue = bestKeyPreservePlaceholder ? templateAttributeValue(bestKeyPreservePlaceholder) : null;
2630
+ if ((existing.templateExpressionCount ?? 0) !== 1 || !existingTemplateFragment) {
2241
2631
  throw new Error(
2242
2632
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2243
2633
  Component: ${args.componentName}
@@ -2247,20 +2637,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2247
2637
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2248
2638
  );
2249
2639
  }
2250
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2251
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2252
- if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
2640
+ const hasExact = requiredKeyTemplateValue ? templateFragmentContainsSingleExpression(existingTemplateValue.parsedTemplate, requiredKeyTemplateValue.parsedTemplate) : false;
2641
+ const hasVarAccess = getBestKeyAccessCandidates(bestKeyVariable).some((candidate) => existingTemplateFragment.expressionSource === candidate);
2642
+ if (!hasExact && !hasVarAccess && bestKeyPreservePlaceholder) {
2253
2643
  throw new Error(
2254
2644
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2255
2645
  Component: ${args.componentName}
2256
2646
  File: ${file}:${locationHint}
2257
2647
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2258
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2648
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2259
2649
 
2260
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2650
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2261
2651
  );
2262
2652
  }
2263
- dataTestId = templateAttributeValue(existing.template);
2653
+ dataTestId = existingTemplateValue;
2654
+ runtimeDataTestId = existingTemplateValue;
2264
2655
  fromExisting = true;
2265
2656
  } else {
2266
2657
  throw new Error(
@@ -2274,18 +2665,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2274
2665
  );
2275
2666
  }
2276
2667
  } else {
2277
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2668
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2278
2669
  throw new Error(
2279
2670
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2280
2671
  Component: ${args.componentName}
2281
2672
  File: ${file}:${locationHint}
2282
2673
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2283
- Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
2674
+ Required placeholder: ${JSON.stringify(bestKeyPreservePlaceholder)}${bestKeyVariable ? ` or an access on "${bestKeyVariable}"` : ""}
2284
2675
 
2285
- Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2676
+ Fix: either (1) include ${bestKeyPreservePlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
2286
2677
  );
2287
2678
  }
2288
2679
  dataTestId = staticAttributeValue(existing.value);
2680
+ runtimeDataTestId = staticAttributeValue(existing.value);
2289
2681
  fromExisting = true;
2290
2682
  }
2291
2683
  }
@@ -2315,8 +2707,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2315
2707
  };
2316
2708
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2317
2709
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2318
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2319
- const isKeyed = formattedDataTestIdForPom.includes("${key}");
2710
+ const formattedDataTestIdForPom = dataTestId.kind === "template" ? toPomKeyPattern(dataTestId) : dataTestId.value;
2711
+ const selectorPatternKind = dataTestId.kind === "template" ? "parameterized" : "static";
2712
+ const selectorPattern = createPomStringPattern(formattedDataTestIdForPom, selectorPatternKind);
2713
+ const selectorIsParameterized = selectorPatternKind === "parameterized";
2320
2714
  const deriveBaseMethodNameFromHint = (hint) => {
2321
2715
  const hintRaw = (hint ?? "").trim();
2322
2716
  const trimEdgeSeparators = (value) => {
@@ -2364,13 +2758,13 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2364
2758
  const roleSuffix = upperFirst(normalizedRole || "Element");
2365
2759
  const baseName = upperFirst(primaryMethodName);
2366
2760
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2367
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2761
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2368
2762
  };
2369
2763
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2370
2764
  const roleSuffix = upperFirst(normalizedRole || "Element");
2371
2765
  const baseName = upperFirst(primaryMethodName);
2372
2766
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2373
- if (!isKeyed) {
2767
+ if (!selectorIsParameterized) {
2374
2768
  return { primary: propertyName };
2375
2769
  }
2376
2770
  const stripped = removeByKeySegment2(propertyName);
@@ -2432,11 +2826,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2432
2826
  return false;
2433
2827
  }
2434
2828
  const existingSelectors = [
2435
- existingPom.formattedDataTestId,
2436
- ...existingPom.alternateFormattedDataTestIds ?? []
2829
+ existingPom.selector,
2830
+ ...existingPom.alternateSelectors ?? []
2437
2831
  ];
2438
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2439
- if (isKeyed && !sharesSelectorIdentity) {
2832
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2833
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2440
2834
  return false;
2441
2835
  }
2442
2836
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2451,12 +2845,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2451
2845
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2452
2846
  return false;
2453
2847
  }
2454
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2455
- existingPom.alternateFormattedDataTestIds ??= [];
2456
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2457
- existingPom.alternateFormattedDataTestIds.push(formattedDataTestIdForPom);
2848
+ if (!pomStringPatternEquals(existingPom.selector, selectorPattern)) {
2849
+ existingPom.alternateSelectors ??= [];
2850
+ if (!(existingPom.alternateSelectors ?? []).some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern))) {
2851
+ existingPom.alternateSelectors.push(selectorPattern);
2458
2852
  }
2459
2853
  }
2854
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
2855
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
2856
+ }
2460
2857
  return true;
2461
2858
  };
2462
2859
  let methodName = "";
@@ -2469,7 +2866,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2469
2866
  let suffix = 1;
2470
2867
  while (true) {
2471
2868
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2472
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2869
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2473
2870
  const actionName = getPrimaryActionMethodName(candidate);
2474
2871
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2475
2872
  let chosenGetterName = getterCandidates.primary;
@@ -2495,7 +2892,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2495
2892
  const baseNameUpper = upperFirst(baseWithSuffix);
2496
2893
  if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
2497
2894
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2498
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2895
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2499
2896
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2500
2897
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2501
2898
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2563,51 +2960,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2563
2960
  Fix: make the element identifiable (e.g. add id/name/inner text or use a more specific click handler name), or switch generation.nameCollisionBehavior to "warn"/"suffix".`
2564
2961
  );
2565
2962
  }
2566
- const params = {};
2567
- if (isKeyed) {
2568
- params.key = keyTypeFromValues;
2569
- }
2963
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2570
2964
  switch (normalizedRole) {
2571
2965
  case "input":
2572
- params.text = "string";
2573
- params.annotationText = 'string = ""';
2574
- if (!isKeyed) delete params.key;
2966
+ parameters = setPomParameter(parameters, "text", "string");
2967
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2575
2968
  break;
2576
2969
  case "select":
2577
- params.value = "string";
2578
- params.annotationText = 'string = ""';
2579
- if (!isKeyed) delete params.key;
2970
+ parameters = setPomParameter(parameters, "value", "string");
2971
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2580
2972
  break;
2581
2973
  case "vselect":
2582
- params.value = "string";
2583
- params.timeOut = "number = 500";
2584
- params.annotationText = 'string = ""';
2585
- if (!isKeyed) delete params.key;
2974
+ parameters = setPomParameter(parameters, "value", "string");
2975
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
2976
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2586
2977
  break;
2587
2978
  case "radio":
2588
- params.annotationText = 'string = ""';
2979
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2589
2980
  break;
2590
2981
  }
2591
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2592
- params.key = keyTypeFromValues;
2593
- }
2982
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2594
2983
  if (addHtmlAttribute && !fromExisting) {
2595
2984
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2596
2985
  }
2597
2986
  const childComponentName = args.element.tag;
2598
2987
  const dataTestIdEntry = {
2599
- value: getAttributeValueText(dataTestId),
2600
- templateLiteral: void 0,
2601
- ...entryOverrides
2988
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
2989
+ getAttributeValueText(dataTestId),
2990
+ dataTestId.kind === "template" ? "parameterized" : "static"
2991
+ ),
2992
+ templateLiteral: entryOverrides.templateLiteral,
2993
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2602
2994
  };
2603
2995
  dataTestIdEntry.pom = {
2604
2996
  nativeRole: normalizedRole,
2605
2997
  methodName,
2606
2998
  getterNameOverride,
2607
- formattedDataTestId: formattedDataTestIdForPom,
2608
- alternateFormattedDataTestIds: void 0,
2999
+ selector: selectorPattern,
3000
+ alternateSelectors: void 0,
2609
3001
  mergeKey: args.pomMergeKey,
2610
- params,
3002
+ parameters: normalizedParameters,
2611
3003
  keyValuesOverride: args.keyValuesOverride ?? null
2612
3004
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2613
3005
  };
@@ -2638,43 +3030,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2638
3030
  }
2639
3031
  };
2640
3032
  const getSignatureForGeneratedMethod = () => {
2641
- const role = normalizedRole;
2642
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2643
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2644
- const keyType = keyTypeFromValues;
2645
- if (isNavigation2) {
2646
- if (needsKey2) {
2647
- return { params: `key: ${keyType}`, argNames: ["key"] };
2648
- }
2649
- return { params: "", argNames: [] };
2650
- }
2651
- switch (role) {
2652
- case "input":
2653
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2654
- case "select":
2655
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2656
- case "vselect":
2657
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2658
- case "radio":
2659
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2660
- default:
2661
- if (needsKey2) {
2662
- return { params: `key: ${keyType}`, argNames: ["key"] };
2663
- }
2664
- return { params: "", argNames: [] };
2665
- }
3033
+ return createPomMethodSignature(normalizedParameters);
2666
3034
  };
2667
3035
  const registerPrimaryOnce = (pom) => {
2668
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2669
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3036
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2670
3037
  const key = JSON.stringify({
2671
3038
  kind: "primary",
2672
3039
  role: pom.nativeRole,
2673
3040
  methodName: pom.methodName,
2674
3041
  getterNameOverride: pom.getterNameOverride ?? null,
2675
- formattedDataTestId: pom.formattedDataTestId,
2676
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2677
- params: stableParams,
3042
+ selector: pom.selector,
3043
+ alternateSelectors: alternates.length ? alternates : void 0,
3044
+ parameters: pom.parameters,
2678
3045
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2679
3046
  emitPrimary: pom.emitPrimary ?? true
2680
3047
  });
@@ -2688,8 +3055,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2688
3055
  }
2689
3056
  };
2690
3057
  const addExtraClickMethod = (spec) => {
2691
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2692
- const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, params: stableParams });
3058
+ const key = JSON.stringify({ kind: spec.kind, selector: spec.selector, keyLiteral: spec.keyLiteral ?? null, parameters: spec.parameters });
2693
3059
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2694
3060
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2695
3061
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2712,7 +3078,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2712
3078
  if (prev === null) {
2713
3079
  return;
2714
3080
  }
2715
- if (signature === null || prev.params !== signature.params) {
3081
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2716
3082
  args.dependencies.generatedMethods.set(name, null);
2717
3083
  }
2718
3084
  };
@@ -2728,23 +3094,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2728
3094
  return candidate;
2729
3095
  };
2730
3096
  const tryGetDirectiveExpressionAst = (dir) => {
2731
- const exp = dir.exp;
2732
- if (!exp) {
2733
- return null;
2734
- }
2735
- if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
2736
- const simple = exp;
2737
- const ast = simple.ast;
2738
- if (ast && "type" in ast) {
2739
- return ast;
2740
- }
2741
- }
2742
- try {
2743
- const raw = args.context ? stringifyExpression(exp) : exp.loc.source;
2744
- return parseExpression(raw, { plugins: ["typescript"] });
2745
- } catch {
2746
- return null;
2747
- }
3097
+ return tryGetDirectiveBabelAst(dir, {
3098
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3099
+ plugins: ["typescript"]
3100
+ });
2748
3101
  };
2749
3102
  const tryGetStaticStringFromBabel = (node) => {
2750
3103
  if (!node) {
@@ -2841,17 +3194,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2841
3194
  name: generatedName2,
2842
3195
  selector: {
2843
3196
  kind: "withinTestIdByLabel",
2844
- rootFormattedDataTestId: wrapperTestId,
2845
- formattedLabel: label,
3197
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3198
+ label: createPomStringPattern(label, "static"),
2846
3199
  exact: true
2847
3200
  },
2848
- params: { annotationText: `string = ""` }
3201
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2849
3202
  });
2850
3203
  if (added2) {
2851
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3204
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2852
3205
  }
2853
3206
  }
2854
- return;
3207
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2855
3208
  }
2856
3209
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2857
3210
  if (dataTestIdEntry.pom) {
@@ -2863,19 +3216,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2863
3216
  name: generatedName,
2864
3217
  selector: {
2865
3218
  kind: "withinTestIdByLabel",
2866
- rootFormattedDataTestId: wrapperTestId,
2867
- formattedLabel: "${value}",
3219
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3220
+ label: createPomStringPattern("${value}", "parameterized"),
2868
3221
  exact: true
2869
3222
  },
2870
- params: { value: "string", annotationText: `string = ""` }
3223
+ parameters: [
3224
+ createPomParameterSpec("value", "string"),
3225
+ createPomParameterSpec("annotationText", `string = ""`)
3226
+ ]
2871
3227
  });
2872
3228
  if (added) {
2873
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3229
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3230
+ createPomParameterSpec("value", "string"),
3231
+ createPomParameterSpec("annotationText", `string = ""`)
3232
+ ]));
2874
3233
  }
2875
- return;
3234
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2876
3235
  }
2877
3236
  const staticKeyValues = args.keyValuesOverride ?? null;
2878
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3237
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2879
3238
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2880
3239
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2881
3240
  if (dataTestIdEntry.pom) {
@@ -2894,19 +3253,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2894
3253
  name: generatedName,
2895
3254
  selector: {
2896
3255
  kind: "testId",
2897
- formattedDataTestId: formattedDataTestIdForPom
3256
+ testId: selectorPattern
2898
3257
  },
2899
3258
  keyLiteral: rawValue,
2900
- params: { wait: "boolean = true", annotationText: 'string = ""' }
3259
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2901
3260
  });
2902
3261
  if (added) {
2903
- registerGeneratedMethodSignature(generatedName, {
2904
- params: `wait: boolean = true, annotationText: string = ""`,
2905
- argNames: ["wait", "annotationText"]
2906
- });
3262
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3263
+ createPomParameterSpec("wait", "boolean = true"),
3264
+ createPomParameterSpec("annotationText", 'string = ""')
3265
+ ]));
2907
3266
  }
2908
3267
  }
2909
- return;
3268
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2910
3269
  }
2911
3270
  if (dataTestIdEntry.pom) {
2912
3271
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2920,6 +3279,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2920
3279
  const generatedName = getGeneratedMethodName();
2921
3280
  registerGeneratedMethodSignature(generatedName, signature);
2922
3281
  }
3282
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2923
3283
  }
2924
3284
  function safeRealpath(value) {
2925
3285
  try {
@@ -3907,113 +4267,6 @@ class VuePomGeneratorError extends Error {
3907
4267
  this.name = "VuePomGeneratorError";
3908
4268
  }
3909
4269
  }
3910
- function splitParameterList(parameters) {
3911
- const parts = [];
3912
- let current = "";
3913
- let braceDepth = 0;
3914
- let bracketDepth = 0;
3915
- let parenDepth = 0;
3916
- let angleDepth = 0;
3917
- let inSingleQuote = false;
3918
- let inDoubleQuote = false;
3919
- let inTemplateString = false;
3920
- for (let index = 0; index < parameters.length; index += 1) {
3921
- const char = parameters[index];
3922
- const previous = index > 0 ? parameters[index - 1] : "";
3923
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3924
- inSingleQuote = !inSingleQuote;
3925
- current += char;
3926
- continue;
3927
- }
3928
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3929
- inDoubleQuote = !inDoubleQuote;
3930
- current += char;
3931
- continue;
3932
- }
3933
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3934
- inTemplateString = !inTemplateString;
3935
- current += char;
3936
- continue;
3937
- }
3938
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3939
- current += char;
3940
- continue;
3941
- }
3942
- switch (char) {
3943
- case "{":
3944
- braceDepth += 1;
3945
- break;
3946
- case "}":
3947
- braceDepth -= 1;
3948
- break;
3949
- case "[":
3950
- bracketDepth += 1;
3951
- break;
3952
- case "]":
3953
- bracketDepth -= 1;
3954
- break;
3955
- case "(":
3956
- parenDepth += 1;
3957
- break;
3958
- case ")":
3959
- parenDepth -= 1;
3960
- break;
3961
- case "<":
3962
- angleDepth += 1;
3963
- break;
3964
- case ">":
3965
- angleDepth -= 1;
3966
- break;
3967
- case ",":
3968
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
3969
- const trimmed2 = current.trim();
3970
- if (trimmed2) {
3971
- parts.push(trimmed2);
3972
- }
3973
- current = "";
3974
- continue;
3975
- }
3976
- break;
3977
- }
3978
- current += char;
3979
- }
3980
- const trimmed = current.trim();
3981
- if (trimmed) {
3982
- parts.push(trimmed);
3983
- }
3984
- return parts;
3985
- }
3986
- function parseParameterSignature(parameter) {
3987
- const colonIndex = parameter.indexOf(":");
3988
- if (colonIndex < 0) {
3989
- return { name: parameter.trim() };
3990
- }
3991
- const rawName = parameter.slice(0, colonIndex).trim();
3992
- const hasQuestionToken = rawName.endsWith("?");
3993
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
3994
- const remainder = parameter.slice(colonIndex + 1).trim();
3995
- const initializerIndex = remainder.lastIndexOf("=");
3996
- if (initializerIndex < 0) {
3997
- return {
3998
- name,
3999
- hasQuestionToken,
4000
- type: remainder || void 0
4001
- };
4002
- }
4003
- return {
4004
- name,
4005
- hasQuestionToken,
4006
- type: remainder.slice(0, initializerIndex).trim() || void 0,
4007
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
4008
- };
4009
- }
4010
- function parseParameterSignatures(parameters) {
4011
- const trimmed = parameters.trim();
4012
- if (!trimmed) {
4013
- return [];
4014
- }
4015
- return splitParameterList(trimmed).map(parseParameterSignature);
4016
- }
4017
4270
  function toPosixRelativePath(fromDir, toFile) {
4018
4271
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
4019
4272
  if (!rel.startsWith(".")) {
@@ -4033,6 +4286,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
4033
4286
  const root = projectRoot ?? process.cwd();
4034
4287
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
4035
4288
  }
4289
+ function createMissingCustomPomDirectoryError(configuredDir, resolvedDir) {
4290
+ return new VuePomGeneratorError(
4291
+ `Custom POM directory "${configuredDir}" does not exist.
4292
+ Resolved path: ${resolvedDir}
4293
+ Create the directory, point generation.playwright.customPoms.dir at the correct location, or remove the customPoms configuration.`
4294
+ );
4295
+ }
4296
+ function createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir) {
4297
+ const renderedClassNames = missingClassNames.map((name) => `"${name}"`).join(", ");
4298
+ return new VuePomGeneratorError(
4299
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.
4300
+ Expected matching helper files/exports under "${configuredDir}".
4301
+ Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.`
4302
+ );
4303
+ }
4036
4304
  function createCustomPomImportCollisionError(exportName, requested) {
4037
4305
  return new VuePomGeneratorError(
4038
4306
  `Custom POM import name collision detected for "${exportName}".
@@ -4171,35 +4439,28 @@ function generateGoToSelfMethod(componentName) {
4171
4439
  })
4172
4440
  ];
4173
4441
  }
4174
- function formatMethodParams(params) {
4175
- if (!params)
4176
- return "";
4177
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4178
- const entries = Object.entries(params);
4179
- if (!entries.length)
4180
- return "";
4181
- const score = (name) => {
4182
- const idx = preferredOrder.indexOf(name);
4183
- return idx < 0 ? 999 : idx;
4184
- };
4185
- return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
4442
+ function getSelectorPatterns(selector) {
4443
+ return selector.kind === "testId" ? [selector.testId] : [selector.rootTestId, selector.label];
4186
4444
  }
4187
4445
  function generateExtraClickMethodMembers(spec) {
4188
4446
  if (spec.kind !== "click") {
4189
4447
  return [];
4190
4448
  }
4191
- const params = spec.params ?? {};
4192
- const signatureParams = formatMethodParams(params);
4193
- const parameters = parseParameterSignatures(signatureParams);
4194
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4195
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
4449
+ const selectorPatterns = getSelectorPatterns(spec.selector);
4450
+ const signatureSpecs = orderPomPatternParameters(
4451
+ spec.parameters,
4452
+ selectorPatterns,
4453
+ { omit: spec.keyLiteral !== void 0 ? ["key"] : [] }
4454
+ );
4455
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
4456
+ const hasAnnotationText = signatureSpecs.some((param) => param.name === "annotationText");
4457
+ const hasWait = signatureSpecs.some((param) => param.name === "wait");
4196
4458
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4197
4459
  const waitArg = hasWait ? "wait" : "true";
4198
4460
  if (spec.selector.kind === "testId") {
4199
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4200
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4461
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4201
4462
  const clickArgs = [];
4202
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4463
+ clickArgs.push(testIdBinding.expression);
4203
4464
  if (hasAnnotationText || hasWait) {
4204
4465
  clickArgs.push(annotationArg);
4205
4466
  }
@@ -4215,20 +4476,16 @@ function generateExtraClickMethodMembers(spec) {
4215
4476
  if (spec.keyLiteral !== void 0) {
4216
4477
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4217
4478
  }
4218
- if (needsTemplate) {
4219
- writer.writeLine(`const testId = ${testIdExpr};`);
4479
+ for (const statement of testIdBinding.setupStatements) {
4480
+ writer.writeLine(statement);
4220
4481
  }
4221
4482
  writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4222
4483
  }
4223
4484
  })
4224
4485
  ];
4225
4486
  }
4226
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4227
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4228
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4229
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4230
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4231
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4487
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4488
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4232
4489
  return [
4233
4490
  createClassMethod({
4234
4491
  name: spec.name,
@@ -4238,13 +4495,13 @@ function generateExtraClickMethodMembers(spec) {
4238
4495
  if (spec.keyLiteral !== void 0) {
4239
4496
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4240
4497
  }
4241
- if (rootNeedsTemplate) {
4242
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4498
+ for (const statement of rootBinding.setupStatements) {
4499
+ writer.writeLine(statement);
4243
4500
  }
4244
- if (labelNeedsTemplate) {
4245
- writer.writeLine(`const label = ${labelExpr};`);
4501
+ for (const statement of labelBinding.setupStatements) {
4502
+ writer.writeLine(statement);
4246
4503
  }
4247
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4504
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg});`);
4248
4505
  }
4249
4506
  })
4250
4507
  ];
@@ -4257,10 +4514,10 @@ function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4257
4514
  targetPageObjectModelClass,
4258
4515
  primary.methodName,
4259
4516
  primary.nativeRole,
4260
- primary.formattedDataTestId,
4261
- primary.alternateFormattedDataTestIds,
4517
+ primary.selector,
4518
+ primary.alternateSelectors,
4262
4519
  primary.getterNameOverride,
4263
- primary.params ?? {}
4520
+ primary.parameters
4264
4521
  );
4265
4522
  }
4266
4523
  function generateMethodsContentForDependencies(dependencies) {
@@ -4268,15 +4525,14 @@ function generateMethodsContentForDependencies(dependencies) {
4268
4525
  const primarySpecsAll = entries.map((e) => ({ pom: e.pom, target: e.targetPageObjectModelClass })).filter((x) => !!x.pom).sort((a, b) => a.pom.methodName.localeCompare(b.pom.methodName));
4269
4526
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4270
4527
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4271
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4272
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4528
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4273
4529
  const key = JSON.stringify({
4274
4530
  role: pom.nativeRole,
4275
4531
  methodName: pom.methodName,
4276
4532
  getterNameOverride: pom.getterNameOverride ?? null,
4277
- testId: pom.formattedDataTestId,
4278
- alternateTestIds: alternates.length ? alternates : void 0,
4279
- params: stableParams,
4533
+ selector: pom.selector,
4534
+ alternateSelectors: alternates.length ? alternates : void 0,
4535
+ parameters: pom.parameters,
4280
4536
  target: target ?? null,
4281
4537
  emitPrimary: pom.emitPrimary ?? true
4282
4538
  });
@@ -4303,6 +4559,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4303
4559
  customPomAttachments = [],
4304
4560
  projectRoot,
4305
4561
  customPomDir,
4562
+ requireCustomPomDir,
4306
4563
  customPomImportAliases,
4307
4564
  customPomImportNameCollisionBehavior = "error",
4308
4565
  testIdAttribute,
@@ -4335,6 +4592,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4335
4592
  customPomAttachments,
4336
4593
  projectRoot,
4337
4594
  customPomDir,
4595
+ requireCustomPomDir,
4338
4596
  customPomImportAliases,
4339
4597
  customPomImportNameCollisionBehavior,
4340
4598
  testIdAttribute,
@@ -4344,6 +4602,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4344
4602
  customPomAttachments,
4345
4603
  projectRoot,
4346
4604
  customPomDir,
4605
+ requireCustomPomDir,
4347
4606
  customPomImportAliases,
4348
4607
  customPomImportNameCollisionBehavior,
4349
4608
  testIdAttribute,
@@ -4399,9 +4658,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4399
4658
  }
4400
4659
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4401
4660
  customPomDir: options.customPomDir,
4661
+ requireCustomPomDir: options.requireCustomPomDir,
4402
4662
  customPomImportAliases: options.customPomImportAliases,
4403
4663
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4404
4664
  });
4665
+ assertCustomPomAttachmentsResolved(
4666
+ options.customPomAttachments ?? [],
4667
+ customPomImportResolution.classIdentifierMap,
4668
+ options.customPomDir ?? "tests/playwright/pom/custom"
4669
+ );
4405
4670
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4406
4671
  const files = [];
4407
4672
  for (const [name, deps] of entries) {
@@ -4560,21 +4825,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4560
4825
  return { filePath, content };
4561
4826
  });
4562
4827
  }
4563
- function toCSharpTestIdExpression(formattedDataTestId) {
4564
- const needsInterpolation = formattedDataTestId.includes("${");
4565
- if (!needsInterpolation) {
4566
- return JSON.stringify(formattedDataTestId);
4567
- }
4568
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4569
- const quoted = JSON.stringify(inner);
4570
- return `$${quoted}`;
4571
- }
4572
- function toCSharpParam(paramTypeExpr) {
4573
- const trimmed = (paramTypeExpr ?? "").trim();
4574
- const eqIdx = trimmed.indexOf("=");
4575
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4576
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4577
- const typePart = left.includes("|") ? "string" : left;
4828
+ function toCSharpParam(param) {
4829
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4578
4830
  let type = "string";
4579
4831
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4580
4832
  type = "bool";
@@ -4582,19 +4834,17 @@ function toCSharpParam(paramTypeExpr) {
4582
4834
  type = "string";
4583
4835
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4584
4836
  type = "int";
4585
- else if (/\d+/.test(typePart) && typePart === "")
4586
- type = "int";
4587
4837
  else if (/\btimeOut\b/i.test(typePart))
4588
4838
  type = "int";
4589
4839
  let defaultExpr;
4590
- if (right !== void 0) {
4840
+ if (param.initializer !== void 0) {
4591
4841
  if (type === "bool") {
4592
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : void 0;
4842
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : void 0;
4593
4843
  } else if (type === "int") {
4594
- const m = right.match(/\d+/);
4844
+ const m = param.initializer.match(/\d+/);
4595
4845
  defaultExpr = m ? m[0] : void 0;
4596
4846
  } else {
4597
- if (right === '""' || right === '""' || right === "''") {
4847
+ if (param.initializer === '""' || param.initializer === "''") {
4598
4848
  defaultExpr = '""';
4599
4849
  }
4600
4850
  }
@@ -4602,17 +4852,15 @@ function toCSharpParam(paramTypeExpr) {
4602
4852
  return { type, defaultExpr };
4603
4853
  }
4604
4854
  function formatCSharpParams(params) {
4605
- if (!params)
4606
- return { signature: "", argNames: [] };
4607
- const entries = Object.entries(params);
4608
- if (!entries.length)
4855
+ const normalizedParams = normalizePomParameters(params);
4856
+ if (!normalizedParams.length)
4609
4857
  return { signature: "", argNames: [] };
4610
4858
  const signatureParts = [];
4611
4859
  const argNames = [];
4612
- for (const [name, typeExpr] of entries) {
4613
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4614
- argNames.push(name);
4615
- signatureParts.push(defaultExpr !== void 0 ? `${type} ${name} = ${defaultExpr}` : `${type} ${name}`);
4860
+ for (const param of normalizedParams) {
4861
+ const { type, defaultExpr } = toCSharpParam(param);
4862
+ argNames.push(param.name);
4863
+ signatureParts.push(defaultExpr !== void 0 ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
4616
4864
  }
4617
4865
  return { signature: signatureParts.join(", "), argNames };
4618
4866
  }
@@ -4703,23 +4951,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4703
4951
  const baseMethodName = upperFirst(pom.methodName);
4704
4952
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4705
4953
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4706
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4707
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4708
- const templateVars = templateVarMatches.map((m) => m[1]);
4709
- const augmentedParams = { ...pom.params };
4710
- for (const v of templateVars) {
4711
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4712
- augmentedParams[v] = "string";
4713
- }
4714
- }
4715
- const orderedParams = Object.fromEntries([
4716
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4717
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4718
- ]);
4954
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
4955
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
4956
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4719
4957
  const { signature, argNames } = formatCSharpParams(orderedParams);
4720
4958
  const args = argNames.join(", ");
4721
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4722
- if (pom.formattedDataTestId.includes("${")) {
4959
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
4960
+ if (selectorIsParameterized) {
4723
4961
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4724
4962
  } else {
4725
4963
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4730,12 +4968,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4730
4968
  if (target) {
4731
4969
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4732
4970
  chunks.push(" {");
4733
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4734
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
4971
+ if (selectorIsParameterized || allTestIds.length <= 1) {
4972
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4735
4973
  chunks.push(` return new ${target}(Page);`);
4736
4974
  } else {
4737
4975
  chunks.push(" Exception? lastError = null;");
4738
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
4976
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4739
4977
  chunks.push(" {");
4740
4978
  chunks.push(" try");
4741
4979
  chunks.push(" {");
@@ -4759,7 +4997,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4759
4997
  }
4760
4998
  chunks.push(` public async Task ${actionName}(${sig})`);
4761
4999
  chunks.push(" {");
4762
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5000
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4763
5001
  const emitActionCall = (locatorAccess) => {
4764
5002
  if (pom.nativeRole === "input") {
4765
5003
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4772,9 +5010,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4772
5010
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4773
5011
  }
4774
5012
  };
4775
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5013
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4776
5014
  chunks.push(" Exception? lastError = null;");
4777
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
5015
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map((testId) => toCSharpPomPatternExpression(testId)).join(", ")} })`);
4778
5016
  chunks.push(" {");
4779
5017
  chunks.push(" try");
4780
5018
  chunks.push(" {");
@@ -4820,7 +5058,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4820
5058
  for (const extra of extras) {
4821
5059
  if (extra.kind !== "click")
4822
5060
  continue;
4823
- const { signature } = formatCSharpParams(extra.params);
5061
+ const extraParams = orderPomPatternParameters(
5062
+ extra.parameters,
5063
+ getSelectorPatterns(extra.selector),
5064
+ { omit: extra.keyLiteral !== void 0 ? ["key"] : [] }
5065
+ );
5066
+ const { signature } = formatCSharpParams(extraParams);
4824
5067
  const extraName = upperFirst(extra.name);
4825
5068
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4826
5069
  chunks.push(" {");
@@ -4828,29 +5071,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4828
5071
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4829
5072
  }
4830
5073
  if (extra.selector.kind === "testId") {
4831
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4832
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4833
- if (needsTemplate) {
4834
- chunks.push(` var testId = ${testIdExpr};`);
4835
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4836
- } else {
4837
- chunks.push(` await LocatorByTestId(${testIdExpr}).ClickAsync();`);
5074
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
5075
+ for (const statement of testIdBinding.setupStatements) {
5076
+ chunks.push(` ${statement}`);
4838
5077
  }
5078
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4839
5079
  } else {
4840
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4841
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4842
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4843
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5080
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5081
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4844
5082
  const exactArg = extra.selector.exact === false ? "false" : "true";
4845
- if (rootNeedsTemplate) {
4846
- chunks.push(` var rootTestId = ${rootExpr};`);
5083
+ for (const statement of rootBinding.setupStatements) {
5084
+ chunks.push(` ${statement}`);
4847
5085
  }
4848
- if (labelNeedsTemplate) {
4849
- chunks.push(` var label = ${labelExpr};`);
5086
+ for (const statement of labelBinding.setupStatements) {
5087
+ chunks.push(` ${statement}`);
4850
5088
  }
4851
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4852
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4853
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5089
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4854
5090
  }
4855
5091
  chunks.push(" }");
4856
5092
  chunks.push("");
@@ -5294,7 +5530,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5294
5530
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5295
5531
  continue;
5296
5532
  const list = methodToChildren.get(name) ?? [];
5297
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5533
+ list.push({ childProp, signature: sig });
5298
5534
  methodToChildren.set(name, list);
5299
5535
  }
5300
5536
  }
@@ -5304,12 +5540,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5304
5540
  return [];
5305
5541
  }
5306
5542
  return passthroughs.map(([methodName, candidates]) => {
5307
- const { childProp, params, argNames } = candidates[0];
5308
- const callArgs = argNames.join(", ");
5543
+ const { childProp, signature } = candidates[0];
5544
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5309
5545
  return createClassMethod({
5310
5546
  name: methodName,
5311
5547
  isAsync: true,
5312
- parameters: parseParameterSignatures(params),
5548
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5313
5549
  statements: [
5314
5550
  `return await this.${childProp}.${methodName}(${callArgs});`
5315
5551
  ]
@@ -5333,8 +5569,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5333
5569
  const list = methodToAttachments.get(methodName) ?? [];
5334
5570
  list.push({
5335
5571
  propertyName: attachment.propertyName,
5336
- params: signature.params,
5337
- argNames: signature.argNames
5572
+ signature
5338
5573
  });
5339
5574
  methodToAttachments.set(methodName, list);
5340
5575
  }
@@ -5345,12 +5580,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5345
5580
  return [];
5346
5581
  }
5347
5582
  return passthroughs.map(([methodName, candidates]) => {
5348
- const { propertyName, params, argNames } = candidates[0];
5349
- const callArgs = argNames.join(", ");
5583
+ const { propertyName, signature } = candidates[0];
5584
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5350
5585
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5351
5586
  return createClassMethod({
5352
5587
  name: methodName,
5353
- parameters: parseParameterSignatures(params),
5588
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5354
5589
  statements: [
5355
5590
  `return ${invocation};`
5356
5591
  ]
@@ -5364,15 +5599,44 @@ function sliceNodeSource(source, node) {
5364
5599
  const snippet = source.slice(node.start, node.end).trim();
5365
5600
  return snippet.length ? snippet : null;
5366
5601
  }
5367
- function getCustomPomCallArgumentName(param) {
5602
+ function getTypeAnnotationSource(source, node) {
5603
+ const rawTypeAnnotation = node.typeAnnotation;
5604
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
5605
+ return void 0;
5606
+ }
5607
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
5608
+ return typeAnnotation && typeof typeAnnotation === "object" ? sliceNodeSource(source, typeAnnotation) ?? void 0 : void 0;
5609
+ }
5610
+ function getCustomPomParameterSpec(source, param) {
5368
5611
  if (param.type === "Identifier") {
5369
- return param.name;
5612
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5613
+ hasQuestionToken: !!param.optional
5614
+ });
5370
5615
  }
5371
5616
  if (param.type === "AssignmentPattern") {
5372
- return param.left.type === "Identifier" ? param.left.name : null;
5617
+ if (param.left.type !== "Identifier") {
5618
+ return null;
5619
+ }
5620
+ const initializer = sliceNodeSource(source, param.right);
5621
+ if (!initializer) {
5622
+ return null;
5623
+ }
5624
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
5625
+ initializer,
5626
+ hasQuestionToken: !!param.left.optional
5627
+ });
5373
5628
  }
5374
5629
  if (param.type === "RestElement") {
5375
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
5630
+ if (param.argument.type !== "Identifier") {
5631
+ return null;
5632
+ }
5633
+ const typeExpression = getTypeAnnotationSource(
5634
+ source,
5635
+ param
5636
+ ) ?? getTypeAnnotationSource(source, param.argument);
5637
+ return createPomParameterSpec(param.argument.name, typeExpression, {
5638
+ isRestParameter: true
5639
+ });
5376
5640
  }
5377
5641
  return null;
5378
5642
  }
@@ -5405,29 +5669,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5405
5669
  if (member.key.type !== "Identifier") {
5406
5670
  continue;
5407
5671
  }
5408
- const params = [];
5409
- const argNames = [];
5672
+ const parameters = [];
5410
5673
  let supported = true;
5411
5674
  member.params.forEach((param) => {
5412
5675
  if (!supported) {
5413
5676
  return;
5414
5677
  }
5415
- const paramSource = sliceNodeSource(source, param);
5416
- const argName = getCustomPomCallArgumentName(param);
5417
- if (!paramSource || !argName) {
5678
+ const parameter = getCustomPomParameterSpec(source, param);
5679
+ if (!parameter) {
5418
5680
  supported = false;
5419
5681
  return;
5420
5682
  }
5421
- params.push(paramSource);
5422
- argNames.push(argName);
5683
+ parameters.push(parameter);
5423
5684
  });
5424
5685
  if (!supported) {
5425
5686
  continue;
5426
5687
  }
5427
- signatures.set(member.key.name, {
5428
- params: params.join(", "),
5429
- argNames
5430
- });
5688
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5431
5689
  }
5432
5690
  }
5433
5691
  return signatures;
@@ -5610,6 +5868,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5610
5868
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5611
5869
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5612
5870
  if (!fs.existsSync(customDirAbs)) {
5871
+ if (options.requireCustomPomDir) {
5872
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
5873
+ }
5613
5874
  return {
5614
5875
  classIdentifierMap,
5615
5876
  methodSignaturesByClass,
@@ -5652,6 +5913,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5652
5913
  importSpecifiersByClass
5653
5914
  };
5654
5915
  }
5916
+ function assertCustomPomAttachmentsResolved(attachments, classIdentifierMap, configuredDir) {
5917
+ const missingClassNames = Array.from(new Set(
5918
+ attachments.map((attachment) => attachment.className).filter((className) => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className))
5919
+ )).sort((left, right) => left.localeCompare(right));
5920
+ if (missingClassNames.length > 0) {
5921
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
5922
+ }
5923
+ }
5655
5924
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5656
5925
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5657
5926
  if (!filePath)
@@ -5680,7 +5949,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5680
5949
  if (!sig)
5681
5950
  continue;
5682
5951
  const list = methodToChildren.get(name) ?? [];
5683
- list.push({ child, params: sig.params, argNames: sig.argNames });
5952
+ list.push({ child, signature: sig });
5684
5953
  methodToChildren.set(name, list);
5685
5954
  }
5686
5955
  }
@@ -5688,12 +5957,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5688
5957
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5689
5958
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5690
5959
  continue;
5691
- const { child, params, argNames } = candidatesForMethod[0];
5692
- const callArgs = argNames.join(", ");
5960
+ const { child, signature } = candidatesForMethod[0];
5961
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5693
5962
  passthroughMembers.push(createClassMethod({
5694
5963
  name: methodName,
5695
5964
  isAsync: true,
5696
- parameters: parseParameterSignatures(params),
5965
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5697
5966
  statements: [
5698
5967
  `return await this.${child}.${methodName}(${callArgs});`
5699
5968
  ]
@@ -5742,9 +6011,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5742
6011
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5743
6012
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5744
6013
  customPomDir: options.customPomDir,
6014
+ requireCustomPomDir: options.requireCustomPomDir,
5745
6015
  customPomImportAliases: options.customPomImportAliases,
5746
6016
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5747
6017
  });
6018
+ assertCustomPomAttachmentsResolved(
6019
+ options.customPomAttachments ?? [],
6020
+ customPomImportResolution.classIdentifierMap,
6021
+ options.customPomDir ?? "tests/playwright/pom/custom"
6022
+ );
5748
6023
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5749
6024
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5750
6025
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5894,8 +6169,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5894
6169
  return candidate;
5895
6170
  };
5896
6171
  for (const dt of dataTestIdSet) {
5897
- const raw = dt.value;
5898
- if (raw.includes("${")) {
6172
+ const raw = dt.selectorValue.formatted;
6173
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5899
6174
  continue;
5900
6175
  }
5901
6176
  const toggleSuffix = "-toggle";
@@ -5985,7 +6260,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
5985
6260
  });
5986
6261
  }
5987
6262
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
5988
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
5989
6263
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
5990
6264
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
5991
6265
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6247,7 +6521,7 @@ function getConditionalDirectiveInfo(element) {
6247
6521
  if (directive.name === "else") {
6248
6522
  const exp2 = directive.exp;
6249
6523
  if (exp2 && (exp2.type === NodeTypes.SIMPLE_EXPRESSION || exp2.type === NodeTypes.COMPOUND_EXPRESSION)) {
6250
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6524
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6251
6525
  return { kind: "else-if", source: source2 };
6252
6526
  }
6253
6527
  return { kind: "else", source: "" };
@@ -6256,13 +6530,13 @@ function getConditionalDirectiveInfo(element) {
6256
6530
  const exp2 = directive.exp;
6257
6531
  if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION && exp2.type !== NodeTypes.COMPOUND_EXPRESSION)
6258
6532
  return null;
6259
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6533
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6260
6534
  return { kind: "else-if", source: source2 };
6261
6535
  }
6262
6536
  const exp = directive.exp;
6263
6537
  if (!exp || exp.type !== NodeTypes.SIMPLE_EXPRESSION && exp.type !== NodeTypes.COMPOUND_EXPRESSION)
6264
6538
  return null;
6265
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
6539
+ const source = getVueExpressionSource(exp, "content", "compiled");
6266
6540
  return { kind: directive.name, source };
6267
6541
  }
6268
6542
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6288,26 +6562,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6288
6562
  };
6289
6563
  try {
6290
6564
  const expr = parseExpression(src, { plugins: ["typescript"] });
6291
- const isNodeType = (n, type) => {
6565
+ const isNodeType2 = (n, type) => {
6292
6566
  return n !== null && n.type === type;
6293
6567
  };
6294
- const isStringLiteralNode = (n) => {
6295
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6568
+ const isStringLiteralNode2 = (n) => {
6569
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6296
6570
  };
6297
- const isIdentifierNode = (n) => {
6298
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6571
+ const isIdentifierNode2 = (n) => {
6572
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6299
6573
  };
6300
6574
  const results = [];
6301
6575
  const walk = (n) => {
6302
6576
  if (!n)
6303
6577
  return;
6304
- if (isStringLiteralNode(n)) {
6578
+ if (isStringLiteralNode2(n)) {
6305
6579
  const v = (n.value ?? "").trim();
6306
6580
  if (isIdentifierish(v)) {
6307
6581
  results.push(v);
6308
6582
  }
6309
6583
  }
6310
- if (isIdentifierNode(n)) {
6584
+ if (isIdentifierNode2(n)) {
6311
6585
  const v = (n.name ?? "").trim();
6312
6586
  if (isIdentifierish(v)) {
6313
6587
  results.push(v);
@@ -6457,34 +6731,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6457
6731
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6458
6732
  return null;
6459
6733
  }
6460
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6734
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6461
6735
  const jsStringLiteral = (value) => {
6462
6736
  return JSON.stringify(value);
6463
6737
  };
6464
6738
  const getTestIdExpressionForNode = () => {
6465
- const existing = findTestIdAttribute(element, testIdAttribute);
6466
- if (!existing) {
6467
- return "undefined";
6468
- }
6469
- if (existing.type === NodeTypes.ATTRIBUTE) {
6470
- const v = existing.value?.content;
6471
- if (!v) {
6472
- return "undefined";
6473
- }
6474
- return jsStringLiteral(v);
6475
- }
6476
- const directive = existing;
6477
- const exp2 = directive.exp;
6478
- if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION) {
6739
+ if (!resolvedRuntimeTestId) {
6479
6740
  return "undefined";
6480
6741
  }
6481
- const content = (exp2.content ?? "").trim();
6482
- if (!content) {
6483
- return "undefined";
6742
+ if (resolvedRuntimeTestId.kind === "static") {
6743
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6484
6744
  }
6485
- return `(${content})`;
6745
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6486
6746
  };
6487
- const testIdExpression2 = getTestIdExpressionForNode();
6747
+ const testIdExpression = getTestIdExpressionForNode();
6488
6748
  const clickDirective = tryGetClickDirective(element);
6489
6749
  if (!clickDirective)
6490
6750
  return;
@@ -6495,10 +6755,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6495
6755
  const exp = clickDirective.exp;
6496
6756
  if (!exp)
6497
6757
  return;
6498
- const existingSource = (exp.loc?.source ?? (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6758
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6499
6759
  if (existingSource.includes(CLICK_EVENT_NAME))
6500
6760
  return;
6501
- const originalExpression = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6761
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6502
6762
  if (!originalExpression)
6503
6763
  return;
6504
6764
  const isStatementBody = (() => {
@@ -6515,7 +6775,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6515
6775
  const statementWrappedHandler = `($event) => {
6516
6776
  const __win = ($event && $event.view) ? $event.view : undefined;
6517
6777
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6518
- const __testIdFromNode = ${testIdExpression2};
6778
+ const __testIdFromNode = ${testIdExpression};
6519
6779
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6520
6780
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6521
6781
  const __emit = (phase, err) => {
@@ -6526,16 +6786,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6526
6786
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6527
6787
  }
6528
6788
  } catch (e) {
6529
- // Instrumentation must never hide failures during e2e strict mode.
6530
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6531
- // Outside strict mode we log and continue so we don't break real user clicks.
6789
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6532
6790
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6533
6791
  if (__w && __w.console && typeof __w.console.error === 'function') {
6534
6792
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6535
6793
  }
6536
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6537
- throw e;
6538
- }
6794
+ throw e;
6539
6795
  }
6540
6796
  };
6541
6797
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6566,7 +6822,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6566
6822
  const expressionWrappedHandler = `($event) => {
6567
6823
  const __win = ($event && $event.view) ? $event.view : undefined;
6568
6824
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6569
- const __testIdFromNode = ${testIdExpression2};
6825
+ const __testIdFromNode = ${testIdExpression};
6570
6826
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6571
6827
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6572
6828
  const __emit = (phase, err) => {
@@ -6577,16 +6833,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6577
6833
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6578
6834
  }
6579
6835
  } catch (e) {
6580
- // Instrumentation must never hide failures during e2e strict mode.
6581
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6582
- // Outside strict mode we log and continue so we don't break real user clicks.
6836
+ // Instrumentation failures should never be silent. Log the root cause and fail fast.
6583
6837
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6584
6838
  if (__w && __w.console && typeof __w.console.error === 'function') {
6585
6839
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6586
6840
  }
6587
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6588
- throw e;
6589
- }
6841
+ throw e;
6590
6842
  }
6591
6843
  };
6592
6844
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6624,10 +6876,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6624
6876
  let previousFileName = "";
6625
6877
  const hierarchyMap = /* @__PURE__ */ new Map();
6626
6878
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6627
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
6879
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6628
6880
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6629
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6630
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
6881
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6631
6882
  const warn = options.warn;
6632
6883
  const vueFilesPathMap = options.vueFilesPathMap;
6633
6884
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6703,7 +6954,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6703
6954
  conditionalHintByIfBranch.set(branch, hint);
6704
6955
  continue;
6705
6956
  }
6706
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
6957
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6707
6958
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6708
6959
  if (stable) {
6709
6960
  conditionalHintByIfBranch.set(branch, stable);
@@ -6765,17 +7016,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6765
7016
  nativeWrappers[element.tag] = { role: "grid" };
6766
7017
  }
6767
7018
  }
6768
- const getBestAvailableKeyValue = () => {
7019
+ const getBestAvailableKeyInfo = () => {
6769
7020
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6770
7021
  const isDirectVForChild = parentNode?.type === NodeTypes.FOR;
6771
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6772
- if (vForKey) return vForKey;
6773
- return getContainedInSlotDataKeyValue(element, hierarchyMap);
7022
+ const vForKeyInfo = (isDirectVForChild ? getKeyDirectiveInfo(element) : null) || getContainedInVForDirectiveKeyInfo(context, element, hierarchyMap);
7023
+ if (vForKeyInfo) {
7024
+ return vForKeyInfo;
7025
+ }
7026
+ return getContainedInSlotDataKeyInfo(element, hierarchyMap);
6774
7027
  };
6775
- const bestKeyInferred = getBestAvailableKeyValue();
6776
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6777
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6778
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7028
+ const bestKeyInfo = getBestAvailableKeyInfo();
7029
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7030
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6779
7031
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6780
7032
  const parentKey = context?.parent ? context.parent : null;
6781
7033
  const conditional = getConditionalDirectiveInfo(element);
@@ -6818,7 +7070,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6818
7070
  if (!cond) {
6819
7071
  conditionalHint = "else";
6820
7072
  } else {
6821
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
7073
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6822
7074
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6823
7075
  }
6824
7076
  }
@@ -6828,7 +7080,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6828
7080
  });
6829
7081
  if (showDirective?.exp && (showDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION)) {
6830
7082
  const exp = showDirective.exp;
6831
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
7083
+ const source = getVueExpressionSource(exp, "content", "compiled");
6832
7084
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6833
7085
  if (showHint) {
6834
7086
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6862,13 +7114,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6862
7114
  const tagSuffix = getTagSuffix();
6863
7115
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6864
7116
  };
7117
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7118
+ const tagSuffix = getTagSuffix();
7119
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7120
+ };
6865
7121
  const getSubmitDataTestId = (identifier) => {
6866
7122
  const tagSuffix = getTagSuffix();
6867
7123
  return `${componentName}-${identifier}${tagSuffix}`;
6868
7124
  };
6869
7125
  const applyResolvedDataTestIdForElement = (args) => {
6870
7126
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6871
- applyResolvedDataTestId({
7127
+ return applyResolvedDataTestId({
6872
7128
  element,
6873
7129
  componentName,
6874
7130
  parentComponentName,
@@ -6878,8 +7134,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6878
7134
  generatedMethodContentByComponent,
6879
7135
  nativeRole,
6880
7136
  preferredGeneratedValue: args.preferredGeneratedValue,
6881
- bestKeyPlaceholder,
6882
- bestKeyVariable,
7137
+ preferredRuntimeValue: args.preferredRuntimeValue,
7138
+ keyInfo: bestKeyInfo,
6883
7139
  keyValuesOverride,
6884
7140
  entryOverrides: args.entryOverrides,
6885
7141
  semanticNameHint: args.semanticNameHint,
@@ -6897,7 +7153,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6897
7153
  return p.type === NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6898
7154
  }) ?? null;
6899
7155
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6900
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7156
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6901
7157
  const loc = element.loc?.start;
6902
7158
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6903
7159
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6906,7 +7162,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6906
7162
  Element: <${element.tag}>
6907
7163
  Handler: ${handlerSource}
6908
7164
 
6909
- Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name. You can also set errorBehavior = "ignore" to keep generic fallback behavior.`
7165
+ Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name.`
6910
7166
  );
6911
7167
  }
6912
7168
  if (nativeWrappersValue) {
@@ -7033,20 +7289,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7033
7289
  contextFilename: context.filename
7034
7290
  });
7035
7291
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
7036
- const idOrName = getIdOrName(element) || void 0;
7292
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
7037
7293
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
7038
7294
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
7039
7295
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
7040
7296
  const testId = getClickDataTestId(clickSuffix);
7041
- applyResolvedDataTestIdForElement({
7297
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7298
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
7042
7299
  preferredGeneratedValue: testId,
7300
+ preferredRuntimeValue: runtimeTestId,
7043
7301
  semanticNameHint: semanticNameHint2,
7044
7302
  semanticNameHintAlternates,
7045
7303
  pomMergeKey
7046
7304
  });
7047
- {
7048
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7049
- }
7305
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7050
7306
  return;
7051
7307
  }
7052
7308
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7056,7 +7312,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7056
7312
  if (!isRecognizedInteractiveRole) {
7057
7313
  return;
7058
7314
  }
7059
- const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7315
+ const identifierHint = getStaticIdOrNameHint(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
7060
7316
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7061
7317
  applyResolvedDataTestIdForElement({
7062
7318
  preferredGeneratedValue,
@@ -7066,7 +7322,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7066
7322
  }
7067
7323
  const isSubmit = element.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7068
7324
  if (isSubmit) {
7069
- const identifier = getIdOrName(element) || innerText;
7325
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7070
7326
  if (!identifier) {
7071
7327
  const loc = element.loc?.start;
7072
7328
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7118,29 +7374,32 @@ function createBuildProcessorPlugin(options) {
7118
7374
  getSourceDirs,
7119
7375
  basePageClassPath,
7120
7376
  normalizedBasePagePath,
7377
+ generation,
7378
+ projectRootRef,
7379
+ nativeWrappers,
7380
+ excludedComponents,
7381
+ getWrapperSearchRoots,
7382
+ getResolvedRouterEntry,
7383
+ loggerRef
7384
+ } = options;
7385
+ const {
7121
7386
  outDir,
7122
7387
  emitLanguages,
7123
7388
  typescriptOutputStructure,
7124
7389
  csharp,
7125
7390
  generateFixtures,
7126
7391
  customPomAttachments,
7127
- projectRootRef,
7128
7392
  customPomDir,
7393
+ requireCustomPomDir,
7129
7394
  customPomImportAliases,
7130
7395
  customPomImportNameCollisionBehavior,
7131
7396
  testIdAttribute,
7132
7397
  nameCollisionBehavior,
7133
- missingSemanticNameBehavior = "error",
7134
7398
  existingIdBehavior,
7135
- nativeWrappers,
7136
- excludedComponents,
7137
- getWrapperSearchRoots,
7138
7399
  routerAwarePoms,
7139
- getResolvedRouterEntry,
7140
7400
  routerType,
7141
- routerModuleShims,
7142
- loggerRef
7143
- } = options;
7401
+ routerModuleShims
7402
+ } = generation;
7144
7403
  let lastGeneratedMetrics = {
7145
7404
  entryCount: 0,
7146
7405
  interactiveComponentCount: 0,
@@ -7239,10 +7498,9 @@ function createBuildProcessorPlugin(options) {
7239
7498
  excludedComponents,
7240
7499
  getViewsDirAbs(),
7241
7500
  {
7242
- existingIdBehavior: existingIdBehavior ?? "preserve",
7501
+ existingIdBehavior: existingIdBehavior ?? "error",
7243
7502
  testIdAttribute,
7244
7503
  nameCollisionBehavior,
7245
- missingSemanticNameBehavior,
7246
7504
  warn: (message) => loggerRef.current.warn(message),
7247
7505
  vueFilesPathMap,
7248
7506
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7342,6 +7600,7 @@ function createBuildProcessorPlugin(options) {
7342
7600
  customPomAttachments,
7343
7601
  projectRoot: projectRootRef.current,
7344
7602
  customPomDir,
7603
+ requireCustomPomDir,
7345
7604
  customPomImportAliases,
7346
7605
  customPomImportNameCollisionBehavior,
7347
7606
  testIdAttribute,
@@ -7373,6 +7632,11 @@ function createDevProcessorPlugin(options) {
7373
7632
  projectRootRef,
7374
7633
  normalizedBasePagePath,
7375
7634
  basePageClassPath,
7635
+ generation,
7636
+ getResolvedRouterEntry,
7637
+ loggerRef
7638
+ } = options;
7639
+ const {
7376
7640
  outDir,
7377
7641
  emitLanguages,
7378
7642
  typescriptOutputStructure,
@@ -7380,18 +7644,16 @@ function createDevProcessorPlugin(options) {
7380
7644
  generateFixtures,
7381
7645
  customPomAttachments,
7382
7646
  customPomDir,
7647
+ requireCustomPomDir,
7383
7648
  customPomImportAliases,
7384
7649
  customPomImportNameCollisionBehavior,
7385
- nameCollisionBehavior = "suffix",
7386
- missingSemanticNameBehavior = "error",
7650
+ nameCollisionBehavior,
7387
7651
  existingIdBehavior,
7388
7652
  testIdAttribute,
7389
7653
  routerAwarePoms,
7390
- getResolvedRouterEntry,
7391
7654
  routerType,
7392
- routerModuleShims,
7393
- loggerRef
7394
- } = options;
7655
+ routerModuleShims
7656
+ } = generation;
7395
7657
  let scheduleVueFileRegen = null;
7396
7658
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7397
7659
  path.resolve(projectRootRef.current),
@@ -7578,9 +7840,8 @@ function createDevProcessorPlugin(options) {
7578
7840
  excludedComponents,
7579
7841
  getViewsDirAbs(),
7580
7842
  {
7581
- existingIdBehavior: existingIdBehavior ?? "preserve",
7843
+ existingIdBehavior: existingIdBehavior ?? "error",
7582
7844
  nameCollisionBehavior,
7583
- missingSemanticNameBehavior,
7584
7845
  testIdAttribute,
7585
7846
  warn: (message) => loggerRef.current.warn(message),
7586
7847
  vueFilesPathMap: provisionalVuePathMap,
@@ -7631,6 +7892,7 @@ function createDevProcessorPlugin(options) {
7631
7892
  customPomAttachments,
7632
7893
  projectRoot: projectRootRef.current,
7633
7894
  customPomDir,
7895
+ requireCustomPomDir,
7634
7896
  customPomImportAliases,
7635
7897
  customPomImportNameCollisionBehavior,
7636
7898
  pageDirs: getPageDirs(),
@@ -7866,27 +8128,30 @@ function createSupportPlugins(options) {
7866
8128
  getViewsDir,
7867
8129
  getSourceDirs,
7868
8130
  getWrapperSearchRoots,
7869
- nameCollisionBehavior = "suffix",
7870
- missingSemanticNameBehavior = "error",
7871
- existingIdBehavior,
8131
+ generation,
8132
+ projectRootRef,
8133
+ basePageClassPath: basePageClassPathOverride,
8134
+ loggerRef
8135
+ } = options;
8136
+ const {
7872
8137
  outDir,
7873
8138
  emitLanguages,
7874
8139
  typescriptOutputStructure,
7875
8140
  csharp,
7876
- routerAwarePoms,
7877
- routerEntry,
7878
- routerType,
7879
- routerModuleShims,
7880
8141
  generateFixtures,
7881
8142
  customPomAttachments,
7882
- projectRootRef,
7883
- basePageClassPath: basePageClassPathOverride,
7884
8143
  customPomDir,
8144
+ requireCustomPomDir,
7885
8145
  customPomImportAliases,
7886
8146
  customPomImportNameCollisionBehavior,
8147
+ nameCollisionBehavior,
8148
+ existingIdBehavior,
7887
8149
  testIdAttribute,
7888
- loggerRef
7889
- } = options;
8150
+ routerAwarePoms,
8151
+ routerEntry,
8152
+ routerType,
8153
+ routerModuleShims
8154
+ } = generation;
7890
8155
  const resolveRouterEntry2 = () => {
7891
8156
  if (!routerAwarePoms)
7892
8157
  return void 0;
@@ -7915,27 +8180,12 @@ function createSupportPlugins(options) {
7915
8180
  getSourceDirs,
7916
8181
  basePageClassPath,
7917
8182
  normalizedBasePagePath,
7918
- outDir,
7919
- emitLanguages,
7920
- typescriptOutputStructure,
7921
- csharp,
7922
- generateFixtures,
7923
- customPomAttachments,
8183
+ generation,
7924
8184
  projectRootRef,
7925
- customPomDir,
7926
- customPomImportAliases,
7927
- customPomImportNameCollisionBehavior,
7928
- testIdAttribute,
7929
- nameCollisionBehavior,
7930
- missingSemanticNameBehavior,
7931
- existingIdBehavior,
7932
8185
  nativeWrappers,
7933
8186
  excludedComponents,
7934
8187
  getWrapperSearchRoots,
7935
- routerAwarePoms,
7936
- routerType,
7937
8188
  getResolvedRouterEntry: resolveRouterEntry2,
7938
- routerModuleShims,
7939
8189
  loggerRef
7940
8190
  });
7941
8191
  const devProcessor = createDevProcessorPlugin({
@@ -7950,23 +8200,8 @@ function createSupportPlugins(options) {
7950
8200
  projectRootRef,
7951
8201
  normalizedBasePagePath,
7952
8202
  basePageClassPath,
7953
- outDir,
7954
- emitLanguages,
7955
- typescriptOutputStructure,
7956
- csharp,
7957
- generateFixtures,
7958
- customPomAttachments,
7959
- customPomDir,
7960
- customPomImportAliases,
7961
- customPomImportNameCollisionBehavior,
7962
- nameCollisionBehavior,
7963
- missingSemanticNameBehavior,
7964
- existingIdBehavior,
7965
- testIdAttribute,
7966
- routerAwarePoms,
7967
- routerType,
8203
+ generation,
7968
8204
  getResolvedRouterEntry: resolveRouterEntry2,
7969
- routerModuleShims,
7970
8205
  loggerRef
7971
8206
  });
7972
8207
  const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
@@ -8303,8 +8538,7 @@ function createVuePluginWithTestIds(options) {
8303
8538
  });
8304
8539
  const api = viteVuePlugin?.api;
8305
8540
  if (!api) {
8306
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8307
- return;
8541
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8308
8542
  }
8309
8543
  const currentOptions = api.options ?? {};
8310
8544
  const currentTemplate = currentOptions.template ?? {};
@@ -8370,33 +8604,6 @@ function assertOneOf(value, allowed, name) {
8370
8604
  }
8371
8605
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8372
8606
  }
8373
- function assertErrorBehavior(value, name) {
8374
- if (!value) {
8375
- return;
8376
- }
8377
- if (value === "ignore" || value === "error") {
8378
- return;
8379
- }
8380
- if (typeof value !== "object" || Array.isArray(value)) {
8381
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8382
- }
8383
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8384
- for (const key of Object.keys(value)) {
8385
- if (!supportedKeys.has(key)) {
8386
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8387
- }
8388
- }
8389
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8390
- }
8391
- function resolveMissingSemanticNameBehavior(value) {
8392
- if (!value) {
8393
- return "error";
8394
- }
8395
- if (value === "ignore" || value === "error") {
8396
- return value;
8397
- }
8398
- return value.missingSemanticNameBehavior ?? "error";
8399
- }
8400
8607
  function readPackageJson(projectRoot) {
8401
8608
  const packageJsonPath = path.join(projectRoot, "package.json");
8402
8609
  if (!fs.existsSync(packageJsonPath)) {
@@ -8505,7 +8712,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8505
8712
  '[vue-pom-generator] vuePluginOwnership="external" requires the resolved Vite Vue plugin, but none was found. Add vue() to your Vite plugins before spreading createVuePomGeneratorPlugins(...).'
8506
8713
  );
8507
8714
  }
8508
- throw new Error("[vue-pom-generator] Nuxt mode requires the resolved Vite Vue plugin, but none was found.");
8715
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8509
8716
  }
8510
8717
  const currentOptions = viteVuePlugin.api.options ?? {};
8511
8718
  const currentTemplate = currentOptions.template ?? {};
@@ -8562,18 +8769,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8562
8769
  const vueGenerationOptions = generationOptions;
8563
8770
  const verbosity = options.logging?.verbosity ?? "warn";
8564
8771
  const vueOptions = options.vueOptions;
8565
- const legacyVueOptions = options;
8566
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8567
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8568
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8569
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8570
- const nativeWrappers = injection.nativeWrappers ?? {};
8571
- const excludedComponents = injection.excludeComponents ?? [];
8572
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8573
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8574
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8575
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8576
- const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
8772
+ const resolvedInjectionOptionsRef = {
8773
+ current: resolveInjectionSupportOptions({
8774
+ isNuxt,
8775
+ viewsDir: injection.viewsDir,
8776
+ componentDirs: injection.componentDirs,
8777
+ layoutDirs: injection.layoutDirs,
8778
+ wrapperSearchRoots: injection.wrapperSearchRoots,
8779
+ nativeWrappers: injection.nativeWrappers,
8780
+ excludedComponents: injection.excludeComponents,
8781
+ existingIdBehavior: injection.existingIdBehavior,
8782
+ testIdAttribute: injection.attribute
8783
+ })
8784
+ };
8785
+ const resolvedInjectionOptions = resolvedInjectionOptionsRef.current;
8786
+ const nativeWrappers = resolvedInjectionOptions.nativeWrappers;
8787
+ const excludedComponents = resolvedInjectionOptions.excludedComponents;
8788
+ const testIdAttribute = resolvedInjectionOptions.testIdAttribute;
8577
8789
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8578
8790
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8579
8791
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8582,27 +8794,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8582
8794
  }
8583
8795
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8584
8796
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8585
- const csharp = generationOptions?.csharp;
8586
- const errorBehavior = options.errorBehavior;
8587
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8588
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8589
8797
  const generateFixtures = generationOptions?.playwright?.fixtures;
8590
8798
  const customPoms = generationOptions?.playwright?.customPoms;
8591
8799
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8592
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8593
8800
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8594
- const resolvedCustomPomImportCollisionBehavior = customPoms?.importNameCollisionBehavior ?? "error";
8801
+ const requireCustomPomDir = customPoms?.dir !== void 0 || resolvedCustomPomAttachments.length > 0 || Object.keys(resolvedCustomPomImportAliases ?? {}).length > 0;
8802
+ const resolvedGenerationOptions = resolveGenerationSupportOptions({
8803
+ outDir: generationOptions?.outDir,
8804
+ emitLanguages: generationOptions?.emit,
8805
+ typescriptOutputStructure: generationOptions?.playwright?.outputStructure,
8806
+ csharp: generationOptions?.csharp,
8807
+ generateFixtures,
8808
+ customPomAttachments: resolvedCustomPomAttachments,
8809
+ customPomDir: customPoms?.dir,
8810
+ requireCustomPomDir,
8811
+ customPomImportAliases: resolvedCustomPomImportAliases,
8812
+ customPomImportNameCollisionBehavior: customPoms?.importNameCollisionBehavior,
8813
+ nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
8814
+ existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
8815
+ testIdAttribute,
8816
+ routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
8817
+ routerEntry,
8818
+ routerType,
8819
+ routerModuleShims
8820
+ });
8595
8821
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8596
- const getPageDirs = () => pageDirsRef.current;
8822
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8597
8823
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8598
- const getComponentDirs = () => componentDirsRef.current;
8599
- const getLayoutDirs = () => layoutDirsRef.current;
8824
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
8825
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8600
8826
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8601
8827
  ...getPageDirs(),
8602
8828
  ...getComponentDirs(),
8603
8829
  ...getLayoutDirs()
8604
8830
  ]));
8605
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
8831
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8606
8832
  const sharedStateKey = JSON.stringify({
8607
8833
  cwd: process.cwd(),
8608
8834
  mode: isNuxt ? "nuxt" : "vue",
@@ -8610,9 +8836,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8610
8836
  componentDirs: isNuxt ? null : getComponentDirs(),
8611
8837
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8612
8838
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8613
- outDir,
8839
+ outDir: resolvedGenerationOptions.outDir,
8614
8840
  testIdAttribute,
8615
- routerType,
8841
+ routerType: resolvedGenerationOptions.routerType,
8616
8842
  vuePluginOwnership
8617
8843
  });
8618
8844
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8633,23 +8859,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8633
8859
  if (isNuxt) {
8634
8860
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8635
8861
  projectRootRef.current = nuxtDiscovery.rootDir;
8636
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8637
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8638
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8639
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
8862
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
8863
+ resolvedInjectionOptionsRef.current,
8864
+ nuxtDiscovery
8865
+ );
8640
8866
  }
8641
8867
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8642
8868
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8643
8869
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8644
8870
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8645
8871
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8646
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8647
8872
  if (generationEnabled) {
8648
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8649
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8650
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8651
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8652
- assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
8873
+ assertNonEmptyString(resolvedGenerationOptions.outDir, "[vue-pom-generator] generation.outDir");
8874
+ assertOneOf(resolvedGenerationOptions.typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8875
+ assertRouterModuleShims(resolvedGenerationOptions.routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8876
+ if (!isNuxt && vueGenerationOptions?.router && resolvedGenerationOptions.routerType === "vue-router") {
8877
+ assertNonEmptyString(resolvedGenerationOptions.routerEntry, "[vue-pom-generator] generation.router.entry");
8653
8878
  }
8654
8879
  }
8655
8880
  if (usesExternalVuePlugin) {
@@ -8671,8 +8896,8 @@ function createVuePomGeneratorPlugins(options = {}) {
8671
8896
  const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8672
8897
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8673
8898
  vueOptions,
8674
- existingIdBehavior,
8675
- nameCollisionBehavior,
8899
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
8900
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8676
8901
  nativeWrappers,
8677
8902
  elementMetadata,
8678
8903
  semanticNameMap,
@@ -8687,7 +8912,6 @@ function createVuePomGeneratorPlugins(options = {}) {
8687
8912
  getProjectRoot: () => projectRootRef.current
8688
8913
  });
8689
8914
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8690
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8691
8915
  const supportPlugins = createSupportPlugins({
8692
8916
  componentTestIds,
8693
8917
  componentHierarchyMap,
@@ -8700,26 +8924,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8700
8924
  getViewsDir,
8701
8925
  getSourceDirs,
8702
8926
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8703
- nameCollisionBehavior,
8704
- missingSemanticNameBehavior,
8705
- existingIdBehavior,
8706
- outDir,
8707
- emitLanguages,
8708
- typescriptOutputStructure,
8709
- csharp,
8710
- routerAwarePoms,
8711
- routerEntry,
8712
- generateFixtures,
8927
+ generation: resolvedGenerationOptions,
8713
8928
  projectRootRef,
8714
8929
  basePageClassPath: basePageClassPathOverride,
8715
- customPomAttachments: resolvedCustomPomAttachments,
8716
- customPomDir: resolvedCustomPomDir,
8717
- customPomImportAliases: resolvedCustomPomImportAliases,
8718
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8719
- testIdAttribute,
8720
- loggerRef,
8721
- routerType,
8722
- routerModuleShims
8930
+ loggerRef
8723
8931
  });
8724
8932
  if (isNuxt) {
8725
8933
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");