@immense/vue-pom-generator 1.0.45 → 1.0.47

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
@@ -13,6 +13,7 @@ If you already use Playwright with `getByTestId`, the point is simple: this pack
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 one aggregated TypeScript POM file** plus a stable `index.ts` barrel.
15
15
  - **Can generate Playwright fixtures** so tests can request `userListPage` instead of constructing `new UserListPage(page)` manually.
16
+ - **Can fail fast on unnameable wrapper-button actions** so complex inline handlers do not silently degrade into low-signal generated APIs.
16
17
  - **Can emit a single C# POM file** for Playwright .NET consumers.
17
18
  - **Exposes `virtual:testids`** so your app can import the current collected test-id manifest at runtime.
18
19
  - **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.
@@ -73,6 +74,7 @@ The generator does not use one naming trick. It layers several signals.
73
74
  - **Router links / `:to` bindings** can contribute route-based naming and typed navigation return types when the target can be resolved.
74
75
  - **Wrapper components** can be explicit (`nativeWrappers`) or inferred from simple local SFC templates.
75
76
  - **Fallback naming exists, but it is intentionally conservative.** That is why `generation.nameCollisionBehavior` exists.
77
+ - **You can opt into stricter wrapper-action generation.** `errorBehavior: "error"` blocks button-like wrapper `:handler` expressions that the generator cannot turn into a semantic action name.
76
78
 
77
79
  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.
78
80
 
