@immense/vue-pom-generator 1.0.38 → 1.0.40

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
@@ -1,167 +1,270 @@
1
1
  # @immense/vue-pom-generator
2
2
 
3
- Vite plugin for Vue 3 apps that:
3
+ `@immense/vue-pom-generator` is a Vite plugin for Vue 3 that does two compile-time jobs:
4
4
 
5
- - Injects stable `data-testid` attributes into interactive elements during Vue template compilation
6
- - Generates Page Object Model (POM) classes for views/components (Playwright-focused by default)
5
+ 1. it injects a test-id attribute into interactive elements in `.vue` templates
6
+ 2. it turns the collected ids into an aggregated Page Object Model library for Playwright, with optional Playwright fixtures and optional C# output
7
7
 
8
- Why you might want it:
8
+ If you already use Playwright with `getByTestId`, the point is simple: this package removes the repetitive work of keeping test ids in sync with Vue templates and then hand-writing page objects around those ids.
9
9
 
10
- - **Less brittle end-to-end tests**: selectors stay stable even if markup/layout shifts.
11
- - **Less boilerplate**: POM generation keeps tests readable and centralized.
12
- - **Not just testing**: a consistent attribute can also help analytics/user-tracking tooling and ad-hoc automation.
10
+ ## What this does
13
11
 
14
- ## Install (npm)
12
+ - **Injects test ids during Vue compilation, not at runtime.** It hooks into the Vue template compiler and rewrites the compiled template output.
13
+ - **Uses real template signals to name ids and methods.** Click handlers, `v-model`, `id`/`name`, `:to`, wrapper configuration, and a few targeted fallbacks all feed the generated API.
14
+ - **Generates one aggregated TypeScript POM file** plus a stable `index.ts` barrel.
15
+ - **Can generate Playwright fixtures** so tests can request `userListPage` instead of constructing `new UserListPage(page)` manually.
16
+ - **Can emit a single C# POM file** for Playwright .NET consumers.
17
+ - **Exposes `virtual:testids`** so your app can import the current collected test-id manifest at runtime.
18
+ - **Ships ESLint rules** to remove legacy manually-authored test ids and to discourage raw locator actions on generated getters.
15
19
 
16
- Package: `@immense/vue-pom-generator`
20
+ ## What this does not do
17
21
 
18
- ```sh
19
- npm install @immense/vue-pom-generator
22
+ - **It is not a runtime DOM crawler.** It only knows what it can learn from Vue SFC templates and router introspection.
23
+ - **It does not fully understand arbitrary wrapper components automatically.** There is some wrapper inference for simple local SFCs and some naming conventions, but serious design-system components still need `injection.nativeWrappers`.
24
+ - **It does not fully fill route params for you.** Generated `goTo()` / `goToSelf()` methods use the discovered route template literally. A route like `/users/:id` still needs a real `/users/123` when you actually navigate.
25
+ - **It does not auto-attach every file in `pom/custom`.** Custom helpers are imported, but they only affect generated classes when you explicitly configure attachments, or when they match the built-in `Toggle` / `Checkbox` widget conventions.
26
+ - **It does not make override classes globally replace generated classes.** `pom/overrides` only changes generated fixture instantiation. Direct imports from the generated barrel still give you the generated class unless you import your override yourself.
27
+ - **The C# emitter is not feature-parity with the TypeScript emitter.** It emits locator/action classes, but not Playwright fixtures, not helper attachments, and not the same route helper surface.
28
+
29
+ ## A minimal Vue example
30
+
31
+ Given a view like this:
32
+
33
+ ```vue
34
+ <!-- src/views/UserEditorPage.vue -->
35
+ <template>
36
+ <form>
37
+ <input v-model="emailAddress" />
38
+ <button @click="save">Save</button>
39
+ </form>
40
+ </template>
20
41
  ```
21
42
 
22
- ## Usage
43
+ The current implementation will generate a surface in this shape:
23
44
 
24
- Exported entrypoints:
45
+ ```html
46
+ <input data-testid="UserEditorPage-EmailAddress-input">
47
+ <button data-testid="UserEditorPage-Save-button">Save</button>
48
+ ```
25
49
 
26
- - `createVuePomGeneratorPlugins()`
27
- - `vuePomGenerator()` (alias)
28
- - `defineVuePomGeneratorConfig()` (typed config helper)
29
- - `@immense/vue-pom-generator/eslint` (ESLint rules for cleanup/enforcement)
50
+ ```ts
51
+ export class UserEditorPage extends BasePage {
52
+ get EmailAddressInput() { /* Playwright locator */ }
53
+ get SaveButton() { /* Playwright locator */ }
54
+
55
+ async typeEmailAddress(text: string, annotationText = "") { /* ... */ }
56
+ async clickSave(wait: boolean = true) { /* ... */ }
57
+ }
58
+ ```
30
59
 
31
- ## Configuration
60
+ That example is intentionally small, but it shows the real contract:
32
61
 
33
- `createVuePomGeneratorPlugins(options)` accepts a `VuePomGeneratorPluginOptions` object, grouped into:
62
+ - the Vue template stays close to normal app code
63
+ - the test ids are derived at compile time
64
+ - the generated class exposes both raw locators and typed action methods
34
65
 
35
- - `injection`: how `data-testid` (or your chosen attribute) is derived/injected
36
- - `generation`: how Page Object Models (POMs) and Playwright helpers are generated
66
+ ## How ids are actually derived today
37
67
 
38
- The generator emits an aggregated output under `generation.outDir` (default `tests/playwright/__generated__`):
68
+ The generator does not use one naming trick. It layers several signals.
39
69
 
40
- - `tests/playwright/__generated__/page-object-models.g.ts` (generated; do not edit)
41
- - `tests/playwright/__generated__/index.ts` (generated stable barrel)
42
- - managed `.gitattributes` files only when you emit outside `__generated__`
70
+ - **Click actions** prefer semantic handler names such as `save`, `openDetails`, or `runImport`.
71
+ - **Inputs and wrapper components** prefer `v-model`, wrapper `valueAttribute`, or related model-like bindings.
72
+ - **Native elements** also consider `id` / `name` attributes.
73
+ - **Router links / `:to` bindings** can contribute route-based naming and typed navigation return types when the target can be resolved.
74
+ - **Wrapper components** can be explicit (`nativeWrappers`) or inferred from simple local SFC templates.
75
+ - **Fallback naming exists, but it is intentionally conservative.** That is why `generation.nameCollisionBehavior` exists.
43
76
 
44
- If `generation.playwright.fixtures` is enabled, it also emits:
77
+ Important limit: wrapper inference is helpful, not magical. The current implementation recursively inspects simple local SFC templates for the first inferable primitive (`input`, `textarea`, `select`, `button`, `vselect`, radio/checkbox inputs). It also recognizes some naming patterns like `*Button`. For anything more complex, configure `nativeWrappers` explicitly.
45
78
 
46
- - `tests/playwright/__generated__/fixtures.g.ts` (generated; do not edit)
79
+ ## Playwright before/after examples
47
80
 
48
- Generated fixtures automatically prefer matching handwritten override classes from
49
- `tests/playwright/pom/overrides/<ClassName>.ts` (or the sibling `overrides/` directory next to
50
- your configured `generation.playwright.customPoms.dir`).
81
+ ### 1) Raw selectors vs generated POM methods
51
82
 
52
- ### Vite config example
83
+ **Before**
53
84
 
54
85
  ```ts
