@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
@@ -9,6 +9,25 @@ import path from "node:path";
9
9
  import process from "node:process";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { generateViewObjectModelMembers, generateViewObjectModelMethodContent } from "../method-generation";
12
+ import {
13
+ createPomMethodSignature,
14
+ createPomParameterSpec,
15
+ getPomParameterArgumentNames,
16
+ normalizePomParameters,
17
+ toTypeScriptPomParameterStructures,
18
+ type PomMethodSignature,
19
+ type PomParameterSpec,
20
+ } from "../pom-params";
21
+ import {
22
+ bindCSharpPomPattern,
23
+ bindTypeScriptPomPattern,
24
+ isParameterizedPomPattern,
25
+ orderPomPatternParameters,
26
+ toCSharpPomPatternExpression,
27
+ toTypeScriptPomPatternExpression,
28
+ uniquePomStringPatterns,
29
+ type PomStringPattern,
30
+ } from "../pom-patterns";
12
31
  import { introspectNuxtPages, parseRouterFileFromCwd } from "../router-introspection";
13
32
  import {
14
33
  addExportAll,
@@ -25,7 +44,6 @@ import {
25
44
  type GetAccessorDeclarationStructure,
26
45
  type MethodDeclarationStructure,
27
46
  type OptionalKind,
28
- type ParameterDeclarationStructure,
29
47
  type PropertyDeclarationStructure,
30
48
  type TypeScriptClassMember,
31
49
  type TypeScriptSourceFile,
@@ -35,6 +53,7 @@ import {
35
53
  IDataTestId,
36
54
  PomExtraClickMethodSpec,
37
55
  PomPrimarySpec,
56
+ PomSelectorSpec,
38
57
  toPascalCase,
39
58
  upperFirst,
40
59
  } from "../utils";
@@ -61,128 +80,6 @@ class VuePomGeneratorError extends Error {
61
80
  }
62
81
  }
63
82
 
64
- function splitParameterList(parameters: string): string[] {
65
- const parts: string[] = [];
66
- let current = "";
67
- let braceDepth = 0;
68
- let bracketDepth = 0;
69
- let parenDepth = 0;
70
- let angleDepth = 0;
71
- let inSingleQuote = false;
72
- let inDoubleQuote = false;
73
- let inTemplateString = false;
74
-
75
- for (let index = 0; index < parameters.length; index += 1) {
76
- const char = parameters[index];
77
- const previous = index > 0 ? parameters[index - 1] : "";
78
-
79
- if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
80
- inSingleQuote = !inSingleQuote;
81
- current += char;
82
- continue;
83
- }
84
- if (char === "\"" && !inSingleQuote && !inTemplateString && previous !== "\\") {
85
- inDoubleQuote = !inDoubleQuote;
86
- current += char;
87
- continue;
88
- }
89
- if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
90
- inTemplateString = !inTemplateString;
91
- current += char;
92
- continue;
93
- }
94
-
95
- if (inSingleQuote || inDoubleQuote || inTemplateString) {
96
- current += char;
97
- continue;
98
- }
99
-
100
- switch (char) {
101
- case "{":
102
- braceDepth += 1;
103
- break;
104
- case "}":
105
- braceDepth -= 1;
106
- break;
107
- case "[":
108
- bracketDepth += 1;
109
- break;
110
- case "]":
111
- bracketDepth -= 1;
112
- break;
113
- case "(":
114
- parenDepth += 1;
115
- break;
116
- case ")":
117
- parenDepth -= 1;
118
- break;
119
- case "<":
120
- angleDepth += 1;
121
- break;
122
- case ">":
123
- angleDepth -= 1;
124
- break;
125
- case ",":
126
- if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
127
- const trimmed = current.trim();
128
- if (trimmed) {
129
- parts.push(trimmed);
130
- }
131
- current = "";
132
- continue;
133
- }
134
- break;
135
- default:
136
- break;
137
- }
138
-
139
- current += char;
140
- }
141
-
142
- const trimmed = current.trim();
143
- if (trimmed) {
144
- parts.push(trimmed);
145
- }
146
-
147
- return parts;
148
- }
149
-
150
- function parseParameterSignature(parameter: string): OptionalKind<ParameterDeclarationStructure> {
151
- const colonIndex = parameter.indexOf(":");
152
- if (colonIndex < 0) {
153
- return { name: parameter.trim() };
154
- }
155
-
156
- const rawName = parameter.slice(0, colonIndex).trim();
157
- const hasQuestionToken = rawName.endsWith("?");
158
- const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
159
- const remainder = parameter.slice(colonIndex + 1).trim();
160
- const initializerIndex = remainder.lastIndexOf("=");
161
-
162
- if (initializerIndex < 0) {
163
- return {
164
- name,
165
- hasQuestionToken,
166
- type: remainder || undefined,
167
- };
168
- }
169
-
170
- return {
171
- name,
172
- hasQuestionToken,
173
- type: remainder.slice(0, initializerIndex).trim() || undefined,
174
- initializer: remainder.slice(initializerIndex + 1).trim() || undefined,
175
- };
176
- }
177
-
178
- function parseParameterSignatures(parameters: string): OptionalKind<ParameterDeclarationStructure>[] {
179
- const trimmed = parameters.trim();
180
- if (!trimmed) {
181
- return [];
182
- }
183
- return splitParameterList(trimmed).map(parseParameterSignature);
184
- }
185
-
186
83
  function toPosixRelativePath(fromDir: string, toFile: string): string {
187
84
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
188
85
  if (!rel.startsWith(".")) {
@@ -214,12 +111,7 @@ interface RouteMeta {
214
111
  template: string;
215
112
  }
216
113
 
217
- interface CustomPomMethodSignature {
218
- params: string;
219
- argNames: string[];
220
- }
221
-
222
- type CustomPomMethodSignatureMap = Map<string, CustomPomMethodSignature>;
114
+ type CustomPomMethodSignatureMap = Map<string, PomMethodSignature>;
223
115
 
224
116
  interface CustomPomAttachment {
225
117
  className: string;
@@ -251,6 +143,27 @@ interface CustomPomImportResolution {
251
143
  importSpecifiersByClass: Record<string, ResolvedCustomPomImportSpecifier>;
252
144
  }
253
145
 
146
+ function createMissingCustomPomDirectoryError(configuredDir: string, resolvedDir: string): VuePomGeneratorError {
147
+ return new VuePomGeneratorError(
148
+ `Custom POM directory "${configuredDir}" does not exist.\n`
149
+ + `Resolved path: ${resolvedDir}\n`
150
+ + "Create the directory, point generation.playwright.customPoms.dir at the correct location, "
151
+ + "or remove the customPoms configuration.",
152
+ );
153
+ }
154
+
155
+ function createMissingCustomPomAttachmentClassError(
156
+ missingClassNames: string[],
157
+ configuredDir: string,
158
+ ): VuePomGeneratorError {
159
+ const renderedClassNames = missingClassNames.map(name => `"${name}"`).join(", ");
160
+ return new VuePomGeneratorError(
161
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.\n`
162
+ + `Expected matching helper files/exports under "${configuredDir}".\n`
163
+ + "Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.",
164
+ );
165
+ }
166
+
254
167
  function createCustomPomImportCollisionError(exportName: string, requested: string): VuePomGeneratorError {
255
168
  return new VuePomGeneratorError(
256
169
  `Custom POM import name collision detected for "${exportName}".\n`
@@ -429,27 +342,10 @@ function generateGoToSelfMethod(componentName: string): TypeScriptClassMember[]
429
342
  ];
430
343
  }
431
344
 
432
- function formatMethodParams(params: Record<string, string> | undefined): string {
433
- if (!params)
434
- return "";
435
-
436
- // Keep output stable and somewhat intuitive.
437
- const preferredOrder = ["key", "value", "text", "timeOut", "annotationText", "wait"];
438
-
439
- const entries = Object.entries(params);
440
- if (!entries.length)
441
- return "";
442
-
443
- const score = (name: string) => {
444
- const idx = preferredOrder.indexOf(name);
445
- return idx < 0 ? 999 : idx;
446
- };
447
-
448
- return entries
449
- .slice()
450
- .sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0]))
451
- .map(([name, typeExpr]) => `${name}: ${typeExpr}`)
452
- .join(", ");
345
+ function getSelectorPatterns(selector: PomSelectorSpec): PomStringPattern[] {
346
+ return selector.kind === "testId"
347
+ ? [selector.testId]
348
+ : [selector.rootTestId, selector.label];
453
349
  }
454
350
 
455
351
  function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScriptClassMember[] {
@@ -457,27 +353,24 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
457
353
  return [];
458
354
  }
459
355
 
460
- const params = spec.params ?? {};
461
- const signatureParams = formatMethodParams(params);
462
- const parameters = parseParameterSignatures(signatureParams);
356
+ const selectorPatterns = getSelectorPatterns(spec.selector);
357
+ const signatureSpecs = orderPomPatternParameters(
358
+ spec.parameters,
359
+ selectorPatterns,
360
+ { omit: spec.keyLiteral !== undefined ? ["key"] : [] },
361
+ );
362
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
463
363
 
464
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
465
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
364
+ const hasAnnotationText = signatureSpecs.some(param => param.name === "annotationText");
365
+ const hasWait = signatureSpecs.some(param => param.name === "wait");
466
366
  const annotationArg = hasAnnotationText ? "annotationText" : "\"\"";
467
367
  const waitArg = hasWait ? "wait" : "true";
468
368
 
469
369
  if (spec.selector.kind === "testId") {
470
- const needsTemplate = spec.selector.formattedDataTestId.includes("${");
471
- const testIdExpr = needsTemplate
472
- ? `\`${spec.selector.formattedDataTestId}\``
473
- : JSON.stringify(spec.selector.formattedDataTestId);
474
-
475
- if (needsTemplate) {
476
- // handled below
477
- }
370
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
478
371
 
479
372
  const clickArgs: string[] = [];
480
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
373
+ clickArgs.push(testIdBinding.expression);
481
374
 
482
375
  if (hasAnnotationText || hasWait) {
483
376
  clickArgs.push(annotationArg);
@@ -495,8 +388,8 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
495
388
  if (spec.keyLiteral !== undefined) {
496
389
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
497
390
  }
498
- if (needsTemplate) {
499
- writer.writeLine(`const testId = ${testIdExpr};`);
391
+ for (const statement of testIdBinding.setupStatements) {
392
+ writer.writeLine(statement);
500
393
  }
501
394
  writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
502
395
  },
@@ -504,17 +397,8 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
504
397
  ];
505
398
  }
506
399
 
507
- const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
508
- const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
509
- const rootExpr = rootNeedsTemplate
510
- ? `\`${spec.selector.rootFormattedDataTestId}\``
511
- : JSON.stringify(spec.selector.rootFormattedDataTestId);
512
- const labelExpr = labelNeedsTemplate
513
- ? `\`${spec.selector.formattedLabel}\``
514
- : JSON.stringify(spec.selector.formattedLabel);
515
-
516
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
517
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
400
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
401
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
518
402
  return [
519
403
  createClassMethod({
520
404
  name: spec.name,
@@ -524,13 +408,13 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
524
408
  if (spec.keyLiteral !== undefined) {
525
409
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
526
410
  }
527
- if (rootNeedsTemplate) {
528
- writer.writeLine(`const rootTestId = ${rootExpr};`);
411
+ for (const statement of rootBinding.setupStatements) {
412
+ writer.writeLine(statement);
529
413
  }
530
- if (labelNeedsTemplate) {
531
- writer.writeLine(`const label = ${labelExpr};`);
414
+ for (const statement of labelBinding.setupStatements) {
415
+ writer.writeLine(statement);
532
416
  }
533
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
417
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg});`);
534
418
  },
535
419
  }),
536
420
  ];
@@ -545,10 +429,10 @@ function generateMethodMembersFromPom(primary: PomPrimarySpec, targetPageObjectM
545
429
  targetPageObjectModelClass,
546
430
  primary.methodName,
547
431
  primary.nativeRole,
548
- primary.formattedDataTestId,
549
- primary.alternateFormattedDataTestIds,
432
+ primary.selector,
433
+ primary.alternateSelectors,
550
434
  primary.getterNameOverride,
551
- primary.params ?? {},
435
+ primary.parameters,
552
436
  );
553
437
  }
554
438
 
@@ -565,17 +449,16 @@ function generateMethodsContentForDependencies(dependencies: IComponentDependenc
565
449
  // When we emit from IR, we must de-dupe here to avoid duplicate getters/methods.
566
450
  const seenPrimaryKeys = new Set<string>();
567
451
  const primarySpecs = primarySpecsAll.filter(({ pom, target }) => {
568
- const stableParams = pom.params
569
- ? Object.fromEntries(Object.entries(pom.params).sort((a, b) => a[0].localeCompare(b[0])))
570
- : undefined;
571
- const alternates = (pom.alternateFormattedDataTestIds ?? []).slice().sort();
452
+ const alternates = (pom.alternateSelectors ?? [])
453
+ .slice()
454
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
572
455
  const key = JSON.stringify({
573
456
  role: pom.nativeRole,
574
457
  methodName: pom.methodName,
575
458
  getterNameOverride: pom.getterNameOverride ?? null,
576
- testId: pom.formattedDataTestId,
577
- alternateTestIds: alternates.length ? alternates : undefined,
578
- params: stableParams,
459
+ selector: pom.selector,
460
+ alternateSelectors: alternates.length ? alternates : undefined,
461
+ parameters: pom.parameters,
579
462
  target: target ?? null,
580
463
  emitPrimary: pom.emitPrimary ?? true,
581
464
  });
@@ -639,6 +522,8 @@ export interface GenerateFilesOptions {
639
522
  * Defaults to <projectRoot>/tests/playwright/pom/custom.
640
523
  */
641
524
  customPomDir?: string;
525
+ /** When true, fail generation if the configured customPomDir does not exist. */
526
+ requireCustomPomDir?: boolean;
642
527
 
643
528
  /**
644
529
  * Optional import aliases for handwritten POM helpers.
@@ -708,6 +593,7 @@ interface BaseGenerateContentOptions {
708
593
 
709
594
  projectRoot?: string;
710
595
  customPomDir?: string;
596
+ requireCustomPomDir?: boolean;
711
597
  customPomImportAliases?: Record<string, string>;
712
598
  customPomClassIdentifierMap?: Record<string, string>;
713
599
  customPomAvailableClassIdentifiers?: Set<string>;
@@ -747,6 +633,7 @@ export async function generateFiles(
747
633
  customPomAttachments = [],
748
634
  projectRoot,
749
635
  customPomDir,
636
+ requireCustomPomDir,
750
637
  customPomImportAliases,
751
638
  customPomImportNameCollisionBehavior = "error",
752
639
  testIdAttribute,
@@ -789,6 +676,7 @@ export async function generateFiles(
789
676
  customPomAttachments,
790
677
  projectRoot,
791
678
  customPomDir,
679
+ requireCustomPomDir,
792
680
  customPomImportAliases,
793
681
  customPomImportNameCollisionBehavior,
794
682
  testIdAttribute,
@@ -799,6 +687,7 @@ export async function generateFiles(
799
687
  customPomAttachments,
800
688
  projectRoot,
801
689
  customPomDir,
690
+ requireCustomPomDir,
802
691
  customPomImportAliases,
803
692
  customPomImportNameCollisionBehavior,
804
693
  testIdAttribute,
@@ -847,6 +736,7 @@ async function generateSplitTypeScriptFiles(
847
736
  customPomAttachments?: GenerateFilesOptions["customPomAttachments"];
848
737
  projectRoot?: GenerateFilesOptions["projectRoot"];
849
738
  customPomDir?: GenerateFilesOptions["customPomDir"];
739
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
850
740
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
851
741
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
852
742
  testIdAttribute?: GenerateFilesOptions["testIdAttribute"];
@@ -882,9 +772,15 @@ async function generateSplitTypeScriptFiles(
882
772
 
883
773
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
884
774
  customPomDir: options.customPomDir,
775
+ requireCustomPomDir: options.requireCustomPomDir,
885
776
  customPomImportAliases: options.customPomImportAliases,
886
777
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior,
887
778
  });
779
+ assertCustomPomAttachmentsResolved(
780
+ options.customPomAttachments ?? [],
781
+ customPomImportResolution.classIdentifierMap,
782
+ options.customPomDir ?? "tests/playwright/pom/custom",
783
+ );
888
784
 
889
785
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
890
786
  const files: GeneratedFileOutput[] = [];
@@ -1071,30 +967,9 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths: string[]): Generat
1071
967
  });
1072
968
  }
1073
969
 
1074
- function toCSharpTestIdExpression(formattedDataTestId: string): string {
1075
- // Convert our `${var}` placeholder format into C# interpolated-string `{var}`.
1076
- const needsInterpolation = formattedDataTestId.includes("${");
1077
- if (!needsInterpolation) {
1078
- return JSON.stringify(formattedDataTestId);
1079
- }
1080
-
1081
- const inner = formattedDataTestId.replace(/\$\{/g, "{");
1082
- // Use verbatim JSON escaping for quotes/backslashes, then adapt to C# string literal.
1083
- // JSON.stringify gives us a JS string literal with escapes, which is close enough for a C# normal string.
1084
- const quoted = JSON.stringify(inner);
1085
- return `$${quoted}`;
1086
- }
1087
-
1088
- function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: string } {
1089
- const trimmed = (paramTypeExpr ?? "").trim();
1090
-
1091
- // Handle default values: "boolean = true", "string = \"\"", "timeOut = 500".
1092
- const eqIdx = trimmed.indexOf("=");
1093
- const left = eqIdx >= 0 ? trimmed.slice(0, eqIdx).trim() : trimmed;
1094
- const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : undefined;
1095
-
970
+ function toCSharpParam(param: PomParameterSpec): { type: string; defaultExpr?: string } {
1096
971
  // Collapse union types to their widest practical type.
1097
- const typePart = left.includes("|") ? "string" : left;
972
+ const typePart = param.type?.includes("|") ? "string" : (param.type ?? "string");
1098
973
 
1099
974
  let type = "string";
1100
975
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
@@ -1103,23 +978,21 @@ function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: str
1103
978
  type = "string";
1104
979
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
1105
980
  type = "int";
1106
- else if (/\d+/.test(typePart) && typePart === "")
1107
- type = "int";
1108
981
  else if (/\btimeOut\b/i.test(typePart))
1109
982
  type = "int";
1110
983
 
1111
984
  let defaultExpr: string | undefined;
1112
- if (right !== undefined) {
985
+ if (param.initializer !== undefined) {
1113
986
  if (type === "bool") {
1114
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : undefined;
987
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : undefined;
1115
988
  }
1116
989
  else if (type === "int") {
1117
- const m = right.match(/\d+/);
990
+ const m = param.initializer.match(/\d+/);
1118
991
  defaultExpr = m ? m[0] : undefined;
1119
992
  }
1120
993
  else {
1121
994
  // string defaults, keep empty string if detected.
1122
- if (right === "\"\"" || right === "\"\"" || right === "''") {
995
+ if (param.initializer === "\"\"" || param.initializer === "''") {
1123
996
  defaultExpr = "\"\"";
1124
997
  }
1125
998
  }
@@ -1128,21 +1001,18 @@ function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: str
1128
1001
  return { type, defaultExpr };
1129
1002
  }
1130
1003
 
1131
- function formatCSharpParams(params: Record<string, string> | undefined): { signature: string; argNames: string[] } {
1132
- if (!params)
1133
- return { signature: "", argNames: [] };
1134
-
1135
- const entries = Object.entries(params);
1136
- if (!entries.length)
1004
+ function formatCSharpParams(params: readonly PomParameterSpec[] | undefined): { signature: string; argNames: string[] } {
1005
+ const normalizedParams = normalizePomParameters(params);
1006
+ if (!normalizedParams.length)
1137
1007
  return { signature: "", argNames: [] };
1138
1008
 
1139
1009
  const signatureParts: string[] = [];
1140
1010
  const argNames: string[] = [];
1141
1011
 
1142
- for (const [name, typeExpr] of entries) {
1143
- const { type, defaultExpr } = toCSharpParam(typeExpr);
1144
- argNames.push(name);
1145
- signatureParts.push(defaultExpr !== undefined ? `${type} ${name} = ${defaultExpr}` : `${type} ${name}`);
1012
+ for (const param of normalizedParams) {
1013
+ const { type, defaultExpr } = toCSharpParam(param);
1014
+ argNames.push(param.name);
1015
+ signatureParts.push(defaultExpr !== undefined ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
1146
1016
  }
1147
1017
 
1148
1018
  return { signature: signatureParts.join(", "), argNames };
@@ -1253,32 +1123,16 @@ function generateAggregatedCSharpFiles(
1253
1123
  const baseMethodName = upperFirst(pom.methodName);
1254
1124
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
1255
1125
  const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
1256
- const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
1257
-
1258
- // Ensure all template variables referenced in formattedDataTestId (e.g. `${key}`)
1259
- // appear in the C# method signature. utils.ts may omit `key` for input/select
1260
- // elements even when the test ID is dynamic, causing CS0103 compile errors.
1261
- const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
1262
- const templateVars = templateVarMatches.map(m => m[1]);
1263
- const augmentedParams: Record<string, string> = { ...pom.params };
1264
- for (const v of templateVars) {
1265
- if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
1266
- augmentedParams[v] = "string";
1267
- }
1268
- }
1269
- // Place template vars first so they precede text/value/annotationText.
1270
- const orderedParams: Record<string, string> = Object.fromEntries([
1271
- ...templateVars.map(v => [v, augmentedParams[v]] as [string, string]),
1272
- ...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k)),
1273
- ]);
1126
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
1127
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
1128
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
1274
1129
 
1275
1130
  const { signature, argNames } = formatCSharpParams(orderedParams);
1276
1131
  const args = argNames.join(", ");
1277
1132
 
1278
- const allTestIds = [pom.formattedDataTestId, ...(pom.alternateFormattedDataTestIds ?? [])]
1279
- .filter((v, idx, arr) => v && arr.indexOf(v) === idx);
1133
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
1280
1134
 
1281
- if (pom.formattedDataTestId.includes("${")) {
1135
+ if (selectorIsParameterized) {
1282
1136
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
1283
1137
  }
1284
1138
  else {
@@ -1300,13 +1154,13 @@ function generateAggregatedCSharpFiles(
1300
1154
  if (target) {
1301
1155
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
1302
1156
  chunks.push(" {");
1303
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
1304
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
1157
+ if (selectorIsParameterized || allTestIds.length <= 1) {
1158
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
1305
1159
  chunks.push(` return new ${target}(Page);`);
1306
1160
  }
1307
1161
  else {
1308
1162
  chunks.push(" Exception? lastError = null;");
1309
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
1163
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map(testId => toCSharpPomPatternExpression(testId)).join(", ")} })`);
1310
1164
  chunks.push(" {");
1311
1165
  chunks.push(" try");
1312
1166
  chunks.push(" {");
@@ -1332,7 +1186,7 @@ function generateAggregatedCSharpFiles(
1332
1186
  chunks.push(` public async Task ${actionName}(${sig})`);
1333
1187
  chunks.push(" {");
1334
1188
 
1335
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
1189
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
1336
1190
 
1337
1191
  const emitActionCall = (locatorAccess: string) => {
1338
1192
  if (pom.nativeRole === "input") {
@@ -1351,9 +1205,9 @@ function generateAggregatedCSharpFiles(
1351
1205
  }
1352
1206
  };
1353
1207
 
1354
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
1208
+ if (!selectorIsParameterized && allTestIds.length > 1) {
1355
1209
  chunks.push(" Exception? lastError = null;");
1356
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
1210
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map(testId => toCSharpPomPatternExpression(testId)).join(", ")} })`);
1357
1211
  chunks.push(" {");
1358
1212
  chunks.push(" try");
1359
1213
  chunks.push(" {");
@@ -1406,7 +1260,12 @@ function generateAggregatedCSharpFiles(
1406
1260
  for (const extra of extras) {
1407
1261
  if (extra.kind !== "click")
1408
1262
  continue;
1409
- const { signature } = formatCSharpParams(extra.params);
1263
+ const extraParams = orderPomPatternParameters(
1264
+ extra.parameters,
1265
+ getSelectorPatterns(extra.selector),
1266
+ { omit: extra.keyLiteral !== undefined ? ["key"] : [] },
1267
+ );
1268
+ const { signature } = formatCSharpParams(extraParams);
1410
1269
 
1411
1270
  const extraName = upperFirst(extra.name);
1412
1271
 
@@ -1417,33 +1276,24 @@ function generateAggregatedCSharpFiles(
1417
1276
  }
1418
1277
 
1419
1278
  if (extra.selector.kind === "testId") {
1420
- const needsTemplate = extra.selector.formattedDataTestId.includes("${");
1421
- const testIdExpr = toCSharpTestIdExpression(extra.selector.formattedDataTestId);
1422
- if (needsTemplate) {
1423
- chunks.push(` var testId = ${testIdExpr};`);
1424
- chunks.push(" await LocatorByTestId(testId).ClickAsync();");
1425
- }
1426
- else {
1427
- chunks.push(` await LocatorByTestId(${testIdExpr}).ClickAsync();`);
1279
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
1280
+ for (const statement of testIdBinding.setupStatements) {
1281
+ chunks.push(` ${statement}`);
1428
1282
  }
1283
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
1429
1284
  }
1430
1285
  else {
1431
- const rootNeedsTemplate = extra.selector.rootFormattedDataTestId.includes("${");
1432
- const labelNeedsTemplate = extra.selector.formattedLabel.includes("${");
1433
- const rootExpr = toCSharpTestIdExpression(extra.selector.rootFormattedDataTestId);
1434
- const labelExpr = toCSharpTestIdExpression(extra.selector.formattedLabel);
1286
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
1287
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
1435
1288
  const exactArg = extra.selector.exact === false ? "false" : "true";
1436
1289
 
1437
- if (rootNeedsTemplate) {
1438
- chunks.push(` var rootTestId = ${rootExpr};`);
1290
+ for (const statement of rootBinding.setupStatements) {
1291
+ chunks.push(` ${statement}`);
1439
1292
  }
1440
- if (labelNeedsTemplate) {
1441
- chunks.push(` var label = ${labelExpr};`);
1293
+ for (const statement of labelBinding.setupStatements) {
1294
+ chunks.push(` ${statement}`);
1442
1295
  }
1443
-
1444
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
1445
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
1446
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
1296
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
1447
1297
  }
1448
1298
  chunks.push(" }");
1449
1299
  chunks.push("");
@@ -1789,8 +1639,8 @@ function prepareViewObjectModelClass(
1789
1639
  propertyName: a.propertyName,
1790
1640
  flatten: a.flatten ?? false,
1791
1641
  methodSignatures: a.flatten
1792
- ? (customPomMethodSignaturesByClass.get(a.className) ?? new Map<string, CustomPomMethodSignature>())
1793
- : new Map<string, CustomPomMethodSignature>(),
1642
+ ? (customPomMethodSignaturesByClass.get(a.className) ?? new Map<string, PomMethodSignature>())
1643
+ : new Map<string, PomMethodSignature>(),
1794
1644
  }));
1795
1645
 
1796
1646
  const widgetInstances = isView
@@ -1997,10 +1847,10 @@ function getViewPassthroughMethods(
1997
1847
  componentHierarchyMap: Map<string, IComponentDependencies>,
1998
1848
  blockedMethodNames: Set<string> = new Set(),
1999
1849
  ) {
2000
- const existingOnView = viewDependencies.generatedMethods ?? new Map<string, { params: string; argNames: string[] } | null>();
1850
+ const existingOnView = viewDependencies.generatedMethods ?? new Map<string, PomMethodSignature | null>();
2001
1851
 
2002
1852
  // methodName -> candidates
2003
- const methodToChildren = new Map<string, Array<{ childProp: string; params: string; argNames: string[] }>>();
1853
+ const methodToChildren = new Map<string, Array<{ childProp: string; signature: PomMethodSignature }>>();
2004
1854
 
2005
1855
  for (const child of childrenComponentSet) {
2006
1856
  const childDeps = componentHierarchyMap.get(child);
@@ -2023,7 +1873,7 @@ function getViewPassthroughMethods(
2023
1873
  continue;
2024
1874
 
2025
1875
  const list = methodToChildren.get(name) ?? [];
2026
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
1876
+ list.push({ childProp, signature: sig });
2027
1877
  methodToChildren.set(name, list);
2028
1878
  }
2029
1879
  }
@@ -2035,12 +1885,12 @@ function getViewPassthroughMethods(
2035
1885
  }
2036
1886
 
2037
1887
  return passthroughs.map(([methodName, candidates]) => {
2038
- const { childProp, params, argNames } = candidates[0];
2039
- const callArgs = argNames.join(", ");
1888
+ const { childProp, signature } = candidates[0];
1889
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2040
1890
  return createClassMethod({
2041
1891
  name: methodName,
2042
1892
  isAsync: true,
2043
- parameters: parseParameterSignatures(params),
1893
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2044
1894
  statements: [
2045
1895
  `return await this.${childProp}.${methodName}(${callArgs});`,
2046
1896
  ],
@@ -2058,8 +1908,8 @@ function getAttachmentPassthroughMethods(
2058
1908
  return [];
2059
1909
  }
2060
1910
 
2061
- const existingOnClass = ownerDependencies.generatedMethods ?? new Map<string, { params: string; argNames: string[] } | null>();
2062
- const methodToAttachments = new Map<string, Array<{ propertyName: string; params: string; argNames: string[] }>>();
1911
+ const existingOnClass = ownerDependencies.generatedMethods ?? new Map<string, PomMethodSignature | null>();
1912
+ const methodToAttachments = new Map<string, Array<{ propertyName: string; signature: PomMethodSignature }>>();
2063
1913
 
2064
1914
  for (const attachment of attachmentsForThisClass) {
2065
1915
  if (!attachment.flatten) {
@@ -2074,8 +1924,7 @@ function getAttachmentPassthroughMethods(
2074
1924
  const list = methodToAttachments.get(methodName) ?? [];
2075
1925
  list.push({
2076
1926
  propertyName: attachment.propertyName,
2077
- params: signature.params,
2078
- argNames: signature.argNames,
1927
+ signature,
2079
1928
  });
2080
1929
  methodToAttachments.set(methodName, list);
2081
1930
  }
@@ -2088,14 +1937,14 @@ function getAttachmentPassthroughMethods(
2088
1937
  }
2089
1938
 
2090
1939
  return passthroughs.map(([methodName, candidates]) => {
2091
- const { propertyName, params, argNames } = candidates[0];
2092
- const callArgs = argNames.join(", ");
1940
+ const { propertyName, signature } = candidates[0];
1941
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2093
1942
  const invocation = callArgs
2094
1943
  ? `this.${propertyName}.${methodName}(${callArgs})`
2095
1944
  : `this.${propertyName}.${methodName}()`;
2096
1945
  return createClassMethod({
2097
1946
  name: methodName,
2098
- parameters: parseParameterSignatures(params),
1947
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2099
1948
  statements: [
2100
1949
  `return ${invocation};`,
2101
1950
  ],
@@ -2112,17 +1961,57 @@ function sliceNodeSource(source: string, node: { start?: number | null; end?: nu
2112
1961
  return snippet.length ? snippet : null;
2113
1962
  }
2114
1963
 
2115
- function getCustomPomCallArgumentName(param: ClassMethod["params"][number]): string | null {
1964
+ function getTypeAnnotationSource(
1965
+ source: string,
1966
+ node: { typeAnnotation?: unknown },
1967
+ ): string | undefined {
1968
+ const rawTypeAnnotation = node.typeAnnotation;
1969
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
1970
+ return undefined;
1971
+ }
1972
+
1973
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
1974
+ return typeAnnotation && typeof typeAnnotation === "object"
1975
+ ? sliceNodeSource(source, typeAnnotation as { start?: number | null; end?: number | null }) ?? undefined
1976
+ : undefined;
1977
+ }
1978
+
1979
+ function getCustomPomParameterSpec(source: string, param: ClassMethod["params"][number]): PomParameterSpec | null {
2116
1980
  if (param.type === "Identifier") {
2117
- return param.name;
1981
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
1982
+ hasQuestionToken: !!param.optional,
1983
+ });
2118
1984
  }
2119
1985
 
2120
1986
  if (param.type === "AssignmentPattern") {
2121
- return param.left.type === "Identifier" ? param.left.name : null;
1987
+ if (param.left.type !== "Identifier") {
1988
+ return null;
1989
+ }
1990
+
1991
+ const initializer = sliceNodeSource(source, param.right);
1992
+ if (!initializer) {
1993
+ return null;
1994
+ }
1995
+
1996
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
1997
+ initializer,
1998
+ hasQuestionToken: !!param.left.optional,
1999
+ });
2122
2000
  }
2123
2001
 
2124
2002
  if (param.type === "RestElement") {
2125
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
2003
+ if (param.argument.type !== "Identifier") {
2004
+ return null;
2005
+ }
2006
+
2007
+ const typeExpression = getTypeAnnotationSource(
2008
+ source,
2009
+ param,
2010
+ ) ?? getTypeAnnotationSource(source, param.argument);
2011
+
2012
+ return createPomParameterSpec(param.argument.name, typeExpression, {
2013
+ isRestParameter: true,
2014
+ });
2126
2015
  }
2127
2016
 
2128
2017
  return null;
@@ -2165,8 +2054,7 @@ function extractCustomPomMethodSignatures(source: string, exportName: string): C
2165
2054
  continue;
2166
2055
  }
2167
2056
 
2168
- const params: string[] = [];
2169
- const argNames: string[] = [];
2057
+ const parameters: PomParameterSpec[] = [];
2170
2058
  let supported = true;
2171
2059
 
2172
2060
  member.params.forEach((param) => {
@@ -2174,25 +2062,20 @@ function extractCustomPomMethodSignatures(source: string, exportName: string): C
2174
2062
  return;
2175
2063
  }
2176
2064
 
2177
- const paramSource = sliceNodeSource(source, param);
2178
- const argName = getCustomPomCallArgumentName(param);
2179
- if (!paramSource || !argName) {
2065
+ const parameter = getCustomPomParameterSpec(source, param);
2066
+ if (!parameter) {
2180
2067
  supported = false;
2181
2068
  return;
2182
2069
  }
2183
2070
 
2184
- params.push(paramSource);
2185
- argNames.push(argName);
2071
+ parameters.push(parameter);
2186
2072
  });
2187
2073
 
2188
2074
  if (!supported) {
2189
2075
  continue;
2190
2076
  }
2191
2077
 
2192
- signatures.set(member.key.name, {
2193
- params: params.join(", "),
2194
- argNames,
2195
- });
2078
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
2196
2079
  }
2197
2080
  }
2198
2081
 
@@ -2390,6 +2273,7 @@ function resolveCustomPomImportResolution(
2390
2273
  projectRoot: string,
2391
2274
  options: {
2392
2275
  customPomDir?: GenerateFilesOptions["customPomDir"];
2276
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
2393
2277
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
2394
2278
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
2395
2279
  } = {},
@@ -2430,6 +2314,9 @@ function resolveCustomPomImportResolution(
2430
2314
  : path.resolve(projectRoot, customDirRelOrAbs);
2431
2315
 
2432
2316
  if (!fs.existsSync(customDirAbs)) {
2317
+ if (options.requireCustomPomDir) {
2318
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
2319
+ }
2433
2320
  return {
2434
2321
  classIdentifierMap,
2435
2322
  methodSignaturesByClass,
@@ -2483,6 +2370,21 @@ function resolveCustomPomImportResolution(
2483
2370
  };
2484
2371
  }
2485
2372
 
2373
+ function assertCustomPomAttachmentsResolved(
2374
+ attachments: readonly CustomPomAttachment[],
2375
+ classIdentifierMap: Record<string, string>,
2376
+ configuredDir: string,
2377
+ ): void {
2378
+ const missingClassNames = Array.from(new Set(
2379
+ attachments
2380
+ .map(attachment => attachment.className)
2381
+ .filter(className => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className)),
2382
+ )).sort((left, right) => left.localeCompare(right));
2383
+ if (missingClassNames.length > 0) {
2384
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
2385
+ }
2386
+ }
2387
+
2486
2388
  function getComposedStubBody(
2487
2389
  targetClassName: string,
2488
2390
  availableClassNames: Set<string>,
@@ -2514,7 +2416,7 @@ function getComposedStubBody(
2514
2416
  if (!childClassNames.length)
2515
2417
  return undefined;
2516
2418
 
2517
- const methodToChildren = new Map<string, Array<{ child: string; params: string; argNames: string[] }>>();
2419
+ const methodToChildren = new Map<string, Array<{ child: string; signature: PomMethodSignature }>>();
2518
2420
  for (const child of childClassNames) {
2519
2421
  const childDeps = depsByClassName.get(child);
2520
2422
  const methods = childDeps?.generatedMethods;
@@ -2525,7 +2427,7 @@ function getComposedStubBody(
2525
2427
  if (!sig)
2526
2428
  continue;
2527
2429
  const list = methodToChildren.get(name) ?? [];
2528
- list.push({ child, params: sig.params, argNames: sig.argNames });
2430
+ list.push({ child, signature: sig });
2529
2431
  methodToChildren.set(name, list);
2530
2432
  }
2531
2433
  }
@@ -2535,13 +2437,13 @@ function getComposedStubBody(
2535
2437
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
2536
2438
  continue;
2537
2439
 
2538
- const { child, params, argNames } = candidatesForMethod[0];
2539
- const callArgs = argNames.join(", ");
2440
+ const { child, signature } = candidatesForMethod[0];
2441
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2540
2442
 
2541
2443
  passthroughMembers.push(createClassMethod({
2542
2444
  name: methodName,
2543
2445
  isAsync: true,
2544
- parameters: parseParameterSignatures(params),
2446
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2545
2447
  statements: [
2546
2448
  `return await this.${child}.${methodName}(${callArgs});`,
2547
2449
  ],
@@ -2579,6 +2481,7 @@ async function generateAggregatedFiles(
2579
2481
  customPomAttachments?: GenerateFilesOptions["customPomAttachments"];
2580
2482
  projectRoot?: GenerateFilesOptions["projectRoot"];
2581
2483
  customPomDir?: GenerateFilesOptions["customPomDir"];
2484
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
2582
2485
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
2583
2486
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
2584
2487
  testIdAttribute?: GenerateFilesOptions["testIdAttribute"];
@@ -2624,9 +2527,15 @@ async function generateAggregatedFiles(
2624
2527
 
2625
2528
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
2626
2529
  customPomDir: options.customPomDir,
2530
+ requireCustomPomDir: options.requireCustomPomDir,
2627
2531
  customPomImportAliases: options.customPomImportAliases,
2628
2532
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior,
2629
2533
  });
2534
+ assertCustomPomAttachmentsResolved(
2535
+ options.customPomAttachments ?? [],
2536
+ customPomImportResolution.classIdentifierMap,
2537
+ options.customPomDir ?? "tests/playwright/pom/custom",
2538
+ );
2630
2539
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
2631
2540
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
2632
2541
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -2826,10 +2735,10 @@ function getWidgetInstancesForView(
2826
2735
  };
2827
2736
 
2828
2737
  for (const dt of dataTestIdSet) {
2829
- const raw = dt.value;
2738
+ const raw = dt.selectorValue.formatted;
2830
2739
 
2831
- // Skip keyed/dynamic test ids; instance fields can't represent those ergonomically.
2832
- if (raw.includes("${")) {
2740
+ // Skip parameterized test ids; instance fields can't represent those ergonomically.
2741
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
2833
2742
  continue;
2834
2743
  }
2835
2744