fix: distinguish WCAG violations from best practices in issue output#206
fix: distinguish WCAG violations from best practices in issue output#206Hardonian wants to merge 1 commit into
Conversation
Addresses issue github#34 - The scanner previously conflated best-practice recommendations with actual WCAG conformance failures in auto-generated issue titles and bodies. This led to Copilot filing PRs claiming WCAG violations when only best-practice rules were triggered. Changes: - Add `ruleType` field to Finding type (wcag | best-practice | experimental) - Extract rule type from axe-core violation tags in findForUrl - Update issue body generator with clear type badge and description - Update issue labels: add wcag-violation / best-practice / experimental labels - Update tests with new label expectations - Fix ESLint error in test plugin (unused vars) - Add missing esbuild devDependency - Include .js files in eslint config
There was a problem hiding this comment.
Pull request overview
This PR updates the scanner’s finding model and issue generation so that best-practice and experimental axe rules are no longer presented as WCAG conformance failures in auto-filed issues/PRs (addresses #34).
Changes:
- Add
ruleTypetoFindingand infer it from axe-core violation tags (wcag*,best-practice,experimental). - Update issue body formatting + acceptance criteria to reflect the finding type, and add type-based GitHub labels.
- Update/extend tests for new issue body headings and label selection; minor tooling/deps updates (eslint + esbuild).
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds esbuild devDependency. |
| package-lock.json | Locks esbuild and platform-specific optional deps. |
| eslint.config.js | Expands ESLint file matching to include .js. |
| .github/scanner-plugins/test-js-file-plugin-loading/index.js | Removes unused plugin parameters to satisfy linting. |
| .github/actions/find/src/types.d.ts | Adds optional ruleType to findings produced by the find action. |
| .github/actions/find/src/findForUrl.ts | Infers ruleType from axe violation tags and passes it into findings. |
| .github/actions/file/src/types.d.ts | Adds optional ruleType to findings consumed by the file action. |
| .github/actions/file/src/openIssue.ts | Adds type-based labels (wcag-violation, best-practice, experimental). |
| .github/actions/file/src/generateIssueBody.ts | Adds type-based heading/badge/acceptance-criteria generation. |
| .github/actions/file/tests/openIssue.test.ts | Adds coverage for best-practice and experimental label paths. |
| .github/actions/file/tests/generateIssueBody.test.ts | Updates expectations for new issue body heading/type text. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
.github/actions/file/src/generateIssueBody.ts:54
ruleTypeLabel.isWcagis used to choose between a WCAG-failure description and a best-practice description, butruleType === 'experimental'setsisWcag: false, so experimental findings will incorrectly render the best-practice message. Consider branching the description on the actual rule type (wcag vs best-practice vs experimental) so experimental is described as non-standard/experimental rather than best-practice.
const ruleTypeSection = `> **Type:** ${ruleTypeLabel.badge}
>
> ${ruleTypeLabel.isWcag
? 'This is a **WCAG conformance failure**. Fixing this issue helps meet WCAG 2.1 accessibility requirements.'
: 'This is a **best practice recommendation**, not a WCAG conformance failure. Fixing it improves accessibility but is not required for WCAG compliance.'
}`
.github/actions/file/src/generateIssueBody.ts:26
- The fallback in
getRuleTypeLabel()treats any non-best-practice/experimentalvalue (includingundefinedor a typo) as a WCAG violation. GivenFinding.ruleTypeis optional and comes from external data (axe tags / plugins), this can silently misclassify findings and reintroduce the “claims WCAG violation” problem when ruleType is missing/unknown. Consider explicitly handlingruleType === 'wcag'vsundefined/unknown(e.g., an "Unclassified" type, or omit the compliance claim unless ruleType iswcag).
function getRuleTypeLabel(ruleType?: string): { heading: string; badge: string; isWcag: boolean } {
if (ruleType === 'best-practice') {
return {
heading: 'Best Practice Recommendation',
badge: '\U0001F4A1 Best Practice',
isWcag: false,
}
}
if (ruleType === 'experimental') {
return {
heading: 'Experimental Rule',
badge: '\U0001F9EA Experimental',
isWcag: false,
}
}
// Default to WCAG violation
return {
heading: 'WCAG Violation',
badge: '\U0001F6A8 WCAG Violation',
isWcag: true,
}
- Files reviewed: 8/11 changed files
- Comments generated: 5
| /** | ||
| * Determines if a finding is a WCAG violation or a best practice recommendation. | ||
| */ |
There was a problem hiding this comment.
The JSDoc for getRuleTypeLabel says it determines WCAG vs best-practice, but the function also handles experimental. Consider updating the comment to reflect all supported rule types (wcag / best-practice / experimental) to avoid confusion.
This issue also appears in the following locations of the same file:
- line 6
- line 49
| // Add rule type label to distinguish WCAG violations from best practices | ||
| if (finding.ruleType === 'best-practice') { | ||
| labels.push('best-practice') | ||
| } else if (finding.ruleType === 'experimental') { | ||
| labels.push('experimental') | ||
| } else { | ||
| // Default to wcag for any WCAG-tagged rule |
There was a problem hiding this comment.
This logic adds wcag-violation for any finding whose ruleType is not exactly best-practice or experimental (including undefined or unexpected values). Since Finding.ruleType is optional and can be omitted by plugins, this can incorrectly label non-WCAG findings as WCAG violations. Consider only adding wcag-violation when finding.ruleType === 'wcag', and either omit the type label or add an explicit "unknown" label when ruleType is missing.
| // Add rule type label to distinguish WCAG violations from best practices | |
| if (finding.ruleType === 'best-practice') { | |
| labels.push('best-practice') | |
| } else if (finding.ruleType === 'experimental') { | |
| labels.push('experimental') | |
| } else { | |
| // Default to wcag for any WCAG-tagged rule | |
| // Add a rule type label only for explicitly recognized rule types | |
| if (finding.ruleType === 'best-practice') { | |
| labels.push('best-practice') | |
| } else if (finding.ruleType === 'experimental') { | |
| labels.push('experimental') | |
| } else if (finding.ruleType === 'wcag') { |
There was a problem hiding this comment.
+1 for this, as a default we don't want to have anything if ruleType is not passed in. One example is our custom reflow scan; this does map to a WCAG violation, however, if we add more built-in scans outside of Axe over time, this won't be relevant. Same with users who create their own custom plugins.
| /** Distinguishes WCAG violations from best practices. One of: "wcag", "best-practice", "experimental" */ | ||
| ruleType?: string |
There was a problem hiding this comment.
ruleType is documented as having only three allowed values, but is typed as a generic string. Consider introducing a RuleType = 'wcag' | 'best-practice' | 'experimental' union (and using it for Finding.ruleType) so typos/unhandled values are caught at compile time and downstream logic (labels/body text) can be made exhaustive.
| export type Finding = { | ||
| scannerType: string | ||
| ruleId?: string | ||
| /** Distinguishes WCAG violations from best practices. One of: "wcag", "best-practice", "experimental" */ | ||
| ruleType?: string |
There was a problem hiding this comment.
ruleType is documented as having only three allowed values, but is typed as a generic string. Consider using a string-literal union (e.g., 'wcag' | 'best-practice' | 'experimental') to make openIssue() / generateIssueBody() logic exhaustive and prevent accidental mislabeling due to typos or unexpected values.
| export type Finding = { | |
| scannerType: string | |
| ruleId?: string | |
| /** Distinguishes WCAG violations from best practices. One of: "wcag", "best-practice", "experimental" */ | |
| ruleType?: string | |
| export type FindingRuleType = 'wcag' | 'best-practice' | 'experimental' | |
| export type Finding = { | |
| scannerType: string | |
| ruleId?: string | |
| /** Distinguishes WCAG violations from best practices. One of: "wcag", "best-practice", "experimental" */ | |
| ruleType?: FindingRuleType |
| // Determine rule type from axe-core tags | ||
| const tags = violation.tags ?? [] | ||
| let ruleType: string | undefined | ||
| if (tags.some((tag: string) => tag.startsWith('wcag'))) { | ||
| ruleType = 'wcag' | ||
| } else if (tags.includes('best-practice')) { | ||
| ruleType = 'best-practice' | ||
| } else if (tags.includes('experimental')) { | ||
| ruleType = 'experimental' | ||
| } |
There was a problem hiding this comment.
ruleType is currently inferred into a string | undefined. Once Finding.ruleType is narrowed to a string-literal union, it would be good to type this local as the same union (or Finding['ruleType']) to avoid accidental values and keep the inference logic aligned with the Finding contract.
lindseywild
left a comment
There was a problem hiding this comment.
Thanks so much for contributing ✨ I've left a few comments, let us know if you have any questions!
| if (ruleType === 'best-practice') { | ||
| return { | ||
| heading: 'Best Practice Recommendation', | ||
| badge: '\U0001F4A1 Best Practice', |
There was a problem hiding this comment.
I think we can also remove "Type: WCAG Violation/Best practice/experimental" line and just have "This is a X failure."
There was a problem hiding this comment.
I agree, the emojis shouldn't be there. They can cause problems with different formats and add noise unnecessarily for screen reader users
| // Add rule type label to distinguish WCAG violations from best practices | ||
| if (finding.ruleType === 'best-practice') { | ||
| labels.push('best-practice') | ||
| } else if (finding.ruleType === 'experimental') { | ||
| labels.push('experimental') | ||
| } else { | ||
| // Default to wcag for any WCAG-tagged rule |
There was a problem hiding this comment.
+1 for this, as a default we don't want to have anything if ruleType is not passed in. One example is our custom reflow scan; this does map to a WCAG violation, however, if we add more built-in scans outside of Axe over time, this won't be relevant. Same with users who create their own custom plugins.
| // via js files still works and there are no regressions | ||
|
|
||
| export default async function TestJsFilePluginLoad({ page, addFinding } = {}) { | ||
| export default async function TestJsFilePluginLoad() { |
| // Determine rule type from axe-core tags | ||
| const tags = violation.tags ?? [] | ||
| let ruleType: string | undefined | ||
| if (tags.some((tag: string) => tag.startsWith('wcag'))) { |
There was a problem hiding this comment.
Minor (micro-optimization opportunity); we're iterating over tags in each condition here (I'm not sure how many tags there are in a violation in general - if the list is small, then this is prob not a big deal). We could run 1 loop, find all the info we need (stored in bools), then check against the bools in the conditions instead.
I suppose this can add up (even if the tags list is small) depending on how many pages we're scanning, how many violations are in each page, etc...
| "@octokit/plugin-throttling": "^11.0.3", | ||
| "@octokit/types": "^16.0.0", | ||
| "@types/node": "^25.6.0", | ||
| "esbuild": "^0.28.0", |
There was a problem hiding this comment.
I'm curious as to why this was added here; it's in the package.json of the find action. I think the find, file, and fix actions are intentionally set up as isolated environments so they can be used individually - which is why esbuild was specifically installed there. For clarity, I don't think we are approaching this like a mono-repo (where you'd install packages at the root level). Have a look at the bootstrap.js file to get a sense of how this is set up.
let us know if you have any questions (or if you ran into an issue that caused you to add this here).
|
@lindseywild would any fails against GitHub's own accessibility requirements that are non-wcag be given a type of 'experimental'? I presume so. I'm thinking about specific component requirements where you want an accessible name to be present on a |
|
@grace-snow I am thinking we wouldn't want a type at all for non-WCAG issues for the exact reason you say! |

Summary
Addresses #34 — The scanner previously conflated best-practice recommendations with actual WCAG conformance failures in auto-generated issues and PRs. This caused Copilot to claim WCAG violations when only best-practice rules were triggered.
Changes
Core Fix
ruleTypefield added toFindingtype — extracted from axe-core violation tags (wcag,best-practice,experimental)wcag-violation,best-practice, orexperimentalfor easy filteringTest Updates
generateIssueBodytest for new heading formatbest-practiceandexperimentallabel paths inopenIssueMaintenance
esbuilddevDependency.jsfiles in eslint configExample Output
Before (misleading):
After (accurate):