@immense/vue-pom-generator 1.0.60 → 1.0.61

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
@@ -693,6 +693,7 @@ What it contains:
693
693
  - `testIdManifest`: each value is a sorted array of collected test ids for that component
694
694
  - `pomManifest`: richer per-component metadata including source file, generated locator/property names, and generated action names
695
695
  - each manifest entry also carries `locatorDescription`, which matches the human-readable label used by generated Playwright locators
696
+ - each manifest entry may also carry `accessibility`, a compile-time review signal with static metadata, accessible-name hints, and `needsReview`
696
697
 
697
698
  What it is good for:
698
699
 
@@ -722,7 +723,7 @@ What it contains:
722
723
 
723
724
  - an object keyed by component/page object model class name
724
725
  - 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
+ - for each entry: test id, semantic name, inferred role, generated property name, generated action names, collected compiler metadata when available, and accessibility review metadata when available
726
727
 
727
728
  ## ESLint rules that actually ship
728
729
 
@@ -1054,6 +1055,13 @@ Set `generation: false` to keep injection and `virtual:testids` but skip emitted
1054
1055
  - **Benefit:** you can keep your own BasePage implementation while still using the generator.
1055
1056
  - **Without it:** the package uses its bundled `class-generation/BasePage.ts`.
1056
1057
 
1058
+ #### `generation.accessibilityAudit`
1059
+
1060
+ - **What it does:** emits warnings for generated interactive elements whose accessible-name signals look weak or unverifiable from compile-time metadata.
1061
+ - **Why it exists:** generated POM coverage can look healthy while the underlying control still needs an accessibility review.
1062
+ - **Benefit:** gives you an opt-in review signal without auto-injecting ARIA or guessing runtime behavior.
1063
+ - **Without it:** accessibility review metadata can still appear in `pomManifest`, but no warnings are emitted during compilation.
1064
+
1057
1065
  ### `generation.router`
1058
1066
 
1059
1067
  If omitted, router introspection is off.
package/RELEASE_NOTES.md CHANGED
@@ -1,45 +1,49 @@
1
- ## Highlights
1
+ # Release Notes: v1.0.61
2
2
 
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
3
+ ## Highlights
4
+
5
+ - **New accessibility audit system** to detect and warn about common ARIA and semantic HTML
6
+ issues during POM generation
7
+ - Audit signals now included in the manifest output for downstream consumption
8
+ - Enhanced compiler metadata utilities to extract role and label information from templates
9
+ - Comprehensive test coverage for the new accessibility audit features
10
10
 
11
11
  ## Changes
12
12
 
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
13
+ ### Accessibility & Quality
14
+ - Added `accessibility-audit.ts` module with audit functions for detecting missing labels,
15
+ invalid ARIA usage, and semantic violations
16
+ - Integrated audit warnings into the Vite plugin pipeline with configurable severity
17
+ - Extended manifest generator to include `auditSignals` for each component
18
+
19
+ ### Plugin & Configuration
20
+ - Added `auditSeverity` option to plugin configuration (default: `"warn"`)
21
+ - Enhanced `ResolvedGenerationOptions` to support audit configuration
22
+ - Updated plugin to emit warnings when accessibility issues are detected during build
16
23
 
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
24
+ ### Compiler & Metadata
25
+ - Improved `compiler-metadata-utils.ts` to extract `role` and `aria-label` attributes
26
+ - Enhanced metadata collector to capture accessibility-related attributes
21
27
 
22
- **Plugin & Integration**
23
- - Updated virtual module system to support description exports
24
- - Enhanced Vite plugin integration for description handling
28
+ ### Documentation
29
+ - Updated README with information about the new accessibility audit feature
25
30
 
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
31
+ ### Testing
32
+ - Added comprehensive test suite in `tests/accessibility-audit.test.ts` (54 new tests)
33
+ - Added tests for resolved generation options
34
+ - Updated existing tests to accommodate new audit functionality
31
35
 
32
36
  ## Breaking Changes
33
37
 
34
- None
38
+ None.
35
39
 
36
40
  ## Pull Requests Included
37
41
 
