Skip to content

Commit 50bc06b

Browse files
Merge branch 'main' of https://github.com/github/accessibility-scanner into group-by-option
# Conflicts: # .github/actions/file/action.yml # .github/actions/file/src/generateIssueBody.ts # .github/actions/file/tests/generateIssueBody.test.ts # README.md # action.yml
2 parents 165e8cb + 8d2bbf0 commit 50bc06b

13 files changed

Lines changed: 198 additions & 24 deletions

File tree

.github/actions/file/action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ inputs:
3232
description: 'When true, log the issues that would be filed without opening, closing, or reopening any issues.'
3333
required: false
3434
default: 'false'
35+
file_best_practice_issues:
36+
description: 'File issues for best-practice findings (accessibility recommendations that are not hard WCAG failures). Disabling only suppresses new issues; existing ones are left untouched.'
37+
required: false
38+
default: 'true'
39+
file_experimental_issues:
40+
description: 'File issues for experimental findings (checks that are not yet stable). Disabling only suppresses new issues; existing ones are left untouched.'
41+
required: false
42+
default: 'true'
3543

3644
outputs:
3745
filings_file:

.github/actions/file/src/generateIssueBody.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,25 @@ ${items}
3131
`
3232
}
3333

34+
const categoryNotice =
35+
finding.category && finding.category !== 'wcag'
36+
? `> [!NOTE]\n> This is ${
37+
finding.category === 'experimental' ? 'an experimental check' : 'a best-practice recommendation'
38+
}, not a definite WCAG failure.\n\n`
39+
: ''
40+
41+
const standardsLine =
42+
finding.category && finding.category !== 'wcag'
43+
? '- [ ] The fix MUST meet the accessibility standards specified by the repository or organization (WCAG 2.2 if applicable).'
44+
: '- [ ] The fix MUST meet WCAG 2.2 guidelines OR the accessibility standards specified by the repository or organization.'
45+
3446
const acceptanceCriteria = `## Acceptance Criteria
3547
- [ ] The specific violation reported in this issue is no longer reproducible.
36-
- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.
48+
${standardsLine}
3749
- [ ] A test SHOULD be added to ensure this specific violation does not regress.
3850
- [ ] This PR MUST NOT introduce any new accessibility issues or regressions.`
3951

40-
const body = `## What
52+
const body = `${categoryNotice}## What
4153
An accessibility scan ${finding.html ? `flagged the element \`${finding.html}\`` : `found an issue on ${finding.url}`} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.
4254
4355
${screenshotSection ?? ''}

.github/actions/file/src/index.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import {GROUP_BY_VALUES, isGroupBy} from './groupBy.js'
1818
import {OctokitResponse} from '@octokit/types'
1919
const OctokitWithThrottling = Octokit.plugin(throttling)
2020

