@immense/vue-pom-generator 1.0.57 → 1.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +7 -19
  2. package/RELEASE_NOTES.md +76 -13
  3. package/class-generation/base-page.ts +6 -13
  4. package/class-generation/index.ts +226 -317
  5. package/class-generation/playwright-types.ts +1 -1
  6. package/click-instrumentation.ts +0 -4
  7. package/dist/class-generation/base-page.d.ts +1 -0
  8. package/dist/class-generation/base-page.d.ts.map +1 -1
  9. package/dist/class-generation/index.d.ts +2 -0
  10. package/dist/class-generation/index.d.ts.map +1 -1
  11. package/dist/class-generation/playwright-types.d.ts +1 -1
  12. package/dist/class-generation/playwright-types.d.ts.map +1 -1
  13. package/dist/click-instrumentation.d.ts +0 -1
  14. package/dist/click-instrumentation.d.ts.map +1 -1
  15. package/dist/index.cjs +1283 -1019
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1285 -1021
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/method-generation.d.ts +4 -2
  20. package/dist/method-generation.d.ts.map +1 -1
  21. package/dist/playwright.config.d.ts.map +1 -1
  22. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  23. package/dist/plugin/nuxt-discovery.d.ts.map +1 -1
  24. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  25. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  26. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  27. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  28. package/dist/plugin/support/build-plugin.d.ts +2 -29
  29. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  31. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  32. package/dist/plugin/support-plugins.d.ts +2 -32
  33. package/dist/plugin/support-plugins.d.ts.map +1 -1
  34. package/dist/plugin/types.d.ts +6 -23
  35. package/dist/plugin/types.d.ts.map +1 -1
  36. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  37. package/dist/pom-params.d.ts +40 -0
  38. package/dist/pom-params.d.ts.map +1 -0
  39. package/dist/pom-patterns.d.ts +31 -0
  40. package/dist/pom-patterns.d.ts.map +1 -0
  41. package/dist/routing/to-directive.d.ts +21 -0
  42. package/dist/routing/to-directive.d.ts.map +1 -1
  43. package/dist/tests/base-page.test.d.ts +2 -0
  44. package/dist/tests/base-page.test.d.ts.map +1 -0
  45. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  46. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  47. package/dist/transform.d.ts +0 -1
  48. package/dist/transform.d.ts.map +1 -1
  49. package/dist/utils.d.ts +129 -63
  50. package/dist/utils.d.ts.map +1 -1
  51. package/package.json +6 -4
  52. package/sequence-diagram.md +6 -6
package/dist/index.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]";
@@ -63,6 +63,21 @@ function createLogger(options) {
63
63
  };
64
64
  }
65
65
  const requireFromModule = createRequire(import.meta.url);
66
+ function resolveNuxtKitEntry(cwd) {
67
+ const attemptResolvers = [
68
+ createRequire(path.resolve(cwd, "package.json")),
69
+ requireFromModule
70
+ ];
71
+ let lastError;
72
+ for (const resolver of attemptResolvers) {
73
+ try {
74
+ return resolver.resolve("@nuxt/kit");
75
+ } catch (error) {
76
+ lastError = error instanceof Error ? error : new Error(String(error));
77
+ }
78
+ }
79
+ throw lastError ?? new Error("Unknown module resolution error");
80
+ }
66
81
  function toUniqueResolvedPaths(paths) {
67
82
  return Array.from(new Set(paths.map((value) => path.resolve(value))));
68
83
  }
@@ -173,7 +188,7 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
173
188
  let loadNuxtConfig;
174
189
  let getLayerDirectories;
175
190
  try {
176
- const nuxtKitEntry = requireFromModule.resolve("@nuxt/kit");
191
+ const nuxtKitEntry = resolveNuxtKitEntry(cwd);
177
192
  ({ loadNuxtConfig, getLayerDirectories } = await import(pathToFileURL(nuxtKitEntry).href));
178
193
  } catch (error) {
179
194
  throw new TypeError(
@@ -189,6 +204,49 @@ async function loadNuxtProjectDiscovery(cwd = process.cwd()) {
189
204
  const nuxtOptions = await loadNuxtConfig({ cwd });
190
205
  return resolveNuxtProjectDiscovery(nuxtOptions, getLayerDirectories, cwd);
191
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
+ }
192
250
  function createTypeScriptProject() {
193
251
  return new Project({
194
252
  useInMemoryFileSystem: true,
@@ -280,16 +338,7 @@ function createClassConstructor(constructorDeclaration) {
280
338
  ...constructorDeclaration
281
339
  };
282
340
  }
283
- function upperFirst$1(value) {
284
- if (!value) {
285
- return value;
286
- }
287
- return value.charAt(0).toUpperCase() + value.slice(1);
288
- }
289
- function hasParam(params, name) {
290
- return Object.prototype.hasOwnProperty.call(params, name);
291
- }
292
- function splitTypeAndInitializer(typeExpression) {
341
+ function splitPomParameterTypeExpression(typeExpression) {
293
342
  const trimmed = typeExpression.trim();
294
343
  const initializerIndex = trimmed.lastIndexOf("=");
295
344
  if (initializerIndex < 0) {
@@ -300,49 +349,237 @@ function splitTypeAndInitializer(typeExpression) {
300
349
  initializer: trimmed.slice(initializerIndex + 1).trim()
301
350
  };
302
351
  }
303
- function createParameter(name, typeExpression) {
304
- 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 };
305
355
  return {
306
356
  name,
307
- type: type || void 0,
308
- initializer
357
+ typeExpression: normalizedTypeExpression,
358
+ type,
359
+ initializer: options.initializer ?? initializer,
360
+ hasQuestionToken: options.hasQuestionToken,
361
+ isRestParameter: options.isRestParameter
309
362
  };
310
363
  }
311
- function createParameters(params) {
312
- 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
+ }));
313
373
  }
314
- 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) {
315
410
  return {
316
- name,
317
- type: options.type,
318
- initializer: options.initializer
411
+ parameters: normalizePomParameters(parameters)
319
412
  };
320
413
  }
321
- function removeByKeySegment(value) {
322
- const idx = value.lastIndexOf("ByKey");
323
- if (idx < 0) {
324
- 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;
325
422
  }
326
- return value.slice(0, idx) + value.slice(idx + "ByKey".length);
423
+ return leftParams.every((param, index) => pomParameterSpecEquals(param, rightParams[index]));
424
+ }
425
+ function pomMethodSignatureEquals(left, right) {
426
+ return pomParameterListEquals(left.parameters, right.parameters);
427
+ }
428
+ function isParameterizedPomPattern(kind) {
429
+ return kind === "parameterized";
430
+ }
431
+ function getTemplateVariables(formatted) {
432
+ const out = [];
433
+ const seen = /* @__PURE__ */ new Set();
434
+ const matches = formatted.matchAll(/\$\{(\w+)\}/g);
435
+ for (const match of matches) {
436
+ const variableName = match[1];
437
+ if (seen.has(variableName)) {
438
+ continue;
439
+ }
440
+ seen.add(variableName);
441
+ out.push(variableName);
442
+ }
443
+ return out;
327
444
  }
328
- function uniqueAlternates(primary, alternates) {
445
+ function createPomStringPattern(formatted, patternKind) {
446
+ return {
447
+ formatted,
448
+ patternKind,
449
+ templateVariables: getTemplateVariables(formatted)
450
+ };
451
+ }
452
+ function getPomPatternVariables(patterns, options = {}) {
329
453
  const out = [];
330
454
  const seen = /* @__PURE__ */ new Set();
331
- seen.add(primary);
332
- for (const a of alternates ?? []) {
333
- if (!a) {
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);
334
477
  continue;
335
478
  }
336
- if (seen.has(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)) {
337
490
  continue;
338
491
  }
339
- seen.add(a);
340
- out.push(a);
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);
341
558
  }
342
559
  return out;
343
560
  }
344
- function testIdExpression(formattedDataTestId) {
345
- 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);
346
583
  }
347
584
  function createAsyncMethod(name, parameters, statements) {
348
585
  return createClassMethod({
@@ -352,17 +589,27 @@ function createAsyncMethod(name, parameters, statements) {
352
589
  statements
353
590
  });
354
591
  }
355
- function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
592
+ function generateClickMethod(methodName, selector, alternateSelectors, parameters) {
356
593
  const name = `click${methodName}`;
357
594
  const noWaitName = `${name}NoWait`;
358
- const baseParameters = createParameters(params);
359
- const argsForForward = Object.keys(params).join(", ");
360
- 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);
361
601
  if (alternates.length > 0) {
362
- const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
602
+ const candidatesExpr = [primaryTestIdExpr, ...alternates.map((id) => toTypeScriptPomPatternExpression(id))].join(", ");
363
603
  const clickMethod = createAsyncMethod(
364
604
  name,
365
- hasParam(params, "key") ? [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })] : [createInlineParameter("wait", { type: "boolean", initializer: "true" })],
605
+ hasSelectorVariables ? [
606
+ ...baseParameters,
607
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
608
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
609
+ ] : [
610
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
611
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
612
+ ],
366
613
  (writer) => {
367
614
  writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
368
615
  writer.writeLine("let lastError: unknown;");
@@ -370,7 +617,7 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
370
617
  writer.writeLine("const locator = this.locatorByTestId(testId);");
371
618
  writer.write("try ").block(() => {
372
619
  writer.write("if (await locator.count()) ").block(() => {
373
- writer.writeLine('await this.clickLocator(locator, "", wait);');
620
+ writer.writeLine("await this.clickLocator(locator, annotationText, wait);");
374
621
  writer.writeLine("return;");
375
622
  });
376
623
  });
@@ -381,60 +628,77 @@ function generateClickMethod(methodName, formattedDataTestId, alternateFormatted
381
628
  writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to click any candidate locator for ${name}.");`);
382
629
  }
383
630
  );
384
- const noWaitArgs = argsForForward ? `${argsForForward}, false` : "false";
631
+ const noWaitArgs = argsForForward ? `${argsForForward}, false, annotationText` : "false, annotationText";
385
632
  const noWaitMethod = createAsyncMethod(
386
633
  noWaitName,
387
- hasParam(params, "key") ? baseParameters : [],
634
+ hasSelectorVariables ? [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
388
635
  (writer) => {
389
636
  writer.writeLine(`await this.${name}(${noWaitArgs});`);
390
637
  }
391
638
  );
392
639
  return [clickMethod, noWaitMethod];
393
640
  }
394
- if (hasParam(params, "key")) {
641
+ if (hasSelectorVariables) {
395
642
  return [
396
- createAsyncMethod(name, [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
397
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, "", wait);`);
398
- }),
399
- createAsyncMethod(noWaitName, baseParameters, (writer) => {
400
- writer.writeLine(`await this.${name}(${argsForForward}, false);`);
401
- })
643
+ createAsyncMethod(
644
+ name,
645
+ [
646
+ ...baseParameters,
647
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
648
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
649
+ ],
650
+ (writer) => {
651
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
652
+ }
653
+ ),
654
+ createAsyncMethod(
655
+ noWaitName,
656
+ [...baseParameters, createInlineParameter("annotationText", { type: "string", initializer: '""' })],
657
+ (writer) => {
658
+ writer.writeLine(`await this.${name}(${argsForForward}, false, annotationText);`);
659
+ }
660
+ )
402
661
  ];
403
662
  }
404
663
  return [
405
- createAsyncMethod(name, [createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
406
- writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", "", wait);`);
407
- }),
408
- createAsyncMethod(noWaitName, [], (writer) => {
409
- writer.writeLine(`await this.${name}(false);`);
410
- })
664
+ createAsyncMethod(
665
+ name,
666
+ [
667
+ createInlineParameter("wait", { type: "boolean", initializer: "true" }),
668
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
669
+ ],
670
+ (writer) => {
671
+ writer.writeLine(`await this.clickByTestId(${primaryTestIdExpr}, annotationText, wait);`);
672
+ }
673
+ ),
674
+ createAsyncMethod(
675
+ noWaitName,
676
+ [createInlineParameter("annotationText", { type: "string", initializer: '""' })],
677
+ (writer) => {
678
+ writer.writeLine(`await this.${name}(false, annotationText);`);
679
+ }
680
+ )
411
681
  ];
412
682
  }