38
- - #24 feat: add manifest and locator descriptions
39
- (https://github.com/immense/vue-pom-generator/pull/24)
42
+ - #25 feat: add accessibility audit metadata and warnings
43
+ (https://github.com/immense/vue-pom-generator/pull/25) by @dkattan
40
44
 
41
45
  ## Testing
42
46
 
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
+ All changes include full test coverage. New test suite validates audit detection for missing
48
+ labels, invalid ARIA attributes, redundant roles, and other semantic issues.
45
49
 
@@ -0,0 +1,20 @@
1
+ import type { ElementMetadata } from "./metadata-collector";
2
+ export interface AccessibilityAuditResult {
3
+ needsReview: boolean;
4
+ accessibleNameSource: "aria-label" | "title" | "text" | "dynamic" | "unknown" | "missing";
5
+ reasons: string[];
6
+ staticAriaLabel?: string;
7
+ staticRole?: string;
8
+ staticTitle?: string;
9
+ staticTextContent?: string;
10
+ }
11
+ export declare function buildAccessibilityAudit(metadata: ElementMetadata | undefined, inferredRole: string | null): AccessibilityAuditResult | undefined;
12
+ export declare function collectAccessibilityReviewWarnings(manifest: Record<string, {
13
+ entries: Array<{
14
+ testId: string;
15
+ generatedPropertyName: string | null;
16
+ inferredRole: string | null;
17
+ accessibility?: AccessibilityAuditResult;
18
+ }>;
19
+ }>): string[];
20
+ //# sourceMappingURL=accessibility-audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility-audit.d.ts","sourceRoot":"","sources":["../accessibility-audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,EAAE,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1F,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAMD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,wBAAwB,GAAG,SAAS,CA4CtC;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QACxC,MAAM,EAAE,MAAM,CAAC;QACf,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;QACrC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,aAAa,CAAC,EAAE,wBAAwB,CAAC;KAC1C,CAAC,CAAA;CAAE,CAAC,GACJ,MAAM,EAAE,CAmBV"}
@@ -1 +1 @@
1
- {"version":3,"file":"compiler-metadata-utils.d.ts","sourceRoot":"","sources":["../compiler-metadata-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACX,oBAAoB,EAErB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,aAAa,GAAG,SAAS,CAAC;AAEvE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,aAAa,GAAE,MAAsB,GAAG,cAAc,CAQ9G;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAcrE;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CA+C/G;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC,EAAE,OAAO,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,eAAe,GAAG,IAAI,CA+DzB"}
1
+ {"version":3,"file":"compiler-metadata-utils.d.ts","sourceRoot":"","sources":["../compiler-metadata-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACX,oBAAoB,EAGrB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,aAAa,GAAG,SAAS,CAAC;AAEvE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,aAAa,GAAE,MAAsB,GAAG,cAAc,CAQ9G;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAcrE;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CA+C/G;AAkCD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC,EAAE,OAAO,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,eAAe,GAAG,IAAI,CA2DzB"}
package/dist/index.cjs CHANGED
@@ -260,6 +260,7 @@ function resolveGenerationSupportOptions(options) {
260
260
  nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
261
261
  existingIdBehavior: options.existingIdBehavior ?? "error",
262
262
  testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
263
+ accessibilityAudit: options.accessibilityAudit ?? false,
263
264
  routerAwarePoms: options.routerAwarePoms ?? false,
264
265
  routerEntry: options.routerEntry,
265
266
  routerType: options.routerType ?? "vue-router",
@@ -8214,6 +8215,59 @@ function createDevProcessorPlugin(options) {
8214
8215
  }
8215
8216
  };
8216
8217
  }
8218
+ function supportsInlineTextAccessibleName(role) {
8219
+ return role === "button" || role === "radio";
8220
+ }
8221
+ function buildAccessibilityAudit(metadata, inferredRole) {
8222
+ if (!metadata || !inferredRole) {
8223
+ return void 0;
8224
+ }
8225
+ const role = normalizePomRoleLabel(inferredRole).toLowerCase();
8226
+ const dynamicProps = new Set(metadata.dynamicProps ?? []);
8227
+ const hasDynamicAccessibleNameSignal = dynamicProps.has("aria-label") || dynamicProps.has("title") || !!metadata.hasDynamicText;
8228
+ let accessibleNameSource;
8229
+ const reasons = [];
8230
+ if (metadata.staticAriaLabel) {
8231
+ accessibleNameSource = "aria-label";
8232
+ } else if (supportsInlineTextAccessibleName(role) && metadata.staticTextContent) {
8233
+ accessibleNameSource = "text";
8234
+ } else if (metadata.staticTitle) {
8235
+ accessibleNameSource = "title";
8236
+ } else if (hasDynamicAccessibleNameSignal) {
8237
+ accessibleNameSource = "dynamic";
8238
+ } else if (role === "input" || role === "select") {
8239
+ accessibleNameSource = "unknown";
8240
+ reasons.push("No inline accessible-name signal was found; the element may rely on external markup such as a separate <label>.");
8241
+ } else {
8242
+ accessibleNameSource = "missing";
8243
+ reasons.push("No compile-time accessible-name signal was found.");
8244
+ }
8245
+ return {
8246
+ needsReview: accessibleNameSource === "unknown" || accessibleNameSource === "missing",
8247
+ accessibleNameSource,
8248
+ reasons,
8249
+ ...metadata.staticAriaLabel ? { staticAriaLabel: metadata.staticAriaLabel } : {},
8250
+ ...metadata.staticRole ? { staticRole: metadata.staticRole } : {},
8251
+ ...metadata.staticTitle ? { staticTitle: metadata.staticTitle } : {},
8252
+ ...metadata.staticTextContent ? { staticTextContent: metadata.staticTextContent } : {}
8253
+ };
8254
+ }
8255
+ function collectAccessibilityReviewWarnings(manifest) {
8256
+ const warnings = [];
8257
+ for (const [componentName, component] of Object.entries(manifest)) {
8258
+ for (const entry of component.entries) {
8259
+ if (!entry.accessibility?.needsReview) {
8260
+ continue;
8261
+ }
8262
+ const entryLabel = entry.generatedPropertyName ?? entry.testId;
8263
+ const reasonText = entry.accessibility.reasons.join(" ");
8264
+ warnings.push(
8265
+ `[vue-pom-generator] Accessibility review suggested for ${componentName}.${entryLabel} (role=${entry.inferredRole ?? "unknown"}, testId=${JSON.stringify(entry.testId)}): ${reasonText}`
8266
+ );
8267
+ }
8268
+ }
8269
+ return warnings;
8270
+ }
8217
8271
  function removeByKeySegment(value) {
8218
8272
  const idx = value.indexOf("ByKey");
8219
8273
  if (idx < 0) {
@@ -8272,6 +8326,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8272
8326
  ...generatedActionName ? [generatedActionName] : [],
8273
8327
  ...extraActionNames.filter((name) => name !== generatedActionName)
8274
8328
  ]));
8329
+ const accessibility = buildAccessibilityAudit(metadata, pom?.nativeRole ?? null);
8275
8330
  return {
8276
8331
  testId,
8277
8332
  selectorPatternKind: entry.selectorValue.patternKind,
@@ -8282,6 +8337,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8282
8337
  nativeRole: pom.nativeRole
8283
8338
  }) : componentName,
8284
8339
  inferredRole: pom?.nativeRole ?? null,
8340
+ ...accessibility ? { accessibility } : {},
8285
8341
  generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
8286
8342
  generatedActionName,
8287
8343
  generatedActionNames,
@@ -8557,6 +8613,33 @@ function parseDynamicProps(dynamicProps) {
8557
8613
  }
8558
8614
  return void 0;
8559
8615
  }