21+
// core.getBooleanInput throws on unset inputs, so apply the default first.
22+
function getBooleanInputWithDefault(name: string, defaultValue: boolean): boolean {
23+
if (!core.getInput(name)) return defaultValue
24+
return core.getBooleanInput(name)
25+
}
26+
2127
export default async function () {
2228
core.info("Started 'file' action")
2329
const findingsFile = core.getInput('findings_file', {required: true})
@@ -38,6 +44,8 @@ export default async function () {
3844
}
3945
const groupBy = groupByInput
4046
const dryRun = core.getBooleanInput('dry_run')
47+
const fileBestPracticeIssues = getBooleanInputWithDefault('file_best_practice_issues', true)
48+
const fileExperimentalIssues = getBooleanInputWithDefault('file_experimental_issues', true)
4149
core.debug(`Input: 'findings_file: ${findingsFile}'`)
4250
core.debug(`Input: 'repository: ${repoWithOwner}'`)
4351
core.debug(`Input: 'base_url: ${baseUrl ?? '(default)'}'`)
@@ -46,6 +54,8 @@ export default async function () {
4654
core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`)
4755
core.debug(`Input: 'group_by: ${groupBy}'`)
4856
core.debug(`Input: 'dry_run: ${dryRun}'`)
57+
core.debug(`Input: 'file_best_practice_issues: ${fileBestPracticeIssues}'`)
58+
core.debug(`Input: 'file_experimental_issues: ${fileExperimentalIssues}'`)
4959

5060
const octokit = new OctokitWithThrottling({
5161
auth: token,
@@ -69,6 +79,9 @@ export default async function () {
6979
})
7080
const filings = updateFilingsWithNewFindings(cachedFilings, findings, groupBy)
7181

82+
// Suppressed new filings are kept out of the cache
83+
const suppressedFilings = new Set<Filing>()
84+
7285
// Fetch closed wontfix issues once up front; a failed fetch reopens as usual
7386
let wontfixIssueNumbers = new Set<number>()
7487
if (!dryRun) {
@@ -88,6 +101,21 @@ export default async function () {
88101
for (const filing of filings) {
89102
let response: OctokitResponse<IssueResponse> | undefined
90103
try {
104+
// Category switches gate only new issues
105+
if (isNewFiling(filing)) {
106+
const category = filing.findings[0].category ?? 'wcag'
107+
if (
108+
(category === 'best-practice' && !fileBestPracticeIssues) ||
109+
(category === 'experimental' && !fileExperimentalIssues)
110+
) {
111+
core.info(
112+
`Skipping new ${category} issue (filing disabled for this category): ${filing.findings[0].problemShort}`,
113+
)
114+
suppressedFilings.add(filing)
115+
continue
116+
}
117+
}
118+
91119
if (dryRun) {
92120
if (isResolvedFiling(filing)) {
93121
dryRunCounts.close++
@@ -190,7 +218,8 @@ export default async function () {
190218
}
191219

192220
const filingsPath = path.join(process.env.RUNNER_TEMP || '/tmp', `filings-${crypto.randomUUID()}.json`)
193-
fs.writeFileSync(filingsPath, JSON.stringify(filings))
221+
const outputFilings = suppressedFilings.size > 0 ? filings.filter(f => !suppressedFilings.has(f)) : filings
222+
fs.writeFileSync(filingsPath, JSON.stringify(outputFilings))
194223
core.setOutput('filings_file', filingsPath)
195224

196225
core.debug(`Output: 'filings_file: ${filingsPath}'`)

.github/actions/file/src/openIssue.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export async function openIssue(octokit: Octokit, repoWithOwner: string, finding
2727
if (primary.ruleId) {
2828
labels.push(`${primary.scannerType} rule: ${primary.ruleId}`)
2929
}
30+
// Flag non-WCAG findings so they can be filtered or triaged separately
31+
if (finding.category && finding.category !== 'wcag') {
32+
labels.push(finding.category)
33+
}
3034

3135
const count = findings.length
3236
const titleSuffix = count > 1 ? ` (${count} occurrences)` : ` on ${new URL(primary.url).pathname}`

.github/actions/file/src/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
export type FindingCategory = 'wcag' | 'best-practice' | 'experimental'
2+
13
export type Finding = {
24
scannerType: string
5+
category?: FindingCategory
36
ruleId?: string
47
url: string
58
html?: string

.github/actions/file/tests/generateIssueBody.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('generateIssueBody', () => {
2626
expect(body).toContain('## What')
2727
expect(body).toContain('## Acceptance Criteria')
2828
expect(body).toContain('The specific violation reported in this issue is no longer reproducible.')
29+
expect(body).toContain('The fix MUST meet WCAG 2.2 guidelines OR')
2930
expect(body).not.toContain('Specifically:')
3031
})
3132

@@ -91,4 +92,31 @@ describe('generateIssueBody', () => {
9192
expect(body).toContain(`- [ ] \`${baseFinding.html}\` on ${baseFinding.url}`)
9293
expect(body).toContain(`- [ ] \`${second.html}\` on ${second.url}`)
9394
})
95+
96+
it('omits the category notice for WCAG findings', () => {
97+
expect(generateIssueBody(baseFinding, 'github/accessibility-scanner')).not.toContain('> [!NOTE]')
98+
expect(generateIssueBody({...baseFinding, category: 'wcag'}, 'github/accessibility-scanner')).not.toContain(
99+
'> [!NOTE]',
100+
)
101+
})
102+
103+
it('includes a best-practice notice for best-practice findings', () => {
104+
const body = generateIssueBody({...baseFinding, category: 'best-practice'}, 'github/accessibility-scanner')
105+
106+
expect(body).toContain('> [!NOTE]')
107+
expect(body).toContain('best-practice recommendation')
108+
expect(body).toContain('not a definite WCAG failure')
109+
expect(body).toContain('WCAG 2.2 if applicable')
110+
expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR')
111+
})
112+
113+
it('includes an experimental notice for experimental findings', () => {
114+
const body = generateIssueBody({...baseFinding, category: 'experimental'}, 'github/accessibility-scanner')
115+
116+
expect(body).toContain('> [!NOTE]')
117+
expect(body).toContain('an experimental check')
118+
expect(body).toContain('not a definite WCAG failure')
119+
expect(body).toContain('WCAG 2.2 if applicable')
120+
expect(body).not.toContain('The fix MUST meet WCAG 2.2 guidelines OR')
121+
})
94122
})

