@immense/vue-pom-generator 1.0.58 → 1.0.60

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.
Files changed (58) hide show
  1. package/README.md +36 -25
  2. package/RELEASE_NOTES.md +29 -31
  3. package/class-generation/base-page.ts +49 -25
  4. package/class-generation/index.ts +243 -333
  5. package/class-generation/playwright-types.ts +1 -1
  6. package/click-instrumentation.ts +0 -4
  7. package/dist/class-generation/base-page.d.ts +8 -4
  8. package/dist/class-generation/base-page.d.ts.map +1 -1
  9. package/dist/class-generation/index.d.ts +2 -0
  10. package/dist/class-generation/index.d.ts.map +1 -1
  11. package/dist/class-generation/playwright-types.d.ts +1 -1
  12. package/dist/class-generation/playwright-types.d.ts.map +1 -1
  13. package/dist/click-instrumentation.d.ts +0 -1
  14. package/dist/click-instrumentation.d.ts.map +1 -1
  15. package/dist/index.cjs +1527 -1064
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1529 -1066
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/manifest-generator.d.ts +35 -1
  20. package/dist/manifest-generator.d.ts.map +1 -1
  21. package/dist/method-generation.d.ts +4 -2
  22. package/dist/method-generation.d.ts.map +1 -1
  23. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  24. package/dist/plugin/resolved-generation-options.d.ts +33 -0
  25. package/dist/plugin/resolved-generation-options.d.ts.map +1 -0
  26. package/dist/plugin/resolved-injection-options.d.ts +27 -0
  27. package/dist/plugin/resolved-injection-options.d.ts.map +1 -0
  28. package/dist/plugin/support/build-plugin.d.ts +2 -29
  29. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  30. package/dist/plugin/support/dev-plugin.d.ts +2 -28
  31. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  32. package/dist/plugin/support/virtual-modules.d.ts +3 -1
  33. package/dist/plugin/support/virtual-modules.d.ts.map +1 -1
  34. package/dist/plugin/support-plugins.d.ts +4 -33
  35. package/dist/plugin/support-plugins.d.ts.map +1 -1
  36. package/dist/plugin/types.d.ts +6 -23
  37. package/dist/plugin/types.d.ts.map +1 -1
  38. package/dist/plugin/vue-plugin.d.ts.map +1 -1
  39. package/dist/pom-discoverability.d.ts +10 -0
  40. package/dist/pom-discoverability.d.ts.map +1 -0
  41. package/dist/pom-params.d.ts +40 -0
  42. package/dist/pom-params.d.ts.map +1 -0
  43. package/dist/pom-patterns.d.ts +31 -0
  44. package/dist/pom-patterns.d.ts.map +1 -0
  45. package/dist/routing/to-directive.d.ts +21 -0
  46. package/dist/routing/to-directive.d.ts.map +1 -1
  47. package/dist/tests/base-page.test.d.ts +2 -0
  48. package/dist/tests/base-page.test.d.ts.map +1 -0
  49. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts +7 -4
  50. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts.map +1 -1
  51. package/dist/tests/resolved-injection-options.test.d.ts +2 -0
  52. package/dist/tests/resolved-injection-options.test.d.ts.map +1 -0
  53. package/dist/transform.d.ts +0 -1
  54. package/dist/transform.d.ts.map +1 -1
  55. package/dist/utils.d.ts +129 -63
  56. package/dist/utils.d.ts.map +1 -1
  57. package/package.json +6 -4
  58. package/sequence-diagram.md +6 -6
package/README.md CHANGED
@@ -12,10 +12,11 @@ If you already use Playwright with `getByTestId`, the point is simple: this pack
12
12
  - **Injects test ids during Vue compilation, not at runtime.** It hooks into the Vue template compiler and rewrites the compiled template output.
13
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
14
  - **Generates TypeScript POM output as either one aggregate or split per class, always with a stable `index.ts` barrel.**
15
+ - **Describes generated Playwright locators** with deterministic human-readable labels via `Locator.describe()`.
15
16
  - **Can generate Playwright fixtures** so tests can request `userListPage` instead of constructing `new UserListPage(page)` manually.
