@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.
- package/README.md +36 -25
- package/RELEASE_NOTES.md +29 -31
- package/class-generation/base-page.ts +49 -25
- package/class-generation/index.ts +243 -333
- package/class-generation/playwright-types.ts +1 -1
- package/click-instrumentation.ts +0 -4
- package/dist/class-generation/base-page.d.ts +8 -4
- 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 +1527 -1064
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1529 -1066
- package/dist/index.mjs.map +1 -1
- package/dist/manifest-generator.d.ts +35 -1
- package/dist/manifest-generator.d.ts.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/virtual-modules.d.ts +3 -1
- package/dist/plugin/support/virtual-modules.d.ts.map +1 -1
- package/dist/plugin/support-plugins.d.ts +4 -33
- 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-discoverability.d.ts +10 -0
- package/dist/pom-discoverability.d.ts.map +1 -0
- 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/fixtures/generated-tsc/base-page.full.d.ts +7 -4
- package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts.map +1 -1
- 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,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
|
-
|
|
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
|
|
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(", ");
|
|
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
|
|
461
|
-
const
|
|
462
|
-
|
|
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 =
|
|
465
|
-
const hasWait =
|
|
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
|
|
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
|
-
|
|
499
|
-
writer.writeLine(
|
|
387
|
+
for (const statement of testIdBinding.setupStatements) {
|
|
388
|
+
writer.writeLine(statement);
|
|
500
389
|
}
|
|
501
|
-
writer.writeLine(`await this.clickByTestId(${
|
|
390
|
+
writer.writeLine(`await this.clickByTestId(${testIdBinding.expression}, ${annotationArg}, ${waitArg}, ${locatorDescription});`);
|
|
502
391
|
},
|
|
503
392
|
}),
|
|
504
393
|
];
|
|
505
394
|
}
|
|
506
395
|
|
|
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;
|
|
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
|
-
|
|
528
|
-
writer.writeLine(
|
|
407
|
+
for (const statement of rootBinding.setupStatements) {
|
|
408
|
+
writer.writeLine(statement);
|
|
529
409
|
}
|
|
530
|
-
|
|
531
|
-
writer.writeLine(
|
|
410
|
+
for (const statement of labelBinding.setupStatements) {
|
|
411
|
+
writer.writeLine(statement);
|
|
532
412
|
}
|
|
533
|
-
writer.writeLine(`await this.clickWithinTestIdByLabel(${
|
|
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(
|
|
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.
|
|
549
|
-
primary.
|
|
433
|
+
primary.selector,
|
|
434
|
+
primary.alternateSelectors,
|
|
550
435
|
primary.getterNameOverride,
|
|
551
|
-
primary.
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
986
|
+
if (param.initializer !== undefined) {
|
|
1113
987
|
if (type === "bool") {
|
|
1114
|
-
defaultExpr =
|
|
988
|
+
defaultExpr = param.initializer.includes("true") ? "true" : param.initializer.includes("false") ? "false" : undefined;
|
|
1115
989
|
}
|
|
1116
990
|
else if (type === "int") {
|
|
1117
|
-
const m =
|
|
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 (
|
|
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:
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
1143
|
-
const { type, defaultExpr } = toCSharpParam(
|
|
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
|
|
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
|
-
]);
|
|
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 =
|
|
1279
|
-
.filter((v, idx, arr) => v && arr.indexOf(v) === idx);
|
|
1134
|
+
const allTestIds = uniquePomStringPatterns(pom.selector, pom.alternateSelectors);
|
|
1280
1135
|
|
|
1281
|
-
if (
|
|
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 (
|
|
1304
|
-
chunks.push(` await ${locatorName}${
|
|
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(
|
|
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 =
|
|
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 (!
|
|
1209
|
+
if (!selectorIsParameterized && allTestIds.length > 1) {
|
|
1355
1210
|
chunks.push(" Exception? lastError = null;");
|
|
1356
|
-
chunks.push(` foreach (var testId in new[] { ${allTestIds.map(
|
|
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
|
|
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
|
|
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();`);
|
|
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
|
|
1432
|
-
const
|
|
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
|
-
|
|
1438
|
-
chunks.push(`
|
|
1291
|
+
for (const statement of rootBinding.setupStatements) {
|
|
1292
|
+
chunks.push(` ${statement}`);
|
|
1439
1293
|
}
|
|
1440
|
-
|
|
1441
|
-
chunks.push(`
|
|
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,
|
|
1793
|
-
: new Map<string,
|
|
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,
|
|
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;
|
|
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,
|
|
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,
|
|
2039
|
-
const callArgs =
|
|
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:
|
|
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,
|
|
2062
|
-
const methodToAttachments = new Map<string, Array<{ propertyName: 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
|
-
|
|
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,
|
|
2092
|
-
const callArgs =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2178
|
-
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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,
|
|
2539
|
-
const callArgs =
|
|
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:
|
|
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.
|
|
2739
|
+
const raw = dt.selectorValue.formatted;
|
|
2830
2740
|
|
|
2831
|
-
// Skip
|
|
2832
|
-
if (
|
|
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
|
|