@immense/vue-pom-generator 1.0.60 → 1.0.62

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,33 +1,28 @@
1
- ## Highlights
1
+ I'll examine the commits to generate accurate release notes.
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
+ Based on the commit details, here are the release notes for v1.0.62:
10
4
 
11
- ## Changes
5
+ ---
6
+
7
+ ## Highlights
12
8
 
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
9
+ - Fixed handler name preservation after Vue compiler transformations
10
+ - Improved fallback logic to extract semantic names from author-written source code
11
+ - Enhanced test coverage for handler attribute parsing edge cases
16
12
 
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
13
+ ## Changes
21
14
 
22
- **Plugin & Integration**
23
- - Updated virtual module system to support description exports
24
- - Enhanced Vite plugin integration for description handling
15
+ **Bug Fixes**
16
+ - Preserve original handler names when Vue compiler rewrites expressions during compilation
17
+ - Extract semantic names from author source (`loc.content`) rather than transformed source when
18
+ they differ
19
+ - Add fallback expression parsing to handle cases where Vue's transformation breaks Babel AST
20
+ parsing
25
21
 
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
22
+ **Testing**
23
+ - Added 38 new test cases for handler attribute transformation scenarios
24
+ - Improved coverage for `utils.ts` edge cases with 36 additional tests
25
+ - Fixed missing `SimpleExpressionNode` import in test files
31
26
 
32
27
  ## Breaking Changes
33
28
 
@@ -35,11 +30,10 @@
35
30
 
36
31
  ## Pull Requests Included
37
32
 
