@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/RELEASE_NOTES.md CHANGED
@@ -1,44 +1,40 @@
1
- ● # Release Notes: v1.0.37
1
+ ● # Release Notes: v1.0.39
2
2
 
3
3
  ## Highlights
4
4
 
5
- - Added support for external Vue plugin ownership, enabling better plugin architecture
6
- extensibility
7
- - Stabilized generator plugin refactor with improved plugin handling and configuration
8
- - Enhanced build plugin with expanded functionality (72+ lines added)
9
- - Refactored test suite with improved coverage and organization
10
- - Added automated PR release-notes preview comments to development workflow
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
- ### Plugin System
15
- - Added external Vue plugin ownership support for more flexible plugin architecture
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
- - Expanded README.md with 30+ lines of additional documentation
18
+ - Significantly expanded README with detailed documentation (+1,118 lines)
23
19
 
24
20
  ### Testing
25
- - Restructured `options.test.ts` with 163 lines of changes for better test coverage and clarity
21
+ - Enhanced `tests/generated-tsc.test.ts` with additional test cases (+89 lines)
26
22
 
27
- ### Code Quality
28
- - Improved class generation logic
29
- - Refined Vue plugin integration points
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 identified.
29
+ None
34
30
 
35
31
  ## Pull Requests Included
36
32
 
37
- - [#1](https://github.com/immense/vue-pom-generator/pull/1) Add PR release-notes preview
38
- comments (@dkattan)
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
- Test suite refactored and expanded in `tests/options.test.ts` with improved organization and
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?: Array<{
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?: Array<{
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)}: Pom.${name},`)
1122
+ .map(name => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`)
1083
1123
  .join("\n");
1084
1124
 
1085
1125
  const componentFixturesTypeEntries = componentClassNames
1086
- .map(name => ` ${lowerFirst(name)}: Pom.${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\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 customPomClassIdentifierMap;
1752
+ return {
1753
+ classIdentifierMap: customPomClassIdentifierMap,
1754
+ methodSignaturesByClass: customPomMethodSignaturesByClass,
1755
+ };
1520
1756
  };
1521
1757
 
1522
- const customPomClassIdentifierMap = addCustomPomImports();
1523
- const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
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?: Array<{
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":"AAIA,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;AAuPD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;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,KAAK,CAAC;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;QAEnC;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;KAC5C,CAAC,CAAC;IAEH,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;AAwCD,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,iBAkFnC"}
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"}