@immense/vue-pom-generator 1.0.58 → 1.0.60

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 (58) hide show
  1. package/README.md +36 -25
  2. package/RELEASE_NOTES.md +29 -31
  3. package/class-generation/base-page.ts +49 -25
  4. package/class-generation/index.ts +243 -333
  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 +8 -4
  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 +1527 -1064
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1529 -1066
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/manifest-generator.d.ts +35 -1
  20. package/dist/manifest-generator.d.ts.map +1 -1
  21. package/dist/method-generation.d.ts +4 -2
  22. package/dist/method-generation.d.ts.map +1 -1
  23. package/dist/plugin/create-vue-pom-generator-plugins.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/virtual-modules.d.ts +3 -1
  33. package/dist/plugin/support/virtual-modules.d.ts.map +1 -1
  34. package/dist/plugin/support-plugins.d.ts +4 -33
  35. package/dist/plugin/support-plugins.d.ts.map +1 -1
  36. package/dist/plugin/types.d.ts +6 -23
  37. package/dist/plugin/types.d.ts.map +1 -1
  38. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  39. package/dist/pom-discoverability.d.ts +10 -0
  40. package/dist/pom-discoverability.d.ts.map +1 -0
  41. package/dist/pom-params.d.ts +40 -0
  42. package/dist/pom-params.d.ts.map +1 -0
  43. package/dist/pom-patterns.d.ts +31 -0
  44. package/dist/pom-patterns.d.ts.map +1 -0
  45. package/dist/routing/to-directive.d.ts +21 -0
  46. package/dist/routing/to-directive.d.ts.map +1 -1
  47. package/dist/tests/base-page.test.d.ts +2 -0
  48. package/dist/tests/base-page.test.d.ts.map +1 -0
  49. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts +7 -4
  50. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts.map +1 -1
  51. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  52. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  53. package/dist/transform.d.ts +0 -1
  54. package/dist/transform.d.ts.map +1 -1
  55. package/dist/utils.d.ts +129 -63
  56. package/dist/utils.d.ts.map +1 -1
  57. package/package.json +6 -4
  58. package/sequence-diagram.md +6 -6
@@ -9,6 +9,26 @@ 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";
31
+ import { buildPomLocatorDescription, stripPomActionPrefix } from "../pom-discoverability";
12
32
  import { introspectNuxtPages, parseRouterFileFromCwd } from "../router-introspection";