16
17
  - **Can fail fast on unnameable wrapper-button actions** so complex inline handlers do not silently degrade into low-signal generated APIs.
17
18
  - **Can emit a single C# POM file** for Playwright .NET consumers.
18
- - **Exposes `virtual:testids`** so your app can import the current collected test-id manifest at runtime.
19
+ - **Exposes `virtual:testids` and `virtual:pom-manifest`** so your app can inspect collected ids and generated POM metadata at runtime.
19
20
  - **Ships ESLint rules** to remove legacy manually-authored test ids, ban raw `page` fixture usage in spec callbacks, and discourage raw locator actions on generated getters.
20
21
 
21
22
  ## What this does not do
@@ -69,12 +70,13 @@ That example is intentionally small, but it shows the real contract:
69
70
  The generator does not use one naming trick. It layers several signals.
70
71
 
71
72
  - **Click actions** prefer semantic handler names such as `save`, `openDetails`, or `runImport`.
73
+ - **Click handlers are instrumented** so generated Playwright helpers can wait on deterministic `__testid_event__` runtime events.
72
74
  - **Inputs and wrapper components** prefer `v-model`, wrapper `valueAttribute`, or related model-like bindings.
73
75
  - **Native elements** also consider `id` / `name` attributes.
74
76
  - **Router links / `:to` bindings** can contribute route-based naming and typed navigation return types when the target can be resolved.
75
77
  - **Wrapper components** can be explicit (`nativeWrappers`) or inferred from simple local SFC templates.
76
78
  - **Fallback naming exists, but it is intentionally conservative.** That is why `generation.nameCollisionBehavior` exists.
77
- - **Wrapper-action generation fails fast by default.** The generator blocks button-like wrapper `:handler` expressions that it cannot turn into a semantic action name; set `errorBehavior: "ignore"` if you explicitly want the old permissive fallback.
79
+ - **Wrapper-action generation fails fast.** The generator blocks button-like wrapper `:handler` expressions that it cannot turn into a semantic action name.
78
80
 
79
81
  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.
80
82
 