@@ -200,6 +202,7 @@ const pomConfig = defineVuePomGeneratorConfig({
200
202
  script: { defineModel: true, propsDestructure: true },
201
203
  },
202
204
  logging: { verbosity: "info" },
205
+ errorBehavior: "error",
203
206
  injection: {
204
207
  attribute: "data-testid",
205
208
  viewsDir: "src/views",
@@ -804,6 +807,18 @@ The sections below follow the actual `VuePomGeneratorPluginOptions` shape from `
804
807
  logging: { verbosity: "debug" }
805
808
  ```
806
809
 
810
+ #### `errorBehavior`
811
+
812
+ - **What it does:** Controls strict/error behavior for generator checks.
813
+ - **Why it exists:** complex inline handlers can otherwise fall through to generic naming, which makes generated APIs harder to discover and review.
814
+ - **Benefit:** `"error"` lets you opt into fail-fast behavior globally, while the object form lets you turn on only the checks you care about.
815
+ - **Without it:** the default is `"ignore"`, so existing permissive fallback behavior remains in place.
816
+ - **Accepted values:**
817
+ - `"ignore"` — keep permissive defaults for all supported checks
818
+ - `"error"` — enable error-on-failure behavior for all supported checks
819
+ - `{ missingSemanticNameBehavior: "error" }` — enable only the button-wrapper semantic-name check
820
+ - **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.
821
+
807
822
  ### `injection`
808
823
 
809
824
  `injection` controls compile-time test-id derivation and template rewriting.
package/RELEASE_NOTES.md CHANGED
@@ -1,52 +1,49 @@
1
1
  ● ## Highlights
2
2
 
3
- - New ESLint rule `no-page-fixture-in-specs` enforces Playwright Page Object Model best
4
- practices
5
- - Dev-mode POM generation now has parity with build mode
6
- - Fail-fast behavior added for dev snapshot generation errors
7
- - Enhanced test coverage with build-serve parity regression tests
8
- - Improved release workflow with PR preview comments
3
+ - **Fail-fast validation** for wrapper handlers that cannot be named, improving error detection
4
+ at build time
5
+ - Added comprehensive validation logic across plugin lifecycle (dev and build modes)
6
+ - Expanded test coverage with 54+ new test cases for handler naming scenarios
7
+ - Enhanced error reporting when anonymous or unnameable handlers are detected
8
+ - Updated documentation with new validation requirements and best practices
9
9
 
10
10
  ## Changes
11
11
 
12
- **Tooling & Linting**
13
- - Added new ESLint rule to prevent `page` fixture usage in spec files (112 lines)
14
- - Updated ESLint configuration and exports
12
+ **Core Validation**
13
+ - Added fail-fast validation in `transform.ts` to detect unnameable wrapper handlers before
14
+ runtime
15
+ - Implemented handler name validation in `create-vue-pom-generator-plugins.ts` with detailed
16
+ error messages
17
+ - Added validation hooks in both `build-plugin.ts` and `dev-plugin.ts` for consistency across
18
+ modes
15
19
 
16
- **Code Generation**
17
- - Fixed keyed POM deduplication logic
18
- - Improved C# navigation return types
19
- - Minor adjustments to class generation
20
+ **Type System & API**
21
+ - Extended plugin types with new validation options and error handling interfaces
22
+ - Added support for configurable validation behavior in plugin options
20
23
 
21
24
  **Testing**
22
- - Added comprehensive vue-plugin-state tests (71 lines)
23
- - Enhanced ESLint rule test coverage (58 new lines)
24
- - Added build-serve parity regression tests
25
- - Updated test assertions for better coverage
25
+ - Added 54 new test cases in `options.test.ts` covering edge cases for handler naming
26
+ - Added 23 new tests in `transform.test.ts` for transformation validation
27
+ - Added 9 coverage tests in `utils-coverage.test.ts`
28
+ - Enhanced `dev-plugin-options.test.ts` with validation scenario tests
26
29
 
27
- **Documentation & CI**
28
- - Updated README with improved guidance (54 lines added)
29
- - Enhanced release workflow with PR release-notes preview comments
30
- - Workflow improvements for Playwright generator
30
+ **Documentation**
31
+ - Updated README.md with guidance on handler naming requirements and validation behavior
31
32
 
32
33
  ## Breaking Changes
33
34
 
34
- None.
35
+ - **Handler naming validation**: The plugin now throws errors when wrapper handlers cannot be
36
+ named (anonymous functions, computed property names, etc.). Code that previously compiled with
37
+ unnameable handlers will now fail at build time. Ensure all wrapper handlers have statically
38
+ determinable names.
35
39
 
36
40
  ## Pull Requests Included
37
41
 
38
- - #7 test: add build–serve parity regression tests
39
- (https://github.com/immense/vue-pom-generator/pull/7)
40
- - #6 fix: fail fast on dev snapshot generation errors
41
- (https://github.com/immense/vue-pom-generator/pull/6)
42
- - #5 fix: dev-mode POM generation parity with build mode
43
- (https://github.com/immense/vue-pom-generator/pull/5)
44
- - #4 Fix keyed POM dedupe and C# navigation returns
45
- (https://github.com/immense/vue-pom-generator/pull/4)
46
- - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
42
+ - #11: feat: fail fast on unnameable wrapper handlers
43
+ (https://github.com/immense/vue-pom-generator/pull/11) by @dkattan
47
44
 
48
45
  ## Testing
49
46
 
50
- All changes include corresponding test coverage. Added 132 lines of new tests across
51
- vue-plugin-state and ESLint rule validation.
47
+ Comprehensive test coverage added with 80+ new test cases spanning handler naming validation,
48
+ transformation scenarios, plugin options, and edge case coverage.
52
49
 
package/dist/index.cjs CHANGED
@@ -5437,6 +5437,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5437
5437
  const existingIdBehavior = options.existingIdBehavior ?? "preserve";
5438
5438
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
5439
5439
  const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
5440
+ const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "ignore";
5440
5441
  const warn = options.warn;
5441
5442
  const vueFilesPathMap = options.vueFilesPathMap;
5442
5443
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -5702,6 +5703,22 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5702
5703
  });
5703
5704
  };
5704
5705
  const { nativeWrappersValue, optionDataTestIdPrefixValue, semanticNameHint } = getNativeWrapperTransformInfo(element, componentName, nativeWrappers);
5706
+ const handlerDirective = element.props.find((p) => {
5707
+ return p.type === compilerCore.NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
5708
+ }) ?? null;
5709
+ const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
5710
+ if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
5711
+ const loc = element.loc?.start;
5712
+ const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
5713
+ const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
5714
+ throw new Error(
5715
+ `[vue-pom-generator] Could not derive a semantic POM action name for button-like wrapper in ${componentName} (${context.filename ?? "unknown"}:${locationHint}).
5716
+ Element: <${element.tag}>
5717
+ Handler: ${handlerSource}
5718
+
5719
+ Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name. You can also set errorBehavior = "ignore" to keep generic fallback behavior.`
5720
+ );
5721
+ }
5705
5722
  if (nativeWrappersValue) {
5706
5723
  if (optionDataTestIdPrefixValue) {
5707
5724
  const existing = existingIdBehavior === "preserve" ? tryGetExistingElementDataTestId(element, testIdAttribute) : null;
@@ -5811,7 +5828,6 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5811
5828
  });
5812
5829
  return;
5813
5830
  }
5814
- const handlerInfo = nodeHandlerAttributeInfo(element);
5815
5831
  if (handlerInfo) {
5816
5832
  const testId = getHandlerAttributeValueDataTestId(handlerInfo.semanticNameHint);
5817
5833
  applyResolvedDataTestIdForElement({
@@ -5920,6 +5936,7 @@ function createBuildProcessorPlugin(options) {
5920
5936
  customPomImportNameCollisionBehavior,
5921
5937
  testIdAttribute,
5922
5938
  nameCollisionBehavior,
5939
+ missingSemanticNameBehavior,
5923
5940
  existingIdBehavior,
5924
5941
  nativeWrappers,
5925
5942
  excludedComponents,
@@ -6030,6 +6047,7 @@ function createBuildProcessorPlugin(options) {
6030
6047
  existingIdBehavior: existingIdBehavior ?? "preserve",
6031
6048
  testIdAttribute,
6032
6049
  nameCollisionBehavior,
6050
+ missingSemanticNameBehavior,
6033
6051
  warn: (message) => loggerRef.current.warn(message),
6034
6052
  vueFilesPathMap,
6035
6053
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -6154,6 +6172,7 @@ function createDevProcessorPlugin(options) {
6154
6172
  customPomImportAliases,
6155
6173
  customPomImportNameCollisionBehavior,
6156
6174
  nameCollisionBehavior = "suffix",
6175
+ missingSemanticNameBehavior,
6157
6176
  existingIdBehavior,
6158
6177
  testIdAttribute,
6159
6178
  routerAwarePoms,
@@ -6318,6 +6337,7 @@ function createDevProcessorPlugin(options) {
6318
6337
  {
6319
6338
  existingIdBehavior: existingIdBehavior ?? "preserve",
6320
6339
  nameCollisionBehavior,
6340
+ missingSemanticNameBehavior,
6321
6341
  testIdAttribute,
6322
6342
  warn: (message) => loggerRef.current.warn(message),
6323
6343
  vueFilesPathMap: targetVuePathMap,
@@ -6558,6 +6578,7 @@ function createSupportPlugins(options) {
6558
6578
  scanDirs,
6559
6579
  getWrapperSearchRoots,
6560
6580
  nameCollisionBehavior = "suffix",
6581
+ missingSemanticNameBehavior,
6561
6582
  existingIdBehavior,
6562
6583
  outDir,
6563
6584
  emitLanguages,
@@ -6613,6 +6634,7 @@ function createSupportPlugins(options) {
6613
6634
  customPomImportNameCollisionBehavior,
6614
6635
  testIdAttribute,
6615
6636
  nameCollisionBehavior,
6637
+ missingSemanticNameBehavior,
6616
6638
  existingIdBehavior,
6617
6639
  nativeWrappers,
6618
6640
  excludedComponents,
@@ -6641,6 +6663,7 @@ function createSupportPlugins(options) {
6641
6663
  customPomImportAliases,
6642
6664
  customPomImportNameCollisionBehavior,
6643
6665
  nameCollisionBehavior,
6666
+ missingSemanticNameBehavior,
6644
6667
  existingIdBehavior,
6645
6668
  testIdAttribute,
6646
6669
  routerAwarePoms,
@@ -7015,6 +7038,41 @@ function assertNonEmptyStringArray(value, name) {
7015
7038
  assertNonEmptyString(entry, `${name}[${index}]`);
7016
7039
  }
7017
7040
  }
7041
+ function assertOneOf(value, allowed, name) {
7042
+ if (!value)
7043
+ return;
7044
+ if (allowed.includes(value)) {
7045
+ return;
7046
+ }
7047
+ throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
7048
+ }
7049
+ function assertErrorBehavior(value, name) {
7050
+ if (!value) {
7051
+ return;
7052
+ }
7053
+ if (value === "ignore" || value === "error") {
7054
+ return;
7055
+ }
7056
+ if (typeof value !== "object" || Array.isArray(value)) {
7057
+ throw new TypeError(`${name} must be "ignore", "error", or an object.`);
7058
+ }
7059
+ const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
7060
+ for (const key of Object.keys(value)) {
7061
+ if (!supportedKeys.has(key)) {
7062
+ throw new TypeError(`${name} contains unsupported key "${key}".`);
7063
+ }
7064
+ }
7065
+ assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
7066
+ }
7067
+ function resolveMissingSemanticNameBehavior(value) {
7068
+ if (!value) {
7069
+ return "ignore";
7070
+ }
7071
+ if (value === "ignore" || value === "error") {
7072
+ return value;
7073
+ }
7074
+ return value.missingSemanticNameBehavior ?? "ignore";
7075
+ }
7018
7076
  function assertRouterModuleShims(value, name) {
7019
7077
  if (!value)
7020
7078
  return;
@@ -7147,6 +7205,8 @@ function createVuePomGeneratorPlugins(options = {}) {
7147
7205
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
7148
7206
  const usesExternalVuePlugin = vuePluginOwnership === "external";
7149
7207
  const csharp = generationOptions?.csharp;
7208
+ const errorBehavior = options.errorBehavior;
7209
+ const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
7150
7210
  const generateFixtures = generationOptions?.playwright?.fixtures;
7151
7211
  const customPoms = generationOptions?.playwright?.customPoms;
7152
7212
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
@@ -7178,6 +7238,7 @@ function createVuePomGeneratorPlugins(options = {}) {
7178
7238
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
7179
7239
  assertNonEmptyString(viewsDir, "[vue-pom-generator] injection.viewsDir");
7180
7240
  assertNonEmptyStringArray(wrapperSearchRoots, "[vue-pom-generator] injection.wrapperSearchRoots");
7241
+ assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
7181
7242
  if (generationEnabled) {
7182
7243
  assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
7183
7244
  assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
@@ -7189,7 +7250,7 @@ function createVuePomGeneratorPlugins(options = {}) {
7189
7250
  applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompilerOptions, isNuxt ? "nuxt" : vuePluginOwnership);
7190
7251
  }
7191
7252
  loggerRef.current.info(`projectRoot=${projectRootRef.current}`);
7192
- loggerRef.current.info(`Active plugins: ${config.plugins.map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
7253
+ loggerRef.current.info(`Active plugins: ${(config.plugins ?? []).map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
7193
7254
  }
7194
7255
  };
7195
7256
  const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, viewsDir);
@@ -7223,6 +7284,7 @@ function createVuePomGeneratorPlugins(options = {}) {
7223
7284
  scanDirs,
7224
7285
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
7225
7286
  nameCollisionBehavior,
7287
+ missingSemanticNameBehavior,
7226
7288
  existingIdBehavior,
7227
7289
  outDir,
7228
7290
  emitLanguages,