55
- import { defineConfig } from "vite";
56
- import { defineVuePomGeneratorConfig, vuePomGenerator } from "@immense/vue-pom-generator";
86
+ import { test, expect } from "@playwright/test";
57
87
 
58
- export default defineConfig(() => {
59
- const vueOptions = {
60
- script: { defineModel: true, propsDestructure: true },
61
- };
88
+ test("saves a user", async ({ page }) => {
89
+ await page.goto("/users/new");
90
+ await page.getByTestId("UserEditorPage-EmailAddress-input").fill("alice@example.com");
91
+ await page.getByTestId("UserEditorPage-Save-button").click();
92
+ await expect(page.getByText("Saved")).toBeVisible();
93
+ });
94
+ ```
62
95
 
63
- const pomConfig = defineVuePomGeneratorConfig({
64
- vueOptions,
65
- logging: { verbosity: "info" },
96
+ **After**
66
97
 
67
- injection: {
68
- // Attribute to inject/read as the test id (default: data-testid)
69
- attribute: "data-testid",
98
+ ```ts
99
+ import { test, expect } from "@playwright/test";
100
+ import { UserEditorPage } from "../tests/playwright/__generated__";
101
+
102
+ test("saves a user", async ({ page }) => {
103
+ const userEditorPage = new UserEditorPage(page);
104
+ await userEditorPage.typeEmailAddress("alice@example.com");
105
+ await userEditorPage.clickSave();
106
+ await expect(page.getByText("Saved")).toBeVisible();
107
+ });
108
+ ```
70
109
 
71
- // Used to classify Vue files as "views" vs components (default: src/views)
72
- viewsDir: "src/views",
110
+ Why this is better:
73
111
 
74
- // Directories to scan for .vue files when building the POM library (default: ["src"])
75
- // For Nuxt, you might want ["app", "components", "pages", "layouts"]
76
- scanDirs: ["src"],
112
+ - selector strings stop leaking into tests
113
+ - refactors are localized to generated output instead of every test file
114
+ - you still keep access to raw locators when you need them
77
115
 
78
- // Optional: extra directories to search when inferring wrapper-component roles for
79
- // components that live outside scanDirs (for example a sibling shared UI package)
80
- wrapperSearchRoots: ["../shared/ui/src/components"],
116
+ Without the generator, you still can write stable Playwright tests, but you keep manually maintaining both the selector strings and the page-object layer around them.
81
117
 
82
- // Optional: wrapper semantics for design-system components
83
- nativeWrappers: {
84
- MyButton: { role: "button" },
85
- MyInput: { role: "input" },
86
- },
118
+ ### 2) Manual page-object construction vs generated fixtures
87
119
 
88
- // Optional: opt specific components out of injection
89
- excludeComponents: ["MyButton"],
120
+ **Before**
90
121
 
91
- // Optional: preserve/overwrite/error when an author already set the attribute
92
- existingIdBehavior: "preserve",
93
- },
122
+ ```ts
123
+ import { test, expect } from "@playwright/test";
124
+ import { UserListPage } from "../tests/playwright/__generated__";
94
125
 
95
- generation: {
96
- // Default: ["ts"]
97
- emit: ["ts", "csharp"],
126
+ test("shows the list", async ({ page }) => {
127
+ const userListPage = new UserListPage(page);
128
+ await userListPage.goTo();
129
+ await expect(userListPage.CreateButton).toBeVisible();
130
+ });
131
+ ```
98
132
 
99
- // C# specific configuration
100
- csharp: {
101
- // The namespace for generated C# classes (default: Playwright.Generated)
102
- namespace: "MyProject.Tests.Generated",
103
- },
133
+ **After**
104
134
 
105
- // Default: tests/playwright/__generated__
106
- outDir: "tests/playwright/__generated__",
107
-
108
- // Controls how to handle duplicate generated member names within a single POM class.
109
- // - "error": fail compilation
110
- // - "warn": warn and suffix
111
- // - "suffix": suffix silently (default)
112
- nameCollisionBehavior: "suffix",
113
-
114
- // Enable router introspection. When provided, router-aware POM helpers are generated.
115
- router: {
116
- // For standard Vue apps:
117
- entry: "src/router.ts",
118
- moduleShims: {
119
- "@/config/app-insights": {
120
- getAppInsights: () => null,
121
- },
122
- "@/store/pinia/app-alert-store": ["useAppAlertsStore"],
135
+ ```ts
136
+ import { test, expect } from "../tests/playwright/__generated__/fixtures.g";
137
+
138
+ test("shows the list", async ({ userListPage }) => {
139
+ await userListPage.goTo();
140
+ await expect(userListPage.CreateButton).toBeVisible();
141
+ });
142
+ ```
143
+
144
+ Why this is better:
145
+
146
+ - less setup noise in tests
147
+ - fixture types stay aligned with generated classes
148
+ - if `tests/playwright/pom/overrides/UserListPage.ts` exists, the fixture will instantiate that override class automatically
149
+
150
+ Without fixtures, you still use the generated POMs, but every test has to construct them manually.
151
+
152
+ ### 3) Nested helper calls vs flattened helper methods
153
+
154
+ Suppose you have a custom grid helper in `tests/playwright/pom/custom/Grid.ts`.
155
+
156
+ **Without `flatten`**
157
+
158
+ ```ts
159
+ test("filters the grid", async ({ usersPage }) => {
160
+ await usersPage.grid.Search("alice@example.com");
161
+ });
162
+ ```
163
+
164
+ **With `flatten: true`**
165
+
166
+ ```ts
167
+ test("filters the grid", async ({ usersPage }) => {
168
+ await usersPage.Search("alice@example.com");
169
+ });
170
+ ```
171
+
172
+ Why this is better:
173
+
174
+ - the test reads like the page API, not the internal helper graph
175
+ - the page still keeps the explicit `grid` property when you want it
176
+
177
+ Without `flatten`, helper composition is still available; it is just more explicit and more nested.
178
+
179
+ ## Install
180
+
181
+ ```sh
182
+ npm install @immense/vue-pom-generator
183
+ ```
184
+
185
+ Exports:
186
+
187
+ - `createVuePomGeneratorPlugins()`
188
+ - `vuePomGenerator()` (alias)
189
+ - `defineVuePomGeneratorConfig()`
190
+ - `@immense/vue-pom-generator/eslint`
191
+
192
+ ## Basic Vite setup
193
+
194
+ ```ts
195
+ import { defineConfig } from "vite";
196
+ import { defineVuePomGeneratorConfig, vuePomGenerator } from "@immense/vue-pom-generator";
197
+
198
+ const pomConfig = defineVuePomGeneratorConfig({
199
+ vueOptions: {
200
+ script: { defineModel: true, propsDestructure: true },
201
+ },
202
+ logging: { verbosity: "info" },
203
+ injection: {
204
+ attribute: "data-testid",
205
+ viewsDir: "src/views",
206
+ scanDirs: ["src"],
207
+ wrapperSearchRoots: ["../shared-ui/src/components"],
208
+ nativeWrappers: {
209
+ AppButton: { role: "button" },
210
+ AppTextField: { role: "input" },
211
+ AppRadioGroup: { role: "radio", requiresOptionDataTestIdPrefix: true },
212
+ },
213
+ excludeComponents: ["LegacyWidget"],
214
+ existingIdBehavior: "preserve",
215
+ },
216
+ generation: {
217
+ emit: ["ts", "csharp"],
218
+ csharp: {
219
+ namespace: "MyProject.Tests.Generated",
220
+ },
221
+ outDir: "tests/playwright/__generated__",
222
+ nameCollisionBehavior: "suffix",
223
+ router: {
224
+ entry: "src/router/index.ts",
225
+ moduleShims: {
226
+ "@/config/app-insights": {
227
+ getAppInsights: () => null,
123
228
  },
124
- // For Nuxt apps (file-based routing):
125
- // type: "nuxt"
229
+ "@/stores/alerts": ["useAlertsStore"],
126
230
  },
127
-
128
- playwright: {
129
- fixtures: true,
130
- customPoms: {
131
- // Default: tests/playwright/pom/custom
132
- dir: "tests/playwright/pom/custom",
133
- importAliases: { MyCheckBox: "CheckboxWidget" },
134
- attachments: [
135
- {
136
- className: "ConfirmationModal",
137
- propertyName: "confirmationModal",
138
- attachWhenUsesComponents: ["Page"],
139
- },
140
- ],
231
+ },
232
+ playwright: {
233
+ fixtures: true,
234
+ customPoms: {
235
+ dir: "tests/playwright/pom/custom",
236
+ importAliases: {
237
+ Grid: "UserGridHelper",
141
238
  },
239
+ attachments: [
240
+ {
241
+ className: "Grid",
242
+ propertyName: "grid",
243
+ attachWhenUsesComponents: ["DataGrid"],
244
+ attachTo: "both",
245
+ flatten: true,
246
+ },
247
+ ],
142
248
  },
143
249
  },
144
- });
250
+ },
251
+ });
145
252
 
146
- return {
147
- plugins: [...vuePomGenerator(pomConfig)],
148
- };
253
+ export default defineConfig({
254
+ plugins: [...vuePomGenerator(pomConfig)],
149
255
  });
150
256
  ```
151
257
 
152
- Notes:
258
+ ### Important Vite ownership rule
153
259
 
154
- - `vuePomGenerator(...)` wires `@vitejs/plugin-vue` internally by default for standard Vue apps.
155
- - Do not pass `vue()` into `createVuePomGeneratorPlugins(...)`; pass Vue plugin options via `vueOptions`.
156
- - When the app should own `vue()` explicitly, set `vuePluginOwnership: "external"` and add `vue()` separately in your Vite config.
260
+ By default, this package creates and returns its own `@vitejs/plugin-vue` instance.
157
261
 
158
- - **Injection is enabled by plugin inclusion** (there is no longer an `injection.enabled` flag).
159
- - **Generation is enabled by default** and can be disabled via `generation: false`.
160
- - **Router-aware POM helpers are enabled** when `generation.router.entry` is provided (the generator will introspect your router).
262
+ That means:
161
263
 
162
- ### External Vue plugin ownership
264
+ - **standard Vue app**: spread `...vuePomGenerator(config)` and do **not** separately pass `vue()` into the same helper
265
+ - **app-owned Vue plugin / Nuxt / special Vite setup**: set `vuePluginOwnership: "external"`, add `vue()` yourself, and let this package patch the resolved Vue plugin instead
163
266
 
164
- If your app should own the core Vue Vite plugin explicitly, add `vue()` yourself and let this package patch the resolved plugin:
267
+ Example:
165
268
 
166
269
  ```ts
167
270
  import vue from "@vitejs/plugin-vue";
@@ -180,71 +283,407 @@ export default defineConfig({
180
283
  });
181
284
  ```
182
285
 
183
- ### `generation.router`
286
+ Nuxt-style routing also uses the resolved app-owned Vue plugin. In practice, if you use `generation.router.type: "nuxt"`, think in terms of external Vue plugin ownership.
287
+
288
+ ## What gets generated
289
+
290
+ By default, generation writes to `tests/playwright/__generated__`.
291
+
292
+ TypeScript output:
293
+
294
+ - `page-object-models.g.ts` — aggregated generated classes
295
+ - `index.ts` — stable barrel re-exporting `page-object-models.g`
296
+ - `_pom-runtime/` — copied runtime support files used by the aggregated output
297
+
298
+ Optional Playwright fixture output:
299
+
300
+ - `fixtures.g.ts` next to the POMs by default
301
+ - or a custom directory / file path when configured
302
+
303
+ Optional C# output:
304
+
305
+ - `page-object-models.g.cs`
306
+
307
+ If you emit outside a `__generated__` path, the generator also manages `.gitattributes` entries for generated files.
308
+
309
+ ## Actual Vite dev/build behavior
310
+
311
+ This is important if you are deciding whether the tool will fit into a real codebase.
312
+
313
+ - **Dev server:** on startup, it scans the configured `scanDirs`, compiles each `.vue` file into a snapshot, writes the aggregated outputs once, then batches add/change/delete events and regenerates incrementally.
314
+ - **Build:** it generates from the richest build pass it sees, which matters because Vite can run multiple passes (for example SSR plus client). The generator avoids letting a thinner pass clobber a richer one.
315
+ - **Always-on virtual module:** `virtual:testids` is registered whether generation is enabled or disabled.
316
+ - **Generation can be disabled:** `generation: false` still keeps compile-time test-id injection and the virtual module, but skips emitted POM files.
317
+
318
+ ## Router-aware navigation: the real semantics
319
+
320
+ This package has two router-related behaviors, and they are easy to overstate.
321
+
322
+ ### 1) Typed navigation methods from `:to`
323
+
324
+ When the generator can statically resolve a `:to` target, it can emit navigation methods that return the target POM type.
325
+
326
+ That enables patterns like:
327
+
328
+ ```ts
329
+ await userListPage.goToCreateUser().typeEmailAddress("alice@example.com");
330
+ ```
331
+
332
+ What is actually supported well today:
333
+
334
+ - literal string paths, such as `:to="'/users'"`
335
+ - object literals with `name` or `path`, such as `:to="{ name: 'users' }"`
336
+ - object literals with `params` keys, enough for target-type resolution
337
+
338
+ What is not fully supported:
339
+
340
+ - arbitrary computed `:to` expressions
341
+ - parameter-aware `goToSelf()` URL filling
342
+ - exposing rich route-param metadata on the generated POM surface
343
+
344
+ ### 2) View-level `route`, `goTo()`, and `goToSelf()`
345
+
346
+ When `generation.router` is enabled, each view POM gets:
347
+
348
+ - `static readonly route: { template: string } | null`
349
+ - `async goTo()`
350
+ - `async goToSelf()`
351
+
352
+ Important caveats:
353
+
354
+ - `goToSelf()` literally does `page.goto(route.template)`
355
+ - a dynamic route template like `/users/:id` stays `/users/:id`
356
+ - if a component is matched by multiple routes, the generator currently picks one route template (the shortest one)
357
+
358
+ So the safe rule is:
359
+
360
+ - use generated `goTo()` for simple/static routes
361
+ - for dynamic routes, navigate with a real URL yourself or wrap that behavior in your override/custom code
362
+
363
+ ### Why `moduleShims` exists
364
+
365
+ Vue-router introspection SSR-loads your router entry through Vite. Real routers often import browser-only or application-only modules that do not belong in an introspection pass.
366
+
367
+ `moduleShims` exists so you can replace those imports just for route discovery.
368
+
369
+ - `string[]` means “create no-op exported functions with these names”
370
+ - `Record<string, fn>` means “use these exact shim implementations”
371
+ - wildcard `*` exports are not supported
372
+
373
+ Without `moduleShims`, router introspection can fail even though your app itself runs fine.
374
+
375
+ ## `pom/custom` and `pom/overrides`: what those folders really mean
376
+
377
+ These two directories solve different problems.
378
+
379
+ ### `pom/custom`
380
+
381
+ Default: `tests/playwright/pom/custom`
382
+
383
+ This directory is for handwritten helper classes that the aggregated TypeScript output can import.
384
+
385
+ It is the default helper directory even if you omit `generation.playwright.customPoms` entirely.
386
+
387
+ Actual current behavior:
388
+
389
+ - the generator scans the directory **non-recursively**
390
+ - it imports top-level `.ts` files only
391
+ - it expects the file basename to match the exported class name (`Grid.ts` → `export class Grid {}`)
392
+ - the helper files are imported into the generated aggregate; they are **not** automatically attached everywhere
393
+
394
+ What it is for:
395
+
396
+ - wrappers around third-party widgets
397
+ - reusable page fragments with custom methods
398
+ - helper objects you want attached conditionally to generated pages/components
399
+
400
+ What it is not:
401
+
402
+ - a magic auto-discovery system that wires every helper into every page
403
+ - a replacement for `attachments`
404
+
405
+ ### `pom/overrides`
406
+
407
+ Default convention: sibling `overrides/` next to `customPoms.dir`
408
+
409
+ If `customPoms.dir` is:
410
+
411
+ ```txt
412
+ tests/playwright/pom/custom
413
+ ```
414
+
415
+ then fixtures look for overrides in:
416
+
417
+ ```txt
418
+ tests/playwright/pom/overrides
419
+ ```
420
+
421
+ Actual current behavior:
422
+
423
+ - the override directory is inferred, not separately configurable
424
+ - generated fixtures check for `overrides/<ClassName>.ts`
425
+ - when the file exists, the fixture instantiates the override class instead of the generated class
426
+ - the generated POM barrel does **not** automatically re-export the override in place of the generated class
427
+
428
+ That means fixture override preference is real, but it is specifically a **fixture-time constructor preference**, not a global import replacement mechanism.
429
+
430
+ A typical override looks like this:
431
+
432
+ ```ts
433
+ // tests/playwright/pom/overrides/UserListPage.ts
434
+ import { UserListPage as GeneratedUserListPage } from "../../__generated__/page-object-models.g";
435
+
436
+ export class UserListPage extends GeneratedUserListPage {
437
+ async openFirstUser() {
438
+ await this.clickOpen();
439
+ }
440
+ }
441
+ ```
442
+
443
+ ## Helper imports, aliasing, conditional wiring, and `flatten`
444
+
445
+ This is where most people need precision.
446
+
447
+ ### Helper imports
448
+
449
+ Every `.ts` file in `customPoms.dir` becomes an import in the aggregated TypeScript output.
450
+
451
+ Benefits:
452
+
453
+ - helpers are typechecked as normal TypeScript
454
+ - generated pages can compose them instead of duplicating logic
455
+
456
+ Without helper imports, generated classes only know about generated pages/components plus the built-in runtime support.
457
+
458
+ ### `importAliases`
459
+
460
+ `importAliases` changes the **local import identifier** used in generated output.
461
+
462
+ Example:
463
+
464
+ ```ts
465
+ customPoms: {
466
+ importAliases: {
467
+ Grid: "UserGridHelper",
468
+ },
469
+ }
470
+ ```
471
+
472
+ This is useful when:
473
+
474
+ - you want a clearer local helper name
475
+ - a helper name would otherwise collide with a generated POM class name
476
+
477
+ Important semantic detail: `attachments[].className` still refers to the helper's real exported class / file basename (`Grid`), **not** the alias (`UserGridHelper`).
478
+
479
+ Without `importAliases`, the generator uses the basename as-is.
184
480
 
185
- Controls router introspection for `:to` analysis and navigation helper generation.
481
+ ### `importNameCollisionBehavior`
186
482
 
187
- - `entry: string`: For standard Vue apps, where router introspection loads your Vue Router definition from. This file must export a **default router factory function** (e.g. `export default makeRouter`).
188
- - `type: "vue-router" | "nuxt"`: The introspection provider. Defaults to `"vue-router"`. Use `"nuxt"` for file-based routing discovery (e.g. `app/pages` or `pages`).
189
- - `moduleShims: Record<string, string[] | Record<string, fn>>`: Optional module-source -> shim definition map used only while introspecting the router.
190
- - Use `string[]` for no-op exported functions (e.g. `["useAppAlertsStore"]`).
191
- - Use `Record<string, fn>` for explicit exported function implementations (e.g. `{ getAppInsights: () => null }`).
483
+ Current options:
192
484
 
193
- ### `generation.playwright.fixtures: boolean | string | { outDir?: string }`
485
+ - `"error"` (default)
486
+ - `"alias"`
194
487
 
195
- When enabled, the generator emits a concrete, strongly typed Playwright fixture module so tests can do:
488
+ Why it exists:
196
489
 
197
- - `test("...", async ({ preferencesPage }) => { ... })`
490
+ - the aggregated file can import both generated classes and handwritten helpers
491
+ - collisions are easy when a helper and a generated class share the same name
198
492
 
199
- Forms:
493
+ What happens:
200
494
 
201
- - `true`: enable with defaults
202
- - `"path"`: enable and write under this directory (or file, if it ends in `.ts`/`.tsx`/`.mts`/`.cts`)
203
- - `{ outDir }`: enable and write fixture outputs under a custom directory
495
+ - `"error"`: generation fails and tells you to rename or alias the helper
496
+ - `"alias"`: the generator auto-aliases the helper import (for example `GridCustom`)
204
497
 
205
- Defaults:
498
+ Without this option, collisions would quietly create ambiguous generated code.
206
499
 
207
- - when `true`: writes `fixtures.g.ts` alongside generated POMs (under `generation.outDir`)
500
+ ### Conditional helper wiring (`attachments`)
208
501
 
209
- Convention:
502
+ Attachments decide when a handwritten helper becomes a property on a generated page/component.
210
503
 
211
- - if `tests/playwright/pom/overrides/<ClassName>.ts` exists, the generated fixture for that page/component
212
- instantiates the override class instead of the raw generated `Pom.<ClassName>`
213
- - the override directory is inferred as the sibling `overrides/` directory next to
214
- `generation.playwright.customPoms.dir`
504
+ Example:
215
505
 
216
- ### Vite config example
506
+ ```ts
507
+ attachments: [
508
+ {
509
+ className: "Grid",
510
+ propertyName: "grid",
511
+ attachWhenUsesComponents: ["DataGrid"],
512
+ attachTo: "both",
513
+ flatten: true,
514
+ },
515
+ ]
516
+ ```
517
+
518
+ Actual semantics:
519
+
520
+ - the helper must exist in `customPoms.dir`
521
+ - the generated page/component must use at least one component named in `attachWhenUsesComponents`
522
+ - matching is based on the component usage collected from the Vue template, not runtime inspection
523
+ - the generated constructor instantiates the helper as `new Helper(page, this)`
524
+ - `attachTo` defaults to `"views"`
525
+
526
+ Why it exists:
527
+
528
+ - you usually do not want every helper on every page
529
+ - helper attachment is often driven by the presence of a specific UI widget
530
+
531
+ Without attachments, your helper class can still exist, but generated POMs will not automatically expose it.
532
+
533
+ Important caveat: if the helper file is missing, the generator currently skips the attachment instead of failing.
534
+
535
+ ### `flatten`
536
+
537
+ `flatten: true` tells the generator to create pass-through methods on the generated class that forward to the attachment.
538
+
539
+ If `Grid` has:
540
+
541
+ ```ts
542
+ export class Grid {
543
+ constructor(page: Page, owner: object) {}
544
+ Search(text: string) {}
545
+ }
546
+ ```
547
+
548
+ then the generated page can expose either:
549
+
550
+ ```ts
551
+ page.grid.Search("alice@example.com");
552
+ ```
217
553
 
218
- - `nativeWrappers` describes common wrapper components (e.g. design-system buttons/inputs)
219
- - `customPom` groups handwritten helper wiring and conditional attachments
220
- - `testIdAttribute` lets you use a different attribute name (e.g. `data-qa`, `data-cy`)
554
+ or, with `flatten: true`:
221
555
 
222
- ### Notes for Playwright users
556
+ ```ts
557
+ page.Search("alice@example.com");
558
+ ```
559
+
560
+ Actual current rules:
561
+
562
+ - only public instance methods are candidates
563
+ - method signatures are parsed from the helper class source
564
+ - complex/unsupported parameter shapes may prevent flattening for that method
565
+ - passthroughs are only emitted when the method name is unambiguous across flatten-enabled attachments
566
+ - passthroughs are skipped if they would collide with an existing generated method, attachment property, child component property, or widget property
567
+ - `flatten` affects methods, not fields/getters
568
+
569
+ Without `flatten`, helper composition still works; you just call through the helper property explicitly.
570
+
571
+ ## Playwright fixtures: actual behavior and caveats
572
+
573
+ When `generation.playwright.fixtures` is enabled, the generator emits a strongly typed Playwright fixture module.
574
+
575
+ What it gives you:
576
+
577
+ - lower-camel-case fixtures for views (`UserListPage` → `userListPage`)
578
+ - lower-camel-case fixtures for component classes too
579
+ - `pomFactory.create(Ctor)` for ad-hoc page-object construction inside tests
580
+ - an `animation` option that wires the generated runtime's pointer settings
581
+
582
+ Current caveats:
223
583
 
224
- This package emits Playwright-oriented helpers (e.g. `page.getByTestId(...)`).
584
+ - there are no generated `openXPage` helpers; tests call `goTo()` explicitly when available
585
+ - override preference only affects fixture construction
586
+ - component fixtures are skipped when their lower-camel-case name would collide with reserved Playwright fixture names such as `page`, `context`, `browser`, or `request`
587
+ - an override class still needs a `new (page)`-compatible constructor because that is what fixtures call
225
588
 
226
- If you change Playwright's `testIdAttribute`, make sure the app actually renders the same attribute.
589
+ ## TypeScript vs C# output
227
590
 
228
- ### Migration helper: `injection.existingIdBehavior`
591
+ ### TypeScript output
229
592
 
230
- When cleaning up a codebase that already has a mix of manually-authored test ids and generated ones:
593
+ This is the main surface and the most complete implementation.
231
594
 
232
- - `"preserve"` (default): leave author-provided ids untouched
233
- - `"overwrite"`: replace existing ids with generated ids
234
- - `"error"`: throw when an existing id is detected (useful for incremental cleanup)
595
+ It includes:
235
596
 
236
- When you want CI/builds to fail on explicit test ids, pair `existingIdBehavior: "error"` with the ESLint cleanup rule exported from `@immense/vue-pom-generator/eslint`.
597
+ - aggregated page/component classes
598
+ - child-component composition
599
+ - typed navigation return types from resolvable `:to`
600
+ - view-level `route` / `goTo()` / `goToSelf()` when router generation is enabled
601
+ - custom helper imports and attachments
602
+ - optional Playwright fixtures
237
603
 
238
- ### ESLint cleanup rule: remove existing test-id attributes
604
+ ### C# output
239
605
 
240
- Use the `remove-existing-test-id-attributes` rule to strip explicit test-id usage from `.vue` files before or while enforcing `existingIdBehavior: "error"`.
606
+ Enable with:
241
607
 
242
- The fixer handles both template attributes like `data-testid="save-button"` and object-literal keys such as `inputAttr: { 'data-testid': 'save-button' }` inside Vue SFC expressions/scripts.
608
+ ```ts
609
+ generation: {
610
+ emit: ["ts", "csharp"],
611
+ }
612
+ ```
613
+
614
+ What the C# emitter currently does well:
615
+
616
+ - emits a single `page-object-models.g.cs`
617
+ - generates locator properties and action methods
618
+ - handles dynamic test-id interpolation
619
+ - supports navigation methods that return target page classes when the target is known
620
+
621
+ What it currently does **not** do:
243
622
 
244
- Add this to your ESLint flat-config file, typically `eslint.config.ts` (or `eslint.config.js` / `eslint.config.mjs` at the project root):
623
+ - generate Playwright fixtures
624
+ - generate helper attachments / flattening
625
+ - generate the same view-level `route` / `goToSelf()` helpers as TypeScript
626
+ - provide feature parity for click instrumentation (`annotationText` is effectively a no-op there)
627
+
628
+ So if you need the full ergonomic surface, TypeScript is the first-class output today.
629
+
630
+ ## `virtual:testids`
631
+
632
+ This package registers a Vite virtual module named `virtual:testids`.
633
+
634
+ Usage:
635
+
636
+ ```ts
637
+ import { testIdManifest } from "virtual:testids";
638
+
639
+ console.log(testIdManifest.UserEditorPage);
640
+ ```
641
+
642
+ What it contains:
643
+
644
+ - an object keyed by component name
645
+ - each value is a sorted array of collected test ids for that component
646
+
647
+ What it is good for:
648
+
649
+ - runtime inspection
650
+ - analytics / logging helpers that need the current generated ids
651
+ - debugging what the generator has collected
652
+
653
+ What it is not:
654
+
655
+ - a full metadata export
656
+ - a generated source file on disk
657
+
658
+ ## ESLint rules that actually ship
659
+
660
+ The package exports `@immense/vue-pom-generator/eslint`.
661
+
662
+ ### `remove-existing-test-id-attributes`
663
+
664
+ This is the migration rule that pairs with `injection.existingIdBehavior`.
665
+
666
+ What it does:
667
+
668
+ - removes explicit static attributes like `data-testid="save-button"`
669
+ - removes bound forms like `:data-testid="buttonId"`
670
+ - supports custom attribute names such as `data-qa`
671
+ - also handles object-literal cases inside Vue SFC expressions/scripts that represent test-id attrs
672
+
673
+ Why it exists:
674
+
675
+ - mixed manual/generated ids are hard to reason about
676
+ - `existingIdBehavior: "error"` is much more usable when a fixer can clean existing code first
677
+
678
+ Recommended usage:
679
+
680
+ 1. run the ESLint rule with `--fix`
681
+ 2. switch `existingIdBehavior` to `"error"`
682
+ 3. keep the rule in CI so manually-authored ids do not creep back in
683
+
684
+ Example flat config:
245
685
 
246
686
  ```ts
247
- // eslint.config.ts (project root)
248
687
  import vueParser from "vue-eslint-parser";
249
688
  import { plugin as vuePomGeneratorEslint } from "@immense/vue-pom-generator/eslint";
250
689
 
@@ -266,19 +705,368 @@ export default [
266
705
  ];
267
706
  ```
268
707
 
269
- Then run ESLint with `--fix` once to remove legacy attributes across the project. After cleanup, keep the rule enabled in CI and set `injection.existingIdBehavior: "error"` so both linting and compilation fail fast when explicit ids sneak back in.
270
-
271
- If you use a custom attribute instead of `data-testid`, configure the rule with an option:
272
-
273
- In that same `eslint.config.ts` file:
708
+ If you use a custom attribute:
274
709
 
275
710
  ```ts
276
- // inside eslint.config.ts
277
- const rules = {
711
+ rules: {
278
712
  "@immense/vue-pom-generator/remove-existing-test-id-attributes": ["error", { attribute: "data-qa" }],
279
- };
713
+ }
280
714
  ```
281
715
 
282
- ## Sequence diagram
716
+ ### `no-raw-locator-action`
717
+
718
+ This rule exists too. It flags direct raw Playwright actions on generated PascalCase getters (for example calling `.click()` directly on a generated getter) so teams use the generated action methods instead.
719
+
720
+ ## Configuration reference
721
+
722
+ The sections below follow the actual `VuePomGeneratorPluginOptions` shape from `plugin/types.ts`.
723
+
724
+ ### Top-level options
725
+
726
+ #### `vueOptions`
727
+
728
+ - **What it does:** Forwards options to `@vitejs/plugin-vue`.
729
+ - **Why it exists:** You still need normal Vue compiler/plugin settings such as `defineModel`, `propsDestructure`, or template compiler tweaks.
730
+ - **Benefit:** You do not lose ordinary Vue plugin configuration just because this package owns the Vue plugin by default.
731
+ - **Without it:** the Vue plugin uses its normal defaults.
732
+ - **Example:**
733
+
734
+ ```ts
735
+ vueOptions: {
736
+ script: { defineModel: true, propsDestructure: true },
737
+ }
738
+ ```
739
+
740
+ #### `vuePluginOwnership`
741
+
742
+ - **What it does:** Chooses whether this package creates `@vitejs/plugin-vue` itself (`"internal"`) or patches an app-owned plugin (`"external"`).
743
+ - **Why it exists:** Some projects want a single explicit `vue()` plugin in their Vite config, and Nuxt relies on the resolved app-owned plugin.
744
+ - **Benefit:** Avoids duplicate Vue-plugin setup and makes Nuxt/external ownership work.
745
+ - **Without it:** standard Vue apps default to `"internal"`.
746
+ - **Example:**
747
+
748
+ ```ts
749
+ vuePluginOwnership: "external"
750
+ ```
751
+
752
+ #### `logging.verbosity`
753
+
754
+ - **What it does:** Controls package log volume.
755
+ - **Why it exists:** Generator startup scans and regen passes can be noisy when you are debugging, but you usually do not want that noise all the time.
756
+ - **Benefit:** Lets you turn on useful lifecycle diagnostics without patching the package.
757
+ - **Without it:** default is `"warn"`.
758
+ - **Example:**
759
+
760
+ ```ts
761
+ logging: { verbosity: "debug" }
762
+ ```
763
+
764
+ ### `injection`
765
+
766
+ `injection` controls compile-time test-id derivation and template rewriting.
767
+
768
+ #### `injection.attribute`
769
+
770
+ - **What it does:** Sets the attribute name that is injected and later treated as the test id.
771
+ - **Why it exists:** Some teams standardize on `data-testid`, others on `data-qa` or `data-cy`.
772
+ - **Benefit:** Keeps the app, Playwright, and generated POMs speaking the same attribute language.
773
+ - **Without it:** the generator uses `data-testid`.
774
+ - **Example:**
775
+
776
+ ```ts
777
+ injection: { attribute: "data-qa" }
778
+ ```
779
+
780
+ #### `injection.viewsDir`
781
+
782
+ - **What it does:** Tells the generator which directory marks a Vue file as a “view” rather than a reusable component.
783
+ - **Why it exists:** Views get page-specific behavior such as `goTo()` / `goToSelf()` and are the main candidates for page fixtures.
784
+ - **Benefit:** Keeps page-level APIs separate from shared component APIs.
785
+ - **Without it:** the generator treats `src/views` as the view root.
786
+ - **Example:**
787
+
788
+ ```ts
789
+ injection: { viewsDir: "app/pages" }
790
+ ```
791
+
792
+ #### `injection.nativeWrappers`
793
+
794
+ - **What it does:** Describes wrapper components so the generator can treat them like native controls.
795
+ - **Why it exists:** Many Vue apps wrap buttons, inputs, selects, radio groups, or third-party widgets behind design-system components.
796
+ - **Benefit:** You get stable control-specific ids and methods instead of generic component-shaped names.
797
+ - **Without it:** the generator relies on native elements, limited wrapper inference, and a few naming conventions.
798
+ - **Example:**
799
+
800
+ ```ts
801
+ injection: {
802
+ nativeWrappers: {
803
+ AppButton: { role: "button" },
804
+ AppTextField: { role: "input" },
805
+ AppSelect: { role: "select", valueAttribute: "name" },
806
+ AppRadioGroup: { role: "radio", requiresOptionDataTestIdPrefix: true },
807
+ },
808
+ }
809
+ ```
810
+
811
+ ##### `nativeWrappers[...].role`
812
+
813
+ - **What it does:** Chooses the native behavior to emulate (`button`, `input`, `select`, `vselect`, `checkbox`, `toggle`, `radio`, `grid`).
814
+ - **Why it exists:** Role drives both test-id suffixes and generated POM method families (`click...`, `type...`, `select...`, etc.).
815
+ - **Benefit:** The generated API matches what the wrapped control actually does.
816
+ - **Without it:** wrapper components may be treated as generic tags unless they can be inferred.
817
+
818
+ ##### `nativeWrappers[...].valueAttribute`
819
+
820
+ - **What it does:** Tells the generator which prop on the wrapper should provide the semantic value/name used in the generated test id.
821
+ - **Why it exists:** Some wrappers do not use `v-model`, but still have a stable value prop such as `name`, `value`, or `field`.
822
+ - **Benefit:** You get meaningful ids and method names from wrapper props instead of fallback names.
823
+ - **Without it:** wrapper naming falls back to model bindings or other weaker signals.
824
+
825
+ ##### `nativeWrappers[...].requiresOptionDataTestIdPrefix`
826
+
827
+ - **What it does:** Adds an `option-data-testid-prefix` attribute for wrappers that need stable option-level ids.
828
+ - **Why it exists:** Radio/select-style wrappers often need a root id plus consistent option ids.
829
+ - **Benefit:** Generated helper methods can target wrapper options consistently.
830
+ - **Without it:** option-level ids may be incomplete or ambiguous.
831
+ - **Caveat:** preserving an existing manual root id on these wrappers can be unsafe, and the current implementation will throw in that case.
832
+
833
+ #### `injection.excludeComponents`
834
+
835
+ - **What it does:** Opts specific component names out of injection/collection.
836
+ - **Why it exists:** Some components are better left alone, or are generated/third-party surfaces you do not want rewritten.
837
+ - **Benefit:** Gives you a practical escape hatch without disabling the plugin globally.
838
+ - **Without it:** all in-scope Vue components are eligible for injection.
839
+ - **Example:**
840
+
841
+ ```ts
842
+ injection: { excludeComponents: ["LegacyWidget"] }
843
+ ```
844
+
845
+ #### `injection.scanDirs`
846
+
847
+ - **What it does:** Sets which directories are scanned for `.vue` files when building the POM graph.
848
+ - **Why it exists:** real projects rarely keep all SFCs under one folder, especially in Nuxt or monorepos.
849
+ - **Benefit:** generation sees the same files your app actually uses.
850
+ - **Without it:** the generator scans `src`.
851
+ - **Example:**
852
+
853
+ ```ts
854
+ injection: { scanDirs: ["src", "components", "layouts"] }
855
+ ```
856
+
857
+ #### `injection.wrapperSearchRoots`
858
+
859
+ - **What it does:** Adds extra roots for wrapper inference outside `scanDirs`.
860
+ - **Why it exists:** wrapper components often live in sibling packages or shared UI workspaces.
861
+ - **Benefit:** local wrapper inference can still work across package boundaries.
862
+ - **Without it:** no extra wrapper lookup is done outside the scanned app directories.
863
+ - **Example:**
864
+
865
+ ```ts
866
+ injection: { wrapperSearchRoots: ["../shared-ui/src/components"] }
867
+ ```
868
+
869
+ #### `injection.existingIdBehavior`
870
+
871
+ - **What it does:** Chooses what happens when a template already has the target attribute.
872
+ - **Why it exists:** migrations usually start from a mixed codebase with manual ids already present.
873
+ - **Benefit:** lets you migrate gradually (`preserve`), force replacement (`overwrite`), or enforce cleanup (`error`).
874
+ - **Without it:** default is `"preserve"`.
875
+ - **Current options:**
876
+ - `"preserve"` — keep the existing attribute
877
+ - `"overwrite"` — replace it with the generated one
878
+ - `"error"` — fail compilation
879
+ - **Important caveat:** `"preserve"` can still throw for wrappers that require option prefixes, because preserving only the root id would leave nested option ids inconsistent.
880
+
881
+ ### `generation`
882
+
883
+ Set `generation: false` to keep injection and `virtual:testids` but skip emitted POM files.
884
+
885
+ #### `generation.outDir`
886
+
887
+ - **What it does:** Sets the output directory for generated files.
888
+ - **Why it exists:** some repos want generated code somewhere other than the default Playwright location.
889
+ - **Benefit:** lets you fit the generator into your existing test layout.
890
+ - **Without it:** output goes to `tests/playwright/__generated__`.
891
+ - **Example:**
892
+
893
+ ```ts
894
+ generation: { outDir: "e2e/generated" }
895
+ ```
896
+
897
+ #### `generation.emit`
898
+
899
+ - **What it does:** Chooses which languages to emit.
900
+ - **Why it exists:** TypeScript is the main target, but some teams also want Playwright .NET classes.
901
+ - **Benefit:** one collection pass can feed both outputs.
902
+ - **Without it:** only TypeScript (`["ts"]`) is emitted.
903
+ - **Example:**
904
+
905
+ ```ts
906
+ generation: { emit: ["ts", "csharp"] }
907
+ ```
908
+
909
+ #### `generation.csharp.namespace`
910
+
911
+ - **What it does:** Sets the namespace for generated C# classes.
912
+ - **Why it exists:** generated code needs to fit your test project's namespace conventions.
913
+ - **Benefit:** avoids immediate manual namespace edits.
914
+ - **Without it:** the namespace defaults to `Playwright.Generated`.
915
+
916
+ #### `generation.nameCollisionBehavior`
917
+
918
+ - **What it does:** Controls what happens when two generated members inside the same class want the same name.
919
+ - **Why it exists:** collisions happen in real templates, especially when multiple elements share the same handler or weak fallback signals.
920
+ - **Benefit:** lets you decide between strictness and convenience.
921
+ - **Without it:** the generator silently suffixes (`"suffix"`).
922
+ - **Current options:**
923
+ - `"error"` — fail fast
924
+ - `"warn"` — warn and suffix
925
+ - `"suffix"` — suffix silently
926
+
927
+ #### `generation.basePageClassPath`
928
+
929
+ - **What it does:** Points at the BasePage runtime template used for generated TypeScript output.
930
+ - **Why it exists:** some teams want to own or customize the generated runtime base class.
931
+ - **Benefit:** you can keep your own BasePage implementation while still using the generator.
932
+ - **Without it:** the package uses its bundled `class-generation/BasePage.ts`.
933
+
934
+ ### `generation.router`
935
+
936
+ If omitted, router introspection is off.
937
+
938
+ #### `generation.router.entry`
939
+
940
+ - **What it does:** Points at the router entry file for standard Vue-router introspection.
941
+ - **Why it exists:** the generator SSR-loads your router to discover names, paths, and target components.
942
+ - **Benefit:** enables typed `:to` navigation targets plus view-level route helpers.
943
+ - **Without it:** no Vue-router introspection happens.
944
+ - **Important detail:** the router module must export a **default router factory function**.
945
+
946
+ #### `generation.router.type`
947
+
948
+ - **What it does:** Chooses the router discovery strategy.
949
+ - **Why it exists:** standard Vue-router apps and Nuxt file-based routing need different discovery paths.
950
+ - **Benefit:** one config field covers both app styles.
951
+ - **Without it:** defaults to `"vue-router"`.
952
+ - **Current options:**
953
+ - `"vue-router"`
954
+ - `"nuxt"`
955
+
956
+ #### `generation.router.moduleShims`
957
+
958
+ - **What it does:** Replaces selected imports only during router introspection.
959
+ - **Why it exists:** router modules often import app-only or browser-only dependencies that should not run during introspection.
960
+ - **Benefit:** keeps route discovery working without reshaping your real router module.
961
+ - **Without it:** the generator attempts to load your router module as-is.
962
+ - **Allowed forms:**
963
+ - `string[]` for no-op exported functions
964
+ - `Record<string, fn>` for explicit shim implementations
965
+ - **Not supported:** wildcard `*` exports
966
+
967
+ ### `generation.playwright`
968
+
969
+ This object holds Playwright-specific additions on top of the generated TypeScript classes.
970
+
971
+ #### `generation.playwright.fixtures`
972
+
973
+ - **What it does:** Enables emitted Playwright fixtures.
974
+ - **Why it exists:** tests become much cleaner when they can request generated page objects as fixtures.
975
+ - **Benefit:** less boilerplate and automatic override preference.
976
+ - **Without it:** you construct generated page objects manually.
977
+ - **Accepted forms:**
978
+ - `true` — emit `fixtures.g.ts` next to the generated POMs
979
+ - `"path"` — if the string ends in `.ts` / `.tsx` / `.mts` / `.cts`, it is treated as a file path; otherwise as an output directory
980
+ - `{ outDir }` — emit to a custom directory
981
+
982
+ #### `generation.playwright.customPoms`
983
+
984
+ - **What it does:** Configures handwritten helper imports and attachments.
985
+ - **Why it exists:** generated code alone is usually not enough for complex widgets or app-specific test abstractions.
986
+ - **Benefit:** lets generated classes compose handwritten helpers instead of forcing you to pick one approach or the other.
987
+ - **Without it:** the generator still uses the default helper directory (`tests/playwright/pom/custom`), but with default naming/collision behavior and no explicit attachments.
988
+
989
+ ### `generation.playwright.customPoms` fields
990
+
991
+ #### `customPoms.dir`
992
+
993
+ - **What it does:** Sets the directory scanned for handwritten helper classes.
994
+ - **Why it exists:** helper code needs a predictable home.
995
+ - **Benefit:** generated output can import your handwritten helpers deterministically.
996
+ - **Without it:** defaults to `tests/playwright/pom/custom`.
997
+
998
+ #### `customPoms.importAliases`
999
+
1000
+ - **What it does:** Maps helper basenames to the local names used in generated imports.
1001
+ - **Why it exists:** generated files may need clearer local names or collision avoidance.
1002
+ - **Benefit:** keeps the aggregate readable and conflict-free.
1003
+ - **Without it:** the basename is used directly, except for built-in alias defaults like `Toggle -> ToggleWidget` and `Checkbox -> CheckboxWidget`.
1004
+
1005
+ #### `customPoms.importNameCollisionBehavior`
1006
+
1007
+ - **What it does:** Controls how helper import names behave when they collide with generated class names.
1008
+ - **Why it exists:** aggregated output shares one import namespace.
1009
+ - **Benefit:** lets you fail hard or auto-alias based on team preference.
1010
+ - **Without it:** the default is `"error"`.
1011
+
1012
+ #### `customPoms.attachments`
1013
+
1014
+ - **What it does:** Declares conditional helper attachments.
1015
+ - **Why it exists:** helper composition is usually widget-driven, not universal.
1016
+ - **Benefit:** pages/components only get the helpers they actually need.
1017
+ - **Without it:** helper files are imported but not attached to generated classes.
1018
+
1019
+ ### `customPoms.attachments[]`
1020
+
1021
+ #### `attachments[].className`
1022
+
1023
+ - **What it does:** Names the helper class to attach.
1024
+ - **Why it exists:** attachments need to reference a specific imported helper.
1025
+ - **Benefit:** explicit helper wiring is easy to read and easy to diff.
1026
+ - **Without it:** no helper is attached.
1027
+ - **Important detail:** this is the real exported class / file basename, not any alias from `importAliases`.
1028
+
1029
+ #### `attachments[].propertyName`
1030
+
1031
+ - **What it does:** Names the property exposed on the generated page/component.
1032
+ - **Why it exists:** attached helpers need a stable public handle.
1033
+ - **Benefit:** makes helper composition obvious at the call site.
1034
+ - **Without it:** there is no property to access.
1035
+
1036
+ #### `attachments[].attachWhenUsesComponents`
1037
+
1038
+ - **What it does:** Lists template component names that trigger the attachment.
1039
+ - **Why it exists:** helper attachment is based on component usage in the collected Vue template graph.
1040
+ - **Benefit:** avoids attaching grid helpers to pages with no grid, modal helpers to pages with no modal, and so on.
1041
+ - **Without it:** the helper never attaches.
1042
+
1043
+ #### `attachments[].attachTo`
1044
+
1045
+ - **What it does:** Limits attachment scope to views, components, or both.
1046
+ - **Why it exists:** some helpers belong only on page objects, others also belong on reusable component POMs.
1047
+ - **Benefit:** keeps the generated surface smaller and more intentional.
1048
+ - **Without it:** the default is `"views"`.
1049
+ - **Current options:**
1050
+ - `"views"`
1051
+ - `"components"`
1052
+ - `"both"`
1053
+
1054
+ #### `attachments[].flatten`
1055
+
1056
+ - **What it does:** Generates direct pass-through methods on the generated class for eligible helper methods.
1057
+ - **Why it exists:** sometimes nested helper syntax is too noisy in tests.
1058
+ - **Benefit:** lets the page API read like a single surface while still being implemented by helpers.
1059
+ - **Without it:** you always call through the helper property explicitly.
1060
+ - **Caveat:** flattening is conservative; ambiguous or colliding method names are skipped.
1061
+
1062
+ ## Practical adoption advice
1063
+
1064
+ If you are evaluating this package critically, the most accurate short version is:
1065
+
1066
+ - it is strongest when your team already wants `getByTestId`-style Playwright tests
1067
+ - it is strongest in TypeScript-first Playwright projects
1068
+ - it is strongest when your Vue component library is either native-heavy or can be described with `nativeWrappers`
1069
+ - it is useful with router-aware pages, but you should treat dynamic-route `goTo()` support as partial and explicit
1070
+ - it becomes much more maintainable when you pair it with the ESLint cleanup rule and a small `pom/custom` folder for the genuinely hard widgets
283
1071
 
284
- See: [`sequence-diagram.md`](./sequence-diagram.md)
1072
+ If that matches your codebase, the package removes a surprising amount of repetitive test maintenance. If it does not, the caveats above are the important ones to believe.