38
- - #24 feat: add manifest and locator descriptions
39
- (https://github.com/immense/vue-pom-generator/pull/24)
33
+ None (direct commits to main)
40
34
 
41
35
  ## Testing
42
36
 
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.
37
+ All changes include comprehensive test coverage. Added 75+ new test cases covering handler
38
+ attribute parsing, Vue compiler transformations, and semantic name extraction edge cases.
45
39
 
@@ -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",
@@ -1801,11 +1802,12 @@ function nodeHandlerAttributeInfo(node) {
1801
1802
  return null;
1802
1803
  }
1803
1804
  const exp = handlerDirective.exp;
1804
- const source = getVueExpressionSource(exp, "content", "compiled");
1805
- if (!source) {
1805
+ const transformedSource = getVueExpressionSource(exp, "content", "compiled");
1806
+ const authorSource = getVueExpressionSource(exp, "loc", "content", "compiled");
1807
+ if (!authorSource) {
1806
1808
  return null;
1807
1809
  }
1808
- const mergeKey = `handler:expr:${source}`;
1810
+ const mergeKey = `handler:expr:${authorSource}`;
1809
1811
  const expr = tryGetDirectiveBabelAst(handlerDirective, {
1810
1812
  preferredViews: ["content", "compiled"],
1811
1813
  plugins: ["typescript", "jsx"],
@@ -1813,9 +1815,7 @@ function nodeHandlerAttributeInfo(node) {
1813
1815
  // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1814
1816
  preferExistingAst: false
1815
1817
  });
1816
- if (!expr) {
1817
- return null;
1818
- }
1818
+ const fallbackExpr = transformedSource !== authorSource ? tryParseBabelExpressionFromSource(authorSource, ["typescript", "jsx"]) : null;
1819
1819
  const isNodeType2 = (node2, type) => {
1820
1820
  return node2 !== null && node2.type === type;
1821
1821
  };
@@ -2051,59 +2051,73 @@ function nodeHandlerAttributeInfo(node) {
2051
2051
  const limited = parts.slice(0, 2);
2052
2052
  return limited.map((p) => `${toPascalCase(p.key)}${p.value}`).join("");
2053
2053
  };
2054
- const direct = getLastIdentifierFromMemberChain(expr);
2055
- if (direct) {
2056
- return { semanticNameHint: toPascalCase(direct), mergeKey };
2057
- }
2058
- if (isArrowFunctionExpressionNode(expr)) {
2059
- const body = expr.body;
2060
- const tryFromCallExpression = (call) => {
2061
- const resolvedCall = isAwaitExpressionNode(call) ? call.argument : call;
2062
- if (!isCallExpressionNode(resolvedCall)) {
2063
- return null;
2064
- }
2065
- const name = getLastIdentifierFromMemberChain(resolvedCall.callee);
2066
- if (!name) {
2067
- return null;
2054
+ const tryFromCallExpression = (call) => {
2055
+ const resolvedCall = isAwaitExpressionNode(call) ? call.argument : call;
2056
+ if (!isCallExpressionNode(resolvedCall)) {
2057
+ return null;
2058
+ }
2059
+ const name = getLastIdentifierFromMemberChain(resolvedCall.callee);
2060
+ if (!name) {
2061
+ return null;
2062
+ }
2063
+ const suffix = getStableSuffixFromCall(resolvedCall);
2064
+ const semanticNameHint2 = suffix ? `${toPascalCase(name)}${suffix}` : toPascalCase(name);
2065
+ return semanticNameHint2;
2066
+ };
2067
+ const resolveSemanticName = (candidateExpr) => {
2068
+ if (!candidateExpr) {
2069
+ return null;
2070
+ }
2071
+ const direct = getLastIdentifierFromMemberChain(candidateExpr);
2072
+ if (direct) {
2073
+ return toPascalCase(direct);
2074
+ }
2075
+ if (isArrowFunctionExpressionNode(candidateExpr)) {
2076
+ const body = candidateExpr.body;
2077
+ const directCall = tryFromCallExpression(body);
2078
+ if (directCall) {
2079
+ return directCall;
2080
+ }
2081
+ if (isAssignmentExpressionNode(body)) {
2082
+ const lhs = getAssignmentTargetName(body.left);
2083
+ if (lhs) {
2084
+ const rhs = stableWordFromValue(body.right);
2085
+ return `Set${toPascalCase(lhs)}${rhs ?? ""}`;
2086
+ }
2068
2087
  }
2069
- const suffix = getStableSuffixFromCall(resolvedCall);
2070
- const semanticNameHint = suffix ? `${toPascalCase(name)}${suffix}` : toPascalCase(name);
2071
- return semanticNameHint;
2072
- };
2073
- const directCall = tryFromCallExpression(body);
2074
- if (directCall) {
2075
- return { semanticNameHint: directCall, mergeKey };
2076
- }
2077
- if (isAssignmentExpressionNode(body)) {
2078
- const lhs = getAssignmentTargetName(body.left);
2079
- if (lhs) {
2080
- const rhs = stableWordFromValue(body.right);
2081
- const semanticNameHint = `Set${toPascalCase(lhs)}${rhs ?? ""}`;
2082
- return { semanticNameHint, mergeKey };
2083
- }
2084
- }
2085
- if (isBlockStatementNode(body)) {
2086
- const stmts = body.body ?? [];
2087
- if (stmts.length > 0) {
2088
- const firstStmt = stmts[0];
2089
- if (isReturnStatementNode(firstStmt)) {
2090
- const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
2091
- if (fromReturn) {
2092
- return { semanticNameHint: fromReturn, mergeKey };
2088
+ if (isBlockStatementNode(body)) {
2089
+ const stmts = body.body ?? [];
2090
+ if (stmts.length > 0) {
2091
+ const firstStmt = stmts[0];
2092
+ if (isReturnStatementNode(firstStmt)) {
2093
+ const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
2094
+ if (fromReturn) {
2095
+ return fromReturn;
2096
+ }
2093
2097
  }
2094
- }
2095
- if (isExpressionStatementNode(firstStmt)) {
2096
- const fromExpr = tryFromCallExpression(firstStmt.expression ?? null);
2097
- if (fromExpr) {
2098
- return { semanticNameHint: fromExpr, mergeKey };
2098
+ if (isExpressionStatementNode(firstStmt)) {
2099
+ const fromExpr = tryFromCallExpression(firstStmt.expression ?? null);
2100
+ if (fromExpr) {
2101
+ return fromExpr;
2102
+ }
2099
2103
  }
2100
2104
  }
2101
2105
  }
2106
+ const bodyName = getLastIdentifierFromMemberChain(body);
2107
+ if (bodyName) {
2108
+ return toPascalCase(bodyName);
2109
+ }
2102
2110
  }
2103
- const bodyName = getLastIdentifierFromMemberChain(body);
2104
- if (bodyName) {
2105
- return { semanticNameHint: toPascalCase(bodyName), mergeKey };
2106
- }
2111
+ return null;
2112
+ };
2113
+ const isVueRefValueAccess = (candidate) => {
2114
+ return isMemberExpressionNode(candidate) && candidate.computed === false && isIdentifierNode2(candidate.property) && candidate.property.name === "value";
2115
+ };
2116
+ const transformedSemanticName = resolveSemanticName(expr);
2117
+ const fallbackSemanticName = resolveSemanticName(fallbackExpr);
2118
+ const semanticNameHint = fallbackSemanticName && transformedSource !== authorSource && isVueRefValueAccess(expr) ? fallbackSemanticName : transformedSemanticName ?? fallbackSemanticName;
2119
+ if (semanticNameHint) {
2120
+ return { semanticNameHint, mergeKey };
2107
2121
  }
2108
2122
  return null;
2109
2123
  }
@@ -8214,6 +8228,59 @@ function createDevProcessorPlugin(options) {
8214
8228
  }
8215
8229
  };
8216
8230
  }
8231
+ function supportsInlineTextAccessibleName(role) {
8232
+ return role === "button" || role === "radio";
8233
+ }
8234
+ function buildAccessibilityAudit(metadata, inferredRole) {
8235
+ if (!metadata || !inferredRole) {
8236
+ return void 0;
8237
+ }
8238
+ const role = normalizePomRoleLabel(inferredRole).toLowerCase();
8239
+ const dynamicProps = new Set(metadata.dynamicProps ?? []);
8240
+ const hasDynamicAccessibleNameSignal = dynamicProps.has("aria-label") || dynamicProps.has("title") || !!metadata.hasDynamicText;
8241
+ let accessibleNameSource;
8242
+ const reasons = [];
8243
+ if (metadata.staticAriaLabel) {
8244
+ accessibleNameSource = "aria-label";
8245
+ } else if (supportsInlineTextAccessibleName(role) && metadata.staticTextContent) {
8246
+ accessibleNameSource = "text";
8247
+ } else if (metadata.staticTitle) {
8248
+ accessibleNameSource = "title";
8249
+ } else if (hasDynamicAccessibleNameSignal) {
8250
+ accessibleNameSource = "dynamic";
8251
+ } else if (role === "input" || role === "select") {
8252
+ accessibleNameSource = "unknown";
8253
+ reasons.push("No inline accessible-name signal was found; the element may rely on external markup such as a separate <label>.");
8254
+ } else {
8255
+ accessibleNameSource = "missing";
8256
+ reasons.push("No compile-time accessible-name signal was found.");
8257
+ }
8258
+ return {
8259
+ needsReview: accessibleNameSource === "unknown" || accessibleNameSource === "missing",
8260
+ accessibleNameSource,
8261
+ reasons,
8262
+ ...metadata.staticAriaLabel ? { staticAriaLabel: metadata.staticAriaLabel } : {},
8263
+ ...metadata.staticRole ? { staticRole: metadata.staticRole } : {},
8264
+ ...metadata.staticTitle ? { staticTitle: metadata.staticTitle } : {},
8265
+ ...metadata.staticTextContent ? { staticTextContent: metadata.staticTextContent } : {}
8266
+ };
8267
+ }
8268
+ function collectAccessibilityReviewWarnings(manifest) {
8269
+ const warnings = [];
8270
+ for (const [componentName, component] of Object.entries(manifest)) {
8271
+ for (const entry of component.entries) {
8272
+ if (!entry.accessibility?.needsReview) {
8273
+ continue;
8274
+ }
8275
+ const entryLabel = entry.generatedPropertyName ?? entry.testId;
8276
+ const reasonText = entry.accessibility.reasons.join(" ");
8277
+ warnings.push(
8278
+ `[vue-pom-generator] Accessibility review suggested for ${componentName}.${entryLabel} (role=${entry.inferredRole ?? "unknown"}, testId=${JSON.stringify(entry.testId)}): ${reasonText}`
8279
+ );
8280
+ }
8281
+ }
8282
+ return warnings;
8283
+ }
8217
8284
  function removeByKeySegment(value) {
8218
8285
  const idx = value.indexOf("ByKey");
8219
8286
  if (idx < 0) {
@@ -8272,6 +8339,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8272
8339
  ...generatedActionName ? [generatedActionName] : [],
8273
8340
  ...extraActionNames.filter((name) => name !== generatedActionName)
8274
8341
  ]));
8342
+ const accessibility = buildAccessibilityAudit(metadata, pom?.nativeRole ?? null);
8275
8343
  return {
8276
8344
  testId,
8277
8345
  selectorPatternKind: entry.selectorValue.patternKind,
@@ -8282,6 +8350,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8282
8350
  nativeRole: pom.nativeRole
8283
8351
  }) : componentName,
8284
8352
  inferredRole: pom?.nativeRole ?? null,
8353
+ ...accessibility ? { accessibility } : {},
8285
8354
  generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
8286
8355
  generatedActionName,
8287
8356
  generatedActionNames,
@@ -8557,6 +8626,33 @@ function parseDynamicProps(dynamicProps) {
8557
8626
  }
8558
8627
  return void 0;
8559
8628
  }
8629
+ function findStaticAttributeValue(element, attributeName) {
8630
+ const attribute = element.props.find(
8631
+ (prop) => prop.type === compilerCore.NodeTypes.ATTRIBUTE && prop.name === attributeName
8632
+ );
8633
+ const value = attribute?.value?.content?.trim();
8634
+ return value ? value : void 0;
8635
+ }
8636
+ function collectStaticTextContent(children) {
8637
+ const parts = [];
8638
+ const visit = (nodes) => {
8639
+ for (const node of nodes) {
8640
+ if (node.type === compilerCore.NodeTypes.TEXT) {
8641
+ const text2 = node.content.replace(/\s+/g, " ").trim();
8642
+ if (text2) {
8643
+ parts.push(text2);
8644
+ }
8645
+ continue;
8646
+ }
8647
+ if (node.type === compilerCore.NodeTypes.ELEMENT) {
8648
+ visit(node.children);
8649
+ }
8650
+ }
8651
+ };
8652
+ visit(children);
8653
+ const text = parts.join(" ").replace(/\s+/g, " ").trim();
8654
+ return text || void 0;
8655
+ }
8560
8656
  function tryCreateElementMetadata(args) {
8561
8657
  const { element, semanticNameMap } = args;
8562
8658
  const testIdAttribute = (args.testIdAttribute ?? "data-testid").trim() || "data-testid";
@@ -8580,13 +8676,9 @@ function tryCreateElementMetadata(args) {
8580
8676
  dynamicPropsList = [content];
8581
8677
  }
8582
8678
  }
8583
- const semanticName = semanticNameMap.get(testId);
8584
- if (!semanticName) {
8585
- return null;
8586
- }
8587
8679
  const metadata = {
8588
8680
  testId,
8589
- semanticName,
8681
+ semanticName: semanticNameMap.get(testId),
8590
8682
  tag: element.tag,
8591
8683
  tagType: element.tagType,
8592
8684
  patchFlag,
@@ -8595,7 +8687,11 @@ function tryCreateElementMetadata(args) {
8595
8687
  hasClickHandler: patchFlag ? Boolean(patchFlag & 32) : void 0,
8596
8688
  hasDynamicClass: patchFlag ? Boolean(patchFlag & 2) : void 0,
8597
8689
  hasDynamicStyle: patchFlag ? Boolean(patchFlag & 4) : void 0,
8598
- hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0
8690
+ hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0,
8691
+ staticAriaLabel: findStaticAttributeValue(element, "aria-label"),
8692
+ staticRole: findStaticAttributeValue(element, "role"),
8693
+ staticTitle: findStaticAttributeValue(element, "title"),
8694
+ staticTextContent: collectStaticTextContent(element.children)
8599
8695
  };
8600
8696
  return metadata;
8601
8697
  }
@@ -8647,6 +8743,7 @@ function extractMetadataAfterTransform(ast, componentName, elementMetadata, sema
8647
8743
  if (componentMetadata.size > 0) {
8648
8744
  elementMetadata.set(componentName, componentMetadata);
8649
8745
  }
8746
+ return componentMetadata;
8650
8747
  }
8651
8748
  function createVuePluginWithTestIds(options) {
8652
8749
  const {
@@ -8661,11 +8758,13 @@ function createVuePluginWithTestIds(options) {
8661
8758
  excludedComponents,
8662
8759
  getViewsDirAbs,
8663
8760
  testIdAttribute,
8761
+ accessibilityAudit,
8664
8762
  loggerRef,
8665
8763
  getSourceDirs,
8666
8764
  getWrapperSearchRoots,
8667
8765
  getProjectRoot
8668
8766
  } = options;
8767
+ const lastAccessibilityWarningSignatureByComponent = /* @__PURE__ */ new Map();
8669
8768
  const getComponentNameFromPath = (filename) => {
8670
8769
  return resolveComponentNameFromPath({
8671
8770
  filename,
@@ -8738,13 +8837,37 @@ function createVuePluginWithTestIds(options) {
8738
8837
  )
8739
8838
  );
8740
8839
  return () => {
8741
- extractMetadataAfterTransform(
8840
+ const componentMetadata = extractMetadataAfterTransform(
8742
8841
  node,
8743
8842
  componentName,
8744
8843
  elementMetadata,
8745
8844
  semanticNameMap,
8746
8845
  testIdAttribute
8747
8846
  );
8847
+ if (!accessibilityAudit) {
8848
+ return;
8849
+ }
8850
+ const dependencies = componentHierarchyMap.get(componentName);
8851
+ if (!dependencies || componentMetadata.size === 0) {
8852
+ return;
8853
+ }
8854
+ const manifest = buildPomManifest(
8855
+ /* @__PURE__ */ new Map([[componentName, dependencies]]),
8856
+ /* @__PURE__ */ new Map([[componentName, componentMetadata]])
8857
+ );
8858
+ const warnings = collectAccessibilityReviewWarnings(manifest);
8859
+ const signature = warnings.join("\n");
8860
+ if (!signature) {
8861
+ lastAccessibilityWarningSignatureByComponent.delete(componentName);
8862
+ return;
8863
+ }
8864
+ if (lastAccessibilityWarningSignatureByComponent.get(componentName) === signature) {
8865
+ return;
8866
+ }
8867
+ lastAccessibilityWarningSignatureByComponent.set(componentName, signature);
8868
+ for (const warning of warnings) {
8869
+ loggerRef.current.warn(warning);
8870
+ }
8748
8871
  };
8749
8872
  }
8750
8873
  let transform = perFileTransform.get(componentName);
@@ -9109,6 +9232,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9109
9232
  nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
9110
9233
  existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
9111
9234
  testIdAttribute,
9235
+ accessibilityAudit: generationOptions?.accessibilityAudit,
9112
9236
  routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
9113
9237
  routerEntry,
9114
9238
  routerType,
@@ -9202,6 +9326,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9202
9326
  excludedComponents,
9203
9327
  getViewsDirAbs,
9204
9328
  testIdAttribute,
9329
+ accessibilityAudit: resolvedGenerationOptions.accessibilityAudit,
9205
9330
  loggerRef,
9206
9331
  getSourceDirs,
9207
9332
  getWrapperSearchRoots: getWrapperSearchRootsAbs,