8616
+ function findStaticAttributeValue(element, attributeName) {
8617
+ const attribute = element.props.find(
8618
+ (prop) => prop.type === compilerCore.NodeTypes.ATTRIBUTE && prop.name === attributeName
8619
+ );
8620
+ const value = attribute?.value?.content?.trim();
8621
+ return value ? value : void 0;
8622
+ }
8623
+ function collectStaticTextContent(children) {
8624
+ const parts = [];
8625
+ const visit = (nodes) => {
8626
+ for (const node of nodes) {
8627
+ if (node.type === compilerCore.NodeTypes.TEXT) {
8628
+ const text2 = node.content.replace(/\s+/g, " ").trim();
8629
+ if (text2) {
8630
+ parts.push(text2);
8631
+ }
8632
+ continue;
8633
+ }
8634
+ if (node.type === compilerCore.NodeTypes.ELEMENT) {
8635
+ visit(node.children);
8636
+ }
8637
+ }
8638
+ };
8639
+ visit(children);
8640
+ const text = parts.join(" ").replace(/\s+/g, " ").trim();
8641
+ return text || void 0;
8642
+ }
8560
8643
  function tryCreateElementMetadata(args) {
8561
8644
  const { element, semanticNameMap } = args;
8562
8645
  const testIdAttribute = (args.testIdAttribute ?? "data-testid").trim() || "data-testid";
@@ -8580,13 +8663,9 @@ function tryCreateElementMetadata(args) {
8580
8663
  dynamicPropsList = [content];
8581
8664
  }
8582
8665
  }
