@immense/vue-pom-generator 1.0.37 → 1.0.39
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 +956 -157
- package/RELEASE_NOTES.md +19 -23
- package/class-generation/index.ts +271 -32
- package/dist/class-generation/index.d.ts +12 -10
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/index.cjs +185 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +185 -15
- package/dist/index.mjs.map +1 -1
- package/dist/plugin/support/build-plugin.d.ts +1 -0
- package/dist/plugin/support/build-plugin.d.ts.map +1 -1
- package/dist/plugin/support/dev-plugin.d.ts +1 -0
- package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
- package/dist/plugin/support-plugins.d.ts +1 -0
- package/dist/plugin/support-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +8 -2
- package/dist/plugin/types.d.ts.map +1 -1
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,44 +1,40 @@
|
|
|
1
|
-
● # Release Notes: v1.0.
|
|
1
|
+
● # Release Notes: v1.0.39
|
|
2
2
|
|
|
3
3
|
## Highlights
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
5
|
+
- **Flattened helper attachments**: New capability in class generation for handling helper
|
|
6
|
+
attachments in a flattened structure
|
|
7
|
+
- **Extensive documentation updates**: README expanded with over 1,100 new lines of
|
|
8
|
+
documentation
|
|
9
|
+
- **Enhanced test coverage**: Added 89 lines of new tests for generated TypeScript compilation
|
|
10
|
+
- **PR automation**: Added release notes preview comments on pull requests
|
|
11
11
|
|
|
12
12
|
## Changes
|
|
13
13
|
|
|
14
|
-
###
|
|
15
|
-
-
|
|
16
|
-
- Refactored `create-vue-pom-generator-plugins.ts` with 134 lines of improvements
|
|
17
|
-
- Enhanced `build-plugin.ts` with additional capabilities and refinements
|
|
18
|
-
- Simplified `dev-plugin.ts` by removing ~34 lines of redundant code
|
|
19
|
-
- Updated plugin types with new interfaces and type definitions
|
|
14
|
+
### Features
|
|
15
|
+
- Add flattened helper attachments support in class generation (`class-generation/index.ts`)
|
|
20
16
|
|
|
21
17
|
### Documentation
|
|
22
|
-
-
|
|
18
|
+
- Significantly expanded README with detailed documentation (+1,118 lines)
|
|
23
19
|
|
|
24
20
|
### Testing
|
|
25
|
-
-
|
|
21
|
+
- Enhanced `tests/generated-tsc.test.ts` with additional test cases (+89 lines)
|
|
26
22
|
|
|
27
|
-
###
|
|
28
|
-
-
|
|
29
|
-
-
|
|
23
|
+
### Tooling
|
|
24
|
+
- Added PR release-notes preview comment automation
|
|
25
|
+
- Minor updates to plugin type definitions and support files
|
|
30
26
|
|
|
31
27
|
## Breaking Changes
|
|
32
28
|
|
|
33
|
-
None
|
|
29
|
+
None
|
|
34
30
|
|
|
35
31
|
## Pull Requests Included
|
|
36
32
|
|
|
37
|
-
-
|
|
38
|
-
|
|
33
|
+
- #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
|
|
34
|
+
by @dkattan
|
|
39
35
|
|
|
40
36
|
## Testing
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
coverage.
|
|
38
|
+
Tests updated and expanded to cover new flattened helper attachments functionality. Added 89
|
|
39
|
+
lines of test coverage in `tests/generated-tsc.test.ts`.
|
|
44
40
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import type { ClassMethod } from "@babel/types";
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import process from "node:process";
|
|
@@ -62,6 +64,28 @@ interface RouteMeta {
|
|
|
62
64
|
template: string;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
interface CustomPomMethodSignature {
|
|
68
|
+
params: string;
|
|
69
|
+
argNames: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type CustomPomMethodSignatureMap = Map<string, CustomPomMethodSignature>;
|
|
73
|
+
|
|
74
|
+
interface CustomPomAttachment {
|
|
75
|
+
className: string;
|
|
76
|
+
propertyName: string;
|
|
77
|
+
attachWhenUsesComponents: string[];
|
|
78
|
+
attachTo?: "views" | "components" | "both";
|
|
79
|
+
flatten?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ResolvedCustomPomAttachment {
|
|
83
|
+
className: string;
|
|
84
|
+
propertyName: string;
|
|
85
|
+
flatten: boolean;
|
|
86
|
+
methodSignatures: CustomPomMethodSignatureMap;
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
async function getRouteMetaByComponent(
|
|
66
90
|
projectRoot?: string,
|
|
67
91
|
routerEntry?: string,
|
|
@@ -321,6 +345,10 @@ export interface GenerateFilesOptions {
|
|
|
321
345
|
* Default output (when `true`):
|
|
322
346
|
* - `<projectRoot>/<outDir>/fixtures.g.ts`
|
|
323
347
|
*
|
|
348
|
+
* Convention:
|
|
349
|
+
* - fixtures automatically prefer matching handwritten override classes from
|
|
350
|
+
* `<dirname(customPomDir)>/overrides/<ClassName>.ts` when present
|
|
351
|
+
*
|
|
324
352
|
* Accepted values:
|
|
325
353
|
* - `true`: enable with defaults
|
|
326
354
|
* - `"path"`: enable and write the fixture file under this directory (resolved relative to projectRoot),
|
|
@@ -364,17 +392,7 @@ export interface GenerateFilesOptions {
|
|
|
364
392
|
* aggregated output (e.g. via `tests/playwright/pom/custom/*.ts` inlining), but we only attach them to
|
|
365
393
|
* view classes that actually use certain components.
|
|
366
394
|
*/
|
|
367
|
-
customPomAttachments?:
|
|
368
|
-
className: string;
|
|
369
|
-
propertyName: string;
|
|
370
|
-
attachWhenUsesComponents: string[];
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Controls whether this attachment is applied to views, components, or both.
|
|
374
|
-
* Defaults to "views" for backwards compatibility.
|
|
375
|
-
*/
|
|
376
|
-
attachTo?: "views" | "components" | "both";
|
|
377
|
-
}>;
|
|
395
|
+
customPomAttachments?: CustomPomAttachment[];
|
|
378
396
|
|
|
379
397
|
/** Attribute name to treat as the test id. Defaults to `data-testid`. */
|
|
380
398
|
testIdAttribute?: string;
|
|
@@ -408,23 +426,14 @@ interface GenerateContentOptions {
|
|
|
408
426
|
/** When true, omit file headers/import blocks that should be shared in an aggregated file. */
|
|
409
427
|
aggregated?: boolean;
|
|
410
428
|
|
|
411
|
-
customPomAttachments?:
|
|
412
|
-
className: string;
|
|
413
|
-
propertyName: string;
|
|
414
|
-
attachWhenUsesComponents: string[];
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Controls whether this attachment is applied to views, components, or both.
|
|
418
|
-
* Defaults to "views" for backwards compatibility.
|
|
419
|
-
*/
|
|
420
|
-
attachTo?: "views" | "components" | "both";
|
|
421
|
-
}>;
|
|
429
|
+
customPomAttachments?: CustomPomAttachment[];
|
|
422
430
|
|
|
423
431
|
projectRoot?: string;
|
|
424
432
|
customPomDir?: string;
|
|
425
433
|
customPomImportAliases?: Record<string, string>;
|
|
426
434
|
customPomClassIdentifierMap?: Record<string, string>;
|
|
427
435
|
customPomAvailableClassIdentifiers?: Set<string>;
|
|
436
|
+
customPomMethodSignaturesByClass?: Map<string, CustomPomMethodSignatureMap>;
|
|
428
437
|
|
|
429
438
|
/** Attribute name to treat as the test id. Defaults to `data-testid`. */
|
|
430
439
|
testIdAttribute?: string;
|
|
@@ -505,6 +514,7 @@ export async function generateFiles(
|
|
|
505
514
|
generateFixtures,
|
|
506
515
|
pomOutDir: outDir,
|
|
507
516
|
projectRoot,
|
|
517
|
+
customPomDir,
|
|
508
518
|
});
|
|
509
519
|
if (fixtureRegistryFile) {
|
|
510
520
|
writeGeneratedFile(fixtureRegistryFile);
|
|
@@ -1004,6 +1014,7 @@ function maybeGenerateFixtureRegistry(
|
|
|
1004
1014
|
generateFixtures: GenerateFilesOptions["generateFixtures"];
|
|
1005
1015
|
pomOutDir: string;
|
|
1006
1016
|
projectRoot?: string;
|
|
1017
|
+
customPomDir?: string;
|
|
1007
1018
|
},
|
|
1008
1019
|
): GeneratedFileOutput | null {
|
|
1009
1020
|
const { generateFixtures, pomOutDir } = options;
|
|
@@ -1030,6 +1041,12 @@ function maybeGenerateFixtureRegistry(
|
|
|
1030
1041
|
? fixtureOutDirRel
|
|
1031
1042
|
: path.resolve(root, fixtureOutDirRel);
|
|
1032
1043
|
|
|
1044
|
+
const customPomDirRel = options.customPomDir ?? "tests/playwright/pom/custom";
|
|
1045
|
+
const customPomDirAbs = path.isAbsolute(customPomDirRel)
|
|
1046
|
+
? customPomDirRel
|
|
1047
|
+
: path.resolve(root, customPomDirRel);
|
|
1048
|
+
const overridePomDirAbs = path.resolve(path.dirname(customPomDirAbs), "overrides");
|
|
1049
|
+
|
|
1033
1050
|
// Resolve the directory that contains the POM barrel export (e.g. <root>/pom).
|
|
1034
1051
|
const pomDirAbs = path.isAbsolute(pomOutDir) ? pomOutDir : path.resolve(root, pomOutDir);
|
|
1035
1052
|
|
|
@@ -1066,6 +1083,29 @@ function maybeGenerateFixtureRegistry(
|
|
|
1066
1083
|
})
|
|
1067
1084
|
.sort((a, b) => a.localeCompare(b));
|
|
1068
1085
|
|
|
1086
|
+
const fixtureClassNames = [...viewClassNames, ...componentClassNames];
|
|
1087
|
+
const overrideCtorEntries = fixtureClassNames
|
|
1088
|
+
.map((name) => {
|
|
1089
|
+
const overrideFilePath = path.join(overridePomDirAbs, `${name}.ts`);
|
|
1090
|
+
if (!fs.existsSync(overrideFilePath))
|
|
1091
|
+
return null;
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
className: name,
|
|
1095
|
+
localIdentifier: `${name}Override`,
|
|
1096
|
+
importSpecifier: stripExtension(toPosixRelativePath(fixtureOutDirAbs, overrideFilePath)),
|
|
1097
|
+
};
|
|
1098
|
+
})
|
|
1099
|
+
.filter((entry): entry is { className: string; localIdentifier: string; importSpecifier: string } => !!entry);
|
|
1100
|
+
const overrideCtorByClassName = new Map(overrideCtorEntries.map(entry => [entry.className, entry.localIdentifier]));
|
|
1101
|
+
const overrideImports = overrideCtorEntries.length
|
|
1102
|
+
? `${overrideCtorEntries
|
|
1103
|
+
.map(entry => `import { ${entry.className} as ${entry.localIdentifier} } from "${entry.importSpecifier}";`)
|
|
1104
|
+
.join("\n")}\n\n`
|
|
1105
|
+
: "";
|
|
1106
|
+
|
|
1107
|
+
const fixtureCtorExpression = (name: string) => overrideCtorByClassName.get(name) ?? `Pom.${name}`;
|
|
1108
|
+
|
|
1069
1109
|
const header = `${eslintSuppressionHeader}/**\n`
|
|
1070
1110
|
+ ` * DO NOT MODIFY BY HAND\n`
|
|
1071
1111
|
+ ` *\n`
|
|
@@ -1079,11 +1119,11 @@ function maybeGenerateFixtureRegistry(
|
|
|
1079
1119
|
// View POMs implement goTo() directly, so fixtures can be strongly typed without
|
|
1080
1120
|
// casting/augmenting at runtime.
|
|
1081
1121
|
const fixturesTypeEntries = viewClassNames
|
|
1082
|
-
.map(name => ` ${lowerFirst(name)}:
|
|
1122
|
+
.map(name => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`)
|
|
1083
1123
|
.join("\n");
|
|
1084
1124
|
|
|
1085
1125
|
const componentFixturesTypeEntries = componentClassNames
|
|
1086
|
-
.map(name => ` ${lowerFirst(name)}:
|
|
1126
|
+
.map(name => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`)
|
|
1087
1127
|
.join("\n");
|
|
1088
1128
|
|
|
1089
1129
|
const pomFactoryType = `export type PomConstructor<T> = new (page: PwPage) => T;\n\n`
|
|
@@ -1100,7 +1140,8 @@ function maybeGenerateFixtureRegistry(
|
|
|
1100
1140
|
}/** Generated Playwright fixtures (typed page objects). */\n\n`
|
|
1101
1141
|
+ `import { expect, test as base } from "@playwright/test";\n`
|
|
1102
1142
|
+ `import type { Page as PwPage } from "@playwright/test";\n`
|
|
1103
|
-
+ `import * as Pom from "${pomImport}";\n
|
|
1143
|
+
+ `import * as Pom from "${pomImport}";\n`
|
|
1144
|
+
+ `${overrideImports}`
|
|
1104
1145
|
+ `export interface PlaywrightOptions {\n`
|
|
1105
1146
|
+ ` animation: Pom.PlaywrightAnimationOptions;\n`
|
|
1106
1147
|
+ `}\n\n`
|
|
@@ -1180,6 +1221,7 @@ function generateViewObjectModelContent(
|
|
|
1180
1221
|
|
|
1181
1222
|
const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
|
|
1182
1223
|
const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? new Set<string>();
|
|
1224
|
+
const customPomMethodSignaturesByClass = options.customPomMethodSignaturesByClass ?? new Map<string, CustomPomMethodSignatureMap>();
|
|
1183
1225
|
|
|
1184
1226
|
const attachmentsForThisClass = customPomAttachments
|
|
1185
1227
|
.filter((a) => {
|
|
@@ -1197,6 +1239,10 @@ function generateViewObjectModelContent(
|
|
|
1197
1239
|
.map(a => ({
|
|
1198
1240
|
className: customPomClassIdentifierMap[a.className]!,
|
|
1199
1241
|
propertyName: a.propertyName,
|
|
1242
|
+
flatten: a.flatten ?? false,
|
|
1243
|
+
methodSignatures: a.flatten
|
|
1244
|
+
? (customPomMethodSignaturesByClass.get(a.className) ?? new Map<string, CustomPomMethodSignature>())
|
|
1245
|
+
: new Map<string, CustomPomMethodSignature>(),
|
|
1200
1246
|
}));
|
|
1201
1247
|
|
|
1202
1248
|
let content: string = "";
|
|
@@ -1259,6 +1305,19 @@ function generateViewObjectModelContent(
|
|
|
1259
1305
|
const componentRefsForInstances = isView
|
|
1260
1306
|
? (usedComponentSet?.size ? usedComponentSet : childrenComponentSet)
|
|
1261
1307
|
: childrenComponentSet;
|
|
1308
|
+
const childInstancePropertyNames = Array.from(componentRefsForInstances)
|
|
1309
|
+
.filter(child => componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size)
|
|
1310
|
+
.map(child => child.split(".vue")[0]);
|
|
1311
|
+
const blockedViewPassthroughMethodNames = new Set(
|
|
1312
|
+
attachmentsForThisClass
|
|
1313
|
+
.filter(a => a.flatten)
|
|
1314
|
+
.flatMap(a => Array.from(a.methodSignatures.keys())),
|
|
1315
|
+
);
|
|
1316
|
+
const reservedAttachmentPassthroughNames = new Set<string>([
|
|
1317
|
+
...attachmentsForThisClass.map(a => a.propertyName),
|
|
1318
|
+
...widgetInstances.map(w => w.propertyName),
|
|
1319
|
+
...childInstancePropertyNames,
|
|
1320
|
+
]);
|
|
1262
1321
|
|
|
1263
1322
|
// Only views get child component instance fields by default.
|
|
1264
1323
|
// Components will only get a constructor/fields when they have explicit custom attachments
|
|
@@ -1271,6 +1330,7 @@ function generateViewObjectModelContent(
|
|
|
1271
1330
|
content += getComponentInstances(new Set(), componentHierarchyMap, attachmentsForThisClass);
|
|
1272
1331
|
content += getConstructor(new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute });
|
|
1273
1332
|
}
|
|
1333
|
+
content += getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames);
|
|
1274
1334
|
|
|
1275
1335
|
// Ergonomics: when a view is primarily composed of a single component POM (e.g. a form),
|
|
1276
1336
|
// allow calling that component's methods directly on the page class.
|
|
@@ -1286,7 +1346,7 @@ function generateViewObjectModelContent(
|
|
|
1286
1346
|
// around a single child component POM. This prevents "layout" components (Page, PageHeader,
|
|
1287
1347
|
// etc.) from injecting lots of noisy passthrough APIs into every view.
|
|
1288
1348
|
if (isView && componentRefsForInstances.size === 1) {
|
|
1289
|
-
content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap);
|
|
1349
|
+
content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap, blockedViewPassthroughMethodNames);
|
|
1290
1350
|
}
|
|
1291
1351
|
|
|
1292
1352
|
if (isView && options.vueRouterFluentChaining) {
|
|
@@ -1307,6 +1367,7 @@ function getViewPassthroughMethods(
|
|
|
1307
1367
|
viewDependencies: IComponentDependencies,
|
|
1308
1368
|
childrenComponentSet: Set<string>,
|
|
1309
1369
|
componentHierarchyMap: Map<string, IComponentDependencies>,
|
|
1370
|
+
blockedMethodNames: Set<string> = new Set(),
|
|
1310
1371
|
) {
|
|
1311
1372
|
const existingOnView = viewDependencies.generatedMethods ?? new Map<string, { params: string; argNames: string[] } | null>();
|
|
1312
1373
|
|
|
@@ -1330,7 +1391,7 @@ function getViewPassthroughMethods(
|
|
|
1330
1391
|
continue; // ambiguous on the child itself
|
|
1331
1392
|
|
|
1332
1393
|
// If the view already has this method name, never generate a pass-through.
|
|
1333
|
-
if (existingOnView.has(name))
|
|
1394
|
+
if (existingOnView.has(name) || blockedMethodNames.has(name))
|
|
1334
1395
|
continue;
|
|
1335
1396
|
|
|
1336
1397
|
const list = methodToChildren.get(name) ?? [];
|
|
@@ -1370,6 +1431,169 @@ function getViewPassthroughMethods(
|
|
|
1370
1431
|
].join("\n");
|
|
1371
1432
|
}
|
|
1372
1433
|
|
|
1434
|
+
function getAttachmentPassthroughMethods(
|
|
1435
|
+
ownerName: string,
|
|
1436
|
+
ownerDependencies: IComponentDependencies,
|
|
1437
|
+
attachmentsForThisClass: ResolvedCustomPomAttachment[],
|
|
1438
|
+
reservedMemberNames: Set<string>,
|
|
1439
|
+
) {
|
|
1440
|
+
if (!attachmentsForThisClass.some(a => a.flatten && a.methodSignatures.size > 0)) {
|
|
1441
|
+
return "";
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const existingOnClass = ownerDependencies.generatedMethods ?? new Map<string, { params: string; argNames: string[] } | null>();
|
|
1445
|
+
const methodToAttachments = new Map<string, Array<{ propertyName: string; params: string; argNames: string[] }>>();
|
|
1446
|
+
|
|
1447
|
+
for (const attachment of attachmentsForThisClass) {
|
|
1448
|
+
if (!attachment.flatten) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
for (const [methodName, signature] of attachment.methodSignatures.entries()) {
|
|
1453
|
+
if (methodName === "constructor" || existingOnClass.has(methodName) || reservedMemberNames.has(methodName)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const list = methodToAttachments.get(methodName) ?? [];
|
|
1458
|
+
list.push({
|
|
1459
|
+
propertyName: attachment.propertyName,
|
|
1460
|
+
params: signature.params,
|
|
1461
|
+
argNames: signature.argNames,
|
|
1462
|
+
});
|
|
1463
|
+
methodToAttachments.set(methodName, list);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const sorted = Array.from(methodToAttachments.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
1468
|
+
const lines: string[] = [];
|
|
1469
|
+
|
|
1470
|
+
for (const [methodName, candidates] of sorted) {
|
|
1471
|
+
if (candidates.length !== 1) {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const { propertyName, params, argNames } = candidates[0];
|
|
1476
|
+
const callArgs = argNames.join(", ");
|
|
1477
|
+
const invocation = callArgs
|
|
1478
|
+
? `this.${propertyName}.${methodName}(${callArgs})`
|
|
1479
|
+
: `this.${propertyName}.${methodName}()`;
|
|
1480
|
+
|
|
1481
|
+
lines.push(
|
|
1482
|
+
"",
|
|
1483
|
+
` ${methodName}(${params}) {`,
|
|
1484
|
+
` return ${invocation};`,
|
|
1485
|
+
" }",
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (!lines.length) {
|
|
1490
|
+
return "";
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return [
|
|
1494
|
+
"",
|
|
1495
|
+
` // Passthrough methods composed from custom helper attachments of ${ownerName}.`,
|
|
1496
|
+
...lines,
|
|
1497
|
+
"",
|
|
1498
|
+
].join("\n");
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function sliceNodeSource(source: string, node: { start?: number | null; end?: number | null }): string | null {
|
|
1502
|
+
if (node.start == null || node.end == null) {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const snippet = source.slice(node.start, node.end).trim();
|
|
1507
|
+
return snippet.length ? snippet : null;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function getCustomPomCallArgumentName(param: ClassMethod["params"][number]): string | null {
|
|
1511
|
+
if (param.type === "Identifier") {
|
|
1512
|
+
return param.name;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (param.type === "AssignmentPattern") {
|
|
1516
|
+
return param.left.type === "Identifier" ? param.left.name : null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (param.type === "RestElement") {
|
|
1520
|
+
return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function extractCustomPomMethodSignatures(source: string, exportName: string): CustomPomMethodSignatureMap {
|
|
1527
|
+
const signatures: CustomPomMethodSignatureMap = new Map();
|
|
1528
|
+
|
|
1529
|
+
let ast: ReturnType<typeof parse>;
|
|
1530
|
+
try {
|
|
1531
|
+
ast = parse(source, {
|
|
1532
|
+
sourceType: "module",
|
|
1533
|
+
plugins: ["typescript", "jsx"],
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return signatures;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
for (const statement of ast.program.body) {
|
|
1541
|
+
if (statement.type !== "ExportNamedDeclaration" || !statement.declaration || statement.declaration.type !== "ClassDeclaration") {
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const declaration = statement.declaration;
|
|
1546
|
+
if (declaration.id?.name !== exportName) {
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
for (const member of declaration.body.body) {
|
|
1551
|
+
if (member.type !== "ClassMethod" || member.kind !== "method" || member.static || member.computed) {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (member.accessibility === "private" || member.accessibility === "protected") {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (member.key.type !== "Identifier") {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const params: string[] = [];
|
|
1564
|
+
const argNames: string[] = [];
|
|
1565
|
+
let supported = true;
|
|
1566
|
+
|
|
1567
|
+
member.params.forEach((param) => {
|
|
1568
|
+
if (!supported) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const paramSource = sliceNodeSource(source, param);
|
|
1573
|
+
const argName = getCustomPomCallArgumentName(param);
|
|
1574
|
+
if (!paramSource || !argName) {
|
|
1575
|
+
supported = false;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
params.push(paramSource);
|
|
1580
|
+
argNames.push(argName);
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
if (!supported) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
signatures.set(member.key.name, {
|
|
1588
|
+
params: params.join(", "),
|
|
1589
|
+
argNames,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return signatures;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1373
1597
|
function ensureDir(dir: string) {
|
|
1374
1598
|
const normalized = dir.replace(/\\/g, "/");
|
|
1375
1599
|
if (!fs.existsSync(normalized)) {
|
|
@@ -1453,6 +1677,7 @@ async function generateAggregatedFiles(
|
|
|
1453
1677
|
]);
|
|
1454
1678
|
const usedImportIdentifiers = new Set<string>();
|
|
1455
1679
|
const customPomClassIdentifierMap: Record<string, string> = {};
|
|
1680
|
+
const customPomMethodSignaturesByClass = new Map<string, CustomPomMethodSignatureMap>();
|
|
1456
1681
|
|
|
1457
1682
|
const ensureUniqueIdentifier = (base: string) => {
|
|
1458
1683
|
let candidate = base;
|
|
@@ -1471,7 +1696,10 @@ async function generateAggregatedFiles(
|
|
|
1471
1696
|
: path.resolve(projectRoot, customDirRelOrAbs);
|
|
1472
1697
|
|
|
1473
1698
|
if (!fs.existsSync(customDirAbs)) {
|
|
1474
|
-
return
|
|
1699
|
+
return {
|
|
1700
|
+
classIdentifierMap: customPomClassIdentifierMap,
|
|
1701
|
+
methodSignaturesByClass: customPomMethodSignaturesByClass,
|
|
1702
|
+
};
|
|
1475
1703
|
}
|
|
1476
1704
|
|
|
1477
1705
|
const files = fs.readdirSync(customDirAbs)
|
|
@@ -1504,8 +1732,13 @@ async function generateAggregatedFiles(
|
|
|
1504
1732
|
localIdentifier = ensureUniqueIdentifier(requested);
|
|
1505
1733
|
}
|
|
1506
1734
|
|
|
1507
|
-
customPomClassIdentifierMap[exportName] = localIdentifier;
|
|
1508
1735
|
const customFileAbs = path.join(customDirAbs, file);
|
|
1736
|
+
customPomClassIdentifierMap[exportName] = localIdentifier;
|
|
1737
|
+
const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
|
|
1738
|
+
if (customPomMethodSignatures.size > 0) {
|
|
1739
|
+
customPomMethodSignaturesByClass.set(exportName, customPomMethodSignatures);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1509
1742
|
const fromOutputDir = outputDir;
|
|
1510
1743
|
const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
|
|
1511
1744
|
if (localIdentifier !== exportName) {
|
|
@@ -1516,11 +1749,16 @@ async function generateAggregatedFiles(
|
|
|
1516
1749
|
}
|
|
1517
1750
|
}
|
|
1518
1751
|
|
|
1519
|
-
return
|
|
1752
|
+
return {
|
|
1753
|
+
classIdentifierMap: customPomClassIdentifierMap,
|
|
1754
|
+
methodSignaturesByClass: customPomMethodSignaturesByClass,
|
|
1755
|
+
};
|
|
1520
1756
|
};
|
|
1521
1757
|
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1758
|
+
const customPomImportResolution = addCustomPomImports();
|
|
1759
|
+
const customPomClassIdentifierMap = customPomImportResolution?.classIdentifierMap ?? {};
|
|
1760
|
+
const customPomMethodSignaturesByClass = customPomImportResolution?.methodSignaturesByClass ?? new Map<string, CustomPomMethodSignatureMap>();
|
|
1761
|
+
const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap));
|
|
1524
1762
|
|
|
1525
1763
|
// Collect any navigation return types referenced by generated methods so we can emit
|
|
1526
1764
|
// stub classes when the destination view has no generated test ids (and therefore no
|
|
@@ -1723,6 +1961,7 @@ async function generateAggregatedFiles(
|
|
|
1723
1961
|
customPomAttachments: options.customPomAttachments ?? [],
|
|
1724
1962
|
customPomClassIdentifierMap,
|
|
1725
1963
|
customPomAvailableClassIdentifiers,
|
|
1964
|
+
customPomMethodSignaturesByClass,
|
|
1726
1965
|
testIdAttribute: options.testIdAttribute,
|
|
1727
1966
|
vueRouterFluentChaining: options.vueRouterFluentChaining,
|
|
1728
1967
|
routeMetaByComponent: options.routeMetaByComponent,
|
|
@@ -4,6 +4,13 @@ export { generateViewObjectModelMethodContent };
|
|
|
4
4
|
interface RouteMeta {
|
|
5
5
|
template: string;
|
|
6
6
|
}
|
|
7
|
+
interface CustomPomAttachment {
|
|
8
|
+
className: string;
|
|
9
|
+
propertyName: string;
|
|
10
|
+
attachWhenUsesComponents: string[];
|
|
11
|
+
attachTo?: "views" | "components" | "both";
|
|
12
|
+
flatten?: boolean;
|
|
13
|
+
}
|
|
7
14
|
export interface GenerateFilesOptions {
|
|
8
15
|
/**
|
|
9
16
|
* Output directory for generated files.
|
|
@@ -17,6 +24,10 @@ export interface GenerateFilesOptions {
|
|
|
17
24
|
* Default output (when `true`):
|
|
18
25
|
* - `<projectRoot>/<outDir>/fixtures.g.ts`
|
|
19
26
|
*
|
|
27
|
+
* Convention:
|
|
28
|
+
* - fixtures automatically prefer matching handwritten override classes from
|
|
29
|
+
* `<dirname(customPomDir)>/overrides/<ClassName>.ts` when present
|
|
30
|
+
*
|
|
20
31
|
* Accepted values:
|
|
21
32
|
* - `true`: enable with defaults
|
|
22
33
|
* - `"path"`: enable and write the fixture file under this directory (resolved relative to projectRoot),
|
|
@@ -57,16 +68,7 @@ export interface GenerateFilesOptions {
|
|
|
57
68
|
* aggregated output (e.g. via `tests/playwright/pom/custom/*.ts` inlining), but we only attach them to
|
|
58
69
|
* view classes that actually use certain components.
|
|
59
70
|
*/
|
|
60
|
-
customPomAttachments?:
|
|
61
|
-
className: string;
|
|
62
|
-
propertyName: string;
|
|
63
|
-
attachWhenUsesComponents: string[];
|
|
64
|
-
/**
|
|
65
|
-
* Controls whether this attachment is applied to views, components, or both.
|
|
66
|
-
* Defaults to "views" for backwards compatibility.
|
|
67
|
-
*/
|
|
68
|
-
attachTo?: "views" | "components" | "both";
|
|
69
|
-
}>;
|
|
71
|
+
customPomAttachments?: CustomPomAttachment[];
|
|
70
72
|
/** Attribute name to treat as the test id. Defaults to `data-testid`. */
|
|
71
73
|
testIdAttribute?: string;
|
|
72
74
|
/** Which POM languages to emit. Defaults to ["ts"]. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA8PD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAE7C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AA+BD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBAmFnC"}
|