@@ -205,7 +207,6 @@ const pomConfig = defineVuePomGeneratorConfig({
205
207
  script: { defineModel: true, propsDestructure: true },
206
208
  },
207
209
  logging: { verbosity: "info" },
208
- errorBehavior: "error",
209
210
  injection: {
210
211
  attribute: "data-testid",
211
212
  viewsDir: "src/views",
@@ -218,7 +219,7 @@ const pomConfig = defineVuePomGeneratorConfig({
218
219
  AppRadioGroup: { role: "radio", requiresOptionDataTestIdPrefix: true },
219
220
  },
220
221
  excludeComponents: ["LegacyWidget"],
221
- existingIdBehavior: "preserve",
222
+ existingIdBehavior: "error",
222
223
  },
223
224
  generation: {
224
225
  emit: ["ts", "csharp"],
@@ -226,7 +227,7 @@ const pomConfig = defineVuePomGeneratorConfig({
226
227
  namespace: "MyProject.Tests.Generated",
227
228
  },
228
229
  outDir: "tests/playwright/__generated__",
229
- nameCollisionBehavior: "suffix",
230
+ nameCollisionBehavior: "error",
230
231
  router: {
231
232
  entry: "src/router/index.ts",
232
233
  moduleShims: {
@@ -347,8 +348,8 @@ This is important if you are deciding whether the tool will fit into a real code
347
348
 
348
349
  - **Dev server:** on startup, it scans the configured Vue page/component/layout directories (or the directories resolved from Nuxt config in Nuxt mode), compiles each `.vue` file into a snapshot, writes the configured TypeScript outputs once, then batches add/change/delete events and regenerates incrementally.
349
350
  - **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.
350
- - **Always-on virtual module:** `virtual:testids` is registered whether generation is enabled or disabled.
351
- - **Generation can be disabled:** `generation: false` still keeps compile-time test-id injection and the virtual module, but skips emitted POM files.
351
+ - **Always-on virtual modules:** `virtual:testids` and `virtual:pom-manifest` are registered whether generation is enabled or disabled.
352
+ - **Generation can be disabled:** `generation: false` still keeps compile-time test-id injection and the virtual modules, but skips emitted POM files.
352
353
 
353
354
  ## Router-aware navigation: the real semantics
354
355
 
@@ -680,27 +681,49 @@ This package registers a Vite virtual module named `virtual:testids`.
680
681
  Usage:
681
682
 
682
683
  ```ts
683
- import { testIdManifest } from "virtual:testids";
684
+ import { pomManifest, testIdManifest } from "virtual:testids";
684
685
 
685
686
  console.log(testIdManifest.UserEditorPage);
687
+ console.log(pomManifest.UserEditorPage.entries);
686
688
  ```
687
689
 
688
690
  What it contains:
689
691
 
690
692
  - an object keyed by component name
691
- - each value is a sorted array of collected test ids for that component
693
+ - `testIdManifest`: each value is a sorted array of collected test ids for that component
694
+ - `pomManifest`: richer per-component metadata including source file, generated locator/property names, and generated action names
695
+ - each manifest entry also carries `locatorDescription`, which matches the human-readable label used by generated Playwright locators
692
696
 
693
697
  What it is good for:
694
698
 
695
699
  - runtime inspection
696
700
  - analytics / logging helpers that need the current generated ids
697
- - debugging what the generator has collected
701
+ - debugging what the generator has collected and generated
702
+ - keeping manifest-driven tools aligned with the same locator descriptions shown in Playwright traces
698
703
 
699
704
  What it is not:
700
705
 
701
- - a full metadata export
702
706
  - a generated source file on disk
703
707
 
708
+ ## `virtual:pom-manifest`
709
+
710
+ This package also registers `virtual:pom-manifest` for consumers that only want the richer discoverability surface.
711
+
712
+ Usage:
713
+
714
+ ```ts
715
+ import { pomManifest } from "virtual:pom-manifest";
716
+
717
+ console.log(pomManifest.UserEditorPage.sourceFile);
718
+ console.log(pomManifest.UserEditorPage.entries.map(entry => entry.generatedActionNames));
719
+ ```
720
+
721
+ What it contains:
722
+
723
+ - an object keyed by component/page object model class name
724
+ - for each component: source file, whether it is a view or component, sorted test ids, and rich entry metadata
725
+ - for each entry: test id, semantic name, inferred role, generated property name, generated action names, and collected compiler metadata when available
726
+
704
727
  ## ESLint rules that actually ship
705
728
 
706
729
  The package exports `@immense/vue-pom-generator/eslint`.
@@ -849,18 +872,6 @@ The sections below follow the actual `VuePomGeneratorPluginOptions` shape from `
849
872
  logging: { verbosity: "debug" }
850
873
  ```
851
874
 
852
- #### `errorBehavior`
853
-
854
- - **What it does:** Controls strict/error behavior for generator checks.
855
- - **Why it exists:** complex inline handlers can otherwise fall through to generic naming, which makes generated APIs harder to discover and review.
856
- - **Benefit:** fail-fast behavior is the default, while `"ignore"` or the object form let you opt back into only the permissive checks you want.
857
- - **Without it:** the default is `"error"`, so unsupported button-wrapper handlers stop generation instead of silently falling back.
858
- - **Accepted values:**
859
- - `"ignore"` — keep permissive defaults for all supported checks
860
- - `"error"` — enable error-on-failure behavior for all supported checks (default)
861
- - `{ missingSemanticNameBehavior: "ignore" }` — opt out only of the button-wrapper semantic-name check
862
- - **Current scope:** this first pass is intentionally narrow. The object form currently supports `missingSemanticNameBehavior`, which targets button-like wrappers with `:handler`; value/model-driven wrappers still use their existing naming flow.
863
-
864
875
  ### `injection`
865
876
 
866
877
  `injection` controls compile-time test-id derivation and template rewriting.
@@ -983,7 +994,7 @@ The sections below follow the actual `VuePomGeneratorPluginOptions` shape from `
983
994
  - **What it does:** Chooses what happens when a template already has the target attribute.
984
995
  - **Why it exists:** migrations usually start from a mixed codebase with manual ids already present.
985
996
  - **Benefit:** lets you migrate gradually (`preserve`), force replacement (`overwrite`), or enforce cleanup (`error`).
986
- - **Without it:** default is `"preserve"`.
997
+ - **Without it:** default is `"error"`.
987
998
  - **Current options:**
988
999
  - `"preserve"` — keep the existing attribute
989
1000
  - `"overwrite"` — replace it with the generated one
@@ -1030,7 +1041,7 @@ Set `generation: false` to keep injection and `virtual:testids` but skip emitted
1030
1041
  - **What it does:** Controls what happens when two generated members inside the same class want the same name.
1031
1042
  - **Why it exists:** collisions happen in real templates, especially when multiple elements share the same handler or weak fallback signals.
1032
1043
  - **Benefit:** lets you decide between strictness and convenience.
1033
- - **Without it:** the generator silently suffixes (`"suffix"`).
1044
+ - **Without it:** the generator fails fast (`"error"`).
1034
1045
  - **Current options:**
1035
1046
  - `"error"` — fail fast
1036
1047
  - `"warn"` — warn and suffix
package/RELEASE_NOTES.md CHANGED
@@ -1,47 +1,45 @@
1
- I'll fetch the actual commits and PRs between v1.0.57 and HEAD to generate accurate release
2
- notes.
1
+ ## Highlights
3
2
 
4
- Based on the commit range v1.0.57..HEAD, only PR #19 is included in this release. Here are the
5
- release notes:
3
+ - **Manifest and locator descriptions**: Page Object Model (POM) manifests now include
4
+ human-readable descriptions for both routes and locators, improving discoverability and
5
+ developer experience
6
+ - **New discoverability module**: Added `pom-discoverability.ts` to centralize description
7
+ generation and introspection logic
8
+ - **Enhanced test coverage**: Expanded tests for class generation, virtual modules, and base
9
+ page functionality
6
10
 
7
- ---
8
-
9
- ## Highlights
11
+ ## Changes
10
12
 
11
- - Pass annotation text through generated click helpers for better test documentation
12
- - Resolve Vue compiler and Nuxt kit dependencies from consuming app setups instead of the
13
- plugin's own tree
14
- - Centralize Playwright video dimension configuration for consistent local and CI test
15
- recordings
13
+ **Core Features**
14
+ - Added manifest and locator description generation for improved POM documentation (#24)
15
+ - Implemented new `pom-discoverability.ts` module for centralized description handling
16
16
 
17
- ## Changes
17
+ **Generator Improvements**
18
+ - Enhanced `manifest-generator.ts` with description support (+256 lines)
19
+ - Updated method generation to include locator descriptions
20
+ - Improved base page class generation with description metadata
18
21
 
19
- **Local integration hardening:**
20
- - Generated click helpers now accept and document annotation text parameters
21
- - Resolve `@vue/compiler-sfc.parse` and `@nuxt/kit` from real consuming-app setups to avoid
22
- version mismatches
23
- - Add centralized Playwright video dimensions configuration (`playwright-video-dimensions.json`)
24
- - Add script to normalize Playwright video settings across local config and CI environments
22
+ **Plugin & Integration**
23
+ - Updated virtual module system to support description exports
24
+ - Enhanced Vite plugin integration for description handling
25
25
 
26
- **Test coverage improvements:**
27
- - Add regression tests for project-local Nuxt kit loading
28
- - Enhance Nuxt discovery test coverage with real-world integration scenarios
26
+ **Testing & Documentation**
27
+ - Added class generation coverage tests
28
+ - Expanded virtual test ID tests with description validation
29
+ - Updated README with new feature documentation
30
+ - Refreshed generated TypeScript fixtures
29
31
 
30
32
  ## Breaking Changes
31
33
 
32
- None.
34
+ None
33
35
 
34
36
  ## Pull Requests Included
35
37
 
36
- - [#19](https://github.com/immense/vue-pom-generator/pull/19) fix: harden local app integration
38
+ - #24 feat: add manifest and locator descriptions
39
+ (https://github.com/immense/vue-pom-generator/pull/24)
37
40
 
38
41
  ## Testing
39
42
 
40
- Validated with:
41
- - `npm run lint`
42
- - `npm run typecheck`
43
- - `npm run build`
44
- - `npm test`
45
-
46
- All tests passing with 258 net lines added across 13 files.
43
+ All changes include corresponding test coverage. Added new test suites for class generation
44
+ coverage and expanded virtual module tests to validate description functionality.
47
45
 
@@ -1,5 +1,5 @@
1
1
  import type { PwLocator, PwPage } from "./playwright-types";
2
- import { TESTID_CLICK_EVENT_NAME, TESTID_CLICK_EVENT_STRICT_FLAG } from "../click-instrumentation";
2
+ import { TESTID_CLICK_EVENT_NAME } from "../click-instrumentation";
3
3
  import type { TestIdClickEventDetail } from "../click-instrumentation";
4
4
  import { Callout } from "./callout";
5
5
  import type { CalloutRenderer } from "./callout";
@@ -118,6 +118,10 @@ export class BasePage {
118
118
  this.pointer = new Pointer(this.page, this.testIdAttribute, this.callout, pointerRenderer);
119
119
  }
120
120
 
121
+ public get screencast(): PwPage["screencast"] {
122
+ return this.page.screencast;
123
+ }
124
+
121
125
  private async waitForTestIdClickEventAfter(testId: string, options?: { timeoutMs?: number }): Promise<void> {
122
126
  if (!REQUIRE_CLICK_EVENT) {
123
127
  return;
@@ -135,7 +139,7 @@ export class BasePage {
135
139
  // In that scenario, the click already did its job; don't fail the test infra.
136
140
  try {
137
141
  await this.page.evaluate(
138
- ({ eventName, strictFlagName, expectedTestId, timeoutMs, requireEvent, debug }) => {
142
+ ({ eventName, expectedTestId, timeoutMs, requireEvent, debug }) => {
139
143
  return new Promise<void>((resolve, reject) => {
140
144
  const g = globalThis;
141
145
  if (!g || typeof g.addEventListener !== "function") {
@@ -143,16 +147,6 @@ export class BasePage {
143
147
  return;
144
148
  }
145
149
 
146
- // Mark strict mode in the page so the injected click wrapper can
147
- // fail fast (no fallback) when instrumentation is expected.
148
- if (requireEvent) {
149
- try {
150
- type GlobalWithFlag = typeof globalThis & { [k: string]: boolean | undefined };
151
- (g as GlobalWithFlag)[strictFlagName] = true;
152
- }
153
- catch { /* noop */ }
154
- }
155
-
156
150
  const cleanup = (timer: ReturnType<typeof setTimeout>, onEvent: (evt: Event) => void) => {
157
151
  clearTimeout(timer);
158
152
  try {
@@ -216,7 +210,6 @@ export class BasePage {
216
210
  },
217
211
  {
218
212
  eventName: TESTID_CLICK_EVENT_NAME,
219
- strictFlagName: TESTID_CLICK_EVENT_STRICT_FLAG,
220
213
  expectedTestId: testId,
221
214
  timeoutMs,
222
215
  requireEvent,
@@ -240,12 +233,22 @@ export class BasePage {
240
233
  return `[${this.testIdAttribute}="${testId}"]`;
241
234
  }
242
235
 
243
- protected locatorByTestId(testId: string): PwLocator {
244
- return this.page.locator(this.selectorForTestId(testId));
236
+ protected describeLocator(locator: PwLocator, description?: string): PwLocator {
237
+ const normalizedDescription = description?.trim();
238
+ return normalizedDescription ? locator.describe(normalizedDescription) : locator;
239
+ }
240
+
241
+ protected locatorByTestId(testId: string, description?: string): PwLocator {
242
+ return this.describeLocator(this.page.locator(this.selectorForTestId(testId)), description);
245
243
  }
246
244
 
247
- protected locatorWithinTestIdByLabel(rootTestId: string, label: string, options?: { exact?: boolean }): PwLocator {
248
- return this.locatorByTestId(rootTestId).getByLabel(label, { exact: options?.exact ?? true });
245
+ protected locatorWithinTestIdByLabel(
246
+ rootTestId: string,
247
+ label: string,
248
+ options?: { exact?: boolean; description?: string },
249
+ ): PwLocator {
250
+ const locator = this.locatorByTestId(rootTestId).getByLabel(label, { exact: options?.exact ?? true });
251
+ return this.describeLocator(locator, options?.description);
249
252
  }
250
253
 
251
254
  /**
@@ -495,8 +498,14 @@ export class BasePage {
495
498
  * Clicks on an element with the specified data-testid
496
499
  * @param testId The data-testid of the element to click
497
500
  */
498
- public async clickByTestId(testId: string, annotationText: string = "", wait: boolean = true): Promise<void> {
499
- await this.pointer.animateCursorToElement(this.selectorForTestId(testId), true, 200, annotationText, {
501
+ public async clickByTestId(
502
+ testId: string,
503
+ annotationText: string = "",
504
+ wait: boolean = true,
505
+ description?: string,
506
+ ): Promise<void> {
507
+ const locator = this.locatorByTestId(testId, description);
508
+ await this.pointer.animateCursorToElement(locator, true, 200, annotationText, {
500
509
  afterClick: async ({ testId: clickedTestId, instrumented }: AfterPointerClickInfo) => {
501
510
  if (!wait) return;
502
511
  if (!clickedTestId || !instrumented) return;
@@ -520,14 +529,23 @@ export class BasePage {
520
529
  label: string,
521
530
  annotationText: string = "",
522
531
  wait: boolean = true,
523
- options?: { exact?: boolean },
532
+ options?: { exact?: boolean; description?: string },
524
533
  ): Promise<void> {
525
- const locator = this.locatorWithinTestIdByLabel(rootTestId, label, { exact: options?.exact });
534
+ const locator = this.locatorWithinTestIdByLabel(rootTestId, label, {
535
+ exact: options?.exact,
536
+ description: options?.description,
537
+ });
526
538
  await this.clickLocator(locator, annotationText, wait);
527
539
  }
528
540
 
529
- protected async fillInputByTestId(testId: string, text: string, annotationText: string = ""): Promise<void> {
530
- await this.pointer.animateCursorToElementAndClickAndFill(this.selectorForTestId(testId), text, true, 200, annotationText, {
541
+ protected async fillInputByTestId(
542
+ testId: string,
543
+ text: string,
544
+ annotationText: string = "",
545
+ description?: string,
546
+ ): Promise<void> {
547
+ const locator = this.locatorByTestId(testId, description);
548
+ await this.pointer.animateCursorToElementAndClickAndFill(locator, text, true, 200, annotationText, {
531
549
  afterClick: async ({ testId: clickedTestId, instrumented }: AfterPointerClickInfo) => {
532
550
  if (!clickedTestId || !instrumented) return;
533
551
  await this.waitForTestIdClickEventAfter(clickedTestId);
@@ -539,8 +557,14 @@ export class BasePage {
539
557
  * Interacts with a vue-select control rooted by a data-testid.
540
558
  * This is emitted frequently by the generator; keeping it here reduces per-page duplicated code.
541
559
  */
542
- protected async selectVSelectByTestId(testId: string, value: string, timeOut: number = 500, annotationText: string = ""): Promise<void> {
543
- const root = this.locatorByTestId(testId);
560
+ protected async selectVSelectByTestId(
561
+ testId: string,
562
+ value: string,
563
+ timeOut: number = 500,
564
+ annotationText: string = "",
565
+ description?: string,
566
+ ): Promise<void> {
567
+ const root = this.locatorByTestId(testId, description);
544
568
  const input = root.locator("input");
545
569
 
546
570
  await this.pointer.animateCursorToElement(input, false, 200, annotationText);