@immense/vue-pom-generator 1.0.21 → 1.0.23

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 CHANGED
@@ -21,7 +21,11 @@ npm install @immense/vue-pom-generator
21
21
 
22
22
  ## Usage
23
23
 
24
- Exported entrypoint: `createVuePomGeneratorPlugins()`.
24
+ Exported entrypoints:
25
+
26
+ - `createVuePomGeneratorPlugins()`
27
+ - `vuePomGenerator()` (alias)
28
+ - `defineVuePomGeneratorConfig()` (typed config helper)
25
29
 
26
30
  ## Configuration
27
31
 
@@ -34,6 +38,7 @@ The generator emits an aggregated output under `generation.outDir` (default `tes
34
38
 
35
39
  - `tests/playwright/generated/page-object-models.g.ts` (generated; do not edit)
36
40
  - `tests/playwright/generated/index.ts` (generated stable barrel)
41
+ - managed `.gitattributes` files alongside generated outputs so GitHub Linguist treats them as generated by default
37
42
 
38
43
  If `generation.playwright.fixtures` is enabled, it also emits:
39
44
 
@@ -43,102 +48,103 @@ If `generation.playwright.fixtures` is enabled, it also emits:
43
48
 
44
49
  ```ts
45
50
  import { defineConfig } from "vite";
46
- import vue from "@vitejs/plugin-vue";
47
- import { createVuePomGeneratorPlugins } from "@immense/vue-pom-generator";
51
+ import { defineVuePomGeneratorConfig, vuePomGenerator } from "@immense/vue-pom-generator";
48
52
 
49
53
  export default defineConfig(() => {
50
54
  const vueOptions = {
51
55
  script: { defineModel: true, propsDestructure: true },
52
56
  };
53
57
 
54
- return {
55
- plugins: [
56
- ...createVuePomGeneratorPlugins({
57
- vueOptions,
58
- logging: { verbosity: "info" },
59
-
60
- injection: {
61
- // Attribute to inject/read as the test id (default: data-testid)
62
- attribute: "data-testid",
63
-
64
- // Used to classify Vue files as "views" vs components (default: src/views)
65
- viewsDir: "src/views",
66
-
67
- // Directories to scan for .vue files when building the POM library (default: ["src"])
68
- // For Nuxt, you might want ["app", "components", "pages", "layouts"]
69
- scanDirs: ["src"],
70
-
71
- // Optional: wrapper semantics for design-system components
72
- nativeWrappers: {
73
- MyButton: { role: "button" },
74
- MyInput: { role: "input" },
58
+ const pomConfig = defineVuePomGeneratorConfig({
59
+ vueOptions,
60
+ logging: { verbosity: "info" },
61
+
62
+ injection: {
63
+ // Attribute to inject/read as the test id (default: data-testid)
64
+ attribute: "data-testid",
65
+
66
+ // Used to classify Vue files as "views" vs components (default: src/views)
67
+ viewsDir: "src/views",
68
+
69
+ // Directories to scan for .vue files when building the POM library (default: ["src"])
70
+ // For Nuxt, you might want ["app", "components", "pages", "layouts"]
71
+ scanDirs: ["src"],
72
+
73
+ // Optional: wrapper semantics for design-system components
74
+ nativeWrappers: {
75
+ MyButton: { role: "button" },
76
+ MyInput: { role: "input" },
77
+ },
78
+
79
+ // Optional: opt specific components out of injection
80
+ excludeComponents: ["MyButton"],
81
+
82
+ // Optional: preserve/overwrite/error when an author already set the attribute
83
+ existingIdBehavior: "preserve",
84
+ },
85
+
86
+ generation: {
87
+ // Default: ["ts"]
88
+ emit: ["ts", "csharp"],
89
+
90
+ // C# specific configuration
91
+ csharp: {
92
+ // The namespace for generated C# classes (default: Playwright.Generated)
93
+ namespace: "MyProject.Tests.Generated",
94
+ },
95
+
96
+ // Default: tests/playwright/generated
97
+ outDir: "tests/playwright/generated",
98
+
99
+ // Controls how to handle duplicate generated member names within a single POM class.
100
+ // - "error": fail compilation
101
+ // - "warn": warn and suffix
102
+ // - "suffix": suffix silently (default)
103
+ nameCollisionBehavior: "suffix",
104
+
105
+ // Enable router introspection. When provided, router-aware POM helpers are generated.
106
+ router: {
107
+ // For standard Vue apps:
108
+ entry: "src/router.ts",
109
+ moduleShims: {
110
+ "@/config/app-insights": {
111
+ getAppInsights: () => null,
75
112
  },
76
-
77
- // Optional: opt specific components out of injection
78
- excludeComponents: ["MyButton"],
79
-
80
- // Optional: preserve/overwrite/error when an author already set the attribute
81
- existingIdBehavior: "preserve",
113
+ "@/store/pinia/app-alert-store": ["useAppAlertsStore"],
82
114
  },
83
-
84
- generation: {
85
- // Default: ["ts"]
86
- emit: ["ts", "csharp"],
87
-
88
- // C# specific configuration
89
- csharp: {
90
- // The namespace for generated C# classes (default: Playwright.Generated)
91
- namespace: "MyProject.Tests.Generated",
92
- },
93
-
94
- // Default: tests/playwright/generated
95
- outDir: "tests/playwright/generated",
96
-
97
- // Controls how to handle duplicate generated member names within a single POM class.
98
- // - "error": fail compilation
99
- // - "warn": warn and suffix
100
- // - "suffix": suffix silently (default)
101
- nameCollisionBehavior: "suffix",
102
-
103
- // Enable router introspection. When provided, router-aware POM helpers are generated.
104
- router: {
105
- // For standard Vue apps:
106
- entry: "src/router.ts",
107
- moduleShims: {
108
- "@/config/app-insights": {
109
- getAppInsights: () => null,
110
- },
111
- "@/store/pinia/app-alert-store": ["useAppAlertsStore"],
115
+ // For Nuxt apps (file-based routing):
116
+ // type: "nuxt"
117
+ },
118
+
119
+ playwright: {
120
+ fixtures: true,
121
+ customPoms: {
122
+ // Default: tests/playwright/pom/custom
123
+ dir: "tests/playwright/pom/custom",
124
+ importAliases: { MyCheckBox: "CheckboxWidget" },
125
+ attachments: [
126
+ {
127
+ className: "ConfirmationModal",
128
+ propertyName: "confirmationModal",
129
+ attachWhenUsesComponents: ["Page"],
112
130
  },
113
- // For Nuxt apps (file-based routing):
114
- // type: "nuxt"
115
- },
116
-
117
- playwright: {
118
- fixtures: true,
119
- customPoms: {
120
- // Default: tests/playwright/pom/custom
121
- dir: "tests/playwright/pom/custom",
122
- importAliases: { MyCheckBox: "CheckboxWidget" },
123
- attachments: [
124
- {
125
- className: "ConfirmationModal",
126
- propertyName: "confirmationModal",
127
- attachWhenUsesComponents: ["Page"],
128
- },
129
- ],
130
- },
131
- },
131
+ ],
132
132
  },
133
- }),
134
- vue(vueOptions),
135
- ],
133
+ },
134
+ },
135
+ });
136
+
137
+ return {
138
+ plugins: [...vuePomGenerator(pomConfig)],
136
139
  };
137
140
  });
138
141
  ```
139
142
 
140
143
  Notes:
141
144
 
145
+ - `vuePomGenerator(...)` already wires `@vitejs/plugin-vue` internally for non-Nuxt apps.
146
+ - Do not pass `vue()` into `createVuePomGeneratorPlugins(...)`; pass Vue options via `vueOptions`.
147
+
142
148
  - **Injection is enabled by plugin inclusion** (there is no longer an `injection.enabled` flag).
143
149
  - **Generation is enabled by default** and can be disabled via `generation: false`.
144
150
  - **Router-aware POM helpers are enabled** when `generation.router.entry` is provided (the generator will introspect your router).
package/RELEASE_NOTES.md CHANGED
@@ -1,44 +1,48 @@
1
- ```markdown
1
+ # Release v1.0.23
2
+
2
3
  ## Highlights
3
4
 
4
- - Added support for typed router module shims, enabling better TypeScript integration with Vue
5
- Router
6
- - Enhanced router introspection capabilities with 150+ lines of new functionality
7
- - Expanded test coverage for router introspection and options handling
8
- - Improved plugin system with updated type definitions
5
+ - Fixed generated POM outputs to be properly marked as generated code
6
+ - Resolved custom POM import name collision handling
7
+ - Security: Updated dependencies via npm audit fix
8
+ - Added automated PR release notes preview comments
9
9
 
10
10
  ## Changes
11
11
 
12
- **Router & TypeScript**
13
- - Implemented typed router module shim support
14
- - Enhanced router introspection logic with improved type inference
15
- - Updated plugin types to accommodate router module shims
12
+ ### Bug Fixes
13
+ - Mark generated POM outputs with generated code markers
14
+ - Handle custom POM import name collisions to prevent conflicts
15
+
16
+ ### Maintenance
17
+ - Updated `package-lock.json` to resolve npm audit security warnings
18
+ - Updated `package.json` dependency version
19
+
20
+ ### CI/Automation
21
+ - Added PR release notes preview comment automation
16
22
 
17
- **Plugin System**
18
- - Updated `create-vue-pom-generator-plugins.ts` with new plugin configuration
19
- - Modified support plugins (build, dev) for typed router compatibility
20
- - Enhanced plugin type definitions
23
+ ### Testing
24
+ - Added 37 lines of tests in `class-generation-coverage.test.ts`
25
+ - Added 93 lines of tests in `generated-tsc.test.ts`
21
26
 
22
- **Testing & Documentation**
23
- - Added new test cases for router introspection (50+ lines)
24
- - Expanded options test coverage
25
- - Updated README with latest usage information
27
+ ### Plugin System
28
+ - Enhanced plugin types with additional configuration options
29
+ - Updated build and dev plugins with new metadata
30
+ - Updated support plugins infrastructure
26
31
 
27
- **Dependencies**
28
- - Version bump to 1.0.21
32
+ ### Documentation
33
+ - Updated README.md
29
34
 
30
35
  ## Breaking Changes
31
36
 
32
- None
37
+ None.
33
38
 
34
39
  ## Pull Requests Included
35
40
 
36
- - [#1](https://github.com/immense/vue-pom-generator/pull/1) Add PR release-notes preview
37
- comments (@dkattan)
41
+ - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
42
+ by @dkattan
38
43
 
39
44
  ## Testing
40
45
 
41
- Comprehensive test coverage added for router introspection functionality and options handling.
42
- All tests passing.
43
- ```
46
+ Added comprehensive test coverage for class generation and TypeScript compilation of generated
47
+ code (130+ new test lines).
44
48
 
@@ -20,6 +20,8 @@ const AUTO_GENERATED_COMMENT
20
20
  + " * This file is auto-generated by vue-pom-generator.\n"
21
21
  + " * Changes should be made in the generator/template, not in the generated output.\n"
22
22
  + " */";
23
+ const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
24
+ const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
23
25
  const eslintSuppressionHeader = "/* eslint-disable perfectionist/sort-imports */\n";
24
26
 
25
27
  function toPosixRelativePath(fromDir: string, toFile: string): string {
@@ -309,6 +311,14 @@ export interface GenerateFilesOptions {
309
311
  */
310
312
  customPomImportAliases?: Record<string, string>;
311
313
 
314
+ /**
315
+ * How to handle collisions between custom POM import identifiers and generated class names.
316
+ *
317
+ * - "error" (default): fail generation with a descriptive error
318
+ * - "alias": auto-alias colliding custom imports (e.g. PersonListPage -> PersonListPageCustom)
319
+ */
320
+ customPomImportNameCollisionBehavior?: "error" | "alias";
321
+
312
322
  /**
313
323
  * Handwritten POM helper attachments. These helpers are assumed to be present in the
314
324
  * aggregated output (e.g. via `tests/playwright/pom/custom/*.ts` inlining), but we only attach them to
@@ -370,6 +380,7 @@ interface GenerateContentOptions {
370
380
  projectRoot?: string;
371
381
  customPomDir?: string;
372
382
  customPomImportAliases?: Record<string, string>;
383
+ customPomClassIdentifierMap?: Record<string, string>;
373
384
 
374
385
  /** Attribute name to treat as the test id. Defaults to `data-testid`. */
375
386
  testIdAttribute?: string;
@@ -380,6 +391,11 @@ interface GenerateContentOptions {
380
391
  routeMetaByComponent?: Record<string, RouteMeta>;
381
392
  }
382
393
 
394
+ interface GeneratedFileOutput {
395
+ filePath: string;
396
+ content: string;
397
+ }
398
+
383
399
  export async function generateFiles(
384
400
  componentHierarchyMap: Map<string, IComponentDependencies>,
385
401
  vueFilesPathMap: Map<string, string>,
@@ -393,6 +409,7 @@ export async function generateFiles(
393
409
  projectRoot,
394
410
  customPomDir,
395
411
  customPomImportAliases,
412
+ customPomImportNameCollisionBehavior = "error",
396
413
  testIdAttribute,
397
414
  emitLanguages: emitLanguagesOverride,
398
415
  csharp,
@@ -410,6 +427,12 @@ export async function generateFiles(
410
427
  const routeMetaByComponent = vueRouterFluentChaining
411
428
  ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType)
412
429
  : undefined;
430
+ const generatedFilePaths: string[] = [];
431
+ const writeGeneratedFile = (file: GeneratedFileOutput) => {
432
+ const resolvedFilePath = path.resolve(file.filePath);
433
+ createFile(resolvedFilePath, file.content);
434
+ generatedFilePaths.push(resolvedFilePath);
435
+ };
413
436
 
414
437
  if (emitLanguages.includes("ts")) {
415
438
  const files = await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
@@ -417,20 +440,24 @@ export async function generateFiles(
417
440
  projectRoot,
418
441
  customPomDir,
419
442
  customPomImportAliases,
443
+ customPomImportNameCollisionBehavior,
420
444
  testIdAttribute,
421
445
  generateFixtures,
422
446
  routeMetaByComponent,
423
447
  vueRouterFluentChaining,
424
448
  });
425
449
  for (const file of files) {
426
- createFile(file.filePath, file.content);
450
+ writeGeneratedFile(file);
427
451
  }
428
452
 
429
- maybeGenerateFixtureRegistry(componentHierarchyMap, {
453
+ const fixtureRegistryFile = maybeGenerateFixtureRegistry(componentHierarchyMap, {
430
454
  generateFixtures,
431
455
  pomOutDir: outDir,
432
456
  projectRoot,
433
457
  });
458
+ if (fixtureRegistryFile) {
459
+ writeGeneratedFile(fixtureRegistryFile);
460
+ }
434
461
  }
435
462
 
436
463
  if (emitLanguages.includes("csharp")) {
@@ -440,9 +467,111 @@ export async function generateFiles(
440
467
  csharp,
441
468
  });
442
469
  for (const file of csFiles) {
443
- createFile(file.filePath, file.content);
470
+ writeGeneratedFile(file);
471
+ }
472
+ }
473
+
474
+ const gitattributesFiles = buildGeneratedGitAttributesFiles(generatedFilePaths);
475
+ for (const file of gitattributesFiles) {
476
+ createFile(file.filePath, file.content);
477
+ }
478
+ }
479
+
480
+ function escapeGitAttributesPattern(value: string): string {
481
+ let output = "";
482
+ for (let i = 0; i < value.length; i++) {
483
+ const char = value[i];
484
+ if (char === "\\") {
485
+ output += "\\\\";
486
+ continue;
487
+ }
488
+ if (char === " ") {
489
+ output += "\\ ";
490
+ continue;
491
+ }
492
+ if (i === 0 && (char === "#" || char === "!")) {
493
+ output += `\\${char}`;
494
+ continue;
495
+ }
496
+ output += char;
497
+ }
498
+ return output;
499
+ }
500
+
501
+ function buildManagedGitAttributesBlock(entries: string[]): string {
502
+ return [
503
+ GENERATED_GITATTRIBUTES_BLOCK_START,
504
+ "# GitHub Linguist: treat generated POM outputs as generated code by default.",
505
+ ...entries,
506
+ GENERATED_GITATTRIBUTES_BLOCK_END,
507
+ "",
508
+ ].join("\n");
509
+ }
510
+
511
+ function findLineEndOffset(content: string, offset: number): number {
512
+ let cursor = offset;
513
+ while (cursor < content.length && content[cursor] !== "\n") {
514
+ cursor++;
515
+ }
516
+ if (cursor < content.length && content[cursor] === "\n") {
517
+ cursor++;
518
+ }
519
+ return cursor;
520
+ }
521
+
522
+ function renderManagedGitAttributesContent(filePath: string, entries: string[]): string {
523
+ const block = buildManagedGitAttributesBlock(entries);
524
+ if (!fs.existsSync(filePath)) {
525
+ return block;
526
+ }
527
+
528
+ const existingContent = fs.readFileSync(filePath, "utf8");
529
+ const blockStart = existingContent.indexOf(GENERATED_GITATTRIBUTES_BLOCK_START);
530
+ const blockEnd = existingContent.indexOf(GENERATED_GITATTRIBUTES_BLOCK_END);
531
+
532
+ if (blockStart === -1 && blockEnd === -1) {
533
+ if (!existingContent.length) {
534
+ return block;
535
+ }
536
+
537
+ const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
538
+ return `${existingContent}${separator}${block}`;
539
+ }
540
+
541
+ if (blockStart === -1 || blockEnd === -1 || blockEnd < blockStart) {
542
+ throw new Error(`[vue-pom-generator] Found malformed managed .gitattributes block at ${filePath}.`);
543
+ }
544
+
545
+ const afterBlock = findLineEndOffset(existingContent, blockEnd);
546
+ return `${existingContent.slice(0, blockStart)}${block}${existingContent.slice(afterBlock)}`;
547
+ }
548
+
549
+ function buildGeneratedGitAttributesFiles(generatedFilePaths: string[]): GeneratedFileOutput[] {
550
+ const entriesByDir = new Map<string, Set<string>>();
551
+
552
+ for (const generatedFilePath of generatedFilePaths) {
553
+ const resolvedFilePath = path.resolve(generatedFilePath);
554
+ if (path.basename(resolvedFilePath) === ".gitattributes") {
555
+ continue;
444
556
  }
557
+
558
+ const dir = path.dirname(resolvedFilePath);
559
+ const entry = `${escapeGitAttributesPattern(path.basename(resolvedFilePath))} linguist-generated`;
560
+ const entries = entriesByDir.get(dir) ?? new Set<string>();
561
+ entries.add(entry);
562
+ entriesByDir.set(dir, entries);
445
563
  }
564
+
565
+ return Array.from(entriesByDir.entries())
566
+ .sort((a, b) => a[0].localeCompare(b[0]))
567
+ .map(([dir, entries]) => {
568
+ const filePath = path.join(dir, ".gitattributes");
569
+ const content = renderManagedGitAttributesContent(
570
+ filePath,
571
+ Array.from(entries).sort((a, b) => a.localeCompare(b)),
572
+ );
573
+ return { filePath, content };
574
+ });
446
575
  }
447
576
 
448
577
  function toCSharpTestIdExpression(formattedDataTestId: string): string {
@@ -532,7 +661,7 @@ function generateAggregatedCSharpFiles(
532
661
  namespace?: string;
533
662
  };
534
663
  } = {},
535
- ): Array<{ filePath: string; content: string }> {
664
+ ): GeneratedFileOutput[] {
536
665
  const outAbs = ensureDir(outDir);
537
666
  const namespace = options.csharp?.namespace ?? "Playwright.Generated";
538
667
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
@@ -790,10 +919,10 @@ function maybeGenerateFixtureRegistry(
790
919
  pomOutDir: string;
791
920
  projectRoot?: string;
792
921
  },
793
- ) {
922
+ ): GeneratedFileOutput | null {
794
923
  const { generateFixtures, pomOutDir } = options;
795
924
  if (!generateFixtures)
796
- return;
925
+ return null;
797
926
 
798
927
  // generateFixtures accepts:
799
928
  // - true: enable fixtures with defaults
@@ -925,7 +1054,10 @@ function maybeGenerateFixtureRegistry(
925
1054
  + `});\n\n`
926
1055
  + `export { test, expect };\n`;
927
1056
 
928
- createFile(path.resolve(fixtureOutDirAbs, fixtureFileName), fixturesContent);
1057
+ return {
1058
+ filePath: path.resolve(fixtureOutDirAbs, fixtureFileName),
1059
+ content: fixturesContent,
1060
+ };
929
1061
 
930
1062
  // No pomFixture is generated; goToSelf is emitted directly on each view POM.
931
1063
  }
@@ -970,7 +1102,10 @@ function generateViewObjectModelContent(
970
1102
  return false;
971
1103
  return a.attachWhenUsesComponents.some(c => hasChildComponent(c));
972
1104
  })
973
- .map(a => ({ className: a.className, propertyName: a.propertyName }));
1105
+ .map(a => ({
1106
+ className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
1107
+ propertyName: a.propertyName,
1108
+ }));
974
1109
 
975
1110
  let content: string = "";
976
1111
 
@@ -1161,12 +1296,13 @@ async function generateAggregatedFiles(
1161
1296
  projectRoot?: GenerateFilesOptions["projectRoot"];
1162
1297
  customPomDir?: GenerateFilesOptions["customPomDir"];
1163
1298
  customPomImportAliases?: GenerateFilesOptions["customPomImportAliases"];
1299
+ customPomImportNameCollisionBehavior?: GenerateFilesOptions["customPomImportNameCollisionBehavior"];
1164
1300
  testIdAttribute?: GenerateFilesOptions["testIdAttribute"];
1165
1301
  generateFixtures?: GenerateFilesOptions["generateFixtures"];
1166
1302
  routeMetaByComponent?: Record<string, RouteMeta>;
1167
1303
  vueRouterFluentChaining?: boolean;
1168
1304
  } = {},
1169
- ) {
1305
+ ): Promise<GeneratedFileOutput[]> {
1170
1306
  const projectRoot = options.projectRoot ?? process.cwd();
1171
1307
  const entries = Array.from(componentHierarchyMap.entries())
1172
1308
  .sort((a, b) => a[0].localeCompare(b[0]));
@@ -1180,6 +1316,7 @@ async function generateAggregatedFiles(
1180
1316
  items: Array<[string, IComponentDependencies]>,
1181
1317
  ) => {
1182
1318
  const imports: string[] = [];
1319
+ const generatedClassNames = new Set(items.map(([name]) => name));
1183
1320
 
1184
1321
  if (!basePageClassPath) {
1185
1322
  throw new Error("basePageClassPath is required for aggregated generation");
@@ -1213,6 +1350,28 @@ async function generateAggregatedFiles(
1213
1350
  Checkbox: "CheckboxWidget",
1214
1351
  ...(options.customPomImportAliases),
1215
1352
  };
1353
+ const importCollisionBehavior = options.customPomImportNameCollisionBehavior ?? "error";
1354
+
1355
+ const reservedIdentifiers = new Set<string>([
1356
+ "PwLocator",
1357
+ "PwPage",
1358
+ "BasePage",
1359
+ "Fluent",
1360
+ ...generatedClassNames,
1361
+ ]);
1362
+ const usedImportIdentifiers = new Set<string>();
1363
+ const customPomClassIdentifierMap: Record<string, string> = {};
1364
+
1365
+ const ensureUniqueIdentifier = (base: string) => {
1366
+ let candidate = base;
1367
+ let i = 2;
1368
+ while (reservedIdentifiers.has(candidate) || usedImportIdentifiers.has(candidate)) {
1369
+ candidate = `${base}${i}`;
1370
+ i++;
1371
+ }
1372
+ usedImportIdentifiers.add(candidate);
1373
+ return candidate;
1374
+ };
1216
1375
 
1217
1376
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
1218
1377
  const customDirAbs = path.isAbsolute(customDirRelOrAbs)
@@ -1231,20 +1390,44 @@ async function generateAggregatedFiles(
1231
1390
  const exportName = file.replace(/\.ts$/i, "");
1232
1391
  // In this repo, custom POMs are authored as `export class <Name> { ... }`.
1233
1392
  // Import by the basename, which matches the class name convention.
1234
- const alias = importAliases[exportName];
1393
+ const requested = importAliases[exportName] ?? exportName;
1394
+ const collidesWithGeneratedClass = generatedClassNames.has(requested);
1395
+ const explicitAliasProvided = Object.prototype.hasOwnProperty.call(importAliases, exportName);
1396
+
1397
+ if (collidesWithGeneratedClass && importCollisionBehavior === "error") {
1398
+ throw new Error(
1399
+ `[vue-pom-generator] Custom POM import name collision detected for "${exportName}".\n`
1400
+ + `The identifier "${requested}" conflicts with a generated POM class.\n`
1401
+ + `Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] to a unique name, `
1402
+ + `or set generation.playwright.customPoms.importNameCollisionBehavior = "alias" to auto-alias collisions.`,
1403
+ );
1404
+ }
1405
+
1406
+ let localIdentifier = requested;
1407
+ if (collidesWithGeneratedClass && importCollisionBehavior === "alias") {
1408
+ const aliasBase = explicitAliasProvided ? requested : `${exportName}Custom`;
1409
+ localIdentifier = ensureUniqueIdentifier(aliasBase);
1410
+ }
1411
+ else {
1412
+ localIdentifier = ensureUniqueIdentifier(requested);
1413
+ }
1414
+
1415
+ customPomClassIdentifierMap[exportName] = localIdentifier;
1235
1416
  const customFileAbs = path.join(customDirAbs, file);
1236
1417
  const fromOutputDir = outputDir;
1237
1418
  const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
1238
- if (alias) {
1239
- imports.push(`import { ${exportName} as ${alias} } from "${importPath}";`);
1419
+ if (localIdentifier !== exportName) {
1420
+ imports.push(`import { ${exportName} as ${localIdentifier} } from "${importPath}";`);
1240
1421
  }
1241
1422
  else {
1242
1423
  imports.push(`import { ${exportName} } from "${importPath}";`);
1243
1424
  }
1244
1425
  }
1426
+
1427
+ return customPomClassIdentifierMap;
1245
1428
  };
1246
1429
 
1247
- addCustomPomImports();
1430
+ const customPomClassIdentifierMap = addCustomPomImports();
1248
1431
 
1249
1432
  // Collect any navigation return types referenced by generated methods so we can emit
1250
1433
  // stub classes when the destination view has no generated test ids (and therefore no
@@ -1258,7 +1441,6 @@ async function generateAggregatedFiles(
1258
1441
  }
1259
1442
  }
1260
1443
 
1261
- const generatedClassNames = new Set(items.map(([name]) => name));
1262
1444
  const stubTargets = Array.from(referencedTargets)
1263
1445
  .filter(t => !generatedClassNames.has(t))
1264
1446
  .sort((a, b) => a.localeCompare(b));
@@ -1446,6 +1628,7 @@ async function generateAggregatedFiles(
1446
1628
  aggregated: true,
1447
1629
 
1448
1630
  customPomAttachments: options.customPomAttachments ?? [],
1631
+ customPomClassIdentifierMap,
1449
1632
  testIdAttribute: options.testIdAttribute,
1450
1633
  vueRouterFluentChaining: options.vueRouterFluentChaining,
1451
1634
  routeMetaByComponent: options.routeMetaByComponent,
@@ -45,6 +45,13 @@ export interface GenerateFilesOptions {
45
45
  * Example: { Toggle: "ToggleWidget" }
46
46
  */
47
47
  customPomImportAliases?: Record<string, string>;
48
+ /**
49
+ * How to handle collisions between custom POM import identifiers and generated class names.
50
+ *
51
+ * - "error" (default): fail generation with a descriptive error
52
+ * - "alias": auto-alias colliding custom imports (e.g. PersonListPage -> PersonListPageCustom)
53
+ */
54
+ customPomImportNameCollisionBehavior?: "error" | "alias";
48
55
  /**
49
56
  * Handwritten POM helper attachments. These helpers are assumed to be present in the
50
57
  * aggregated output (e.g. via `tests/playwright/pom/custom/*.ts` inlining), but we only attach them to
@@ -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;AA4ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+MD,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;;;;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,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAiCD,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,iBA2DnC"}
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;AA+MD,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,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAuCD,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,iBA2EnC"}
@@ -1 +1 @@
1
- {"version":3,"file":"eslint.config.d.ts","sourceRoot":"","sources":["../eslint.config.ts"],"names":[],"mappings":";AAEA,wBAwFG"}
1
+ {"version":3,"file":"eslint.config.d.ts","sourceRoot":"","sources":["../eslint.config.ts"],"names":[],"mappings":";AAEA,wBA0FG"}