8583
- const semanticName = semanticNameMap.get(testId);
8584
- if (!semanticName) {
8585
- return null;
8586
- }
8587
8666
  const metadata = {
8588
8667
  testId,
8589
- semanticName,
8668
+ semanticName: semanticNameMap.get(testId),
8590
8669
  tag: element.tag,
8591
8670
  tagType: element.tagType,
8592
8671
  patchFlag,
@@ -8595,7 +8674,11 @@ function tryCreateElementMetadata(args) {
8595
8674
  hasClickHandler: patchFlag ? Boolean(patchFlag & 32) : void 0,
8596
8675
  hasDynamicClass: patchFlag ? Boolean(patchFlag & 2) : void 0,
8597
8676
  hasDynamicStyle: patchFlag ? Boolean(patchFlag & 4) : void 0,
8598
- hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0
8677
+ hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0,
8678
+ staticAriaLabel: findStaticAttributeValue(element, "aria-label"),
8679
+ staticRole: findStaticAttributeValue(element, "role"),
8680
+ staticTitle: findStaticAttributeValue(element, "title"),
8681
+ staticTextContent: collectStaticTextContent(element.children)
8599
8682
  };
8600
8683
  return metadata;
8601
8684
  }
@@ -8647,6 +8730,7 @@ function extractMetadataAfterTransform(ast, componentName, elementMetadata, sema
8647
8730
  if (componentMetadata.size > 0) {
8648
8731
  elementMetadata.set(componentName, componentMetadata);
8649
8732
  }
8733
+ return componentMetadata;
8650
8734
  }
8651
8735
  function createVuePluginWithTestIds(options) {
8652
8736
  const {
@@ -8661,11 +8745,13 @@ function createVuePluginWithTestIds(options) {
8661
8745
  excludedComponents,
8662
8746
  getViewsDirAbs,
8663
8747
  testIdAttribute,
8748
+ accessibilityAudit,
8664
8749
  loggerRef,
8665
8750
  getSourceDirs,
8666
8751
  getWrapperSearchRoots,
8667
8752
  getProjectRoot
8668
8753
  } = options;
8754
+ const lastAccessibilityWarningSignatureByComponent = /* @__PURE__ */ new Map();
8669
8755
  const getComponentNameFromPath = (filename) => {
8670
8756
  return resolveComponentNameFromPath({
8671
8757
  filename,
@@ -8738,13 +8824,37 @@ function createVuePluginWithTestIds(options) {
8738
8824
  )
8739
8825
  );
8740
8826
  return () => {
8741
- extractMetadataAfterTransform(
8827
+ const componentMetadata = extractMetadataAfterTransform(
8742
8828
  node,
8743
8829
  componentName,
8744
8830
  elementMetadata,
8745
8831
  semanticNameMap,
8746
8832
  testIdAttribute
8747
8833
  );
8834
+ if (!accessibilityAudit) {
8835
+ return;
8836
+ }
8837
+ const dependencies = componentHierarchyMap.get(componentName);
8838
+ if (!dependencies || componentMetadata.size === 0) {
8839
+ return;
8840
+ }
8841
+ const manifest = buildPomManifest(
8842
+ /* @__PURE__ */ new Map([[componentName, dependencies]]),
8843
+ /* @__PURE__ */ new Map([[componentName, componentMetadata]])
8844
+ );
8845
+ const warnings = collectAccessibilityReviewWarnings(manifest);
8846
+ const signature = warnings.join("\n");
8847
+ if (!signature) {
8848
+ lastAccessibilityWarningSignatureByComponent.delete(componentName);
8849
+ return;
8850
+ }
8851
+ if (lastAccessibilityWarningSignatureByComponent.get(componentName) === signature) {
8852
+ return;
8853
+ }
8854
+ lastAccessibilityWarningSignatureByComponent.set(componentName, signature);
8855
+ for (const warning of warnings) {
8856
+ loggerRef.current.warn(warning);
8857
+ }
8748
8858
  };
8749
8859
  }
8750
8860
  let transform = perFileTransform.get(componentName);
@@ -9109,6 +9219,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9109
9219
  nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
9110
9220
  existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
9111
9221
  testIdAttribute,
9222
+ accessibilityAudit: generationOptions?.accessibilityAudit,
9112
9223
  routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
9113
9224
  routerEntry,
9114
9225
  routerType,
@@ -9202,6 +9313,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9202
9313
  excludedComponents,
9203
9314
  getViewsDirAbs,
9204
9315
  testIdAttribute,
9316
+ accessibilityAudit: resolvedGenerationOptions.accessibilityAudit,
9205
9317
  loggerRef,
9206
9318
  getSourceDirs,
9207
9319
  getWrapperSearchRoots: getWrapperSearchRootsAbs,