-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #315 from GSA/a11y-refactor
Refactor a11y scan
- Loading branch information
Showing
13 changed files
with
539 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Logger } from 'pino'; | ||
import { getHttpsUrl } from '../../util'; | ||
import { CoreInputDto } from '../../core.input.dto'; | ||
import { Page } from 'puppeteer'; | ||
import { AccessibilityScan } from 'entities/scan-data.entity'; | ||
import { AxePuppeteer } from '@axe-core/puppeteer'; | ||
import { aggregateResults } from './results-aggregator'; | ||
|
||
export const createAccessibilityScanner = ( | ||
logger: Logger, | ||
input: CoreInputDto, | ||
) => { | ||
logger.info('Starting a11y scan...'); | ||
|
||
return async (page: Page): Promise<AccessibilityScan> => { | ||
page.on('console', (message) => console.log('PAGE LOG:', message.text())); | ||
page.on('error', (error) => console.log('ERROR LOG:', error)); | ||
|
||
await page.goto(getHttpsUrl(input.url)); | ||
|
||
const axeScanResult = await new AxePuppeteer(page).analyze(); | ||
const violationResults = axeScanResult.violations; | ||
|
||
const { resultsSummary, resultsList } = aggregateResults(violationResults); | ||
|
||
const accessibilityResults = Object.keys(resultsSummary).length | ||
? JSON.stringify(resultsSummary) | ||
: null; | ||
const accessibilityResultsList = resultsList.length | ||
? JSON.stringify(resultsList) | ||
: null; | ||
|
||
return { | ||
accessibilityResults, | ||
accessibilityResultsList, | ||
}; | ||
}; | ||
}; |
44 changes: 44 additions & 0 deletions
44
libs/core-scanner/src/pages/accessibility/results-aggregator.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { promises as fs } from 'fs'; | ||
import { join } from 'path'; | ||
import { Result } from 'axe-core'; | ||
import { aggregateResults } from './results-aggregator'; | ||
|
||
async function readJsonFile(filePath) { | ||
try { | ||
const jsonString = await fs.readFile(filePath, 'utf8'); | ||
const jsonObject = JSON.parse(jsonString); | ||
return jsonObject; | ||
} catch (error) { | ||
console.error('Error reading the file:', error); | ||
} | ||
} | ||
|
||
describe('aggregateResults', () => { | ||
it('should aggregate results from a list of one result', async () => { | ||
const results: Result[] = await readJsonFile( | ||
join(__dirname, './test-fixtures/results1Raw.json'), | ||
); | ||
|
||
const result = aggregateResults(results); | ||
|
||
const expectedResult = await readJsonFile( | ||
join(__dirname, './test-fixtures/results1Expected.json'), | ||
); | ||
|
||
expect(result).toEqual(expectedResult); | ||
}); | ||
|
||
it('should aggregate results from a list of two results', async () => { | ||
const results: Result[] = await readJsonFile( | ||
join(__dirname, './test-fixtures/results2Raw.json'), | ||
); | ||
|
||
const result = aggregateResults(results); | ||
|
||
const expectedResult = await readJsonFile( | ||
join(__dirname, './test-fixtures/results2Expected.json'), | ||
); | ||
|
||
expect(result).toEqual(expectedResult); | ||
}); | ||
}); |
141 changes: 141 additions & 0 deletions
141
libs/core-scanner/src/pages/accessibility/results-aggregator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
Result, | ||
NodeResult, | ||
CheckResult, | ||
TagValue, | ||
ImpactValue, | ||
UnlabelledFrameSelector, | ||
RelatedNode, | ||
} from 'axe-core'; | ||
|
||
type ResultSubset = { | ||
description: string; | ||
helpUrl: string; | ||
id: string; | ||
tags: TagValue[]; | ||
nodes: NodeResultSubset[]; | ||
}; | ||
|
||
type NodeResultSubset = { | ||
html: string; | ||
impact?: ImpactValue; | ||
xpath?: string[]; | ||
ancestry?: UnlabelledFrameSelector; | ||
any: CheckResultSubset[]; | ||
all: CheckResultSubset[]; | ||
none: CheckResultSubset[]; | ||
element?: HTMLElement; | ||
}; | ||
|
||
type CheckResultSubset = { | ||
id: string; | ||
impact: string; | ||
message: string; | ||
relatedNodes?: RelatedNode[]; | ||
}; | ||
|
||
type AggregatedResults = { | ||
resultsSummary: Record<string, number>; | ||
resultsList: ResultSubset[]; | ||
}; | ||
|
||
export function aggregateResults(results: Result[]): AggregatedResults { | ||
const resultsSummary = {}; | ||
const rawResultsList = []; | ||
|
||
// Mapping of a11y violation categories to axe-core Result id values | ||
const resultCategoryMapping = { | ||
aria: [ | ||
'aria-allowed-attr', | ||
'aria-deprecated-role', | ||
'aria-hidden-body', | ||
'aria-hidden-focus', | ||
'aria-prohibited-attr', | ||
'aria-required-attr', | ||
'aria-required-children', | ||
'aria-required-parent', | ||
'aria-roles', | ||
'aria-tooltip-name', | ||
'aria-valid-attr-value', | ||
'aria-valid-attr', | ||
], | ||
'auto-updating': ['meta-refresh'], | ||
contrast: ['color-contrast'], | ||
flash: ['blink', 'marquee'], | ||
'form-names': ['aria-input-field-name', 'input-field-name', 'select-name'], | ||
'frames-iframes': ['frame-title'], | ||
images: [ | ||
'area-alt', | ||
'image-alt', | ||
'input-image-alt', | ||
'object-alt', | ||
'role-img-alt', | ||
'svg-img-alt', | ||
], | ||
'keyboard-access': [ | ||
'frame-focusable-content', | ||
'scrollable-region-focusable', | ||
], | ||
language: ['html-lang-valid', 'valid-lang', 'html-has-lang'], | ||
'link-purpose': ['link-name'], | ||
lists: ['definition-list', 'dlitem', 'list', 'listitem'], | ||
'page-titled': ['document-title'], | ||
tables: ['td-headers-attr', 'th-has-data-cells'], | ||
'user-control-name': [ | ||
'aria-command-name', | ||
'aria-meter-name', | ||
'aria-progressbar-name', | ||
'aria-toggle-field-name', | ||
'button-name', | ||
], | ||
}; | ||
|
||
results.forEach((result) => { | ||
for (const categorys in resultCategoryMapping) { | ||
if (resultCategoryMapping[categorys].includes(result.id)) { | ||
resultsSummary[categorys] = resultsSummary[categorys] | ||
? resultsSummary[categorys] + 1 | ||
: 1; | ||
rawResultsList.push(result); | ||
break; | ||
} | ||
} | ||
}); | ||
|
||
return { | ||
resultsSummary, | ||
resultsList: getResultsListSubset(rawResultsList), | ||
}; | ||
} | ||
|
||
function getResultsListSubset(results: Result[]): ResultSubset[] { | ||
return results.map((result) => ({ | ||
description: result.description, | ||
helpUrl: result.helpUrl, | ||
id: result.id, | ||
tags: result.tags, | ||
nodes: getNodeResultsSubset(result.nodes), | ||
})); | ||
} | ||
|
||
function getNodeResultsSubset(nodes: NodeResult[]): NodeResultSubset[] { | ||
return nodes.map((node) => ({ | ||
html: node.html, | ||
impact: node.impact, | ||
xpath: node.xpath, | ||
ancestry: node.ancestry, | ||
any: getCheckResultSubset(node.any), | ||
all: getCheckResultSubset(node.all), | ||
none: getCheckResultSubset(node.none), | ||
element: node.element, | ||
})); | ||
} | ||
|
||
function getCheckResultSubset(checks: CheckResult[]): CheckResultSubset[] { | ||
return checks.map((check) => ({ | ||
id: check.id, | ||
impact: check.impact, | ||
message: check.message, | ||
relatedNodes: check.relatedNodes, | ||
})); | ||
} |
Oops, something went wrong.