413
- function generateRadioMethod(methodName, formattedDataTestId) {
683
+ function generateRadioMethod(methodName, selector, parameters) {
414
684
  const name = `select${methodName}`;
415
- const hasKey = formattedDataTestId.includes("${key}");
416
- const parameters = hasKey ? [
417
- createInlineParameter("key", { type: "string" }),
418
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
419
- ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
420
- const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
685
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
686
+ const methodParameters = createParameters(selectorParams);
687
+ const testIdExpr = toTypeScriptPomPatternExpression(selector);
421
688
  return [
422
- createAsyncMethod(name, parameters, (writer) => {
689
+ createAsyncMethod(name, methodParameters, (writer) => {
423
690
  writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
424
691
  })
425
692
  ];
426
693
  }
427
- function generateSelectMethod(methodName, formattedDataTestId) {
694
+ function generateSelectMethod(methodName, selector, parameters) {
428
695
  const name = `select${methodName}`;
429
- const needsKey = formattedDataTestId.includes("${key}");
430
- const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
696
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
697
+ const selectorExpr = `this.selectorForTestId(${toTypeScriptPomPatternExpression(selector)})`;
431
698
  return [
432
699
  createAsyncMethod(
433
700
  name,
434
- [
435
- createInlineParameter("value", { type: "string" }),
436
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
437
- ],
701
+ createParameters(selectorParams),
438
702
  (writer) => {
439
703
  writer.writeLine(`const selector = ${selectorExpr};`);
440
704
  writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
@@ -443,33 +707,28 @@ function generateSelectMethod(methodName, formattedDataTestId) {
443
707
  )
444
708
  ];
445
709
  }
446
- function generateVSelectMethod(methodName, formattedDataTestId) {
710
+ function generateVSelectMethod(methodName, selector, parameters) {
447
711
  const name = `select${methodName}`;
712
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
448
713
  return [
449
714
  createAsyncMethod(
450
715
  name,
451
- [
452
- createInlineParameter("value", { type: "string" }),
453
- createInlineParameter("timeOut", { type: "number", initializer: "500" }),
454
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
455
- ],
716
+ createParameters(selectorParams),
456
717
  (writer) => {
457
- writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
718
+ writer.writeLine(`await this.selectVSelectByTestId(${toTypeScriptPomPatternExpression(selector)}, value, timeOut, annotationText);`);
458
719
  }
459
720
  )
460
721
  ];
461
722
  }
462
- function generateTypeMethod(methodName, formattedDataTestId) {
723
+ function generateTypeMethod(methodName, selector, parameters) {
463
724
  const name = `type${methodName}`;
725
+ const selectorParams = orderPomPatternParameters(parameters, [selector]);
464
726
  return [
465
727
  createAsyncMethod(
466
728
  name,
467
- [
468
- createInlineParameter("text", { type: "string" }),
469
- createInlineParameter("annotationText", { type: "string", initializer: '""' })
470
- ],
729
+ createParameters(selectorParams),
471
730
  (writer) => {
472
- writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
731
+ writer.writeLine(`await this.fillInputByTestId(${toTypeScriptPomPatternExpression(selector)}, text, annotationText);`);
473
732
  }
474
733
  )
475
734
  ];
@@ -484,30 +743,31 @@ function isAllDigits(value) {
484
743
  }
485
744
  return true;
486
745
  }
487
- function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
746
+ function generateGetElementByDataTestId(methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
488
747
  const roleSuffix = upperFirst$1(nativeRole || "Element");
489
748
  const baseName = upperFirst$1(methodName);
490
749
  const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
491
750
  const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
492
751
  const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
493
- const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
494
- if (needsKey) {
495
- 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";
496
756
  const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
497
757
  return [
498
758
  createClassGetter({
499
759
  name: keyedPropertyName,
500
760
  statements: [
501
- `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
761
+ `return this.keyedLocators((${indexedVariable}: ${keyType}) => this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)}));`
502
762
  ]
503
763
  })
504
764
  ];
505
765
  }
506
766
  const finalPropertyName = getterNameOverride ?? propertyName;
507
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
767
+ const alternates = uniquePomStringPatterns(selector, alternateSelectors).slice(1);
508
768
  if (alternates.length > 0) {
509
- const all = [formattedDataTestId, ...alternates];
510
- 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})`);
511
771
  return [
512
772
  createClassGetter({
513
773
  name: finalPropertyName,
@@ -518,21 +778,22 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
518
778
  return [
519
779
  createClassGetter({
520
780
  name: finalPropertyName,
521
- statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
781
+ statements: [`return this.locatorByTestId(${toTypeScriptPomPatternExpression(selector)});`]
522
782
  })
523
783
  ];
524
784
  }
525
785
  function generateNavigationMethod(args) {
526
- const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
786
+ const { targetPageObjectModelClass: target, baseMethodName, selector, alternateSelectors, parameters } = args;
527
787
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
528
- const parameters = createParameters(params);
529
- const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
530
- 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(", ");
531
792
  if (alternates.length > 0) {
532
793
  return [
533
794
  createClassMethod({
534
795
  name: methodName,
535
- parameters,
796
+ parameters: methodParameters,
536
797
  returnType: `Fluent<${target}>`,
537
798
  statements: (writer) => {
538
799
  writer.write("return this.fluent(async () => ").block(() => {
@@ -560,11 +821,11 @@ function generateNavigationMethod(args) {
560
821
  return [
561
822
  createClassMethod({
562
823
  name: methodName,
563
- parameters,
824
+ parameters: methodParameters,
564
825
  returnType: `Fluent<${target}>`,
565
826
  statements: (writer) => {
566
827
  writer.write("return this.fluent(async () => ").block(() => {
567
- writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
828
+ writer.writeLine(`await this.clickByTestId(${toTypeScriptPomPatternExpression(selector)});`);
568
829
  writer.writeLine(`return new ${target}(this.page);`);
569
830
  });
570
831
  writer.writeLine(");");
@@ -572,15 +833,15 @@ function generateNavigationMethod(args) {
572
833
  })
573
834
  ];
574
835
  }
575
- function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
836
+ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, selector, alternateSelectors, getterNameOverride, parameters) {
576
837
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
577
838
  const members = generateGetElementByDataTestId(
578
839
  baseMethodName,
579
840
  nativeRole,
580
- formattedDataTestId,
581
- alternateFormattedDataTestIds,
841
+ selector,
842
+ alternateSelectors,
582
843
  getterNameOverride,
583
- params
844
+ parameters
584
845
  );
585
846
  if (targetPageObjectModelClass) {
586
847
  return [
@@ -588,25 +849,25 @@ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName,
588
849
  ...generateNavigationMethod({
589
850
  targetPageObjectModelClass,
590
851
  baseMethodName,
591
- formattedDataTestId,
592
- alternateFormattedDataTestIds,
593
- params
852
+ selector,
853
+ alternateSelectors,
854
+ parameters
594
855
  })
595
856
  ];
596
857
  }
597
858
  if (nativeRole === "select") {
598
- return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
859
+ return [...members, ...generateSelectMethod(baseMethodName, selector, parameters)];
599
860
  }
600
861
  if (nativeRole === "vselect") {
601
- return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
862
+ return [...members, ...generateVSelectMethod(baseMethodName, selector, parameters)];
602
863
  }
603
864
  if (nativeRole === "input") {
604
- return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
865
+ return [...members, ...generateTypeMethod(baseMethodName, selector, parameters)];
605
866
  }
606
867
  if (nativeRole === "radio") {
607
- return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
868
+ return [...members, ...generateRadioMethod(baseMethodName || "Radio", selector, parameters)];
608
869
  }
609
- return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
870
+ return [...members, ...generateClickMethod(baseMethodName, selector, alternateSelectors, parameters)];
610
871
  }
611
872
  function isSimpleExpressionNode(value) {
612
873
  return value !== null && "type" in value && value.type === NodeTypes.SIMPLE_EXPRESSION;
@@ -646,43 +907,72 @@ function buildPlaceholderParams(keys) {
646
907
  params[k] = "__placeholder__";
647
908
  return params;
648
909
  }
649
- function getRouteLocationLikeFromToDirective(toDirective) {
650
- if (!toDirective.exp)
651
- return null;
652
- const exp = toDirective.exp;
653
- 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();
654
950
  let expr;
655
951
  try {
656
952
  expr = parseExpression(rawSource, { plugins: ["typescript"] });
657
- } catch {
658
- 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
+ };
659
960
  }
660
- const isNodeType = (node, type) => {
661
- return node !== null && node.type === type;
662
- };
663
- const isStringLiteralNode = (node) => {
664
- return isNodeType(node, "StringLiteral") && typeof node.value === "string";
665
- };
666
- const isIdentifierNode = (node) => {
667
- return isNodeType(node, "Identifier") && typeof node.name === "string";
668
- };
669
- const isObjectPropertyNode = (node) => {
670
- if (!isNodeType(node, "ObjectProperty"))
671
- return false;
672
- const n = node;
673
- return typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
674
- };
675
- const isObjectExpressionNode = (node) => {
676
- if (!isNodeType(node, "ObjectExpression"))
677
- return false;
678
- const n = node;
679
- return Array.isArray(n.properties);
680
- };
681
961
  if (isStringLiteralNode(expr)) {
682
- return expr.value;
962
+ return {
963
+ kind: "resolved",
964
+ rawSource,
965
+ target: expr.value,
966
+ routeNameKey: null,
967
+ paramKeys: []
968
+ };
683
969
  }
684
970
  if (!isObjectExpressionNode(expr)) {
685
- return null;
971
+ return {
972
+ kind: "unsupported",
973
+ rawSource,
974
+ reason: "dynamic-expression"
975
+ };
686
976
  }
687
977
  const getStringField = (fieldName) => {
688
978
  const prop = expr.properties.find((p) => {
@@ -703,7 +993,7 @@ function getRouteLocationLikeFromToDirective(toDirective) {
703
993
  const key = p.key;
704
994
  return isIdentifierNode(key) && key.name === "params" || isStringLiteralNode(key) && key.value === "params";
705
995
  });
706
- let params;
996
+ let paramKeys = [];
707
997
  if (paramsProp && isObjectPropertyNode(paramsProp) && isObjectExpressionNode(paramsProp.value)) {
708
998
  const keys = [];
709
999
  for (const prop of paramsProp.value.properties) {
@@ -715,47 +1005,50 @@ function getRouteLocationLikeFromToDirective(toDirective) {
715
1005
  else if (isStringLiteralNode(key))
716
1006
  keys.push(key.value);
717
1007
  }
718
- if (keys.length) {
719
- params = buildPlaceholderParams(Array.from(new Set(keys)));
720
- }
1008
+ paramKeys = Array.from(new Set(keys));
721
1009
  }
722
1010
  if (name) {
723
- 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
+ };
724
1026
  }
725
1027
  if (path2) {
726
- return { path: path2, params };
1028
+ return {
1029
+ kind: "resolved",
1030
+ rawSource,
1031
+ target: { path: path2 },
1032
+ routeNameKey: null,
1033
+ paramKeys
1034
+ };
727
1035
  }
728
- return null;
729
- }
730
- function toDirectiveObjectFieldNameValue$1(toDirective) {
731
- const to = getRouteLocationLikeFromToDirective(toDirective);
732
- if (!to || typeof to === "string")
733
- return null;
734
- const name = to.name;
735
- if (typeof name !== "string")
736
- return null;
737
- const trimmed = name.trim();
738
- if (!trimmed.length)
739
- return null;
740
- return toPascalCaseRouteKey(trimmed);
741
- }
742
- function getRouteNameKeyFromToDirective(toDirective) {
743
- const objectName = toDirectiveObjectFieldNameValue$1(toDirective);
744
- if (objectName)
745
- return objectName;
746
- return null;
1036
+ return {
1037
+ kind: "unsupported",
1038
+ rawSource,
1039
+ reason: "missing-name-or-path"
1040
+ };
747
1041
  }
748
1042
  function tryResolveToDirectiveTargetComponentName(toDirective) {
749
- const to = getRouteLocationLikeFromToDirective(toDirective);
750
- if (to && resolveToComponentName) {
751
- const resolved = resolveToComponentName(to);
1043
+ const analysis = analyzeToDirectiveTarget(toDirective);
1044
+ if (analysis.kind === "resolved" && resolveToComponentName) {
1045
+ const resolved = resolveToComponentName(materializeResolvedRouteTarget(analysis.target, analysis.paramKeys));
752
1046
  if (resolved)
753
1047
  return resolved;
754
1048
  }
755
- const key = getRouteNameKeyFromToDirective(toDirective);
756
- if (!key || !routeNameToComponentName)
1049
+ if (analysis.kind !== "resolved" || !analysis.routeNameKey || !routeNameToComponentName)
757
1050
  return null;
758
- return routeNameToComponentName.get(key) ?? null;
1051
+ return routeNameToComponentName.get(analysis.routeNameKey) ?? null;
759
1052
  }
760
1053
  function getDataTestIdFromGroupOption(text) {
761
1054
  return text.replace(/[-_]/g, " ").split(" ").filter((a) => a).map((str) => {
@@ -790,11 +1083,81 @@ function staticAttributeValue(value) {
790
1083
  return { kind: "static", value };
791
1084
  }
792
1085
  function templateAttributeValue(template) {
793
- 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 };
794
1091
  }
795
1092
  function getAttributeValueText(value) {
796
1093
  return value.kind === "static" ? value.value : value.template;
797
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
+ }
798
1161
  function toPascalCase(str) {
799
1162
  const cleaned = (str ?? "").replace(/\$\{[^}]*\}/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
800
1163
  if (!cleaned) {
@@ -836,32 +1199,14 @@ function tryGetClickDirective(node) {
836
1199
  function nodeHasClickDirective(node) {
837
1200
  return tryGetClickDirective(node) !== void 0;
838
1201
  }
839
- function getTemplateSlotScope(node) {
1202
+ function findTemplateSlotScopeExpression(node) {
840
1203
  if (node.tag !== "template") {
841
1204
  return null;
842
1205
  }
843
1206
  const slotProp = node.props.find((prop) => {
844
1207
  return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot";
845
1208
  });
846
- if (slotProp?.exp) {
847
- if (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
848
- return slotProp.exp.content;
849
- }
850
- if (slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) {
851
- return stringifyExpression(slotProp.exp);
852
- }
853
- }
854
- return null;
855
- }
856
- function isSimpleScopeIdentifier(value) {
857
- if (!value) {
858
- return false;
859
- }
860
- try {
861
- return isIdentifier(parseExpression(value, { plugins: ["typescript"] }));
862
- } catch {
863
- return false;
864
- }
1209
+ return slotProp?.exp && (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION || slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) ? slotProp.exp : null;
865
1210
  }
866
1211
  function buildSlotScopeFallbackKeyExpression(identifier) {
867
1212
  return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
@@ -963,43 +1308,64 @@ function tryGetSlotScopeKeyCandidate(node) {
963
1308
  }
964
1309
  return best;
965
1310
  }
966
- function tryGetTemplateSlotScopeKeyExpression(scope) {
967
- const trimmed = scope.trim();
968
- 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) {
969
1321
  return null;
970
1322
  }
971
- if (isSimpleScopeIdentifier(trimmed)) {
972
- return buildSlotScopeFallbackKeyExpression(trimmed);
1323
+ try {
1324
+ return parseExpression(rawSource, { plugins: ["typescript"] });
1325
+ } catch {
973
1326
  }
974
1327
  try {
975
- const parsed = parse(`(${trimmed}) => {}`, {
1328
+ const parsed = parse(`(${rawSource}) => {}`, {
976
1329
  sourceType: "module",
977
1330
  plugins: ["typescript"]
978
1331
  });
979
1332
  const statement = parsed.program.body[0];
980
1333
  if (statement && isExpressionStatement(statement) && isArrowFunctionExpression(statement.expression)) {
981
- return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
1334
+ return statement.expression.params[0] ?? null;
982
1335
  }
983
1336
  } catch {
1337
+ return null;
984
1338
  }
985
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
986
- const inner = trimmed.slice(1, -1).trim();
987
- let cutIdx = -1;
988
- const commaIdx = inner.indexOf(",");
989
- const colonIdx = inner.indexOf(":");
990
- if (commaIdx !== -1 && colonIdx !== -1) {
991
- cutIdx = Math.min(commaIdx, colonIdx);
992
- } else if (commaIdx !== -1) {
993
- cutIdx = commaIdx;
994
- } else if (colonIdx !== -1) {
995
- cutIdx = colonIdx;
996
- }
997
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
998
- if (first && isSimpleScopeIdentifier(first)) {
999
- return buildSlotScopeFallbackKeyExpression(first);
1000
- }
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;
1001
1358
  }
1002
- 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;
1003
1369
  }
1004
1370
  function nodeHasToDirective(node) {
1005
1371
  const toDirective = findDirectiveByName(node, "bind", "to");
@@ -1008,51 +1374,160 @@ function nodeHasToDirective(node) {
1008
1374
  }
1009
1375
  return void 0;
1010
1376
  }
1011
- function nodeHasForDirective(node) {
1012
- return node.props.some(
1013
- (attr) => attr.type === NodeTypes.DIRECTIVE && attr.name === "for"
1014
- );
1015
- }
1016
1377
  function getKeyDirective(node) {
1017
1378
  return findDirectiveByName(node, "bind", "key") ?? null;
1018
1379
  }
1019
- function getKeyDirectiveValue(node, _context = null) {
1020
- const keyDirective = getKeyDirective(node);
1021
- const rawSource = keyDirective?.exp?.loc.source?.trim();
1022
- if (rawSource) {
1023
- return `\${${rawSource}}`;
1380
+ function tryUnwrapTemplateLiteralSource(source) {
1381
+ const rawSource = source.trim();
1382
+ if (!rawSource) {
1383
+ return null;
1384
+ }
1385
+ let ast = null;
1386
+ try {
1387
+ ast = parseExpression(rawSource, { plugins: ["typescript"] });
1388
+ } catch {
1389
+ return null;
1024
1390
  }
1025
- if (keyDirective?.exp) {
1026
- const value = stringifyExpression(keyDirective.exp);
1027
- if (value)
1028
- return `\${${value}}`;
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;
1029
1423
  }
1030
- 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);
1031
1510
  }
1032
1511
  function getModelBindingValues(node) {
1033
1512
  let vModel = "";
1034
1513
  const vModelDirective = findDirectiveByName(node, "model");
1035
- if (vModelDirective?.exp?.loc.source) {
1036
- 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"));
1037
1516
  }
1038
1517
  let modelValue = null;
1039
1518
  const modelValueDirective = findDirectiveByName(node, "bind", "modelValue");
1040
- if (modelValueDirective?.exp?.ast) {
1041
- 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);
1042
1526
  modelValue = mv;
1043
1527
  }
1044
1528
  return { vModel, modelValue };
1045
1529
  }
1046
- function getSelfClosingForDirectiveKeyAttrValue(node) {
1047
- if (node.isSelfClosing) {
1048
- const hasForDirective = nodeHasForDirective(node);
1049
- if (hasForDirective) {
1050
- return getKeyDirectiveValue(node);
1051
- }
1052
- }
1053
- return null;
1054
- }
1055
- function getIdOrName(node) {
1530
+ function getStaticIdOrNameHint(node) {
1056
1531
  let idAttr = findAttributeByKey(node, "id");
1057
1532
  if (!idAttr) {
1058
1533
  idAttr = findAttributeByKey(node, "name");
@@ -1060,8 +1535,9 @@ function getIdOrName(node) {
1060
1535
  let identifier = idAttr?.value?.content ?? "";
1061
1536
  if (!identifier) {
1062
1537
  const dynamicIdAttr = findDirectiveByName(node, "bind", "id");
1063
- if (dynamicIdAttr?.exp) {
1064
- identifier = `\${someUniqueValueToDifferentiateInstanceFromOthersOnPageUsuallyAnId}`;
1538
+ const dynamicNameAttr = findDirectiveByName(node, "bind", "name");
1539
+ if (dynamicIdAttr?.exp || dynamicNameAttr?.exp) {
1540
+ return "";
1065
1541
  }
1066
1542
  }
1067
1543
  if (identifier.includes("-")) {
@@ -1072,20 +1548,20 @@ function getIdOrName(node) {
1072
1548
  }
1073
1549
  return identifier;
1074
1550
  }
1075
- function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
1551
+ function getContainedInSlotDataKeyInfo(node, hierarchyMap2) {
1076
1552
  let parent = getParent(hierarchyMap2, node);
1077
1553
  while (parent) {
1078
1554
  if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
1079
- const scope = getTemplateSlotScope(parent);
1080
- if (scope) {
1081
- return tryGetTemplateSlotScopeKeyExpression(scope);
1555
+ const slotScopeExpression = findTemplateSlotScopeExpression(parent);
1556
+ if (slotScopeExpression) {
1557
+ return tryGetTemplateSlotScopeKeyInfo(slotScopeExpression);
1082
1558
  }
1083
1559
  }
1084
1560
  parent = getParent(hierarchyMap2, parent);
1085
1561
  }
1086
1562
  return null;
1087
1563
  }
1088
- function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1564
+ function getContainedInVForDirectiveKeyInfo(context, node, hierarchyMap2) {
1089
1565
  if (!context.scopes.vFor || context.scopes.vFor === 0) {
1090
1566
  return null;
1091
1567
  }
@@ -1094,8 +1570,7 @@ function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
1094
1570
  if (parent.type === NodeTypes.ELEMENT) {
1095
1571
  const forDirective = findDirectiveByName(parent, "for");
1096
1572
  if (forDirective) {
1097
- const keyValue = getKeyDirectiveValue(parent);
1098
- return keyValue;
1573
+ return getKeyDirectiveInfo(parent);
1099
1574
  }
1100
1575
  }
1101
1576
  parent = getParent(hierarchyMap2, parent);
@@ -1121,13 +1596,7 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
1121
1596
  if (simpleSourceExp.constType === ConstantTypes.NOT_CONSTANT) {
1122
1597
  return null;
1123
1598
  }
1124
- const iterableRaw = (() => {
1125
- try {
1126
- return stringifyExpression(simpleSourceExp).trim();
1127
- } catch {
1128
- return (simpleSourceExp.loc?.source ?? "").trim();
1129
- }
1130
- })();
1599
+ const iterableRaw = getVueExpressionSource(simpleSourceExp, "compiled", "loc");
1131
1600
  if (!iterableRaw) {
1132
1601
  return null;
1133
1602
  }
@@ -1183,91 +1652,95 @@ function nodeHandlerAttributeInfo(node) {
1183
1652
  return null;
1184
1653
  }
1185
1654
  const exp = handlerDirective.exp;
1186
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
1655
+ const source = getVueExpressionSource(exp, "content", "compiled");
1187
1656
  if (!source) {
1188
1657
  return null;
1189
1658
  }
1190
1659
  const mergeKey = `handler:expr:${source}`;
1191
- let expr;
1192
- try {
1193
- expr = parseExpression(source, { plugins: ["typescript", "jsx"] });
1194
- } 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) {
1195
1668
  return null;
1196
1669
  }
1197
- const isNodeType = (node2, type) => {
1670
+ const isNodeType2 = (node2, type) => {
1198
1671
  return node2 !== null && node2.type === type;
1199
1672
  };
1200
- const isIdentifierNode = (node2) => {
1201
- return isNodeType(node2, "Identifier") && typeof node2.name === "string";
1673
+ const isIdentifierNode2 = (node2) => {
1674
+ return isNodeType2(node2, "Identifier") && typeof node2.name === "string";
1202
1675
  };
1203
- const isStringLiteralNode = (node2) => {
1204
- return isNodeType(node2, "StringLiteral") && typeof node2.value === "string";
1676
+ const isStringLiteralNode2 = (node2) => {
1677
+ return isNodeType2(node2, "StringLiteral") && typeof node2.value === "string";
1205
1678
  };
1206
1679
  const isBooleanLiteralNode = (node2) => {
1207
- return isNodeType(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1680
+ return isNodeType2(node2, "BooleanLiteral") && typeof node2.value === "boolean";
1208
1681
  };
1209
1682
  const isNumericLiteralNode = (node2) => {
1210
- return isNodeType(node2, "NumericLiteral") && typeof node2.value === "number";
1683
+ return isNodeType2(node2, "NumericLiteral") && typeof node2.value === "number";
1211
1684
  };
1212
1685
  const isNullLiteralNode = (node2) => {
1213
- return isNodeType(node2, "NullLiteral");
1686
+ return isNodeType2(node2, "NullLiteral");
1214
1687
  };
1215
1688
  const isMemberExpressionNode = (node2) => {
1216
- if (!isNodeType(node2, "MemberExpression"))
1689
+ if (!isNodeType2(node2, "MemberExpression"))
1217
1690
  return false;
1218
1691
  const n = node2;
1219
1692
  return typeof n.computed === "boolean" && typeof n.object === "object" && n.object !== null && typeof n.property === "object" && n.property !== null;
1220
1693
  };
1221
1694
  const isCallExpressionNode = (node2) => {
1222
- if (!isNodeType(node2, "CallExpression"))
1695
+ if (!isNodeType2(node2, "CallExpression"))
1223
1696
  return false;
1224
1697
  const n = node2;
1225
1698
  return typeof n.callee === "object" && n.callee !== null && Array.isArray(n.arguments);
1226
1699
  };
1227
1700
  const isAwaitExpressionNode = (node2) => {
1228
- if (!isNodeType(node2, "AwaitExpression"))
1701
+ if (!isNodeType2(node2, "AwaitExpression"))
1229
1702
  return false;
1230
1703
  const n = node2;
1231
1704
  return typeof n.argument === "object" && n.argument !== null;
1232
1705
  };
1233
1706
  const isAssignmentExpressionNode = (node2) => {
1234
- if (!isNodeType(node2, "AssignmentExpression"))
1707
+ if (!isNodeType2(node2, "AssignmentExpression"))
1235
1708
  return false;
1236
1709
  const n = node2;
1237
1710
  return typeof n.left === "object" && n.left !== null && typeof n.right === "object" && n.right !== null;
1238
1711
  };
1239
1712
  const isArrowFunctionExpressionNode = (node2) => {
1240
- if (!isNodeType(node2, "ArrowFunctionExpression"))
1713
+ if (!isNodeType2(node2, "ArrowFunctionExpression"))
1241
1714
  return false;
1242
1715
  const n = node2;
1243
1716
  return typeof n.body === "object" && n.body !== null;
1244
1717
  };
1245
1718
  const isBlockStatementNode = (node2) => {
1246
- if (!isNodeType(node2, "BlockStatement"))
1719
+ if (!isNodeType2(node2, "BlockStatement"))
1247
1720
  return false;
1248
1721
  const n = node2;
1249
1722
  return Array.isArray(n.body);
1250
1723
  };
1251
1724
  const isExpressionStatementNode = (node2) => {
1252
- if (!isNodeType(node2, "ExpressionStatement"))
1725
+ if (!isNodeType2(node2, "ExpressionStatement"))
1253
1726
  return false;
1254
1727
  const n = node2;
1255
1728
  return typeof n.expression === "object" && n.expression !== null;
1256
1729
  };
1257
1730
  const isReturnStatementNode = (node2) => {
1258
- if (!isNodeType(node2, "ReturnStatement"))
1731
+ if (!isNodeType2(node2, "ReturnStatement"))
1259
1732
  return false;
1260
1733
  const n = node2;
1261
1734
  return typeof n.argument === "object" || n.argument === null;
1262
1735
  };
1263
- const isObjectExpressionNode = (node2) => {
1264
- if (!isNodeType(node2, "ObjectExpression"))
1736
+ const isObjectExpressionNode2 = (node2) => {
1737
+ if (!isNodeType2(node2, "ObjectExpression"))
1265
1738
  return false;
1266
1739
  const n = node2;
1267
1740
  return Array.isArray(n.properties);
1268
1741
  };
1269
- const isObjectPropertyNode = (node2) => {
1270
- if (!isNodeType(node2, "ObjectProperty"))
1742
+ const isObjectPropertyNode2 = (node2) => {
1743
+ if (!isNodeType2(node2, "ObjectProperty"))
1271
1744
  return false;
1272
1745
  const n = node2;
1273
1746
  return typeof n.computed === "boolean" && typeof n.key === "object" && n.key !== null && typeof n.value === "object" && n.value !== null;
@@ -1275,16 +1748,16 @@ function nodeHandlerAttributeInfo(node) {
1275
1748
  const getLastIdentifierFromMemberChain = (node2) => {
1276
1749
  if (!node2)
1277
1750
  return null;
1278
- if (isIdentifierNode(node2))
1751
+ if (isIdentifierNode2(node2))
1279
1752
  return node2.name;
1280
1753
  if (isMemberExpressionNode(node2)) {
1281
1754
  const prop = node2.property;
1282
1755
  if (node2.computed === false) {
1283
- if (isIdentifierNode(prop))
1756
+ if (isIdentifierNode2(prop))
1284
1757
  return prop.name;
1285
1758
  }
1286
1759
  if (node2.computed === true) {
1287
- if (isStringLiteralNode(prop))
1760
+ if (isStringLiteralNode2(prop))
1288
1761
  return prop.value;
1289
1762
  }
1290
1763
  }
@@ -1294,7 +1767,7 @@ function nodeHandlerAttributeInfo(node) {
1294
1767
  if (!node2) {
1295
1768
  return null;
1296
1769
  }
1297
- if (isIdentifierNode(node2)) {
1770
+ if (isIdentifierNode2(node2)) {
1298
1771
  return node2.name;
1299
1772
  }
1300
1773
  if (isMemberExpressionNode(node2)) {
@@ -1306,11 +1779,11 @@ function nodeHandlerAttributeInfo(node) {
1306
1779
  if (!lhs) {
1307
1780
  return null;
1308
1781
  }
1309
- if (isIdentifierNode(lhs)) {
1782
+ if (isIdentifierNode2(lhs)) {
1310
1783
  return lhs.name;
1311
1784
  }
1312
1785
  if (isMemberExpressionNode(lhs)) {
1313
- if (lhs.computed === false && isIdentifierNode(lhs.property) && lhs.property.name === "value") {
1786
+ if (lhs.computed === false && isIdentifierNode2(lhs.property) && lhs.property.name === "value") {
1314
1787
  return getLastIdentifierFromMemberChain(lhs.object);
1315
1788
  }
1316
1789
  return getLastIdentifierFromMemberChain(lhs);
@@ -1318,7 +1791,7 @@ function nodeHandlerAttributeInfo(node) {
1318
1791
  return null;
1319
1792
  };
1320
1793
  const isTemplateLiteralNode = (node2) => {
1321
- if (!isNodeType(node2, "TemplateLiteral")) {
1794
+ if (!isNodeType2(node2, "TemplateLiteral")) {
1322
1795
  return false;
1323
1796
  }
1324
1797
  const n = node2;
@@ -1337,7 +1810,7 @@ function nodeHandlerAttributeInfo(node) {
1337
1810
  if (isNullLiteralNode(arg)) {
1338
1811
  return "Null";
1339
1812
  }
1340
- if (isStringLiteralNode(arg)) {
1813
+ if (isStringLiteralNode2(arg)) {
1341
1814
  const cleaned = (arg.value ?? "").trim();
1342
1815
  if (!cleaned) {
1343
1816
  return null;
@@ -1363,7 +1836,7 @@ function nodeHandlerAttributeInfo(node) {
1363
1836
  return toPascalCase(stableName.slice(0, 24));
1364
1837
  }
1365
1838
  }
1366
- if (isIdentifierNode(arg)) {
1839
+ if (isIdentifierNode2(arg)) {
1367
1840
  const firstChar = arg.name.charAt(0);
1368
1841
  const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1369
1842
  if (isUpperAlpha) {
@@ -1375,7 +1848,7 @@ function nodeHandlerAttributeInfo(node) {
1375
1848
  const getStableSuffixFromCall = (call) => {
1376
1849
  const args = call.arguments ?? [];
1377
1850
  const first = args.length > 0 ? args[0] : null;
1378
- if (!isObjectExpressionNode(first)) {
1851
+ if (!isObjectExpressionNode2(first)) {
1379
1852
  const parts2 = [];
1380
1853
  for (const arg of args.slice(0, 4)) {
1381
1854
  const w = stableWordFromValue(arg ?? null);
@@ -1394,20 +1867,20 @@ function nodeHandlerAttributeInfo(node) {
1394
1867
  }
1395
1868
  const parts = [];
1396
1869
  for (const prop of first.properties ?? []) {
1397
- if (!isObjectPropertyNode(prop)) {
1870
+ if (!isObjectPropertyNode2(prop)) {
1398
1871
  continue;
1399
1872
  }
1400
1873
  if (prop.computed) {
1401
1874
  continue;
1402
1875
  }
1403
- 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;
1404
1877
  if (!keyName) {
1405
1878
  continue;
1406
1879
  }
1407
1880
  let valueWord = null;
1408
1881
  if (isBooleanLiteralNode(prop.value)) {
1409
1882
  valueWord = prop.value.value ? "True" : "False";
1410
- } else if (isStringLiteralNode(prop.value)) {
1883
+ } else if (isStringLiteralNode2(prop.value)) {
1411
1884
  const cleaned = (prop.value.value ?? "").trim();
1412
1885
  if (cleaned) {
1413
1886
  valueWord = toPascalCase(cleaned.slice(0, 24));
@@ -1536,13 +2009,18 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1536
2009
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
1537
2010
  }
1538
2011
  const attrDynamic = findDirectiveByName(node, "bind", attributeKey);
1539
- if (attrDynamic && "exp" in attrDynamic && attrDynamic.exp && "ast" in attrDynamic.exp && attrDynamic.exp.ast) {
1540
- let value = attrDynamic.exp.loc.source;
1541
- 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)) {
1542
2020
  return staticAttributeValue(`${actualFileName}-${value.replaceAll(".", "")}-${role}`);
1543
2021
  }
1544
- if (attrDynamic.exp.ast?.type === "CallExpression") {
1545
- value = stringifyExpression(attrDynamic.exp);
2022
+ if (isCallExpression(attrDynamicAst) || isOptionalCallExpression(attrDynamicAst)) {
2023
+ value = getVueExpressionSource(attrDynamic.exp, "compiled", "loc");
1546
2024
  return templateAttributeValue(`${actualFileName}-\${${value}}-${role}`);
1547
2025
  }
1548
2026
  return staticAttributeValue(`${actualFileName}-${value}-${role}`);
@@ -1550,16 +2028,16 @@ function getDataTestIdValueFromValueAttribute(node, actualFileName, attributeKey
1550
2028
  return null;
1551
2029
  }
1552
2030
  function generateToDirectiveDataTestId(componentName, node, toDirective, context, hierarchyMap2, nativeWrappers) {
1553
- const key = getKeyDirectiveValue(node, context) || getSelfClosingForDirectiveKeyAttrValue(node) || getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2);
1554
- if (key) {
1555
- 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)}`);
1556
2034
  } else {
1557
2035
  let name = toDirectiveObjectFieldNameValue(toDirective);
1558
2036
  if (!name) {
1559
2037
  if (toDirective.exp == null) {
1560
2038
  return null;
1561
2039
  }
1562
- const source = stringifyExpression(toDirective.exp);
2040
+ const source = getVueExpressionSource(toDirective.exp, "compiled", "loc");
1563
2041
  const toAst = toDirective.exp.ast;
1564
2042
  const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && isTemplateLiteral(toAst);
1565
2043
  return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
@@ -1583,44 +2061,46 @@ function toDirectiveObjectFieldNameValue(node) {
1583
2061
  if (!node.exp || node.exp.type !== NodeTypes.COMPOUND_EXPRESSION && node.exp.type !== NodeTypes.SIMPLE_EXPRESSION) {
1584
2062
  return null;
1585
2063
  }
1586
- const source = node.exp.loc.source.trim();
1587
- try {
1588
- const expr = parseExpression(source, { plugins: ["typescript"] });
1589
- const isNodeType = (n, type) => {
1590
- return n !== null && n.type === type;
1591
- };
1592
- const isStringLiteralNode = (n) => {
1593
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
1594
- };
1595
- const isIdentifierNode = (n) => {
1596
- return isNodeType(n, "Identifier") && typeof n.name === "string";
1597
- };
1598
- const isObjectPropertyNode = (n) => {
1599
- if (!isNodeType(n, "ObjectProperty"))
1600
- return false;
1601
- const nn = n;
1602
- return typeof nn.key === "object" && nn.key !== null && typeof nn.value === "object" && nn.value !== null;
1603
- };
1604
- const isObjectExpressionNode = (n) => {
1605
- if (!isNodeType(n, "ObjectExpression"))
1606
- return false;
1607
- const nn = n;
1608
- return Array.isArray(nn.properties);
1609
- };
1610
- if (!isObjectExpressionNode(expr))
1611
- return null;
1612
- const nameProp = expr.properties.find((p) => {
1613
- if (!isObjectPropertyNode(p))
1614
- return false;
1615
- const key = p.key;
1616
- return isIdentifierNode(key) && key.name === "name" || isStringLiteralNode(key) && key.value === "name";
1617
- });
1618
- if (!nameProp || !isObjectPropertyNode(nameProp) || !isStringLiteralNode(nameProp.value))
1619
- return null;
1620
- return toPascalCase(nameProp.value.value);
1621
- } catch {
2064
+ const expr = tryGetDirectiveBabelAst(node, {
2065
+ preferredViews: ["loc", "compiled"],
2066
+ plugins: ["typescript"],
2067
+ preferExistingAst: false
2068
+ });
2069
+ if (!expr) {
1622
2070
  return null;
1623
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);
1624
2104
  }
1625
2105
  function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
1626
2106
  const click = clickDirective ?? tryGetClickDirective(node);
@@ -1630,7 +2110,7 @@ function getComposedClickHandlerContent(node, _context, innerText, clickDirectiv
1630
2110
  let handlerName = "";
1631
2111
  if (click.exp) {
1632
2112
  const exp = click.exp;
1633
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
2113
+ const source = getVueExpressionSource(exp, "content", "compiled");
1634
2114
  if (source) {
1635
2115
  const parsed = tryParseBabelAstFromHandlerSource(source);
1636
2116
  if (parsed) {
@@ -1651,9 +2131,9 @@ function tryParseBabelAstFromHandlerSource(source) {
1651
2131
  const trimmed = source.trim();
1652
2132
  if (!trimmed)
1653
2133
  return null;
1654
- try {
1655
- return parseExpression(trimmed, { plugins: ["typescript", "jsx"] });
1656
- } catch {
2134
+ const expressionAst = tryParseBabelExpressionFromSource(trimmed, ["typescript", "jsx"]);
2135
+ if (expressionAst) {
2136
+ return expressionAst;
1657
2137
  }
1658
2138
  try {
1659
2139
  return parse(trimmed, { sourceType: "module", plugins: ["typescript", "jsx"] });
@@ -1992,23 +2472,24 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
1992
2472
  return null;
1993
2473
  }
1994
2474
  const simpleExp = exp;
1995
- const ast = simpleExp.ast;
1996
- if (ast && typeof ast === "object" && "type" in ast && ast.type === "TemplateLiteral") {
1997
- const tl = ast;
1998
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
1999
- const expressionCount = (tl.expressions ?? []).length;
2000
- const isStatic = expressionCount === 0;
2001
- const raw2 = (simpleExp.content ?? "").trim();
2002
- 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;
2003
2482
  if (isStatic) {
2004
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2483
+ return { value: unwrappedTemplateLiteral.template, isDynamic: false, isStaticLiteral: true };
2005
2484
  }
2485
+ const templateValue = templateAttributeValue(unwrappedTemplateLiteral.template);
2006
2486
  return {
2007
- value: unwrappedTemplate,
2487
+ value: templateValue.template,
2008
2488
  isDynamic: true,
2009
2489
  isStaticLiteral: false,
2010
- template: unwrappedTemplate,
2011
- templateExpressionCount: expressionCount
2490
+ template: templateValue.template,
2491
+ parsedTemplate: templateValue.parsedTemplate,
2492
+ templateExpressionCount: unwrappedTemplateLiteral.expressionCount
2012
2493
  };
2013
2494
  }
2014
2495
  if (ast && typeof ast === "object" && "type" in ast && ast.type === "StringLiteral") {
@@ -2021,59 +2502,27 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
2021
2502
  }
2022
2503
  const preservableReference = tryGetPreservableDynamicReferenceExpression(ast);
2023
2504
  if (preservableReference) {
2505
+ const templateValue = templateAttributeValue(`\${${preservableReference}}`);
2024
2506
  return {
2025
2507
  value: preservableReference,
2026
- isDynamic: true,
2027
- isStaticLiteral: false,
2028
- template: `\${${preservableReference}}`,
2029
- templateExpressionCount: 1,
2030
- rawExpression: preservableReference
2031
- };
2032
- }
2033
- const raw = (simpleExp.content ?? "").trim();
2034
- if (!raw) {
2035
- return null;
2036
- }
2037
- try {
2038
- const ast2 = parseExpression(raw, { plugins: ["typescript"] });
2039
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
2040
- const tl = ast2;
2041
- const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
2042
- const expressionCount = (tl.expressions ?? []).length;
2043
- const isStatic = expressionCount === 0;
2044
- const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
2045
- if (isStatic) {
2046
- return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
2047
- }
2048
- return {
2049
- value: unwrappedTemplate,
2050
- isDynamic: true,
2051
- isStaticLiteral: false,
2052
- template: unwrappedTemplate,
2053
- templateExpressionCount: expressionCount
2054
- };
2055
- }
2056
- if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
2057
- const sl = ast2;
2058
- return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
2059
- }
2060
- const preservableReference2 = tryGetPreservableDynamicReferenceExpression(ast2);
2061
- if (preservableReference2) {
2062
- return {
2063
- value: preservableReference2,
2064
- isDynamic: true,
2065
- isStaticLiteral: false,
2066
- template: `\${${preservableReference2}}`,
2067
- templateExpressionCount: 1,
2068
- rawExpression: preservableReference2
2069
- };
2070
- }
2071
- } 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;
2072
2519
  }
2073
2520
  return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
2074
2521
  }
2075
2522
  function isTemplatePlaceholder(part) {
2076
- 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 === "";
2077
2526
  }
2078
2527
  function isAllCapsOrDigits(value) {
2079
2528
  if (value.length <= 1) {
@@ -2126,33 +2575,14 @@ function safeMethodNameFromParts(parts) {
2126
2575
  }
2127
2576
  return name;
2128
2577
  }
2129
- function replaceAllTemplateExpressionsWithKey(template) {
2578
+ function toPomKeyPattern(templateValue) {
2579
+ const { templateLiteral } = templateValue.parsedTemplate;
2130
2580
  let out = "";
2131
- let i = 0;
2132
- while (i < template.length) {
2133
- const start = template.indexOf("${", i);
2134
- if (start < 0) {
2135
- out += template.slice(i);
2136
- break;
2137
- }
2138
- out += template.slice(i, start);
2139
- let depth = 1;
2140
- let j = start + 2;
2141
- while (j < template.length && depth > 0) {
2142
- if (template[j] === "{") {
2143
- depth++;
2144
- } else if (template[j] === "}") {
2145
- depth--;
2146
- }
2147
- j++;
2148
- }
2149
- const end = depth === 0 ? j - 1 : -1;
2150
- if (end < 0) {
2151
- out += template.slice(start);
2152
- 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}";
2153
2585
  }
2154
- out += "${key}";
2155
- i = end + 1;
2156
2586
  }
2157
2587
  return out;
2158
2588
  }
@@ -2160,8 +2590,8 @@ function applyResolvedDataTestId(args) {
2160
2590
  const addHtmlAttribute = args.addHtmlAttribute ?? true;
2161
2591
  const entryOverrides = args.entryOverrides ?? {};
2162
2592
  const testIdAttribute = args.testIdAttribute ?? "data-testid";
2163
- const existingIdBehavior = args.existingIdBehavior ?? "preserve";
2164
- const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
2593
+ const existingIdBehavior = args.existingIdBehavior ?? "error";
2594
+ const nameCollisionBehavior = args.nameCollisionBehavior ?? "error";
2165
2595
  const warn = args.warn;
2166
2596
  const getBestKeyAccessCandidates = (expr) => {
2167
2597
  if (!expr) {
@@ -2170,7 +2600,10 @@ function applyResolvedDataTestId(args) {
2170
2600
  return splitNullishCoalescingExpression(expr);
2171
2601
  };
2172
2602
  let dataTestId = args.preferredGeneratedValue;
2603
+ let runtimeDataTestId = args.preferredRuntimeValue ?? args.preferredGeneratedValue;
2173
2604
  let fromExisting = false;
2605
+ const bestKeyPreservePlaceholder = args.keyInfo?.runtimeFragment ?? null;
2606
+ const bestKeyVariable = args.keyInfo?.rawExpression ?? null;
2174
2607
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
2175
2608
  if (existing) {
2176
2609
  const loc = args.element.loc?.start;
@@ -2191,8 +2624,10 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
2191
2624
  if (existingIdBehavior === "preserve") {
2192
2625
  if (existing.isDynamic) {
2193
2626
  if (existing.template) {
2194
- const existingTemplate = existing.template;
2195
- 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) {
2196
2631
  throw new Error(
2197
2632
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
2198
2633
  Component: ${args.componentName}
@@ -2202,20 +2637,21 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2202
2637
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
2203
2638
  );
2204
2639
  }
2205
- const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
2206
- const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
2207
- 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) {
2208
2643
  throw new Error(
2209
2644
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2210
2645
  Component: ${args.componentName}
2211
2646
  File: ${file}:${locationHint}
2212
2647
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2213
- 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}"` : ""}
2214
2649
 
2215
- 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.`
2216
2651
  );
2217
2652
  }
2218
- dataTestId = templateAttributeValue(existing.template);
2653
+ dataTestId = existingTemplateValue;
2654
+ runtimeDataTestId = existingTemplateValue;
2219
2655
  fromExisting = true;
2220
2656
  } else {
2221
2657
  throw new Error(
@@ -2229,18 +2665,19 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
2229
2665
  );
2230
2666
  }
2231
2667
  } else {
2232
- if (args.bestKeyPlaceholder && existing.isStaticLiteral) {
2668
+ if (bestKeyPreservePlaceholder && existing.isStaticLiteral) {
2233
2669
  throw new Error(
2234
2670
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
2235
2671
  Component: ${args.componentName}
2236
2672
  File: ${file}:${locationHint}
2237
2673
  Existing ${attrLabel}: ${JSON.stringify(existing.value)}
2238
- 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}"` : ""}
2239
2675
 
2240
- 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.`
2241
2677
  );
2242
2678
  }
2243
2679
  dataTestId = staticAttributeValue(existing.value);
2680
+ runtimeDataTestId = staticAttributeValue(existing.value);
2244
2681
  fromExisting = true;
2245
2682
  }
2246
2683
  }
@@ -2270,8 +2707,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2270
2707
  };
2271
2708
  const normalizedRole = normalizeNativeRole(args.nativeRole) ?? "button";
2272
2709
  const targetPageObjectModelClass = entryOverrides.targetPageObjectModelClass;
2273
- const formattedDataTestIdForPom = dataTestId.kind === "template" ? replaceAllTemplateExpressionsWithKey(dataTestId.template) : dataTestId.value;
2274
- 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";
2275
2714
  const deriveBaseMethodNameFromHint = (hint) => {
2276
2715
  const hintRaw = (hint ?? "").trim();
2277
2716
  const trimEdgeSeparators = (value) => {
@@ -2319,13 +2758,13 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2319
2758
  const roleSuffix = upperFirst(normalizedRole || "Element");
2320
2759
  const baseName = upperFirst(primaryMethodName);
2321
2760
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2322
- return isKeyed ? removeByKeySegment2(propertyName) : propertyName;
2761
+ return selectorIsParameterized ? removeByKeySegment2(propertyName) : propertyName;
2323
2762
  };
2324
2763
  const getPrimaryGetterNameCandidates = (primaryMethodName) => {
2325
2764
  const roleSuffix = upperFirst(normalizedRole || "Element");
2326
2765
  const baseName = upperFirst(primaryMethodName);
2327
2766
  const propertyName = hasRoleSuffix(baseName, roleSuffix) ? baseName : `${baseName}${roleSuffix}`;
2328
- if (!isKeyed) {
2767
+ if (!selectorIsParameterized) {
2329
2768
  return { primary: propertyName };
2330
2769
  }
2331
2770
  const stripped = removeByKeySegment2(propertyName);
@@ -2387,11 +2826,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2387
2826
  return false;
2388
2827
  }
2389
2828
  const existingSelectors = [
2390
- existingPom.formattedDataTestId,
2391
- ...existingPom.alternateFormattedDataTestIds ?? []
2829
+ existingPom.selector,
2830
+ ...existingPom.alternateSelectors ?? []
2392
2831
  ];
2393
- const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2394
- if (isKeyed && !sharesSelectorIdentity) {
2832
+ const sharesSelectorIdentity = existingSelectors.some((existingSelector) => pomStringPatternEquals(existingSelector, selectorPattern));
2833
+ if (selectorIsParameterized && !sharesSelectorIdentity) {
2395
2834
  return false;
2396
2835
  }
2397
2836
  if (!mergeKey && !sharesSelectorIdentity) {
@@ -2406,12 +2845,15 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2406
2845
  if ((existingEntry.targetPageObjectModelClass ?? null) !== (targetPageObjectModelClass ?? null)) {
2407
2846
  return false;
2408
2847
  }
2409
- if (existingPom.formattedDataTestId !== formattedDataTestIdForPom) {
2410
- existingPom.alternateFormattedDataTestIds ??= [];
2411
- if (!existingPom.alternateFormattedDataTestIds.includes(formattedDataTestIdForPom)) {
2412
- 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);
2413
2852
  }
2414
2853
  }
2854
+ if (selectorIsParameterized && !existingPom.parameters.some((param) => param.name === "key")) {
2855
+ existingPom.parameters = [createPomParameterSpec("key", keyTypeFromValues), ...existingPom.parameters];
2856
+ }
2415
2857
  return true;
2416
2858
  };
2417
2859
  let methodName = "";
@@ -2424,7 +2866,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2424
2866
  let suffix = 1;
2425
2867
  while (true) {
2426
2868
  const baseWithSuffix = suffix === 1 ? base : `${base}${suffix}`;
2427
- const candidate = isKeyed ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2869
+ const candidate = selectorIsParameterized ? `${baseWithSuffix}ByKey` : baseWithSuffix;
2428
2870
  const actionName = getPrimaryActionMethodName(candidate);
2429
2871
  const getterCandidates = getPrimaryGetterNameCandidates(candidate);
2430
2872
  let chosenGetterName = getterCandidates.primary;
@@ -2450,7 +2892,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2450
2892
  const baseNameUpper = upperFirst(baseWithSuffix);
2451
2893
  if (!hasRoleSuffix(baseNameUpper, roleSuffix)) {
2452
2894
  const baseWithRoleSuffix = `${baseWithSuffix}${roleSuffix}`;
2453
- const candidateWithRoleSuffix = isKeyed ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2895
+ const candidateWithRoleSuffix = selectorIsParameterized ? `${baseWithRoleSuffix}ByKey` : baseWithRoleSuffix;
2454
2896
  const actionNameWithRoleSuffix = getPrimaryActionMethodName(candidateWithRoleSuffix);
2455
2897
  const getterCandidatesWithRoleSuffix = getPrimaryGetterNameCandidates(candidateWithRoleSuffix);
2456
2898
  let chosenGetterNameWithRoleSuffix = getterCandidatesWithRoleSuffix.primary;
@@ -2518,51 +2960,46 @@ Conflicts: getter=${last.getterName}, method=${last.actionName}
2518
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".`
2519
2961
  );
2520
2962
  }
2521
- const params = {};
2522
- if (isKeyed) {
2523
- params.key = keyTypeFromValues;
2524
- }
2963
+ let parameters = selectorIsParameterized ? [createPomParameterSpec("key", keyTypeFromValues)] : [];
2525
2964
  switch (normalizedRole) {
2526
2965
  case "input":
2527
- params.text = "string";
2528
- params.annotationText = 'string = ""';
2529
- if (!isKeyed) delete params.key;
2966
+ parameters = setPomParameter(parameters, "text", "string");
2967
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2530
2968
  break;
2531
2969
  case "select":
2532
- params.value = "string";
2533
- params.annotationText = 'string = ""';
2534
- if (!isKeyed) delete params.key;
2970
+ parameters = setPomParameter(parameters, "value", "string");
2971
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2535
2972
  break;
2536
2973
  case "vselect":
2537
- params.value = "string";
2538
- params.timeOut = "number = 500";
2539
- params.annotationText = 'string = ""';
2540
- if (!isKeyed) delete params.key;
2974
+ parameters = setPomParameter(parameters, "value", "string");
2975
+ parameters = setPomParameter(parameters, "timeOut", "number = 500");
2976
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2541
2977
  break;
2542
2978
  case "radio":
2543
- params.annotationText = 'string = ""';
2979
+ parameters = setPomParameter(parameters, "annotationText", 'string = ""');
2544
2980
  break;
2545
2981
  }
2546
- if (keyTypeFromValues !== "string" && Object.prototype.hasOwnProperty.call(params, "key")) {
2547
- params.key = keyTypeFromValues;
2548
- }
2982
+ const normalizedParameters = selectorIsParameterized ? setPomParameter(parameters, "key", keyTypeFromValues) : removePomParameter(parameters, "key");
2549
2983
  if (addHtmlAttribute && !fromExisting) {
2550
2984
  upsertAttribute(args.element, testIdAttribute, dataTestId);
2551
2985
  }
2552
2986
  const childComponentName = args.element.tag;
2553
2987
  const dataTestIdEntry = {
2554
- value: getAttributeValueText(dataTestId),
2555
- templateLiteral: void 0,
2556
- ...entryOverrides
2988
+ selectorValue: entryOverrides.selectorValue ?? createPomStringPattern(
2989
+ getAttributeValueText(dataTestId),
2990
+ dataTestId.kind === "template" ? "parameterized" : "static"
2991
+ ),
2992
+ templateLiteral: entryOverrides.templateLiteral,
2993
+ targetPageObjectModelClass: entryOverrides.targetPageObjectModelClass
2557
2994
  };
2558
2995
  dataTestIdEntry.pom = {
2559
2996
  nativeRole: normalizedRole,
2560
2997
  methodName,
2561
2998
  getterNameOverride,
2562
- formattedDataTestId: formattedDataTestIdForPom,
2563
- alternateFormattedDataTestIds: void 0,
2999
+ selector: selectorPattern,
3000
+ alternateSelectors: void 0,
2564
3001
  mergeKey: args.pomMergeKey,
2565
- params,
3002
+ parameters: normalizedParameters,
2566
3003
  keyValuesOverride: args.keyValuesOverride ?? null
2567
3004
  // emitPrimary defaults to true; special cases (including merge) may set it to false below.
2568
3005
  };
@@ -2593,43 +3030,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2593
3030
  }
2594
3031
  };
2595
3032
  const getSignatureForGeneratedMethod = () => {
2596
- const role = normalizedRole;
2597
- const isNavigation2 = !!dataTestIdEntry.targetPageObjectModelClass;
2598
- const needsKey2 = Object.prototype.hasOwnProperty.call(params, "key");
2599
- const keyType = keyTypeFromValues;
2600
- if (isNavigation2) {
2601
- if (needsKey2) {
2602
- return { params: `key: ${keyType}`, argNames: ["key"] };
2603
- }
2604
- return { params: "", argNames: [] };
2605
- }
2606
- switch (role) {
2607
- case "input":
2608
- return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
2609
- case "select":
2610
- return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
2611
- case "vselect":
2612
- return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
2613
- case "radio":
2614
- return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
2615
- default:
2616
- if (needsKey2) {
2617
- return { params: `key: ${keyType}`, argNames: ["key"] };
2618
- }
2619
- return { params: "", argNames: [] };
2620
- }
3033
+ return createPomMethodSignature(normalizedParameters);
2621
3034
  };
2622
3035
  const registerPrimaryOnce = (pom) => {
2623
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2624
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
3036
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
2625
3037
  const key = JSON.stringify({
2626
3038
  kind: "primary",
2627
3039
  role: pom.nativeRole,
2628
3040
  methodName: pom.methodName,
2629
3041
  getterNameOverride: pom.getterNameOverride ?? null,
2630
- formattedDataTestId: pom.formattedDataTestId,
2631
- alternateFormattedDataTestIds: alternates.length ? alternates : void 0,
2632
- params: stableParams,
3042
+ selector: pom.selector,
3043
+ alternateSelectors: alternates.length ? alternates : void 0,
3044
+ parameters: pom.parameters,
2633
3045
  target: dataTestIdEntry.targetPageObjectModelClass ?? null,
2634
3046
  emitPrimary: pom.emitPrimary ?? true
2635
3047
  });
@@ -2643,8 +3055,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2643
3055
  }
2644
3056
  };
2645
3057
  const addExtraClickMethod = (spec) => {
2646
- const stableParams = spec.params ? Object.fromEntries(Object.entries(spec.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
2647
- 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 });
2648
3059
  const seen = args.generatedMethodContentByComponent.get(args.parentComponentName) ?? /* @__PURE__ */ new Set();
2649
3060
  if (!args.generatedMethodContentByComponent.has(args.parentComponentName)) {
2650
3061
  args.generatedMethodContentByComponent.set(args.parentComponentName, seen);
@@ -2667,7 +3078,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2667
3078
  if (prev === null) {
2668
3079
  return;
2669
3080
  }
2670
- if (signature === null || prev.params !== signature.params) {
3081
+ if (signature === null || !pomMethodSignatureEquals(prev, signature)) {
2671
3082
  args.dependencies.generatedMethods.set(name, null);
2672
3083
  }
2673
3084
  };
@@ -2683,23 +3094,10 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2683
3094
  return candidate;
2684
3095
  };
2685
3096
  const tryGetDirectiveExpressionAst = (dir) => {
2686
- const exp = dir.exp;
2687
- if (!exp) {
2688
- return null;
2689
- }
2690
- if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
2691
- const simple = exp;
2692
- const ast = simple.ast;
2693
- if (ast && "type" in ast) {
2694
- return ast;
2695
- }
2696
- }
2697
- try {
2698
- const raw = args.context ? stringifyExpression(exp) : exp.loc.source;
2699
- return parseExpression(raw, { plugins: ["typescript"] });
2700
- } catch {
2701
- return null;
2702
- }
3097
+ return tryGetDirectiveBabelAst(dir, {
3098
+ preferredViews: args.context ? ["compiled", "loc"] : ["loc", "compiled"],
3099
+ plugins: ["typescript"]
3100
+ });
2703
3101
  };
2704
3102
  const tryGetStaticStringFromBabel = (node) => {
2705
3103
  if (!node) {
@@ -2796,17 +3194,17 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2796
3194
  name: generatedName2,
2797
3195
  selector: {
2798
3196
  kind: "withinTestIdByLabel",
2799
- rootFormattedDataTestId: wrapperTestId,
2800
- formattedLabel: label,
3197
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3198
+ label: createPomStringPattern(label, "static"),
2801
3199
  exact: true
2802
3200
  },
2803
- params: { annotationText: `string = ""` }
3201
+ parameters: [createPomParameterSpec("annotationText", `string = ""`)]
2804
3202
  });
2805
3203
  if (added2) {
2806
- registerGeneratedMethodSignature(generatedName2, { params: `annotationText: string = ""`, argNames: ["annotationText"] });
3204
+ registerGeneratedMethodSignature(generatedName2, createPomMethodSignature([createPomParameterSpec("annotationText", `string = ""`)]));
2807
3205
  }
2808
3206
  }
2809
- return;
3207
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2810
3208
  }
2811
3209
  const generatedName = ensureUniqueGeneratedName(`select${upperFirst(methodName || "Radio")}`);
2812
3210
  if (dataTestIdEntry.pom) {
@@ -2818,19 +3216,25 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2818
3216
  name: generatedName,
2819
3217
  selector: {
2820
3218
  kind: "withinTestIdByLabel",
2821
- rootFormattedDataTestId: wrapperTestId,
2822
- formattedLabel: "${value}",
3219
+ rootTestId: createPomStringPattern(wrapperTestId, selectorPatternKind),
3220
+ label: createPomStringPattern("${value}", "parameterized"),
2823
3221
  exact: true
2824
3222
  },
2825
- params: { value: "string", annotationText: `string = ""` }
3223
+ parameters: [
3224
+ createPomParameterSpec("value", "string"),
3225
+ createPomParameterSpec("annotationText", `string = ""`)
3226
+ ]
2826
3227
  });
2827
3228
  if (added) {
2828
- registerGeneratedMethodSignature(generatedName, { params: `value: string, annotationText: string = ""`, argNames: ["value", "annotationText"] });
3229
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3230
+ createPomParameterSpec("value", "string"),
3231
+ createPomParameterSpec("annotationText", `string = ""`)
3232
+ ]));
2829
3233
  }
2830
- return;
3234
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2831
3235
  }
2832
3236
  const staticKeyValues = args.keyValuesOverride ?? null;
2833
- const needsKey = Object.prototype.hasOwnProperty.call(params, "key") && typeof formattedDataTestIdForPom === "string" && formattedDataTestIdForPom.includes("${key}");
3237
+ const needsKey = hasPomParameter(normalizedParameters, "key") && selectorIsParameterized;
2834
3238
  const isNavigation = !!dataTestIdEntry.targetPageObjectModelClass;
2835
3239
  if (staticKeyValues && staticKeyValues.length > 0 && needsKey && !isNavigation && normalizedRole !== "input" && normalizedRole !== "select" && normalizedRole !== "vselect" && normalizedRole !== "radio") {
2836
3240
  if (dataTestIdEntry.pom) {
@@ -2849,16 +3253,19 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2849
3253
  name: generatedName,
2850
3254
  selector: {
2851
3255
  kind: "testId",
2852
- formattedDataTestId: formattedDataTestIdForPom
3256
+ testId: selectorPattern
2853
3257
  },
2854
3258
  keyLiteral: rawValue,
2855
- params: { wait: "boolean = true" }
3259
+ parameters: [createPomParameterSpec("wait", "boolean = true"), createPomParameterSpec("annotationText", 'string = ""')]
2856
3260
  });
2857
3261
  if (added) {
2858
- registerGeneratedMethodSignature(generatedName, { params: `wait: boolean = true`, argNames: ["wait"] });
3262
+ registerGeneratedMethodSignature(generatedName, createPomMethodSignature([
3263
+ createPomParameterSpec("wait", "boolean = true"),
3264
+ createPomParameterSpec("annotationText", 'string = ""')
3265
+ ]));
2859
3266
  }
2860
3267
  }
2861
- return;
3268
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2862
3269
  }
2863
3270
  if (dataTestIdEntry.pom) {
2864
3271
  if (dataTestIdEntry.pom.emitPrimary !== false) {
@@ -2872,6 +3279,7 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
2872
3279
  const generatedName = getGeneratedMethodName();
2873
3280
  registerGeneratedMethodSignature(generatedName, signature);
2874
3281
  }
3282
+ return { selectorValue: dataTestId, runtimeValue: runtimeDataTestId, fromExisting };
2875
3283
  }
2876
3284
  function safeRealpath(value) {
2877
3285
  try {
@@ -3859,113 +4267,6 @@ class VuePomGeneratorError extends Error {
3859
4267
  this.name = "VuePomGeneratorError";
3860
4268
  }
3861
4269
  }
3862
- function splitParameterList(parameters) {
3863
- const parts = [];
3864
- let current = "";
3865
- let braceDepth = 0;
3866
- let bracketDepth = 0;
3867
- let parenDepth = 0;
3868
- let angleDepth = 0;
3869
- let inSingleQuote = false;
3870
- let inDoubleQuote = false;
3871
- let inTemplateString = false;
3872
- for (let index = 0; index < parameters.length; index += 1) {
3873
- const char = parameters[index];
3874
- const previous = index > 0 ? parameters[index - 1] : "";
3875
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3876
- inSingleQuote = !inSingleQuote;
3877
- current += char;
3878
- continue;
3879
- }
3880
- if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3881
- inDoubleQuote = !inDoubleQuote;
3882
- current += char;
3883
- continue;
3884
- }
3885
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3886
- inTemplateString = !inTemplateString;
3887
- current += char;
3888
- continue;
3889
- }
3890
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
3891
- current += char;
3892
- continue;
3893
- }
3894
- switch (char) {
3895
- case "{":
3896
- braceDepth += 1;
3897
- break;
3898
- case "}":
3899
- braceDepth -= 1;
3900
- break;
3901
- case "[":
3902
- bracketDepth += 1;
3903
- break;
3904
- case "]":
3905
- bracketDepth -= 1;
3906
- break;
3907
- case "(":
3908
- parenDepth += 1;
3909
- break;
3910
- case ")":
3911
- parenDepth -= 1;
3912
- break;
3913
- case "<":
3914
- angleDepth += 1;
3915
- break;
3916
- case ">":
3917
- angleDepth -= 1;
3918
- break;
3919
- case ",":
3920
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
3921
- const trimmed2 = current.trim();
3922
- if (trimmed2) {
3923
- parts.push(trimmed2);
3924
- }
3925
- current = "";
3926
- continue;
3927
- }
3928
- break;
3929
- }
3930
- current += char;
3931
- }
3932
- const trimmed = current.trim();
3933
- if (trimmed) {
3934
- parts.push(trimmed);
3935
- }
3936
- return parts;
3937
- }
3938
- function parseParameterSignature(parameter) {
3939
- const colonIndex = parameter.indexOf(":");
3940
- if (colonIndex < 0) {
3941
- return { name: parameter.trim() };
3942
- }
3943
- const rawName = parameter.slice(0, colonIndex).trim();
3944
- const hasQuestionToken = rawName.endsWith("?");
3945
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
3946
- const remainder = parameter.slice(colonIndex + 1).trim();
3947
- const initializerIndex = remainder.lastIndexOf("=");
3948
- if (initializerIndex < 0) {
3949
- return {
3950
- name,
3951
- hasQuestionToken,
3952
- type: remainder || void 0
3953
- };
3954
- }
3955
- return {
3956
- name,
3957
- hasQuestionToken,
3958
- type: remainder.slice(0, initializerIndex).trim() || void 0,
3959
- initializer: remainder.slice(initializerIndex + 1).trim() || void 0
3960
- };
3961
- }
3962
- function parseParameterSignatures(parameters) {
3963
- const trimmed = parameters.trim();
3964
- if (!trimmed) {
3965
- return [];
3966
- }
3967
- return splitParameterList(trimmed).map(parseParameterSignature);
3968
- }
3969
4270
  function toPosixRelativePath(fromDir, toFile) {
3970
4271
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
3971
4272
  if (!rel.startsWith(".")) {
@@ -3985,6 +4286,21 @@ function resolveRouterEntry(projectRoot, routerEntry) {
3985
4286
  const root = projectRoot ?? process.cwd();
3986
4287
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
3987
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
+ }
3988
4304
  function createCustomPomImportCollisionError(exportName, requested) {
3989
4305
  return new VuePomGeneratorError(
3990
4306
  `Custom POM import name collision detected for "${exportName}".
@@ -4123,35 +4439,28 @@ function generateGoToSelfMethod(componentName) {
4123
4439
  })
4124
4440
  ];
4125
4441
  }
4126
- function formatMethodParams(params) {
4127
- if (!params)
4128
- return "";
4129
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
4130
- const entries = Object.entries(params);
4131
- if (!entries.length)
4132
- return "";
4133
- const score = (name) => {
4134
- const idx = preferredOrder.indexOf(name);
4135
- return idx < 0 ? 999 : idx;
4136
- };
4137
- 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];
4138
4444
  }
4139
4445
  function generateExtraClickMethodMembers(spec) {
4140
4446
  if (spec.kind !== "click") {
4141
4447
  return [];
4142
4448
  }
4143
- const params = spec.params ?? {};
4144
- const signatureParams = formatMethodParams(params);
4145
- const parameters = parseParameterSignatures(signatureParams);
4146
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
4147
- 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");
4148
4458
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
4149
4459
  const waitArg = hasWait ? "wait" : "true";
4150
4460
  if (spec.selector.kind === "testId") {
4151
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
4152
- const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
4461
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
4153
4462
  const clickArgs = [];
4154
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
4463
+ clickArgs.push(testIdBinding.expression);
4155
4464
  if (hasAnnotationText || hasWait) {
4156
4465
  clickArgs.push(annotationArg);
4157
4466
  }
@@ -4167,20 +4476,16 @@ function generateExtraClickMethodMembers(spec) {
4167
4476
  if (spec.keyLiteral !== void 0) {
4168
4477
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4169
4478
  }
4170
- if (needsTemplate) {
4171
- writer.writeLine(`const testId = ${testIdExpr};`);
4479
+ for (const statement of testIdBinding.setupStatements) {
4480
+ writer.writeLine(statement);
4172
4481
  }
4173
4482
  writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
4174
4483
  }
4175
4484
  })
4176
4485
  ];
4177
4486
  }
4178
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
4179
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
4180
- const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
4181
- const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
4182
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4183
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4487
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
4488
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
4184
4489
  return [
4185
4490
  createClassMethod({
4186
4491
  name: spec.name,
@@ -4190,13 +4495,13 @@ function generateExtraClickMethodMembers(spec) {
4190
4495
  if (spec.keyLiteral !== void 0) {
4191
4496
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
4192
4497
  }
4193
- if (rootNeedsTemplate) {
4194
- writer.writeLine(`const rootTestId = ${rootExpr};`);
4498
+ for (const statement of rootBinding.setupStatements) {
4499
+ writer.writeLine(statement);
4195
4500
  }
4196
- if (labelNeedsTemplate) {
4197
- writer.writeLine(`const label = ${labelExpr};`);
4501
+ for (const statement of labelBinding.setupStatements) {
4502
+ writer.writeLine(statement);
4198
4503
  }
4199
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
4504
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg});`);
4200
4505
  }
4201
4506
  })
4202
4507
  ];
@@ -4209,10 +4514,10 @@ function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
4209
4514
  targetPageObjectModelClass,
4210
4515
  primary.methodName,
4211
4516
  primary.nativeRole,
4212
- primary.formattedDataTestId,
4213
- primary.alternateFormattedDataTestIds,
4517
+ primary.selector,
4518
+ primary.alternateSelectors,
4214
4519
  primary.getterNameOverride,
4215
- primary.params ?? {}
4520
+ primary.parameters
4216
4521
  );
4217
4522
  }
4218
4523
  function generateMethodsContentForDependencies(dependencies) {
@@ -4220,15 +4525,14 @@ function generateMethodsContentForDependencies(dependencies) {
4220
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));
4221
4526
  const seenPrimaryKeys = /* @__PURE__ */ new Set();
4222
4527
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
4223
- const stableParams = pom.params ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0]))) : void 0;
4224
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
4528
+ const alternates = (pom.alternateSelectors ?? []).slice().sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
4225
4529
  const key = JSON.stringify({
4226
4530
  role: pom.nativeRole,
4227
4531
  methodName: pom.methodName,
4228
4532
  getterNameOverride: pom.getterNameOverride ?? null,
4229
- testId: pom.formattedDataTestId,
4230
- alternateTestIds: alternates.length ? alternates : void 0,
4231
- params: stableParams,
4533
+ selector: pom.selector,
4534
+ alternateSelectors: alternates.length ? alternates : void 0,
4535
+ parameters: pom.parameters,
4232
4536
  target: target ?? null,
4233
4537
  emitPrimary: pom.emitPrimary ?? true
4234
4538
  });
@@ -4255,6 +4559,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4255
4559
  customPomAttachments = [],
4256
4560
  projectRoot,
4257
4561
  customPomDir,
4562
+ requireCustomPomDir,
4258
4563
  customPomImportAliases,
4259
4564
  customPomImportNameCollisionBehavior = "error",
4260
4565
  testIdAttribute,
@@ -4287,6 +4592,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4287
4592
  customPomAttachments,
4288
4593
  projectRoot,
4289
4594
  customPomDir,
4595
+ requireCustomPomDir,
4290
4596
  customPomImportAliases,
4291
4597
  customPomImportNameCollisionBehavior,
4292
4598
  testIdAttribute,
@@ -4296,6 +4602,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
4296
4602
  customPomAttachments,
4297
4603
  projectRoot,
4298
4604
  customPomDir,
4605
+ requireCustomPomDir,
4299
4606
  customPomImportAliases,
4300
4607
  customPomImportNameCollisionBehavior,
4301
4608
  testIdAttribute,
@@ -4351,9 +4658,15 @@ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathM
4351
4658
  }
4352
4659
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
4353
4660
  customPomDir: options.customPomDir,
4661
+ requireCustomPomDir: options.requireCustomPomDir,
4354
4662
  customPomImportAliases: options.customPomImportAliases,
4355
4663
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
4356
4664
  });
4665
+ assertCustomPomAttachmentsResolved(
4666
+ options.customPomAttachments ?? [],
4667
+ customPomImportResolution.classIdentifierMap,
4668
+ options.customPomDir ?? "tests/playwright/pom/custom"
4669
+ );
4357
4670
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
4358
4671
  const files = [];
4359
4672
  for (const [name, deps] of entries) {
@@ -4512,21 +4825,8 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
4512
4825
  return { filePath, content };
4513
4826
  });
4514
4827
  }
4515
- function toCSharpTestIdExpression(formattedDataTestId) {
4516
- const needsInterpolation = formattedDataTestId.includes("${");
4517
- if (!needsInterpolation) {
4518
- return JSON.stringify(formattedDataTestId);
4519
- }
4520
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
4521
- const quoted = JSON.stringify(inner);
4522
- return `$${quoted}`;
4523
- }
4524
- function toCSharpParam(paramTypeExpr) {
4525
- const trimmed = (paramTypeExpr ?? "").trim();
4526
- const eqIdx = trimmed.indexOf("=");
4527
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
4528
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
4529
- const typePart = left.includes("|") ? "string" : left;
4828
+ function toCSharpParam(param) {
4829
+ const typePart = param.type?.includes("|") ? "string" : param.type ?? "string";
4530
4830
  let type = "string";
4531
4831
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
4532
4832
  type = "bool";
@@ -4534,19 +4834,17 @@ function toCSharpParam(paramTypeExpr) {
4534
4834
  type = "string";
4535
4835
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
4536
4836
  type = "int";
4537
- else if (/\d+/.test(typePart) && typePart === "")
4538
- type = "int";
4539
4837
  else if (/\btimeOut\b/i.test(typePart))
4540
4838
  type = "int";
4541
4839
  let defaultExpr;
4542
- if (right !== void 0) {
4840
+ if (param.initializer !== void 0) {
4543
4841
  if (type === "bool") {
4544
- 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;
4545
4843
  } else if (type === "int") {
4546
- const m = right.match(/\d+/);
4844
+ const m = param.initializer.match(/\d+/);
4547
4845
  defaultExpr = m ? m[0] : void 0;
4548
4846
  } else {
4549
- if (right === '""' || right === '""' || right === "''") {
4847
+ if (param.initializer === '""' || param.initializer === "''") {
4550
4848
  defaultExpr = '""';
4551
4849
  }
4552
4850
  }
@@ -4554,17 +4852,15 @@ function toCSharpParam(paramTypeExpr) {
4554
4852
  return { type, defaultExpr };
4555
4853
  }
4556
4854
  function formatCSharpParams(params) {
4557
- if (!params)
4558
- return { signature: "", argNames: [] };
4559
- const entries = Object.entries(params);
4560
- if (!entries.length)
4855
+ const normalizedParams = normalizePomParameters(params);
4856
+ if (!normalizedParams.length)
4561
4857
  return { signature: "", argNames: [] };
4562
4858
  const signatureParts = [];
4563
4859
  const argNames = [];
4564
- for (const [name, typeExpr] of entries) {
4565
- const { type, defaultExpr } = toCSharpParam(typeExpr);
4566
- argNames.push(name);
4567
- 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}`);
4568
4864
  }
4569
4865
  return { signature: signatureParts.join(", "), argNames };
4570
4866
  }
@@ -4655,23 +4951,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4655
4951
  const baseMethodName = upperFirst(pom.methodName);
4656
4952
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
4657
4953
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
4658
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
4659
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
4660
- const templateVars = templateVarMatches.map((m) => m[1]);
4661
- const augmentedParams = { ...pom.params };
4662
- for (const v of templateVars) {
4663
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
4664
- augmentedParams[v] = "string";
4665
- }
4666
- }
4667
- const orderedParams = Object.fromEntries([
4668
- ...templateVars.map((v) => [v, augmentedParams[v]]),
4669
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
4670
- ]);
4954
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
4955
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
4956
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
4671
4957
  const { signature, argNames } = formatCSharpParams(orderedParams);
4672
4958
  const args = argNames.join(", ");
4673
- const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
4674
- if (pom.formattedDataTestId.includes("${")) {
4959
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
4960
+ if (selectorIsParameterized) {
4675
4961
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
4676
4962
  } else {
4677
4963
  chunks.push(` public ILocator ${locatorName} => LocatorByTestId(${testIdExpr});`);
@@ -4682,12 +4968,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4682
4968
  if (target) {
4683
4969
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
4684
4970
  chunks.push(" {");
4685
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
4686
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
4971
+ if (selectorIsParameterized || allTestIds.length <= 1) {
4972
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
4687
4973
  chunks.push(` return new ${target}(Page);`);
4688
4974
  } else {
4689
4975
  chunks.push(" Exception? lastError = null;");
4690
- 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(", ")} })`);
4691
4977
  chunks.push(" {");
4692
4978
  chunks.push(" try");
4693
4979
  chunks.push(" {");
@@ -4711,7 +4997,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4711
4997
  }
4712
4998
  chunks.push(` public async Task ${actionName}(${sig})`);
4713
4999
  chunks.push(" {");
4714
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
5000
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
4715
5001
  const emitActionCall = (locatorAccess) => {
4716
5002
  if (pom.nativeRole === "input") {
4717
5003
  chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
@@ -4724,9 +5010,9 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4724
5010
  chunks.push(` await ${locatorAccess}.ClickAsync();`);
4725
5011
  }
4726
5012
  };
4727
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
5013
+ if (!selectorIsParameterized && allTestIds.length > 1) {
4728
5014
  chunks.push(" Exception? lastError = null;");
4729
- 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(", ")} })`);
4730
5016
  chunks.push(" {");
4731
5017
  chunks.push(" try");
4732
5018
  chunks.push(" {");
@@ -4772,7 +5058,12 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4772
5058
  for (const extra of extras) {
4773
5059
  if (extra.kind !== "click")
4774
5060
  continue;
4775
- 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);
4776
5067
  const extraName = upperFirst(extra.name);
4777
5068
  chunks.push(` public async Task ${extraName}Async(${signature})`);
4778
5069
  chunks.push(" {");
@@ -4780,29 +5071,22 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
4780
5071
  chunks.push(` var key = ${JSON.stringify(extra.keyLiteral)};`);
4781
5072
  }
4782
5073
  if (extra.selector.kind === "testId") {
4783
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
4784
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
4785
- if (needsTemplate) {
4786
- chunks.push(` var testId = ${testIdExpr};`);
4787
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
4788
- } else {
4789
- 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}`);
4790
5077
  }
5078
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
4791
5079
  } else {
4792
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
4793
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
4794
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
4795
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
5080
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
5081
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
4796
5082
  const exactArg = extra.selector.exact === false ? "false" : "true";
4797
- if (rootNeedsTemplate) {
4798
- chunks.push(` var rootTestId = ${rootExpr};`);
5083
+ for (const statement of rootBinding.setupStatements) {
5084
+ chunks.push(` ${statement}`);
4799
5085
  }
4800
- if (labelNeedsTemplate) {
4801
- chunks.push(` var label = ${labelExpr};`);
5086
+ for (const statement of labelBinding.setupStatements) {
5087
+ chunks.push(` ${statement}`);
4802
5088
  }
4803
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
4804
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
4805
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
5089
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
4806
5090
  }
4807
5091
  chunks.push(" }");
4808
5092
  chunks.push("");
@@ -5246,7 +5530,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5246
5530
  if (existingOnView.has(name) || blockedMethodNames.has(name))
5247
5531
  continue;
5248
5532
  const list = methodToChildren.get(name) ?? [];
5249
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
5533
+ list.push({ childProp, signature: sig });
5250
5534
  methodToChildren.set(name, list);
5251
5535
  }
5252
5536
  }
@@ -5256,12 +5540,12 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
5256
5540
  return [];
5257
5541
  }
5258
5542
  return passthroughs.map(([methodName, candidates]) => {
5259
- const { childProp, params, argNames } = candidates[0];
5260
- const callArgs = argNames.join(", ");
5543
+ const { childProp, signature } = candidates[0];
5544
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5261
5545
  return createClassMethod({
5262
5546
  name: methodName,
5263
5547
  isAsync: true,
5264
- parameters: parseParameterSignatures(params),
5548
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5265
5549
  statements: [
5266
5550
  `return await this.${childProp}.${methodName}(${callArgs});`
5267
5551
  ]
@@ -5285,8 +5569,7 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5285
5569
  const list = methodToAttachments.get(methodName) ?? [];
5286
5570
  list.push({
5287
5571
  propertyName: attachment.propertyName,
5288
- params: signature.params,
5289
- argNames: signature.argNames
5572
+ signature
5290
5573
  });
5291
5574
  methodToAttachments.set(methodName, list);
5292
5575
  }
@@ -5297,12 +5580,12 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
5297
5580
  return [];
5298
5581
  }
5299
5582
  return passthroughs.map(([methodName, candidates]) => {
5300
- const { propertyName, params, argNames } = candidates[0];
5301
- const callArgs = argNames.join(", ");
5583
+ const { propertyName, signature } = candidates[0];
5584
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5302
5585
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
5303
5586
  return createClassMethod({
5304
5587
  name: methodName,
5305
- parameters: parseParameterSignatures(params),
5588
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5306
5589
  statements: [
5307
5590
  `return ${invocation};`
5308
5591
  ]
@@ -5316,15 +5599,44 @@ function sliceNodeSource(source, node) {
5316
5599
  const snippet = source.slice(node.start, node.end).trim();
5317
5600
  return snippet.length ? snippet : null;
5318
5601
  }
5319
- 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) {
5320
5611
  if (param.type === "Identifier") {
5321
- return param.name;
5612
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
5613
+ hasQuestionToken: !!param.optional
5614
+ });
5322
5615
  }
5323
5616
  if (param.type === "AssignmentPattern") {
5324
- 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
+ });
5325
5628
  }
5326
5629
  if (param.type === "RestElement") {
5327
- 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
+ });
5328
5640
  }
5329
5641
  return null;
5330
5642
  }
@@ -5357,29 +5669,23 @@ function extractCustomPomMethodSignatures(source, exportName) {
5357
5669
  if (member.key.type !== "Identifier") {
5358
5670
  continue;
5359
5671
  }
5360
- const params = [];
5361
- const argNames = [];
5672
+ const parameters = [];
5362
5673
  let supported = true;
5363
5674
  member.params.forEach((param) => {
5364
5675
  if (!supported) {
5365
5676
  return;
5366
5677
  }
5367
- const paramSource = sliceNodeSource(source, param);
5368
- const argName = getCustomPomCallArgumentName(param);
5369
- if (!paramSource || !argName) {
5678
+ const parameter = getCustomPomParameterSpec(source, param);
5679
+ if (!parameter) {
5370
5680
  supported = false;
5371
5681
  return;
5372
5682
  }
5373
- params.push(paramSource);
5374
- argNames.push(argName);
5683
+ parameters.push(parameter);
5375
5684
  });
5376
5685
  if (!supported) {
5377
5686
  continue;
5378
5687
  }
5379
- signatures.set(member.key.name, {
5380
- params: params.join(", "),
5381
- argNames
5382
- });
5688
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
5383
5689
  }
5384
5690
  }
5385
5691
  return signatures;
@@ -5562,6 +5868,9 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5562
5868
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5563
5869
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5564
5870
  if (!fs.existsSync(customDirAbs)) {
5871
+ if (options.requireCustomPomDir) {
5872
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
5873
+ }
5565
5874
  return {
5566
5875
  classIdentifierMap,
5567
5876
  methodSignaturesByClass,
@@ -5604,6 +5913,14 @@ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, opti
5604
5913
  importSpecifiersByClass
5605
5914
  };
5606
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
+ }
5607
5924
  function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5608
5925
  const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5609
5926
  if (!filePath)
@@ -5632,7 +5949,7 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5632
5949
  if (!sig)
5633
5950
  continue;
5634
5951
  const list = methodToChildren.get(name) ?? [];
5635
- list.push({ child, params: sig.params, argNames: sig.argNames });
5952
+ list.push({ child, signature: sig });
5636
5953
  methodToChildren.set(name, list);
5637
5954
  }
5638
5955
  }
@@ -5640,12 +5957,12 @@ function getComposedStubBody(targetClassName, availableClassNames, depsByClassNa
5640
5957
  for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5641
5958
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
5642
5959
  continue;
5643
- const { child, params, argNames } = candidatesForMethod[0];
5644
- const callArgs = argNames.join(", ");
5960
+ const { child, signature } = candidatesForMethod[0];
5961
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
5645
5962
  passthroughMembers.push(createClassMethod({
5646
5963
  name: methodName,
5647
5964
  isAsync: true,
5648
- parameters: parseParameterSignatures(params),
5965
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
5649
5966
  statements: [
5650
5967
  `return await this.${child}.${methodName}(${callArgs});`
5651
5968
  ]
@@ -5694,9 +6011,15 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
5694
6011
  imports.push(`export * from "${runtimeClassGenRel}/base-page";`);
5695
6012
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5696
6013
  customPomDir: options.customPomDir,
6014
+ requireCustomPomDir: options.requireCustomPomDir,
5697
6015
  customPomImportAliases: options.customPomImportAliases,
5698
6016
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5699
6017
  });
6018
+ assertCustomPomAttachmentsResolved(
6019
+ options.customPomAttachments ?? [],
6020
+ customPomImportResolution.classIdentifierMap,
6021
+ options.customPomDir ?? "tests/playwright/pom/custom"
6022
+ );
5700
6023
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5701
6024
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5702
6025
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -5846,8 +6169,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
5846
6169
  return candidate;
5847
6170
  };
5848
6171
  for (const dt of dataTestIdSet) {
5849
- const raw = dt.value;
5850
- if (raw.includes("${")) {
6172
+ const raw = dt.selectorValue.formatted;
6173
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
5851
6174
  continue;
5852
6175
  }
5853
6176
  const toggleSuffix = "-toggle";
@@ -5937,7 +6260,6 @@ function getConstructor(childrenComponent, componentHierarchyMap, attachmentsFor
5937
6260
  });
5938
6261
  }
5939
6262
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
5940
- const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
5941
6263
  const CLICK_EVENT_NAME = TESTID_CLICK_EVENT_NAME;
5942
6264
  const inferredNativeWrapperConfigByLookup = /* @__PURE__ */ new Map();
5943
6265
  const inferredSfcPathByLookup = /* @__PURE__ */ new Map();
@@ -6199,7 +6521,7 @@ function getConditionalDirectiveInfo(element) {
6199
6521
  if (directive.name === "else") {
6200
6522
  const exp2 = directive.exp;
6201
6523
  if (exp2 && (exp2.type === NodeTypes.SIMPLE_EXPRESSION || exp2.type === NodeTypes.COMPOUND_EXPRESSION)) {
6202
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6524
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6203
6525
  return { kind: "else-if", source: source2 };
6204
6526
  }
6205
6527
  return { kind: "else", source: "" };
@@ -6208,13 +6530,13 @@ function getConditionalDirectiveInfo(element) {
6208
6530
  const exp2 = directive.exp;
6209
6531
  if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION && exp2.type !== NodeTypes.COMPOUND_EXPRESSION)
6210
6532
  return null;
6211
- const source2 = (exp2.type === NodeTypes.SIMPLE_EXPRESSION ? exp2.content : stringifyExpression(exp2)).trim();
6533
+ const source2 = getVueExpressionSource(exp2, "content", "compiled");
6212
6534
  return { kind: "else-if", source: source2 };
6213
6535
  }
6214
6536
  const exp = directive.exp;
6215
6537
  if (!exp || exp.type !== NodeTypes.SIMPLE_EXPRESSION && exp.type !== NodeTypes.COMPOUND_EXPRESSION)
6216
6538
  return null;
6217
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
6539
+ const source = getVueExpressionSource(exp, "content", "compiled");
6218
6540
  return { kind: directive.name, source };
6219
6541
  }
6220
6542
  function tryExtractStableHintFromConditionalExpressionSource(source) {
@@ -6240,26 +6562,26 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
6240
6562
  };
6241
6563
  try {
6242
6564
  const expr = parseExpression(src, { plugins: ["typescript"] });
6243
- const isNodeType = (n, type) => {
6565
+ const isNodeType2 = (n, type) => {
6244
6566
  return n !== null && n.type === type;
6245
6567
  };
6246
- const isStringLiteralNode = (n) => {
6247
- return isNodeType(n, "StringLiteral") && typeof n.value === "string";
6568
+ const isStringLiteralNode2 = (n) => {
6569
+ return isNodeType2(n, "StringLiteral") && typeof n.value === "string";
6248
6570
  };
6249
- const isIdentifierNode = (n) => {
6250
- return isNodeType(n, "Identifier") && typeof n.name === "string";
6571
+ const isIdentifierNode2 = (n) => {
6572
+ return isNodeType2(n, "Identifier") && typeof n.name === "string";
6251
6573
  };
6252
6574
  const results = [];
6253
6575
  const walk = (n) => {
6254
6576
  if (!n)
6255
6577
  return;
6256
- if (isStringLiteralNode(n)) {
6578
+ if (isStringLiteralNode2(n)) {
6257
6579
  const v = (n.value ?? "").trim();
6258
6580
  if (isIdentifierish(v)) {
6259
6581
  results.push(v);
6260
6582
  }
6261
6583
  }
6262
- if (isIdentifierNode(n)) {
6584
+ if (isIdentifierNode2(n)) {
6263
6585
  const v = (n.name ?? "").trim();
6264
6586
  if (isIdentifierish(v)) {
6265
6587
  results.push(v);
@@ -6409,34 +6731,20 @@ ${buildSearchRootsKey(normalizedSearchRoots)}`;
6409
6731
  inferredNativeWrapperConfigByLookup.set(cacheKey, { role: "" });
6410
6732
  return null;
6411
6733
  }
6412
- function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6734
+ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedRuntimeTestId) {
6413
6735
  const jsStringLiteral = (value) => {
6414
6736
  return JSON.stringify(value);
6415
6737
  };
6416
6738
  const getTestIdExpressionForNode = () => {
6417
- const existing = findTestIdAttribute(element, testIdAttribute);
6418
- if (!existing) {
6419
- return "undefined";
6420
- }
6421
- if (existing.type === NodeTypes.ATTRIBUTE) {
6422
- const v = existing.value?.content;
6423
- if (!v) {
6424
- return "undefined";
6425
- }
6426
- return jsStringLiteral(v);
6427
- }
6428
- const directive = existing;
6429
- const exp2 = directive.exp;
6430
- if (!exp2 || exp2.type !== NodeTypes.SIMPLE_EXPRESSION) {
6739
+ if (!resolvedRuntimeTestId) {
6431
6740
  return "undefined";
6432
6741
  }
6433
- const content = (exp2.content ?? "").trim();
6434
- if (!content) {
6435
- return "undefined";
6742
+ if (resolvedRuntimeTestId.kind === "static") {
6743
+ return jsStringLiteral(resolvedRuntimeTestId.value);
6436
6744
  }
6437
- return `(${content})`;
6745
+ return `(${renderTemplateLiteralExpression(resolvedRuntimeTestId)})`;
6438
6746
  };
6439
- const testIdExpression2 = getTestIdExpressionForNode();
6747
+ const testIdExpression = getTestIdExpressionForNode();
6440
6748
  const clickDirective = tryGetClickDirective(element);
6441
6749
  if (!clickDirective)
6442
6750
  return;
@@ -6447,10 +6755,10 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6447
6755
  const exp = clickDirective.exp;
6448
6756
  if (!exp)
6449
6757
  return;
6450
- const existingSource = (exp.loc?.source ?? (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : "")).trim();
6758
+ const existingSource = getVueExpressionSource(exp, "loc", "content");
6451
6759
  if (existingSource.includes(CLICK_EVENT_NAME))
6452
6760
  return;
6453
- const originalExpression = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc?.source ?? "").trim();
6761
+ const originalExpression = getVueExpressionSource(exp, "content", "loc");
6454
6762
  if (!originalExpression)
6455
6763
  return;
6456
6764
  const isStatementBody = (() => {
@@ -6467,7 +6775,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6467
6775
  const statementWrappedHandler = `($event) => {
6468
6776
  const __win = ($event && $event.view) ? $event.view : undefined;
6469
6777
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6470
- const __testIdFromNode = ${testIdExpression2};
6778
+ const __testIdFromNode = ${testIdExpression};
6471
6779
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6472
6780
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6473
6781
  const __emit = (phase, err) => {
@@ -6478,16 +6786,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6478
6786
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6479
6787
  }
6480
6788
  } catch (e) {
6481
- // Instrumentation must never hide failures during e2e strict mode.
6482
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6483
- // 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.
6484
6790
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6485
6791
  if (__w && __w.console && typeof __w.console.error === 'function') {
6486
6792
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6487
6793
  }
6488
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6489
- throw e;
6490
- }
6794
+ throw e;
6491
6795
  }
6492
6796
  };
6493
6797
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6518,7 +6822,7 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6518
6822
  const expressionWrappedHandler = `($event) => {
6519
6823
  const __win = ($event && $event.view) ? $event.view : undefined;
6520
6824
  const __target = ($event && $event.currentTarget) ? $event.currentTarget : undefined;
6521
- const __testIdFromNode = ${testIdExpression2};
6825
+ const __testIdFromNode = ${testIdExpression};
6522
6826
  const __testIdFromTarget = (__target && typeof __target.getAttribute === 'function') ? __target.getAttribute(${jsStringLiteral(testIdAttribute)}) : undefined;
6523
6827
  const __testId = (__testIdFromNode ?? __testIdFromTarget);
6524
6828
  const __emit = (phase, err) => {
@@ -6529,16 +6833,12 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6529
6833
  __w.dispatchEvent(new __CustomEvent('${CLICK_EVENT_NAME}', { detail: { testId: __testId, phase, err: err ? String(err) : undefined } }));
6530
6834
  }
6531
6835
  } catch (e) {
6532
- // Instrumentation must never hide failures during e2e strict mode.
6533
- // In strict mode we rethrow so tests fail fast and the underlying problem is visible.
6534
- // 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.
6535
6837
  const __w = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
6536
6838
  if (__w && __w.console && typeof __w.console.error === 'function') {
6537
6839
  __w.console.error('[testid-click-event] failed to emit ${CLICK_EVENT_NAME}', e);
6538
6840
  }
6539
- if (__w && (__w[${JSON.stringify(TESTID_CLICK_EVENT_STRICT_FLAG)}] === true)) {
6540
- throw e;
6541
- }
6841
+ throw e;
6542
6842
  }
6543
6843
  };
6544
6844
  const __w2 = __win || (__target && __target.ownerDocument && __target.ownerDocument.defaultView);
@@ -6576,10 +6876,9 @@ function tryWrapClickDirectiveForTestEvents(element, testIdAttribute) {
6576
6876
  let previousFileName = "";
6577
6877
  const hierarchyMap = /* @__PURE__ */ new Map();
6578
6878
  function createTestIdTransform(componentName, componentHierarchyMap, nativeWrappers = {}, excludedComponents = [], viewsDirAbs, options = {}) {
6579
- const existingIdBehavior = options.existingIdBehavior ?? "preserve";
6879
+ const existingIdBehavior = options.existingIdBehavior ?? "error";
6580
6880
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
6581
- const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6582
- const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "error";
6881
+ const nameCollisionBehavior = options.nameCollisionBehavior ?? "error";
6583
6882
  const warn = options.warn;
6584
6883
  const vueFilesPathMap = options.vueFilesPathMap;
6585
6884
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -6655,7 +6954,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6655
6954
  conditionalHintByIfBranch.set(branch, hint);
6656
6955
  continue;
6657
6956
  }
6658
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
6957
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6659
6958
  const stable = tryExtractStableHintFromConditionalExpressionSource(condSource);
6660
6959
  if (stable) {
6661
6960
  conditionalHintByIfBranch.set(branch, stable);
@@ -6717,17 +7016,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6717
7016
  nativeWrappers[element.tag] = { role: "grid" };
6718
7017
  }
6719
7018
  }
6720
- const getBestAvailableKeyValue = () => {
7019
+ const getBestAvailableKeyInfo = () => {
6721
7020
  const parentNode = context.parent && typeof context.parent === "object" ? context.parent : null;
6722
7021
  const isDirectVForChild = parentNode?.type === NodeTypes.FOR;
6723
- const vForKey = (isDirectVForChild ? getKeyDirectiveValue(element, context) : null) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
6724
- if (vForKey) return vForKey;
6725
- 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);
6726
7027
  };
6727
- const bestKeyInferred = getBestAvailableKeyValue();
6728
- const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
6729
- const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
6730
- const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
7028
+ const bestKeyInfo = getBestAvailableKeyInfo();
7029
+ const bestKeyPlaceholder = bestKeyInfo?.selectorFragment ?? null;
7030
+ const bestRuntimeKeyPlaceholder = bestKeyInfo?.runtimeFragment ?? null;
6731
7031
  const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
6732
7032
  const parentKey = context?.parent ? context.parent : null;
6733
7033
  const conditional = getConditionalDirectiveInfo(element);
@@ -6770,7 +7070,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6770
7070
  if (!cond) {
6771
7071
  conditionalHint = "else";
6772
7072
  } else {
6773
- const condSource = (cond.type === NodeTypes.SIMPLE_EXPRESSION ? cond.content : stringifyExpression(cond)).trim();
7073
+ const condSource = getVueExpressionSource(cond, "content", "compiled");
6774
7074
  conditionalHint = tryExtractStableHintFromConditionalExpressionSource(condSource) ?? "if";
6775
7075
  }
6776
7076
  }
@@ -6780,7 +7080,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6780
7080
  });
6781
7081
  if (showDirective?.exp && (showDirective.exp.type === NodeTypes.SIMPLE_EXPRESSION || showDirective.exp.type === NodeTypes.COMPOUND_EXPRESSION)) {
6782
7082
  const exp = showDirective.exp;
6783
- const source = (exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : stringifyExpression(exp)).trim();
7083
+ const source = getVueExpressionSource(exp, "content", "compiled");
6784
7084
  const showHint = tryExtractStableHintFromConditionalExpressionSource(source);
6785
7085
  if (showHint) {
6786
7086
  conditionalHint = conditionalHint ? `${conditionalHint} ${showHint}` : showHint;
@@ -6814,13 +7114,17 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6814
7114
  const tagSuffix = getTagSuffix();
6815
7115
  return bestKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
6816
7116
  };
7117
+ const getClickRuntimeDataTestId = (clickSuffix) => {
7118
+ const tagSuffix = getTagSuffix();
7119
+ return bestRuntimeKeyPlaceholder ? templateAttributeValue(`${componentName}-${bestRuntimeKeyPlaceholder}${clickSuffix}${tagSuffix}`) : staticAttributeValue(`${componentName}${clickSuffix}${tagSuffix}`);
7120
+ };
6817
7121
  const getSubmitDataTestId = (identifier) => {
6818
7122
  const tagSuffix = getTagSuffix();
6819
7123
  return `${componentName}-${identifier}${tagSuffix}`;
6820
7124
  };
6821
7125
  const applyResolvedDataTestIdForElement = (args) => {
6822
7126
  const nativeRole = args.nativeRoleOverride ?? getNativeRoleFromTagSuffix();
6823
- applyResolvedDataTestId({
7127
+ return applyResolvedDataTestId({
6824
7128
  element,
6825
7129
  componentName,
6826
7130
  parentComponentName,
@@ -6830,8 +7134,8 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6830
7134
  generatedMethodContentByComponent,
6831
7135
  nativeRole,
6832
7136
  preferredGeneratedValue: args.preferredGeneratedValue,
6833
- bestKeyPlaceholder,
6834
- bestKeyVariable,
7137
+ preferredRuntimeValue: args.preferredRuntimeValue,
7138
+ keyInfo: bestKeyInfo,
6835
7139
  keyValuesOverride,
6836
7140
  entryOverrides: args.entryOverrides,
6837
7141
  semanticNameHint: args.semanticNameHint,
@@ -6849,7 +7153,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6849
7153
  return p.type === NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6850
7154
  }) ?? null;
6851
7155
  const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6852
- if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
7156
+ if (nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6853
7157
  const loc = element.loc?.start;
6854
7158
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6855
7159
  const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
@@ -6858,7 +7162,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
6858
7162
  Element: <${element.tag}>
6859
7163
  Handler: ${handlerSource}
6860
7164
 
6861
- 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.`
6862
7166
  );
6863
7167
  }
6864
7168
  if (nativeWrappersValue) {
@@ -6985,20 +7289,20 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
6985
7289
  contextFilename: context.filename
6986
7290
  });
6987
7291
  const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
6988
- const idOrName = getIdOrName(element) || void 0;
7292
+ const idOrName = getStaticIdOrNameHint(element) || void 0;
6989
7293
  const semanticHintCandidates = [clickHint, idOrName, innerText, conditionalHint].map((value) => (value ?? "").trim()).filter(Boolean).filter((value, index, values) => values.indexOf(value) === index);
6990
7294
  const [semanticNameHint2, ...semanticNameHintAlternates] = semanticHintCandidates;
6991
7295
  const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
6992
7296
  const testId = getClickDataTestId(clickSuffix);
6993
- applyResolvedDataTestIdForElement({
7297
+ const runtimeTestId = getClickRuntimeDataTestId(clickSuffix);
7298
+ const resolvedDataTestId = applyResolvedDataTestIdForElement({
6994
7299
  preferredGeneratedValue: testId,
7300
+ preferredRuntimeValue: runtimeTestId,
6995
7301
  semanticNameHint: semanticNameHint2,
6996
7302
  semanticNameHintAlternates,
6997
7303
  pomMergeKey
6998
7304
  });
6999
- {
7000
- tryWrapClickDirectiveForTestEvents(element, testIdAttribute);
7001
- }
7305
+ tryWrapClickDirectiveForTestEvents(element, testIdAttribute, resolvedDataTestId.runtimeValue);
7002
7306
  return;
7003
7307
  }
7004
7308
  const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
@@ -7008,7 +7312,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7008
7312
  if (!isRecognizedInteractiveRole) {
7009
7313
  return;
7010
7314
  }
7011
- 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;
7012
7316
  const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
7013
7317
  applyResolvedDataTestIdForElement({
7014
7318
  preferredGeneratedValue,
@@ -7018,7 +7322,7 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
7018
7322
  }
7019
7323
  const isSubmit = element.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === "type")?.value?.content === "submit";
7020
7324
  if (isSubmit) {
7021
- const identifier = getIdOrName(element) || innerText;
7325
+ const identifier = getStaticIdOrNameHint(element) || innerText;
7022
7326
  if (!identifier) {
7023
7327
  const loc = element.loc?.start;
7024
7328
  const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
@@ -7070,29 +7374,32 @@ function createBuildProcessorPlugin(options) {
7070
7374
  getSourceDirs,
7071
7375
  basePageClassPath,
7072
7376
  normalizedBasePagePath,
7377
+ generation,
7378
+ projectRootRef,
7379
+ nativeWrappers,
7380
+ excludedComponents,
7381
+ getWrapperSearchRoots,
7382
+ getResolvedRouterEntry,
7383
+ loggerRef
7384
+ } = options;
7385
+ const {
7073
7386
  outDir,
7074
7387
  emitLanguages,
7075
7388
  typescriptOutputStructure,
7076
7389
  csharp,
7077
7390
  generateFixtures,
7078
7391
  customPomAttachments,
7079
- projectRootRef,
7080
7392
  customPomDir,
7393
+ requireCustomPomDir,
7081
7394
  customPomImportAliases,
7082
7395
  customPomImportNameCollisionBehavior,
7083
7396
  testIdAttribute,
7084
7397
  nameCollisionBehavior,
7085
- missingSemanticNameBehavior = "error",
7086
7398
  existingIdBehavior,
7087
- nativeWrappers,
7088
- excludedComponents,
7089
- getWrapperSearchRoots,
7090
7399
  routerAwarePoms,
7091
- getResolvedRouterEntry,
7092
7400
  routerType,
7093
- routerModuleShims,
7094
- loggerRef
7095
- } = options;
7401
+ routerModuleShims
7402
+ } = generation;
7096
7403
  let lastGeneratedMetrics = {
7097
7404
  entryCount: 0,
7098
7405
  interactiveComponentCount: 0,
@@ -7191,10 +7498,9 @@ function createBuildProcessorPlugin(options) {
7191
7498
  excludedComponents,
7192
7499
  getViewsDirAbs(),
7193
7500
  {
7194
- existingIdBehavior: existingIdBehavior ?? "preserve",
7501
+ existingIdBehavior: existingIdBehavior ?? "error",
7195
7502
  testIdAttribute,
7196
7503
  nameCollisionBehavior,
7197
- missingSemanticNameBehavior,
7198
7504
  warn: (message) => loggerRef.current.warn(message),
7199
7505
  vueFilesPathMap,
7200
7506
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -7294,6 +7600,7 @@ function createBuildProcessorPlugin(options) {
7294
7600
  customPomAttachments,
7295
7601
  projectRoot: projectRootRef.current,
7296
7602
  customPomDir,
7603
+ requireCustomPomDir,
7297
7604
  customPomImportAliases,
7298
7605
  customPomImportNameCollisionBehavior,
7299
7606
  testIdAttribute,
@@ -7325,6 +7632,11 @@ function createDevProcessorPlugin(options) {
7325
7632
  projectRootRef,
7326
7633
  normalizedBasePagePath,
7327
7634
  basePageClassPath,
7635
+ generation,
7636
+ getResolvedRouterEntry,
7637
+ loggerRef
7638
+ } = options;
7639
+ const {
7328
7640
  outDir,
7329
7641
  emitLanguages,
7330
7642
  typescriptOutputStructure,
@@ -7332,18 +7644,16 @@ function createDevProcessorPlugin(options) {
7332
7644
  generateFixtures,
7333
7645
  customPomAttachments,
7334
7646
  customPomDir,
7647
+ requireCustomPomDir,
7335
7648
  customPomImportAliases,
7336
7649
  customPomImportNameCollisionBehavior,
7337
- nameCollisionBehavior = "suffix",
7338
- missingSemanticNameBehavior = "error",
7650
+ nameCollisionBehavior,
7339
7651
  existingIdBehavior,
7340
7652
  testIdAttribute,
7341
7653
  routerAwarePoms,
7342
- getResolvedRouterEntry,
7343
7654
  routerType,
7344
- routerModuleShims,
7345
- loggerRef
7346
- } = options;
7655
+ routerModuleShims
7656
+ } = generation;
7347
7657
  let scheduleVueFileRegen = null;
7348
7658
  const getProjectRootCandidates = () => Array.from(/* @__PURE__ */ new Set([
7349
7659
  path.resolve(projectRootRef.current),
@@ -7530,9 +7840,8 @@ function createDevProcessorPlugin(options) {
7530
7840
  excludedComponents,
7531
7841
  getViewsDirAbs(),
7532
7842
  {
7533
- existingIdBehavior: existingIdBehavior ?? "preserve",
7843
+ existingIdBehavior: existingIdBehavior ?? "error",
7534
7844
  nameCollisionBehavior,
7535
- missingSemanticNameBehavior,
7536
7845
  testIdAttribute,
7537
7846
  warn: (message) => loggerRef.current.warn(message),
7538
7847
  vueFilesPathMap: provisionalVuePathMap,
@@ -7583,6 +7892,7 @@ function createDevProcessorPlugin(options) {
7583
7892
  customPomAttachments,
7584
7893
  projectRoot: projectRootRef.current,
7585
7894
  customPomDir,
7895
+ requireCustomPomDir,
7586
7896
  customPomImportAliases,
7587
7897
  customPomImportNameCollisionBehavior,
7588
7898
  pageDirs: getPageDirs(),
@@ -7818,27 +8128,30 @@ function createSupportPlugins(options) {
7818
8128
  getViewsDir,
7819
8129
  getSourceDirs,
7820
8130
  getWrapperSearchRoots,
7821
- nameCollisionBehavior = "suffix",
7822
- missingSemanticNameBehavior = "error",
7823
- existingIdBehavior,
8131
+ generation,
8132
+ projectRootRef,
8133
+ basePageClassPath: basePageClassPathOverride,
8134
+ loggerRef
8135
+ } = options;
8136
+ const {
7824
8137
  outDir,
7825
8138
  emitLanguages,
7826
8139
  typescriptOutputStructure,
7827
8140
  csharp,
7828
- routerAwarePoms,
7829
- routerEntry,
7830
- routerType,
7831
- routerModuleShims,
7832
8141
  generateFixtures,
7833
8142
  customPomAttachments,
7834
- projectRootRef,
7835
- basePageClassPath: basePageClassPathOverride,
7836
8143
  customPomDir,
8144
+ requireCustomPomDir,
7837
8145
  customPomImportAliases,
7838
8146
  customPomImportNameCollisionBehavior,
8147
+ nameCollisionBehavior,
8148
+ existingIdBehavior,
7839
8149
  testIdAttribute,
7840
- loggerRef
7841
- } = options;
8150
+ routerAwarePoms,
8151
+ routerEntry,
8152
+ routerType,
8153
+ routerModuleShims
8154
+ } = generation;
7842
8155
  const resolveRouterEntry2 = () => {
7843
8156
  if (!routerAwarePoms)
7844
8157
  return void 0;
@@ -7867,27 +8180,12 @@ function createSupportPlugins(options) {
7867
8180
  getSourceDirs,
7868
8181
  basePageClassPath,
7869
8182
  normalizedBasePagePath,
7870
- outDir,
7871
- emitLanguages,
7872
- typescriptOutputStructure,
7873
- csharp,
7874
- generateFixtures,
7875
- customPomAttachments,
8183
+ generation,
7876
8184
  projectRootRef,
7877
- customPomDir,
7878
- customPomImportAliases,
7879
- customPomImportNameCollisionBehavior,
7880
- testIdAttribute,
7881
- nameCollisionBehavior,
7882
- missingSemanticNameBehavior,
7883
- existingIdBehavior,
7884
8185
  nativeWrappers,
7885
8186
  excludedComponents,
7886
8187
  getWrapperSearchRoots,
7887
- routerAwarePoms,
7888
- routerType,
7889
8188
  getResolvedRouterEntry: resolveRouterEntry2,
7890
- routerModuleShims,
7891
8189
  loggerRef
7892
8190
  });
7893
8191
  const devProcessor = createDevProcessorPlugin({
@@ -7902,23 +8200,8 @@ function createSupportPlugins(options) {
7902
8200
  projectRootRef,
7903
8201
  normalizedBasePagePath,
7904
8202
  basePageClassPath,
7905
- outDir,
7906
- emitLanguages,
7907
- typescriptOutputStructure,
7908
- csharp,
7909
- generateFixtures,
7910
- customPomAttachments,
7911
- customPomDir,
7912
- customPomImportAliases,
7913
- customPomImportNameCollisionBehavior,
7914
- nameCollisionBehavior,
7915
- missingSemanticNameBehavior,
7916
- existingIdBehavior,
7917
- testIdAttribute,
7918
- routerAwarePoms,
7919
- routerType,
8203
+ generation,
7920
8204
  getResolvedRouterEntry: resolveRouterEntry2,
7921
- routerModuleShims,
7922
8205
  loggerRef
7923
8206
  });
7924
8207
  const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
@@ -8019,6 +8302,13 @@ function tryCreateElementMetadata(args) {
8019
8302
  };
8020
8303
  return metadata;
8021
8304
  }
8305
+ function resolveCompilerSfcParse(compilerSfc) {
8306
+ const parse2 = compilerSfc.parse ?? compilerSfc.default?.parse;
8307
+ if (typeof parse2 !== "function") {
8308
+ throw new TypeError("[vue-pom-generator] Failed to resolve @vue/compiler-sfc.parse.");
8309
+ }
8310
+ return parse2;
8311
+ }
8022
8312
  function extractMetadataAfterTransform(ast, componentName, elementMetadata, semanticNameMap, testIdAttribute) {
8023
8313
  const componentMetadata = /* @__PURE__ */ new Map();
8024
8314
  function traverseNode(node) {
@@ -8215,7 +8505,8 @@ function createVuePluginWithTestIds(options) {
8215
8505
  }
8216
8506
  const componentName = getComponentNameFromPath(cleanPath);
8217
8507
  loggerRef.current.debug(`Collecting metadata for ${cleanPath} (component: ${componentName})`);
8218
- const { parse: parse2 } = await import("@vue/compiler-sfc");
8508
+ const compilerSfc = await import("@vue/compiler-sfc");
8509
+ const parse2 = resolveCompilerSfcParse(compilerSfc);
8219
8510
  const compilerDom2 = await import("@vue/compiler-dom");
8220
8511
  const compile = compilerDom2.compile;
8221
8512
  const { descriptor } = parse2(code, { filename: cleanPath });
@@ -8247,8 +8538,7 @@ function createVuePluginWithTestIds(options) {
8247
8538
  });
8248
8539
  const api = viteVuePlugin?.api;
8249
8540
  if (!api) {
8250
- loggerRef.current.warn("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8251
- return;
8541
+ throw new Error("[vue-pom-generator] Nuxt bridge could not find vite:vue plugin to patch.");
8252
8542
  }
8253
8543
  const currentOptions = api.options ?? {};
8254
8544
  const currentTemplate = currentOptions.template ?? {};
@@ -8314,33 +8604,6 @@ function assertOneOf(value, allowed, name) {
8314
8604
  }
8315
8605
  throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
8316
8606
  }
8317
- function assertErrorBehavior(value, name) {
8318
- if (!value) {
8319
- return;
8320
- }
8321
- if (value === "ignore" || value === "error") {
8322
- return;
8323
- }
8324
- if (typeof value !== "object" || Array.isArray(value)) {
8325
- throw new TypeError(`${name} must be "ignore", "error", or an object.`);
8326
- }
8327
- const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
8328
- for (const key of Object.keys(value)) {
8329
- if (!supportedKeys.has(key)) {
8330
- throw new TypeError(`${name} contains unsupported key "${key}".`);
8331
- }
8332
- }
8333
- assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
8334
- }
8335
- function resolveMissingSemanticNameBehavior(value) {
8336
- if (!value) {
8337
- return "error";
8338
- }
8339
- if (value === "ignore" || value === "error") {
8340
- return value;
8341
- }
8342
- return value.missingSemanticNameBehavior ?? "error";
8343
- }
8344
8607
  function readPackageJson(projectRoot) {
8345
8608
  const packageJsonPath = path.join(projectRoot, "package.json");
8346
8609
  if (!fs.existsSync(packageJsonPath)) {
@@ -8449,7 +8712,7 @@ function applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompile
8449
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(...).'
8450
8713
  );
8451
8714
  }
8452
- 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.");
8453
8716
  }
8454
8717
  const currentOptions = viteVuePlugin.api.options ?? {};
8455
8718
  const currentTemplate = currentOptions.template ?? {};
@@ -8506,18 +8769,23 @@ function createVuePomGeneratorPlugins(options = {}) {
8506
8769
  const vueGenerationOptions = generationOptions;
8507
8770
  const verbosity = options.logging?.verbosity ?? "warn";
8508
8771
  const vueOptions = options.vueOptions;
8509
- const legacyVueOptions = options;
8510
- const pageDirsRef = { current: !isNuxt ? [legacyVueOptions.injection?.viewsDir ?? "src/views"] : ["app/pages"] };
8511
- const componentDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.componentDirs ?? ["src/components"] : ["app/components"] };
8512
- const layoutDirsRef = { current: !isNuxt ? legacyVueOptions.injection?.layoutDirs ?? ["src/layouts"] : ["app/layouts"] };
8513
- const wrapperSearchRootsRef = { current: !isNuxt ? legacyVueOptions.injection?.wrapperSearchRoots ?? [] : [] };
8514
- const nativeWrappers = injection.nativeWrappers ?? {};
8515
- const excludedComponents = injection.excludeComponents ?? [];
8516
- const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
8517
- const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
8518
- const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
8519
- const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
8520
- 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;
8521
8789
  const routerEntry = !isNuxt ? vueGenerationOptions?.router?.entry : void 0;
8522
8790
  const routerType = isNuxt ? "nuxt" : vueGenerationOptions?.router?.type ?? "vue-router";
8523
8791
  const routerModuleShims = !isNuxt ? vueGenerationOptions?.router?.moduleShims : void 0;
@@ -8526,27 +8794,41 @@ function createVuePomGeneratorPlugins(options = {}) {
8526
8794
  }
8527
8795
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
8528
8796
  const usesExternalVuePlugin = vuePluginOwnership === "external";
8529
- const csharp = generationOptions?.csharp;
8530
- const errorBehavior = options.errorBehavior;
8531
- const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
8532
- const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
8533
8797
  const generateFixtures = generationOptions?.playwright?.fixtures;
8534
8798
  const customPoms = generationOptions?.playwright?.customPoms;
8535
8799
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
8536
- const resolvedCustomPomDir = customPoms?.dir ?? "tests/playwright/pom/custom";
8537
8800
  const resolvedCustomPomImportAliases = customPoms?.importAliases;
8538
- 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
+ });
8539
8821
  const basePageClassPathOverride = generationOptions?.basePageClassPath;
8540
- const getPageDirs = () => pageDirsRef.current;
8822
+ const getPageDirs = () => resolvedInjectionOptionsRef.current.pageDirs;
8541
8823
  const getViewsDir = () => getPageDirs()[0] ?? "src/views";
8542
- const getComponentDirs = () => componentDirsRef.current;
8543
- const getLayoutDirs = () => layoutDirsRef.current;
8824
+ const getComponentDirs = () => resolvedInjectionOptionsRef.current.componentDirs;
8825
+ const getLayoutDirs = () => resolvedInjectionOptionsRef.current.layoutDirs;
8544
8826
  const getSourceDirs = () => Array.from(/* @__PURE__ */ new Set([
8545
8827
  ...getPageDirs(),
8546
8828
  ...getComponentDirs(),
8547
8829
  ...getLayoutDirs()
8548
8830
  ]));
8549
- const getWrapperSearchRoots = () => wrapperSearchRootsRef.current;
8831
+ const getWrapperSearchRoots = () => resolvedInjectionOptionsRef.current.wrapperSearchRoots;
8550
8832
  const sharedStateKey = JSON.stringify({
8551
8833
  cwd: process.cwd(),
8552
8834
  mode: isNuxt ? "nuxt" : "vue",
@@ -8554,9 +8836,9 @@ function createVuePomGeneratorPlugins(options = {}) {
8554
8836
  componentDirs: isNuxt ? null : getComponentDirs(),
8555
8837
  layoutDirs: isNuxt ? null : getLayoutDirs(),
8556
8838
  wrapperSearchRoots: isNuxt ? null : getWrapperSearchRoots(),
8557
- outDir,
8839
+ outDir: resolvedGenerationOptions.outDir,
8558
8840
  testIdAttribute,
8559
- routerType,
8841
+ routerType: resolvedGenerationOptions.routerType,
8560
8842
  vuePluginOwnership
8561
8843
  });
8562
8844
  const sharedState = getSharedGeneratorState(sharedStateKey);
@@ -8577,23 +8859,22 @@ function createVuePomGeneratorPlugins(options = {}) {
8577
8859
  if (isNuxt) {
8578
8860
  const nuxtDiscovery = await loadNuxtProjectDiscovery(process.cwd());
8579
8861
  projectRootRef.current = nuxtDiscovery.rootDir;
8580
- pageDirsRef.current = nuxtDiscovery.pageDirs.length ? nuxtDiscovery.pageDirs : [path.resolve(nuxtDiscovery.srcDir, "pages")];
8581
- componentDirsRef.current = nuxtDiscovery.componentDirs;
8582
- layoutDirsRef.current = nuxtDiscovery.layoutDirs;
8583
- wrapperSearchRootsRef.current = nuxtDiscovery.wrapperSearchRoots;
8862
+ resolvedInjectionOptionsRef.current = applyNuxtDiscoveryToInjectionOptions(
8863
+ resolvedInjectionOptionsRef.current,
8864
+ nuxtDiscovery
8865
+ );
8584
8866
  }
8585
8867
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
8586
8868
  assertNonEmptyString(getViewsDir(), "[vue-pom-generator] injection.viewsDir");
8587
8869
  assertNonEmptyStringArray(getComponentDirs(), "[vue-pom-generator] injection.componentDirs");
8588
8870
  assertNonEmptyStringArray(getLayoutDirs(), "[vue-pom-generator] injection.layoutDirs");
8589
8871
  assertNonEmptyStringArray(getWrapperSearchRoots(), "[vue-pom-generator] injection.wrapperSearchRoots");
8590
- assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
8591
8872
  if (generationEnabled) {
8592
- assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
8593
- assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
8594
- assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
8595
- if (!isNuxt && vueGenerationOptions?.router && routerType === "vue-router") {
8596
- 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");
8597
8878
  }
8598
8879
  }
8599
8880
  if (usesExternalVuePlugin) {
@@ -8615,8 +8896,8 @@ function createVuePomGeneratorPlugins(options = {}) {
8615
8896
  const { componentTestIds, elementMetadata, semanticNameMap, componentHierarchyMap, vueFilesPathMap } = sharedState;
8616
8897
  const { metadataCollectorPlugin, internalVuePlugin, templateCompilerOptions } = createVuePluginWithTestIds({
8617
8898
  vueOptions,
8618
- existingIdBehavior,
8619
- nameCollisionBehavior,
8899
+ existingIdBehavior: resolvedGenerationOptions.existingIdBehavior,
8900
+ nameCollisionBehavior: resolvedGenerationOptions.nameCollisionBehavior,
8620
8901
  nativeWrappers,
8621
8902
  elementMetadata,
8622
8903
  semanticNameMap,
@@ -8631,7 +8912,6 @@ function createVuePomGeneratorPlugins(options = {}) {
8631
8912
  getProjectRoot: () => projectRootRef.current
8632
8913
  });
8633
8914
  templateCompilerOptionsForResolvedPlugin = templateCompilerOptions;
8634
- const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
8635
8915
  const supportPlugins = createSupportPlugins({
8636
8916
  componentTestIds,
8637
8917
  componentHierarchyMap,
@@ -8644,26 +8924,10 @@ function createVuePomGeneratorPlugins(options = {}) {
8644
8924
  getViewsDir,
8645
8925
  getSourceDirs,
8646
8926
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
8647
- nameCollisionBehavior,
8648
- missingSemanticNameBehavior,
8649
- existingIdBehavior,
8650
- outDir,
8651
- emitLanguages,
8652
- typescriptOutputStructure,
8653
- csharp,
8654
- routerAwarePoms,
8655
- routerEntry,
8656
- generateFixtures,
8927
+ generation: resolvedGenerationOptions,
8657
8928
  projectRootRef,
8658
8929
  basePageClassPath: basePageClassPathOverride,
8659
- customPomAttachments: resolvedCustomPomAttachments,
8660
- customPomDir: resolvedCustomPomDir,
8661
- customPomImportAliases: resolvedCustomPomImportAliases,
8662
- customPomImportNameCollisionBehavior: resolvedCustomPomImportCollisionBehavior,
8663
- testIdAttribute,
8664
- loggerRef,
8665
- routerType,
8666
- routerModuleShims
8930
+ loggerRef
8667
8931
  });
8668
8932
  if (isNuxt) {
8669
8933
  loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");