@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/dist/index.mjs CHANGED
@@ -219,6 +219,7 @@ function resolveGenerationSupportOptions(options) {
219
219
  nameCollisionBehavior: options.nameCollisionBehavior ?? "error",
220
220
  existingIdBehavior: options.existingIdBehavior ?? "error",
221
221
  testIdAttribute: (options.testIdAttribute ?? "data-testid").trim() || "data-testid",
222
+ accessibilityAudit: options.accessibilityAudit ?? false,
222
223
  routerAwarePoms: options.routerAwarePoms ?? false,
223
224
  routerEntry: options.routerEntry,
224
225
  routerType: options.routerType ?? "vue-router",
@@ -1760,11 +1761,12 @@ function nodeHandlerAttributeInfo(node) {
1760
1761
  return null;
1761
1762
  }
1762
1763
  const exp = handlerDirective.exp;
1763
- const source = getVueExpressionSource(exp, "content", "compiled");
1764
- if (!source) {
1764
+ const transformedSource = getVueExpressionSource(exp, "content", "compiled");
1765
+ const authorSource = getVueExpressionSource(exp, "loc", "content", "compiled");
1766
+ if (!authorSource) {
1765
1767
  return null;
1766
1768
  }
1767
- const mergeKey = `handler:expr:${source}`;
1769
+ const mergeKey = `handler:expr:${authorSource}`;
1768
1770
  const expr = tryGetDirectiveBabelAst(handlerDirective, {
1769
1771
  preferredViews: ["content", "compiled"],
1770
1772
  plugins: ["typescript", "jsx"],
@@ -1772,9 +1774,7 @@ function nodeHandlerAttributeInfo(node) {
1772
1774
  // That is fine for Vue codegen, but our semantic-name extraction needs a normal Babel parse tree.
1773
1775
  preferExistingAst: false
1774
1776
  });
1775
- if (!expr) {
1776
- return null;
1777
- }
1777
+ const fallbackExpr = transformedSource !== authorSource ? tryParseBabelExpressionFromSource(authorSource, ["typescript", "jsx"]) : null;
1778
1778
  const isNodeType2 = (node2, type) => {
1779
1779
  return node2 !== null && node2.type === type;
1780
1780
  };
@@ -2010,59 +2010,73 @@ function nodeHandlerAttributeInfo(node) {
2010
2010
  const limited = parts.slice(0, 2);
2011
2011
  return limited.map((p) => `${toPascalCase(p.key)}${p.value}`).join("");
2012
2012
  };
2013
- const direct = getLastIdentifierFromMemberChain(expr);
2014
- if (direct) {
2015
- return { semanticNameHint: toPascalCase(direct), mergeKey };
2016
- }
2017
- if (isArrowFunctionExpressionNode(expr)) {
2018
- const body = expr.body;
2019
- const tryFromCallExpression = (call) => {
2020
- const resolvedCall = isAwaitExpressionNode(call) ? call.argument : call;
2021
- if (!isCallExpressionNode(resolvedCall)) {
2022
- return null;
2023
- }
2024
- const name = getLastIdentifierFromMemberChain(resolvedCall.callee);
2025
- if (!name) {
2026
- return null;
2013
+ const tryFromCallExpression = (call) => {
2014
+ const resolvedCall = isAwaitExpressionNode(call) ? call.argument : call;
2015
+ if (!isCallExpressionNode(resolvedCall)) {
2016
+ return null;
2017
+ }
2018
+ const name = getLastIdentifierFromMemberChain(resolvedCall.callee);
2019
+ if (!name) {
2020
+ return null;
2021
+ }
2022
+ const suffix = getStableSuffixFromCall(resolvedCall);
2023
+ const semanticNameHint2 = suffix ? `${toPascalCase(name)}${suffix}` : toPascalCase(name);
2024
+ return semanticNameHint2;
2025
+ };
2026
+ const resolveSemanticName = (candidateExpr) => {
2027
+ if (!candidateExpr) {
2028
+ return null;
2029
+ }
2030
+ const direct = getLastIdentifierFromMemberChain(candidateExpr);
2031
+ if (direct) {
2032
+ return toPascalCase(direct);
2033
+ }
2034
+ if (isArrowFunctionExpressionNode(candidateExpr)) {
2035
+ const body = candidateExpr.body;
2036
+ const directCall = tryFromCallExpression(body);
2037
+ if (directCall) {
2038
+ return directCall;
2039
+ }
2040
+ if (isAssignmentExpressionNode(body)) {
2041
+ const lhs = getAssignmentTargetName(body.left);
2042
+ if (lhs) {
2043
+ const rhs = stableWordFromValue(body.right);
2044
+ return `Set${toPascalCase(lhs)}${rhs ?? ""}`;
2045
+ }
2027
2046
  }
2028
- const suffix = getStableSuffixFromCall(resolvedCall);
2029
- const semanticNameHint = suffix ? `${toPascalCase(name)}${suffix}` : toPascalCase(name);
2030
- return semanticNameHint;
2031
- };
2032
- const directCall = tryFromCallExpression(body);
2033
- if (directCall) {
2034
- return { semanticNameHint: directCall, mergeKey };
2035
- }
2036
- if (isAssignmentExpressionNode(body)) {
2037
- const lhs = getAssignmentTargetName(body.left);
2038
- if (lhs) {
2039
- const rhs = stableWordFromValue(body.right);
2040
- const semanticNameHint = `Set${toPascalCase(lhs)}${rhs ?? ""}`;
2041
- return { semanticNameHint, mergeKey };
2042
- }
2043
- }
2044
- if (isBlockStatementNode(body)) {
2045
- const stmts = body.body ?? [];
2046
- if (stmts.length > 0) {
2047
- const firstStmt = stmts[0];
2048
- if (isReturnStatementNode(firstStmt)) {
2049
- const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
2050
- if (fromReturn) {
2051
- return { semanticNameHint: fromReturn, mergeKey };
2047
+ if (isBlockStatementNode(body)) {
2048
+ const stmts = body.body ?? [];
2049
+ if (stmts.length > 0) {
2050
+ const firstStmt = stmts[0];
2051
+ if (isReturnStatementNode(firstStmt)) {
2052
+ const fromReturn = tryFromCallExpression(firstStmt.argument ?? null);
2053
+ if (fromReturn) {
2054
+ return fromReturn;
2055
+ }
2052
2056
  }
2053
- }
2054
- if (isExpressionStatementNode(firstStmt)) {
2055
- const fromExpr = tryFromCallExpression(firstStmt.expression ?? null);
2056
- if (fromExpr) {
2057
- return { semanticNameHint: fromExpr, mergeKey };
2057
+ if (isExpressionStatementNode(firstStmt)) {
2058
+ const fromExpr = tryFromCallExpression(firstStmt.expression ?? null);
2059
+ if (fromExpr) {
2060
+ return fromExpr;
2061
+ }
2058
2062
  }
2059
2063
  }
2060
2064
  }
2065
+ const bodyName = getLastIdentifierFromMemberChain(body);
2066
+ if (bodyName) {
2067
+ return toPascalCase(bodyName);
2068
+ }
2061
2069
  }
2062
- const bodyName = getLastIdentifierFromMemberChain(body);
2063
- if (bodyName) {
2064
- return { semanticNameHint: toPascalCase(bodyName), mergeKey };
2065
- }
2070
+ return null;
2071
+ };
2072
+ const isVueRefValueAccess = (candidate) => {
2073
+ return isMemberExpressionNode(candidate) && candidate.computed === false && isIdentifierNode2(candidate.property) && candidate.property.name === "value";
2074
+ };
2075
+ const transformedSemanticName = resolveSemanticName(expr);
2076
+ const fallbackSemanticName = resolveSemanticName(fallbackExpr);
2077
+ const semanticNameHint = fallbackSemanticName && transformedSource !== authorSource && isVueRefValueAccess(expr) ? fallbackSemanticName : transformedSemanticName ?? fallbackSemanticName;
2078
+ if (semanticNameHint) {
2079
+ return { semanticNameHint, mergeKey };
2066
2080
  }
2067
2081
  return null;
2068
2082
  }
@@ -8173,6 +8187,59 @@ function createDevProcessorPlugin(options) {
8173
8187
  }
8174
8188
  };
8175
8189
  }
8190
+ function supportsInlineTextAccessibleName(role) {
8191
+ return role === "button" || role === "radio";
8192
+ }
8193
+ function buildAccessibilityAudit(metadata, inferredRole) {
8194
+ if (!metadata || !inferredRole) {
8195
+ return void 0;
8196
+ }
8197
+ const role = normalizePomRoleLabel(inferredRole).toLowerCase();
8198
+ const dynamicProps = new Set(metadata.dynamicProps ?? []);
8199
+ const hasDynamicAccessibleNameSignal = dynamicProps.has("aria-label") || dynamicProps.has("title") || !!metadata.hasDynamicText;
8200
+ let accessibleNameSource;
8201
+ const reasons = [];
8202
+ if (metadata.staticAriaLabel) {
8203
+ accessibleNameSource = "aria-label";
8204
+ } else if (supportsInlineTextAccessibleName(role) && metadata.staticTextContent) {
8205
+ accessibleNameSource = "text";
8206
+ } else if (metadata.staticTitle) {
8207
+ accessibleNameSource = "title";
8208
+ } else if (hasDynamicAccessibleNameSignal) {
8209
+ accessibleNameSource = "dynamic";
8210
+ } else if (role === "input" || role === "select") {
8211
+ accessibleNameSource = "unknown";
8212
+ reasons.push("No inline accessible-name signal was found; the element may rely on external markup such as a separate <label>.");
8213
+ } else {
8214
+ accessibleNameSource = "missing";
8215
+ reasons.push("No compile-time accessible-name signal was found.");
8216
+ }
8217
+ return {
8218
+ needsReview: accessibleNameSource === "unknown" || accessibleNameSource === "missing",
8219
+ accessibleNameSource,
8220
+ reasons,
8221
+ ...metadata.staticAriaLabel ? { staticAriaLabel: metadata.staticAriaLabel } : {},
8222
+ ...metadata.staticRole ? { staticRole: metadata.staticRole } : {},
8223
+ ...metadata.staticTitle ? { staticTitle: metadata.staticTitle } : {},
8224
+ ...metadata.staticTextContent ? { staticTextContent: metadata.staticTextContent } : {}
8225
+ };
8226
+ }
8227
+ function collectAccessibilityReviewWarnings(manifest) {
8228
+ const warnings = [];
8229
+ for (const [componentName, component] of Object.entries(manifest)) {
8230
+ for (const entry of component.entries) {
8231
+ if (!entry.accessibility?.needsReview) {
8232
+ continue;
8233
+ }
8234
+ const entryLabel = entry.generatedPropertyName ?? entry.testId;
8235
+ const reasonText = entry.accessibility.reasons.join(" ");
8236
+ warnings.push(
8237
+ `[vue-pom-generator] Accessibility review suggested for ${componentName}.${entryLabel} (role=${entry.inferredRole ?? "unknown"}, testId=${JSON.stringify(entry.testId)}): ${reasonText}`
8238
+ );
8239
+ }
8240
+ }
8241
+ return warnings;
8242
+ }
8176
8243
  function removeByKeySegment(value) {
8177
8244
  const idx = value.indexOf("ByKey");
8178
8245
  if (idx < 0) {
@@ -8231,6 +8298,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8231
8298
  ...generatedActionName ? [generatedActionName] : [],
8232
8299
  ...extraActionNames.filter((name) => name !== generatedActionName)
8233
8300
  ]));
8301
+ const accessibility = buildAccessibilityAudit(metadata, pom?.nativeRole ?? null);
8234
8302
  return {
8235
8303
  testId,
8236
8304
  selectorPatternKind: entry.selectorValue.patternKind,
@@ -8241,6 +8309,7 @@ function getManifestEntry(componentName, entry, componentMetadata, extraMethods)
8241
8309
  nativeRole: pom.nativeRole
8242
8310
  }) : componentName,
8243
8311
  inferredRole: pom?.nativeRole ?? null,
8312
+ ...accessibility ? { accessibility } : {},
8244
8313
  generatedPropertyName: pom ? getGeneratedPropertyName(pom) : null,
8245
8314
  generatedActionName,
8246
8315
  generatedActionNames,
@@ -8516,6 +8585,33 @@ function parseDynamicProps(dynamicProps) {
8516
8585
  }
8517
8586
  return void 0;
8518
8587
  }
8588
+ function findStaticAttributeValue(element, attributeName) {
8589
+ const attribute = element.props.find(
8590
+ (prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === attributeName
8591
+ );
8592
+ const value = attribute?.value?.content?.trim();
8593
+ return value ? value : void 0;
8594
+ }
8595
+ function collectStaticTextContent(children) {
8596
+ const parts = [];
8597
+ const visit = (nodes) => {
8598
+ for (const node of nodes) {
8599
+ if (node.type === NodeTypes.TEXT) {
8600
+ const text2 = node.content.replace(/\s+/g, " ").trim();
8601
+ if (text2) {
8602
+ parts.push(text2);
8603
+ }
8604
+ continue;
8605
+ }
8606
+ if (node.type === NodeTypes.ELEMENT) {
8607
+ visit(node.children);
8608
+ }
8609
+ }
8610
+ };
8611
+ visit(children);
8612
+ const text = parts.join(" ").replace(/\s+/g, " ").trim();
8613
+ return text || void 0;
8614
+ }
8519
8615
  function tryCreateElementMetadata(args) {
8520
8616
  const { element, semanticNameMap } = args;
8521
8617
  const testIdAttribute = (args.testIdAttribute ?? "data-testid").trim() || "data-testid";
@@ -8539,13 +8635,9 @@ function tryCreateElementMetadata(args) {
8539
8635
  dynamicPropsList = [content];
8540
8636
  }
8541
8637
  }
8542
- const semanticName = semanticNameMap.get(testId);
8543
- if (!semanticName) {
8544
- return null;
8545
- }
8546
8638
  const metadata = {
8547
8639
  testId,
8548
- semanticName,
8640
+ semanticName: semanticNameMap.get(testId),
8549
8641
  tag: element.tag,
8550
8642
  tagType: element.tagType,
8551
8643
  patchFlag,
@@ -8554,7 +8646,11 @@ function tryCreateElementMetadata(args) {
8554
8646
  hasClickHandler: patchFlag ? Boolean(patchFlag & 32) : void 0,
8555
8647
  hasDynamicClass: patchFlag ? Boolean(patchFlag & 2) : void 0,
8556
8648
  hasDynamicStyle: patchFlag ? Boolean(patchFlag & 4) : void 0,
8557
- hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0
8649
+ hasDynamicText: patchFlag ? Boolean(patchFlag & 1) : void 0,
8650
+ staticAriaLabel: findStaticAttributeValue(element, "aria-label"),
8651
+ staticRole: findStaticAttributeValue(element, "role"),
8652
+ staticTitle: findStaticAttributeValue(element, "title"),
8653
+ staticTextContent: collectStaticTextContent(element.children)
8558
8654
  };
8559
8655
  return metadata;
8560
8656
  }
@@ -8606,6 +8702,7 @@ function extractMetadataAfterTransform(ast, componentName, elementMetadata, sema
8606
8702
  if (componentMetadata.size > 0) {
8607
8703
  elementMetadata.set(componentName, componentMetadata);
8608
8704
  }
8705
+ return componentMetadata;
8609
8706
  }
8610
8707
  function createVuePluginWithTestIds(options) {
8611
8708
  const {
@@ -8620,11 +8717,13 @@ function createVuePluginWithTestIds(options) {
8620
8717
  excludedComponents,
8621
8718
  getViewsDirAbs,
8622
8719
  testIdAttribute,
8720
+ accessibilityAudit,
8623
8721
  loggerRef,
8624
8722
  getSourceDirs,
8625
8723
  getWrapperSearchRoots,
8626
8724
  getProjectRoot
8627
8725
  } = options;
8726
+ const lastAccessibilityWarningSignatureByComponent = /* @__PURE__ */ new Map();
8628
8727
  const getComponentNameFromPath = (filename) => {
8629
8728
  return resolveComponentNameFromPath({
8630
8729
  filename,
@@ -8697,13 +8796,37 @@ function createVuePluginWithTestIds(options) {
8697
8796
  )
8698
8797
  );
8699
8798
  return () => {
8700
- extractMetadataAfterTransform(
8799
+ const componentMetadata = extractMetadataAfterTransform(
8701
8800
  node,
8702
8801
  componentName,
8703
8802
  elementMetadata,
8704
8803
  semanticNameMap,
8705
8804
  testIdAttribute
8706
8805
  );
8806
+ if (!accessibilityAudit) {
8807
+ return;
8808
+ }
8809
+ const dependencies = componentHierarchyMap.get(componentName);
8810
+ if (!dependencies || componentMetadata.size === 0) {
8811
+ return;
8812
+ }
8813
+ const manifest = buildPomManifest(
8814
+ /* @__PURE__ */ new Map([[componentName, dependencies]]),
8815
+ /* @__PURE__ */ new Map([[componentName, componentMetadata]])
8816
+ );
8817
+ const warnings = collectAccessibilityReviewWarnings(manifest);
8818
+ const signature = warnings.join("\n");
8819
+ if (!signature) {
8820
+ lastAccessibilityWarningSignatureByComponent.delete(componentName);
8821
+ return;
8822
+ }
8823
+ if (lastAccessibilityWarningSignatureByComponent.get(componentName) === signature) {
8824
+ return;
8825
+ }
8826
+ lastAccessibilityWarningSignatureByComponent.set(componentName, signature);
8827
+ for (const warning of warnings) {
8828
+ loggerRef.current.warn(warning);
8829
+ }
8707
8830
  };
8708
8831
  }
8709
8832
  let transform = perFileTransform.get(componentName);
@@ -9068,6 +9191,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9068
9191
  nameCollisionBehavior: generationOptions?.nameCollisionBehavior,
9069
9192
  existingIdBehavior: resolvedInjectionOptions.existingIdBehavior,
9070
9193
  testIdAttribute,
9194
+ accessibilityAudit: generationOptions?.accessibilityAudit,
9071
9195
  routerAwarePoms: typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt",
9072
9196
  routerEntry,
9073
9197
  routerType,
@@ -9161,6 +9285,7 @@ function createVuePomGeneratorPlugins(options = {}) {
9161
9285
  excludedComponents,
9162
9286
  getViewsDirAbs,
9163
9287
  testIdAttribute,
9288
+ accessibilityAudit: resolvedGenerationOptions.accessibilityAudit,
9164
9289
  loggerRef,
9165
9290
  getSourceDirs,
9166
9291
  getWrapperSearchRoots: getWrapperSearchRootsAbs,