@immense/vue-pom-generator 1.0.58 → 1.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -18
- package/RELEASE_NOTES.md +75 -29
- package/class-generation/base-page.ts +6 -13
- package/class-generation/index.ts +226 -317
- package/class-generation/playwright-types.ts +1 -1
- package/click-instrumentation.ts +0 -4
- package/dist/class-generation/base-page.d.ts +1 -0
- package/dist/class-generation/base-page.d.ts.map +1 -1
- package/dist/class-generation/index.d.ts +2 -0
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/class-generation/playwright-types.d.ts +1 -1
- package/dist/class-generation/playwright-types.d.ts.map +1 -1
- package/dist/click-instrumentation.d.ts +0 -1
- package/dist/click-instrumentation.d.ts.map +1 -1
- package/dist/index.cjs +1216 -1008
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1218 -1010
- package/dist/index.mjs.map +1 -1
- package/dist/method-generation.d.ts +4 -2
- package/dist/method-generation.d.ts.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
- package/dist/plugin/resolved-generation-options.d.ts +33 -0
- package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
- package/dist/plugin/resolved-injection-options.d.ts +27 -0
- package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
- package/dist/plugin/support/build-plugin.d.ts +2 -29
- package/dist/plugin/support/build-plugin.d.ts.map +1 -1
- package/dist/plugin/support/dev-plugin.d.ts +2 -28
- package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
- package/dist/plugin/support-plugins.d.ts +2 -32
- package/dist/plugin/support-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +6 -23
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/vue-plugin.d.ts.map +1 -1
- package/dist/pom-params.d.ts +40 -0
- package/dist/pom-params.d.ts.map +1 -0
- package/dist/pom-patterns.d.ts +31 -0
- package/dist/pom-patterns.d.ts.map +1 -0
- package/dist/routing/to-directive.d.ts +21 -0
- package/dist/routing/to-directive.d.ts.map +1 -1
- package/dist/tests/base-page.test.d.ts +2 -0
- package/dist/tests/base-page.test.d.ts.map +1 -0
- package/dist/tests/resolved-injection-options.test.d.ts +2 -0
- package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
- package/dist/transform.d.ts +0 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/utils.d.ts +129 -63
- package/dist/utils.d.ts.map +1 -1
- package/package.json +6 -4
- 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
|
-
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
461
|
-
const
|
|
462
|
-
|
|
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 =
|
|
465
|
-
const hasWait =
|
|
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
|
|
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(
|
|
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
|
-
|
|
499
|
-
writer.writeLine(
|
|
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
|
|
508
|
-
const
|
|
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
|
-
|
|
528
|
-
writer.writeLine(
|
|
411
|
+
for (const statement of rootBinding.setupStatements) {
|
|
412
|
+
writer.writeLine(statement);
|
|
529
413
|
}
|
|
530
|
-
|
|
531
|
-
writer.writeLine(
|
|
414
|
+
for (const statement of labelBinding.setupStatements) {
|
|
415
|
+
writer.writeLine(statement);
|
|
532
416
|
}
|
|
533
|
-
writer.writeLine(`await this.clickWithinTestIdByLabel(${
|
|
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.
|
|
549
|
-
primary.
|
|
432
|
+
primary.selector,
|
|
433
|
+
primary.alternateSelectors,
|
|
550
434
|
primary.getterNameOverride,
|
|
551
|
-
primary.
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
985
|
+
if (param.initializer !== undefined) {
|
|
1113
986
|
if (type === "bool") {
|
|
1114
|
-
defaultExpr =
|
|
987
|
+
defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : undefined;
|
|
1115
988
|
}
|
|
1116
989
|
else if (type === "int") {
|
|
1117
|
-
const m =
|
|
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 (
|
|
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:
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
1143
|
-
const { type, defaultExpr } = toCSharpParam(
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
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 =
|
|
1279
|
-
.filter((v, idx, arr) => v && arr.indexOf(v) === idx);
|
|
1133
|
+
const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
|
|
1280
1134
|
|
|
1281
|
-
if (
|
|
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 (
|
|
1304
|
-
chunks.push(` await ${locatorName}${
|
|
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(
|
|
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 =
|
|
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 (!
|
|
1208
|
+
if (!selectorIsParameterized && allTestIds.length > 1) {
|
|
1355
1209
|
chunks.push(" Exception? lastError = null;");
|
|
1356
|
-
chunks.push(` foreach (var testId in new[] { ${allTestIds.map(
|
|
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
|
|
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
|
|
1421
|
-
const
|
|
1422
|
-
|
|
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
|
|
1432
|
-
const
|
|
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
|
-
|
|
1438
|
-
chunks.push(`
|
|
1290
|
+
for (const statement of rootBinding.setupStatements) {
|
|
1291
|
+
chunks.push(` ${statement}`);
|
|
1439
1292
|
}
|
|
1440
|
-
|
|
1441
|
-
chunks.push(`
|
|
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,
|
|
1793
|
-
: new Map<string,
|
|
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,
|
|
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;
|
|
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,
|
|
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,
|
|
2039
|
-
const callArgs =
|
|
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:
|
|
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,
|
|
2062
|
-
const methodToAttachments = new Map<string, Array<{ propertyName: 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
|
-
|
|
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,
|
|
2092
|
-
const callArgs =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2178
|
-
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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,
|
|
2539
|
-
const callArgs =
|
|
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:
|
|
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.
|
|
2738
|
+
const raw = dt.selectorValue.formatted;
|
|
2830
2739
|
|
|
2831
|
-
// Skip
|
|
2832
|
-
if (
|
|
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
|
|