.github/actions/file/tests/openIssue.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ describe('openIssue', () => {
6565
)
6666
})
6767

68+
it('adds a category label for non-WCAG findings', async () => {
69+
const octokit = mockOctokit()
70+
await openIssue(octokit, 'org/repo', {...baseFinding, category: 'best-practice'})
71+
72+
expect(octokit.request).toHaveBeenCalledWith(
73+
expect.any(String),
74+
expect.objectContaining({
75+
labels: ['axe-scanning-issue', 'axe rule: color-contrast', 'best-practice'],
76+
}),
77+
)
78+
})
79+
80+
it('does not add a category label for WCAG findings', async () => {
81+
const octokit = mockOctokit()
82+
await openIssue(octokit, 'org/repo', {...baseFinding, category: 'wcag'})
83+
84+
const labels = octokit.request.mock.calls[0][1].labels
85+
expect(labels).not.toContain('wcag')
86+
expect(labels).not.toContain('best-practice')
87+
expect(labels).not.toContain('experimental')
88+
})
89+
6890
it('truncates long titles with ellipsis', async () => {
6991
const octokit = mockOctokit()
7092
const longFinding = {

.github/actions/find/src/findForUrl.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ColorSchemePreference, Finding, ReducedMotionPreference, UrlConfig} from './types.d.js'
1+
import type {ColorSchemePreference, Finding, FindingCategory, ReducedMotionPreference, UrlConfig} from './types.d.js'
22
import {AxeBuilder} from '@axe-core/playwright'
33
import playwright from 'playwright'
44
import {AuthContext} from './AuthContext.js'
@@ -87,6 +87,7 @@ async function runAxeScan({
8787
for (const violation of rawFindings.violations) {
8888
await addFinding({
8989
scannerType: 'axe',
90+
category: categorizeAxeViolation(violation.tags),
9091
url,
9192
html: violation.nodes[0].html.replace(/'/g, '&apos;'),
9293
problemShort: violation.help.toLowerCase().replace(/'/g, '&apos;'),
@@ -98,3 +99,11 @@ async function runAxeScan({
9899
}
99100
}
100101
}
102+
103+
// Maps an Axe violation's tags to a conformance tier. Experimental is checked
104+
// first because some experimental rules also carry a wcag* tag.
105+
function categorizeAxeViolation(tags: string[]): FindingCategory {
106+
if (tags.includes('experimental')) return 'experimental'
107+
if (tags.includes('best-practice')) return 'best-practice'
108+
return 'wcag'
109+
}

.github/actions/find/src/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
export type FindingCategory = 'wcag' | 'best-practice' | 'experimental'
2+
13
export type Finding = {
24
scannerType: string
5+
category?: FindingCategory
36
url: string
47
html?: string
58
problemShort: string

.github/actions/find/tests/findForUrl.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,40 @@ describe('findForUrl', () => {
117117
expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0)
118118
})
119119
})
120+
121+
describe('axe finding categorization', () => {
122+
function axeViolation(tags: string[]) {
123+
return {
124+
id: 'some-rule',
125+
help: 'Help',
126+
helpUrl: 'https://example.com',
127+
description: 'Description',
128+
tags,
129+
nodes: [{html: '<div></div>', failureSummary: 'summary'}],
130+
}
131+
}
132+
133+
async function categoryFor(tags: string[]) {
134+
clearAll()
135+
actionInput = JSON.stringify(['axe'])
136+
vi.mocked(AxeBuilder.prototype.analyze).mockResolvedValueOnce({
137+
violations: [axeViolation(tags)],
138+
} as unknown as axe.AxeResults)
139+
140+
const findings = await findForUrl('test.com')
141+
return findings[0].category
142+
}
143+
144+
it('categorizes a violation with only wcag tags as wcag', async () => {
145+
expect(await categoryFor(['wcag2a', 'wcag111'])).toBe('wcag')
146+
})
147+
148+
it('categorizes a violation with a best-practice tag as best-practice', async () => {
149+
expect(await categoryFor(['cat.semantics', 'best-practice'])).toBe('best-practice')
150+
})
151+
152+
it('categorizes a violation with an experimental tag as experimental, even alongside wcag tags', async () => {
153+
expect(await categoryFor(['wcag2a', 'experimental'])).toBe('experimental')
154+
})
155+
})
120156
})

0 commit comments

Comments
 (0)