13
33
  import {
14
34
  addExportAll,
@@ -25,7 +45,6 @@ import {
25
45
  type GetAccessorDeclarationStructure,
26
46
  type MethodDeclarationStructure,
27
47
  type OptionalKind,
28
- type ParameterDeclarationStructure,
29
48
  type PropertyDeclarationStructure,
30
49
  type TypeScriptClassMember,
31
50
  type TypeScriptSourceFile,
@@ -35,6 +54,7 @@ import {
35
54
  IDataTestId,
36
55
  PomExtraClickMethodSpec,
37
56
  PomPrimarySpec,
57
+ PomSelectorSpec,
38
58
  toPascalCase,
39
59
  upperFirst,
40
60
  } from "../utils";
@@ -61,128 +81,6 @@ class VuePomGeneratorError extends Error {
61
81
  }
62
82
  }
63
83
 
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
84
  function toPosixRelativePath(fromDir: string, toFile: string): string {
187
85
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
188
86
  if (!rel.startsWith(".")) {
@@ -214,12 +112,7 @@ interface RouteMeta {
214
112
  template: string;
215
113
  }
216
114
 
217
- interface CustomPomMethodSignature {
218
- params: string;
219
- argNames: string[];
220
- }
221
-
222
- type CustomPomMethodSignatureMap = Map<string, CustomPomMethodSignature>;
115
+ type CustomPomMethodSignatureMap = Map<string, PomMethodSignature>;
223
116
 
224
117
  interface CustomPomAttachment {
225
118
  className: string;
@@ -251,6 +144,27 @@ interface CustomPomImportResolution {
251
144
  importSpecifiersByClass: Record<string, ResolvedCustomPomImportSpecifier>;
252
145
  }
253
146
 
147
+ function createMissingCustomPomDirectoryError(configuredDir: string, resolvedDir: string): VuePomGeneratorError {
148
+ return new VuePomGeneratorError(
149
+ `Custom POM directory "${configuredDir}" does not exist.\n`
150
+ + `Resolved path: ${resolvedDir}\n`
151
+ + "Create the directory, point generation.playwright.customPoms.dir at the correct location, "
152
+ + "or remove the customPoms configuration.",
153
+ );
154
+ }
155
+
156
+ function createMissingCustomPomAttachmentClassError(
157
+ missingClassNames: string[],
158
+ configuredDir: string,
159
+ ): VuePomGeneratorError {
160
+ const renderedClassNames = missingClassNames.map(name => `"${name}"`).join(", ");
161
+ return new VuePomGeneratorError(
162
+ `Custom POM attachments reference missing helper classes: ${renderedClassNames}.\n`
163
+ + `Expected matching helper files/exports under "${configuredDir}".\n`
164
+ + "Add the missing helper classes or remove the corresponding generation.playwright.customPoms.attachments entries.",
165
+ );
166
+ }
167
+
254
168
  function createCustomPomImportCollisionError(exportName: string, requested: string): VuePomGeneratorError {
255
169
  return new VuePomGeneratorError(
256
170
  `Custom POM import name collision detected for "${exportName}".\n`
@@ -429,62 +343,37 @@ function generateGoToSelfMethod(componentName: string): TypeScriptClassMember[]
429
343
  ];
430
344
  }
431
345
 
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(", ");
346
+ function getSelectorPatterns(selector: PomSelectorSpec): PomStringPattern[] {
347
+ return selector.kind === "testId"
348
+ ? [selector.testId]
349
+ : [selector.rootTestId, selector.label];
453
350
  }
454
351
 
455
- function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScriptClassMember[] {
352
+ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec, componentName: string): TypeScriptClassMember[] {
456
353
  if (spec.kind !== "click") {
457
354
  return [];
458
355
  }
459
356
 
460
- const params = spec.params ?? {};
461
- const signatureParams = formatMethodParams(params);
462
- const parameters = parseParameterSignatures(signatureParams);
357
+ const selectorPatterns = getSelectorPatterns(spec.selector);
358
+ const signatureSpecs = orderPomPatternParameters(
359
+ spec.parameters,
360
+ selectorPatterns,
361
+ { omit: spec.keyLiteral !== undefined ? ["key"] : [] },
362
+ );
363
+ const parameters = toTypeScriptPomParameterStructures(signatureSpecs);
463
364
 
464
- const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
465
- const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
365
+ const hasAnnotationText = signatureSpecs.some(param => param.name === "annotationText");
366
+ const hasWait = signatureSpecs.some(param => param.name === "wait");
466
367
  const annotationArg = hasAnnotationText ? "annotationText" : "\"\"";
467
368
  const waitArg = hasWait ? "wait" : "true";
369
+ const locatorDescription = JSON.stringify(buildPomLocatorDescription({
370
+ componentName,
371
+ methodName: stripPomActionPrefix(spec.name),
372
+ nativeRole: "button",
373
+ }));
468
374
 
469
375
  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
- }
478
-
479
- const clickArgs: string[] = [];
480
- clickArgs.push(needsTemplate ? "testId" : testIdExpr);
481
-
482
- if (hasAnnotationText || hasWait) {
483
- clickArgs.push(annotationArg);
484
- }
485
- if (hasWait) {
486
- clickArgs.push(waitArg);
487
- }
376
+ const testIdBinding = bindTypeScriptPomPattern(spec.selector.testId, "testId");
488
377
 
489
378
  return [
490
379
  createClassMethod({
@@ -495,26 +384,17 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
495
384
  if (spec.keyLiteral !== undefined) {
496
385
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
497
386
  }
498
- if (needsTemplate) {
499
- writer.writeLine(`const testId = ${testIdExpr};`);
387
+ for (const statement of testIdBinding.setupStatements) {
388
+ writer.writeLine(statement);
500
389
  }
501
- writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
390
+ writer.writeLine(`await this.clickByTestId(${testIdBinding.expression}, ${annotationArg}, ${waitArg}, ${locatorDescription});`);
502
391
  },
503
392
  }),
504
393
  ];
505
394
  }
506
395
 
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;
396
+ const rootBinding = bindTypeScriptPomPattern(spec.selector.rootTestId, "rootTestId");
397
+ const labelBinding = bindTypeScriptPomPattern(spec.selector.label, "label");
518
398
  return [
519
399
  createClassMethod({
520
400
  name: spec.name,
@@ -524,35 +404,40 @@ function generateExtraClickMethodMembers(spec: PomExtraClickMethodSpec): TypeScr
524
404
  if (spec.keyLiteral !== undefined) {
525
405
  writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
526
406
  }
527
- if (rootNeedsTemplate) {
528
- writer.writeLine(`const rootTestId = ${rootExpr};`);
407
+ for (const statement of rootBinding.setupStatements) {
408
+ writer.writeLine(statement);
529
409
  }
530
- if (labelNeedsTemplate) {
531
- writer.writeLine(`const label = ${labelExpr};`);
410
+ for (const statement of labelBinding.setupStatements) {
411
+ writer.writeLine(statement);
532
412
  }
533
- writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
413
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootBinding.expression}, ${labelBinding.expression}, ${annotationArg}, ${waitArg}, { description: ${locatorDescription} });`);
534
414
  },
535
415
  }),
536
416
  ];
537
417
  }
538
418
 
539
- function generateMethodMembersFromPom(primary: PomPrimarySpec, targetPageObjectModelClass?: string): TypeScriptClassMember[] {
419
+ function generateMethodMembersFromPom(
420
+ componentName: string,
421
+ primary: PomPrimarySpec,
422
+ targetPageObjectModelClass?: string,
423
+ ): TypeScriptClassMember[] {
540
424
  if (primary.emitPrimary === false) {
541
425
  return [];
542
426
  }
543
427
 
544
428
  return generateViewObjectModelMembers(
429
+ componentName,
545
430
  targetPageObjectModelClass,
546
431
  primary.methodName,
547
432
  primary.nativeRole,
548
- primary.formattedDataTestId,
549
- primary.alternateFormattedDataTestIds,
433
+ primary.selector,
434
+ primary.alternateSelectors,
550
435
  primary.getterNameOverride,
551
- primary.params ?? {},
436
+ primary.parameters,
552
437
  );
553
438
  }
554
439
 
555
- function generateMethodsContentForDependencies(dependencies: IComponentDependencies): TypeScriptClassMember[] {
440
+ function generateMethodsContentForDependencies(componentName: string, dependencies: IComponentDependencies): TypeScriptClassMember[] {
556
441
  const entries = Array.from(dependencies.dataTestIdSet ?? []);
557
442
  const primarySpecsAll = entries
558
443
  .map(e => ({ pom: e.pom, target: e.targetPageObjectModelClass }))
@@ -565,17 +450,16 @@ function generateMethodsContentForDependencies(dependencies: IComponentDependenc
565
450
  // When we emit from IR, we must de-dupe here to avoid duplicate getters/methods.
566
451
  const seenPrimaryKeys = new Set<string>();
567
452
  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();
453
+ const alternates = (pom.alternateSelectors ?? [])
454
+ .slice()
455
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
572
456
  const key = JSON.stringify({
573
457
  role: pom.nativeRole,
574
458
  methodName: pom.methodName,
575
459
  getterNameOverride: pom.getterNameOverride ?? null,
576
- testId: pom.formattedDataTestId,
577
- alternateTestIds: alternates.length ? alternates : undefined,
578
- params: stableParams,
460
+ selector: pom.selector,
461
+ alternateSelectors: alternates.length ? alternates : undefined,
462
+ parameters: pom.parameters,
579
463
  target: target ?? null,
580
464
  emitPrimary: pom.emitPrimary ?? true,
581
465
  });
@@ -592,11 +476,11 @@ function generateMethodsContentForDependencies(dependencies: IComponentDependenc
592
476
 
593
477
  const members: TypeScriptClassMember[] = [];
594
478
  for (const { pom, target } of primarySpecs) {
595
- members.push(...generateMethodMembersFromPom(pom, target));
479
+ members.push(...generateMethodMembersFromPom(componentName, pom, target));
596
480
  }
597
481
 
598
482
  for (const extra of extras) {
599
- members.push(...generateExtraClickMethodMembers(extra));
483
+ members.push(...generateExtraClickMethodMembers(extra, componentName));
600
484
  }
601
485
 
602
486
  return members;
@@ -639,6 +523,8 @@ export interface GenerateFilesOptions {
639
523
  * Defaults to <projectRoot>/tests/playwright/pom/custom.
640
524
  */
641
525
  customPomDir?: string;
526
+ /** When true, fail generation if the configured customPomDir does not exist. */
527
+ requireCustomPomDir?: boolean;
642
528
 
643
529
  /**
644
530
  * Optional import aliases for handwritten POM helpers.
@@ -708,6 +594,7 @@ interface BaseGenerateContentOptions {
708
594
 
709
595
  projectRoot?: string;
710
596
  customPomDir?: string;
597
+ requireCustomPomDir?: boolean;
711
598
  customPomImportAliases?: Record<string, string>;
712
599
  customPomClassIdentifierMap?: Record<string, string>;
713
600
  customPomAvailableClassIdentifiers?: Set<string>;
@@ -747,6 +634,7 @@ export async function generateFiles(
747
634
  customPomAttachments = [],
748
635
  projectRoot,
749
636
  customPomDir,
637
+ requireCustomPomDir,
750
638
  customPomImportAliases,
751
639
  customPomImportNameCollisionBehavior = "error",
752
640
  testIdAttribute,
@@ -789,6 +677,7 @@ export async function generateFiles(
789
677
  customPomAttachments,
790
678
  projectRoot,
791
679
  customPomDir,
680
+ requireCustomPomDir,
792
681
  customPomImportAliases,
793
682
  customPomImportNameCollisionBehavior,
794
683
  testIdAttribute,
@@ -799,6 +688,7 @@ export async function generateFiles(
799
688
  customPomAttachments,
800
689
  projectRoot,
801
690
  customPomDir,
691
+ requireCustomPomDir,
802
692
  customPomImportAliases,
803
693
  customPomImportNameCollisionBehavior,
804
694
  testIdAttribute,
@@ -847,6 +737,7 @@ async function generateSplitTypeScriptFiles(
847
737
  customPomAttachments?: GenerateFilesOptions["customPomAttachments"];
848
738
  projectRoot?: GenerateFilesOptions["projectRoot"];
849
739
  customPomDir?: GenerateFilesOptions["customPomDir"];
740
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
850
741
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
851
742
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
852
743
  testIdAttribute?: GenerateFilesOptions["testIdAttribute"];
@@ -882,9 +773,15 @@ async function generateSplitTypeScriptFiles(
882
773
 
883
774
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
884
775
  customPomDir: options.customPomDir,
776
+ requireCustomPomDir: options.requireCustomPomDir,
885
777
  customPomImportAliases: options.customPomImportAliases,
886
778
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior,
887
779
  });
780
+ assertCustomPomAttachmentsResolved(
781
+ options.customPomAttachments ?? [],
782
+ customPomImportResolution.classIdentifierMap,
783
+ options.customPomDir ?? "tests/playwright/pom/custom",
784
+ );
888
785
 
889
786
  const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "base-page.ts");
890
787
  const files: GeneratedFileOutput[] = [];
@@ -1071,30 +968,9 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths: string[]): Generat
1071
968
  });
1072
969
  }
1073
970
 
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
-
971
+ function toCSharpParam(param: PomParameterSpec): { type: string; defaultExpr?: string } {
1096
972
  // Collapse union types to their widest practical type.
1097
- const typePart = left.includes("|") ? "string" : left;
973
+ const typePart = param.type?.includes("|") ? "string" : (param.type ?? "string");
1098
974
 
1099
975
  let type = "string";
1100
976
  if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
@@ -1103,23 +979,21 @@ function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: str
1103
979
  type = "string";
1104
980
  else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
1105
981
  type = "int";
1106
- else if (/\d+/.test(typePart) && typePart === "")
1107
- type = "int";
1108
982
  else if (/\btimeOut\b/i.test(typePart))
1109
983
  type = "int";
1110
984
 
1111
985
  let defaultExpr: string | undefined;
1112
- if (right !== undefined) {
986
+ if (param.initializer !== undefined) {
1113
987
  if (type === "bool") {
1114
- defaultExpr = right.includes("true") ? "true" : right.includes("false") ? "false" : undefined;
988
+ defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : undefined;
1115
989
  }
1116
990
  else if (type === "int") {
1117
- const m = right.match(/\d+/);
991
+ const m = param.initializer.match(/\d+/);
1118
992
  defaultExpr = m ? m[0] : undefined;
1119
993
  }
1120
994
  else {
1121
995
  // string defaults, keep empty string if detected.
1122
- if (right === "\"\"" || right === "\"\"" || right === "''") {
996
+ if (param.initializer === "\"\"" || param.initializer === "''") {
1123
997
  defaultExpr = "\"\"";
1124
998
  }
1125
999
  }
@@ -1128,21 +1002,18 @@ function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: str
1128
1002
  return { type, defaultExpr };
1129
1003
  }
1130
1004
 
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)
1005
+ function formatCSharpParams(params: readonly PomParameterSpec[] | undefined): { signature: string; argNames: string[] } {
1006
+ const normalizedParams = normalizePomParameters(params);
1007
+ if (!normalizedParams.length)
1137
1008
  return { signature: "", argNames: [] };
1138
1009
 
1139
1010
  const signatureParts: string[] = [];
1140
1011
  const argNames: string[] = [];
1141
1012
 
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}`);
1013
+ for (const param of normalizedParams) {
1014
+ const { type, defaultExpr } = toCSharpParam(param);
1015
+ argNames.push(param.name);
1016
+ signatureParts.push(defaultExpr !== undefined ? `${type} ${param.name} = ${defaultExpr}` : `${type} ${param.name}`);
1146
1017
  }
1147
1018
 
1148
1019
  return { signature: signatureParts.join(", "), argNames };
@@ -1253,32 +1124,16 @@ function generateAggregatedCSharpFiles(
1253
1124
  const baseMethodName = upperFirst(pom.methodName);
1254
1125
  const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
1255
1126
  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
- ]);
1127
+ const selectorIsParameterized = isParameterizedPomPattern(pom.selector.patternKind);
1128
+ const testIdExpr = toCSharpPomPatternExpression(pom.selector);
1129
+ const orderedParams = orderPomPatternParameters(pom.parameters, [pom.selector]);
1274
1130
 
1275
1131
  const { signature, argNames } = formatCSharpParams(orderedParams);
1276
1132
  const args = argNames.join(", ");
1277
1133
 
1278
- const allTestIds = [pom.formattedDataTestId, ...(pom.alternateFormattedDataTestIds ?? [])]
1279
- .filter((v, idx, arr) => v && arr.indexOf(v) === idx);
1134
+ const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
1280
1135
 
1281
- if (pom.formattedDataTestId.includes("${")) {
1136
+ if (selectorIsParameterized) {
1282
1137
  chunks.push(` public ILocator ${locatorName}(${signature}) => LocatorByTestId(${testIdExpr});`);
1283
1138
  }
1284
1139
  else {
@@ -1300,13 +1155,13 @@ function generateAggregatedCSharpFiles(
1300
1155
  if (target) {
1301
1156
  chunks.push(` public async Task<${target}> ${actionName}(${sig})`);
1302
1157
  chunks.push(" {");
1303
- if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
1304
- chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
1158
+ if (selectorIsParameterized || allTestIds.length <= 1) {
1159
+ chunks.push(` await ${locatorName}${selectorIsParameterized ? `(${args})` : ""}.ClickAsync();`);
1305
1160
  chunks.push(` return new ${target}(Page);`);
1306
1161
  }
1307
1162
  else {
1308
1163
  chunks.push(" Exception? lastError = null;");
1309
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
1164
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map(testId => toCSharpPomPatternExpression(testId)).join(", ")} })`);
1310
1165
  chunks.push(" {");
1311
1166
  chunks.push(" try");
1312
1167
  chunks.push(" {");
@@ -1332,7 +1187,7 @@ function generateAggregatedCSharpFiles(
1332
1187
  chunks.push(` public async Task ${actionName}(${sig})`);
1333
1188
  chunks.push(" {");
1334
1189
 
1335
- const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
1190
+ const callSuffix = selectorIsParameterized ? `(${args})` : "";
1336
1191
 
1337
1192
  const emitActionCall = (locatorAccess: string) => {
1338
1193
  if (pom.nativeRole === "input") {
@@ -1351,9 +1206,9 @@ function generateAggregatedCSharpFiles(
1351
1206
  }
1352
1207
  };
1353
1208
 
1354
- if (!pom.formattedDataTestId.includes("${") && allTestIds.length > 1) {
1209
+ if (!selectorIsParameterized && allTestIds.length > 1) {
1355
1210
  chunks.push(" Exception? lastError = null;");
1356
- chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
1211
+ chunks.push(` foreach (var testId in new[] { ${allTestIds.map(testId => toCSharpPomPatternExpression(testId)).join(", ")} })`);
1357
1212
  chunks.push(" {");
1358
1213
  chunks.push(" try");
1359
1214
  chunks.push(" {");
@@ -1406,7 +1261,12 @@ function generateAggregatedCSharpFiles(
1406
1261
  for (const extra of extras) {
1407
1262
  if (extra.kind !== "click")
1408
1263
  continue;
1409
- const { signature } = formatCSharpParams(extra.params);
1264
+ const extraParams = orderPomPatternParameters(
1265
+ extra.parameters,
1266
+ getSelectorPatterns(extra.selector),
1267
+ { omit: extra.keyLiteral !== undefined ? ["key"] : [] },
1268
+ );
1269
+ const { signature } = formatCSharpParams(extraParams);
1410
1270
 
1411
1271
  const extraName = upperFirst(extra.name);
1412
1272
 
@@ -1417,33 +1277,24 @@ function generateAggregatedCSharpFiles(
1417
1277
  }
1418
1278
 
1419
1279
  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();`);
1280
+ const testIdBinding = bindCSharpPomPattern(extra.selector.testId, "testId");
1281
+ for (const statement of testIdBinding.setupStatements) {
1282
+ chunks.push(` ${statement}`);
1428
1283
  }
1284
+ chunks.push(` await LocatorByTestId(${testIdBinding.expression}).ClickAsync();`);
1429
1285
  }
1430
1286
  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);
1287
+ const rootBinding = bindCSharpPomPattern(extra.selector.rootTestId, "rootTestId");
1288
+ const labelBinding = bindCSharpPomPattern(extra.selector.label, "label");
1435
1289
  const exactArg = extra.selector.exact === false ? "false" : "true";
1436
1290
 
1437
- if (rootNeedsTemplate) {
1438
- chunks.push(` var rootTestId = ${rootExpr};`);
1291
+ for (const statement of rootBinding.setupStatements) {
1292
+ chunks.push(` ${statement}`);
1439
1293
  }
1440
- if (labelNeedsTemplate) {
1441
- chunks.push(` var label = ${labelExpr};`);
1294
+ for (const statement of labelBinding.setupStatements) {
1295
+ chunks.push(` ${statement}`);
1442
1296
  }
1443
-
1444
- const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
1445
- const labelArg = labelNeedsTemplate ? "label" : labelExpr;
1446
- chunks.push(` await ClickWithinTestIdByLabelAsync(${rootArg}, ${labelArg}, ${exactArg});`);
1297
+ chunks.push(` await ClickWithinTestIdByLabelAsync(${rootBinding.expression}, ${labelBinding.expression}, ${exactArg});`);
1447
1298
  }
1448
1299
  chunks.push(" }");
1449
1300
  chunks.push("");
@@ -1789,8 +1640,8 @@ function prepareViewObjectModelClass(
1789
1640
  propertyName: a.propertyName,
1790
1641
  flatten: a.flatten ?? false,
1791
1642
  methodSignatures: a.flatten
1792
- ? (customPomMethodSignaturesByClass.get(a.className) ?? new Map<string, CustomPomMethodSignature>())
1793
- : new Map<string, CustomPomMethodSignature>(),
1643
+ ? (customPomMethodSignaturesByClass.get(a.className) ?? new Map<string, PomMethodSignature>())
1644
+ : new Map<string, PomMethodSignature>(),
1794
1645
  }));
1795
1646
 
1796
1647
  const widgetInstances = isView
@@ -1848,7 +1699,7 @@ function prepareViewObjectModelClass(
1848
1699
  members.push(...generateGoToSelfMethod(className));
1849
1700
  }
1850
1701
 
1851
- members.push(...generateMethodsContentForDependencies(dependencies));
1702
+ members.push(...generateMethodsContentForDependencies(componentName, dependencies));
1852
1703
 
1853
1704
  return {
1854
1705
  className,
@@ -1997,10 +1848,10 @@ function getViewPassthroughMethods(
1997
1848
  componentHierarchyMap: Map<string, IComponentDependencies>,
1998
1849
  blockedMethodNames: Set<string> = new Set(),
1999
1850
  ) {
2000
- const existingOnView = viewDependencies.generatedMethods ?? new Map<string, { params: string; argNames: string[] } | null>();
1851
+ const existingOnView = viewDependencies.generatedMethods ?? new Map<string, PomMethodSignature | null>();
2001
1852
 
2002
1853
  // methodName -> candidates
2003
- const methodToChildren = new Map<string, Array<{ childProp: string; params: string; argNames: string[] }>>();
1854
+ const methodToChildren = new Map<string, Array<{ childProp: string; signature: PomMethodSignature }>>();
2004
1855
 
2005
1856
  for (const child of childrenComponentSet) {
2006
1857
  const childDeps = componentHierarchyMap.get(child);
@@ -2023,7 +1874,7 @@ function getViewPassthroughMethods(
2023
1874
  continue;
2024
1875
 
2025
1876
  const list = methodToChildren.get(name) ?? [];
2026
- list.push({ childProp, params: sig.params, argNames: sig.argNames });
1877
+ list.push({ childProp, signature: sig });
2027
1878
  methodToChildren.set(name, list);
2028
1879
  }
2029
1880
  }
@@ -2035,12 +1886,12 @@ function getViewPassthroughMethods(
2035
1886
  }
2036
1887
 
2037
1888
  return passthroughs.map(([methodName, candidates]) => {
2038
- const { childProp, params, argNames } = candidates[0];
2039
- const callArgs = argNames.join(", ");
1889
+ const { childProp, signature } = candidates[0];
1890
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2040
1891
  return createClassMethod({
2041
1892
  name: methodName,
2042
1893
  isAsync: true,
2043
- parameters: parseParameterSignatures(params),
1894
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2044
1895
  statements: [
2045
1896
  `return await this.${childProp}.${methodName}(${callArgs});`,
2046
1897
  ],
@@ -2058,8 +1909,8 @@ function getAttachmentPassthroughMethods(
2058
1909
  return [];
2059
1910
  }
2060
1911
 
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[] }>>();
1912
+ const existingOnClass = ownerDependencies.generatedMethods ?? new Map<string, PomMethodSignature | null>();
1913
+ const methodToAttachments = new Map<string, Array<{ propertyName: string; signature: PomMethodSignature }>>();
2063
1914
 
2064
1915
  for (const attachment of attachmentsForThisClass) {
2065
1916
  if (!attachment.flatten) {
@@ -2074,8 +1925,7 @@ function getAttachmentPassthroughMethods(
2074
1925
  const list = methodToAttachments.get(methodName) ?? [];
2075
1926
  list.push({
2076
1927
  propertyName: attachment.propertyName,
2077
- params: signature.params,
2078
- argNames: signature.argNames,
1928
+ signature,
2079
1929
  });
2080
1930
  methodToAttachments.set(methodName, list);
2081
1931
  }
@@ -2088,14 +1938,14 @@ function getAttachmentPassthroughMethods(
2088
1938
  }
2089
1939
 
2090
1940
  return passthroughs.map(([methodName, candidates]) => {
2091
- const { propertyName, params, argNames } = candidates[0];
2092
- const callArgs = argNames.join(", ");
1941
+ const { propertyName, signature } = candidates[0];
1942
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2093
1943
  const invocation = callArgs
2094
1944
  ? `this.${propertyName}.${methodName}(${callArgs})`
2095
1945
  : `this.${propertyName}.${methodName}()`;
2096
1946
  return createClassMethod({
2097
1947
  name: methodName,
2098
- parameters: parseParameterSignatures(params),
1948
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2099
1949
  statements: [
2100
1950
  `return ${invocation};`,
2101
1951
  ],
@@ -2112,17 +1962,57 @@ function sliceNodeSource(source: string, node: { start?: number | null; end?: nu
2112
1962
  return snippet.length ? snippet : null;
2113
1963
  }
2114
1964
 
2115
- function getCustomPomCallArgumentName(param: ClassMethod["params"][number]): string | null {
1965
+ function getTypeAnnotationSource(
1966
+ source: string,
1967
+ node: { typeAnnotation?: unknown },
1968
+ ): string | undefined {
1969
+ const rawTypeAnnotation = node.typeAnnotation;
1970
+ if (!rawTypeAnnotation || typeof rawTypeAnnotation !== "object" || !("type" in rawTypeAnnotation) || rawTypeAnnotation.type !== "TSTypeAnnotation" || !("typeAnnotation" in rawTypeAnnotation)) {
1971
+ return undefined;
1972
+ }
1973
+
1974
+ const typeAnnotation = rawTypeAnnotation.typeAnnotation;
1975
+ return typeAnnotation && typeof typeAnnotation === "object"
1976
+ ? sliceNodeSource(source, typeAnnotation as { start?: number | null; end?: number | null }) ?? undefined
1977
+ : undefined;
1978
+ }
1979
+
1980
+ function getCustomPomParameterSpec(source: string, param: ClassMethod["params"][number]): PomParameterSpec | null {
2116
1981
  if (param.type === "Identifier") {
2117
- return param.name;
1982
+ return createPomParameterSpec(param.name, getTypeAnnotationSource(source, param), {
1983
+ hasQuestionToken: !!param.optional,
1984
+ });
2118
1985
  }
2119
1986
 
2120
1987
  if (param.type === "AssignmentPattern") {
2121
- return param.left.type === "Identifier" ? param.left.name : null;
1988
+ if (param.left.type !== "Identifier") {
1989
+ return null;
1990
+ }
1991
+
1992
+ const initializer = sliceNodeSource(source, param.right);
1993
+ if (!initializer) {
1994
+ return null;
1995
+ }
1996
+
1997
+ return createPomParameterSpec(param.left.name, getTypeAnnotationSource(source, param.left), {
1998
+ initializer,
1999
+ hasQuestionToken: !!param.left.optional,
2000
+ });
2122
2001
  }
2123
2002
 
2124
2003
  if (param.type === "RestElement") {
2125
- return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
2004
+ if (param.argument.type !== "Identifier") {
2005
+ return null;
2006
+ }
2007
+
2008
+ const typeExpression = getTypeAnnotationSource(
2009
+ source,
2010
+ param,
2011
+ ) ?? getTypeAnnotationSource(source, param.argument);
2012
+
2013
+ return createPomParameterSpec(param.argument.name, typeExpression, {
2014
+ isRestParameter: true,
2015
+ });
2126
2016
  }
2127
2017
 
2128
2018
  return null;
@@ -2165,8 +2055,7 @@ function extractCustomPomMethodSignatures(source: string, exportName: string): C
2165
2055
  continue;
2166
2056
  }
2167
2057
 
2168
- const params: string[] = [];
2169
- const argNames: string[] = [];
2058
+ const parameters: PomParameterSpec[] = [];
2170
2059
  let supported = true;
2171
2060
 
2172
2061
  member.params.forEach((param) => {
@@ -2174,25 +2063,20 @@ function extractCustomPomMethodSignatures(source: string, exportName: string): C
2174
2063
  return;
2175
2064
  }
2176
2065
 
2177
- const paramSource = sliceNodeSource(source, param);
2178
- const argName = getCustomPomCallArgumentName(param);
2179
- if (!paramSource || !argName) {
2066
+ const parameter = getCustomPomParameterSpec(source, param);
2067
+ if (!parameter) {
2180
2068
  supported = false;
2181
2069
  return;
2182
2070
  }
2183
2071
 
2184
- params.push(paramSource);
2185
- argNames.push(argName);
2072
+ parameters.push(parameter);
2186
2073
  });
2187
2074
 
2188
2075
  if (!supported) {
2189
2076
  continue;
2190
2077
  }
2191
2078
 
2192
- signatures.set(member.key.name, {
2193
- params: params.join(", "),
2194
- argNames,
2195
- });
2079
+ signatures.set(member.key.name, createPomMethodSignature(parameters));
2196
2080
  }
2197
2081
  }
2198
2082
 
@@ -2390,6 +2274,7 @@ function resolveCustomPomImportResolution(
2390
2274
  projectRoot: string,
2391
2275
  options: {
2392
2276
  customPomDir?: GenerateFilesOptions["customPomDir"];
2277
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
2393
2278
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
2394
2279
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
2395
2280
  } = {},
@@ -2430,6 +2315,9 @@ function resolveCustomPomImportResolution(
2430
2315
  : path.resolve(projectRoot, customDirRelOrAbs);
2431
2316
 
2432
2317
  if (!fs.existsSync(customDirAbs)) {
2318
+ if (options.requireCustomPomDir) {
2319
+ throw createMissingCustomPomDirectoryError(customDirRelOrAbs, customDirAbs);
2320
+ }
2433
2321
  return {
2434
2322
  classIdentifierMap,
2435
2323
  methodSignaturesByClass,
@@ -2483,6 +2371,21 @@ function resolveCustomPomImportResolution(
2483
2371
  };
2484
2372
  }
2485
2373
 
2374
+ function assertCustomPomAttachmentsResolved(
2375
+ attachments: readonly CustomPomAttachment[],
2376
+ classIdentifierMap: Record<string, string>,
2377
+ configuredDir: string,
2378
+ ): void {
2379
+ const missingClassNames = Array.from(new Set(
2380
+ attachments
2381
+ .map(attachment => attachment.className)
2382
+ .filter(className => !Object.prototype.hasOwnProperty.call(classIdentifierMap, className)),
2383
+ )).sort((left, right) => left.localeCompare(right));
2384
+ if (missingClassNames.length > 0) {
2385
+ throw createMissingCustomPomAttachmentClassError(missingClassNames, configuredDir);
2386
+ }
2387
+ }
2388
+
2486
2389
  function getComposedStubBody(
2487
2390
  targetClassName: string,
2488
2391
  availableClassNames: Set<string>,
@@ -2514,7 +2417,7 @@ function getComposedStubBody(
2514
2417
  if (!childClassNames.length)
2515
2418
  return undefined;
2516
2419
 
2517
- const methodToChildren = new Map<string, Array<{ child: string; params: string; argNames: string[] }>>();
2420
+ const methodToChildren = new Map<string, Array<{ child: string; signature: PomMethodSignature }>>();
2518
2421
  for (const child of childClassNames) {
2519
2422
  const childDeps = depsByClassName.get(child);
2520
2423
  const methods = childDeps?.generatedMethods;
@@ -2525,7 +2428,7 @@ function getComposedStubBody(
2525
2428
  if (!sig)
2526
2429
  continue;
2527
2430
  const list = methodToChildren.get(name) ?? [];
2528
- list.push({ child, params: sig.params, argNames: sig.argNames });
2431
+ list.push({ child, signature: sig });
2529
2432
  methodToChildren.set(name, list);
2530
2433
  }
2531
2434
  }
@@ -2535,13 +2438,13 @@ function getComposedStubBody(
2535
2438
  if (candidatesForMethod.length !== 1 || methodName === "constructor")
2536
2439
  continue;
2537
2440
 
2538
- const { child, params, argNames } = candidatesForMethod[0];
2539
- const callArgs = argNames.join(", ");
2441
+ const { child, signature } = candidatesForMethod[0];
2442
+ const callArgs = getPomParameterArgumentNames(signature.parameters).join(", ");
2540
2443
 
2541
2444
  passthroughMembers.push(createClassMethod({
2542
2445
  name: methodName,
2543
2446
  isAsync: true,
2544
- parameters: parseParameterSignatures(params),
2447
+ parameters: toTypeScriptPomParameterStructures(signature.parameters),
2545
2448
  statements: [
2546
2449
  `return await this.${child}.${methodName}(${callArgs});`,
2547
2450
  ],
@@ -2579,6 +2482,7 @@ async function generateAggregatedFiles(
2579
2482
  customPomAttachments?: GenerateFilesOptions["customPomAttachments"];
2580
2483
  projectRoot?: GenerateFilesOptions["projectRoot"];
2581
2484
  customPomDir?: GenerateFilesOptions["customPomDir"];
2485
+ requireCustomPomDir?: GenerateFilesOptions["requireCustomPomDir"];
2582
2486
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
2583
2487
  customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
2584
2488
  testIdAttribute?: GenerateFilesOptions["testIdAttribute"];
@@ -2624,9 +2528,15 @@ async function generateAggregatedFiles(
2624
2528
 
2625
2529
  const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
2626
2530
  customPomDir: options.customPomDir,
2531
+ requireCustomPomDir: options.requireCustomPomDir,
2627
2532
  customPomImportAliases: options.customPomImportAliases,
2628
2533
  customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior,
2629
2534
  });
2535
+ assertCustomPomAttachmentsResolved(
2536
+ options.customPomAttachments ?? [],
2537
+ customPomImportResolution.classIdentifierMap,
2538
+ options.customPomDir ?? "tests/playwright/pom/custom",
2539
+ );
2630
2540
  const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
2631
2541
  const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
2632
2542
  const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
@@ -2826,10 +2736,10 @@ function getWidgetInstancesForView(
2826
2736
  };
2827
2737
 
2828
2738
  for (const dt of dataTestIdSet) {
2829
- const raw = dt.value;
2739
+ const raw = dt.selectorValue.formatted;
2830
2740
 
2831
- // Skip keyed/dynamic test ids; instance fields can't represent those ergonomically.
2832
- if (raw.includes("${")) {
2741
+ // Skip parameterized test ids; instance fields can't represent those ergonomically.
2742
+ if (isParameterizedPomPattern(dt.selectorValue.patternKind)) {
2833
2743
  continue;
2834
2744
  }
2835
2745