@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 +87 -81
- package/RELEASE_NOTES.md +30 -26
- package/class-generation/index.ts +197 -14
- package/dist/class-generation/index.d.ts +7 -0
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/eslint.config.d.ts.map +1 -1
- package/dist/index.cjs +216 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +217 -35
- package/dist/index.mjs.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.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 +10 -1
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/router-introspection.d.ts.map +1 -1
- package/dist/routing/to-directive.d.ts.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/playwright/pomFixture.d.ts +0 -51
- package/dist/playwright/pomFixture.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -21,7 +21,11 @@ npm install @immense/vue-pom-generator
|
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
|
-
Exported
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
●
|
|
1
|
+
● # Release v1.0.23
|
|
2
|
+
|
|
2
3
|
## Highlights
|
|
3
4
|
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
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
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Updated
|
|
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
|
-
|
|
28
|
-
-
|
|
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
|
-
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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 => ({
|
|
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
|
|
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 (
|
|
1239
|
-
imports.push(`import { ${exportName} as ${
|
|
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;
|
|
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,
|
|
1
|
+
{"version":3,"file":"eslint.config.d.ts","sourceRoot":"","sources":["../eslint.config.ts"],"names":[],"mappings":";AAEA,wBA0FG"}
|