Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: assert what axe checks matches our a11y audits #12699

Merged
merged 17 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions lighthouse-core/gather/gatherers/accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ const axeLibSource = require('../../lib/axe.js').source;
const pageFunctions = require('../../lib/page-functions.js');

/**
* This is run in the page, not Lighthouse itself.
* axe.run returns a promise which fulfills with a results object
* containing any violations.
* @return {Promise<LH.Artifacts.Accessibility>}
* @param {Array<import('axe-core/axe').resultGroups>} resultTypes
* @param {{colorContrastEnabled: boolean}} opts
* @return {Promise<import('axe-core/axe').AxeResults>}
*/
/* c8 ignore start */
async function runA11yChecks() {
async function runAxe(resultTypes, opts = {colorContrastEnabled: true}) {
/** @type {import('axe-core/axe')} */
// @ts-expect-error - axe defined by axeLibSource
const axe = window.axe;
Expand All @@ -38,8 +37,9 @@ async function runA11yChecks() {
'wcag2aa',
],
},
resultTypes: ['violations', 'inapplicable'],
resultTypes,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like resultTypes is a bit weird. We definitely have incomplete results in our artifacts and I was very confused just noticing for the first time that 'incomplete' is not in this array :)

The docs say

The resultTypes option can be used to limit the number of nodes for a rule to a maximum of one. This can be useful for improving performance on very large or complicated pages when you are only interested in certain types of results.

After axe has processed all rules normally, it generates a unique selector for all nodes in all rules. This process can be time consuming, especially for pages with lots of nodes. By limiting the nodes to a maximum of one for result types you are not interested in, you can greatly speed up the tail end performance of axe.

so I think resultTypes can remain the same since we just need the id

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

over in #12536 (comment) we also talked about the value of having passes information in the artifact as well. Could start with
passes: axeResults.passes.map(result => ({id: result.id})), like notApplicable currently does to get started on that for minimal artifact cost, apparently no additional axe time (since resultTypes wasn't limiting that anyways), and it's forward compatible if we ever need to do the full createAxeRuleResultArtifact on passes since it'll still be extending {id: string}.

That would mean only opts.colorContrastEnabled would have to be custom between the two things (and maybe wouldn't need a separate method unless folks really want it).

rules: {
// Consider http://go/prcpg for expert review of the aXe rules.
'tabindex': {enabled: true},
'accesskeys': {enabled: true},
'heading-order': {enabled: true},
Expand All @@ -60,9 +60,27 @@ async function runA11yChecks() {
// https://github.com/dequelabs/axe-core/issues/2958
'nested-interactive': {enabled: false},
'frame-focusable-content': {enabled: false},
'color-contrast': {enabled: opts.colorContrastEnabled}, // See gatherer's test for explanation
'aria-roledescription': {enabled: false},
'scrollable-region-focusable': {enabled: false},
// TODO(paulirish): create audits and enable these 3.
'input-button-name': {enabled: false},
'role-img-alt': {enabled: false},
'select-name': {enabled: false},
Comment on lines +66 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has this list changed since this pr?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funnily enough, nope!

},
});


return axeResults;
}

/**
* Run aXe and adjust results for Lighthouse consumption
* @return {Promise<LH.Artifacts.Accessibility>}
*/
async function runA11yChecks() {
const axeResults = await runAxe(['violations', 'inapplicable']);

// axe just scrolled the page, scroll back to the top of the page so that element positions
// are relative to the top of the page
document.documentElement.scrollTop = 0;
Expand Down Expand Up @@ -137,9 +155,17 @@ class Accessibility extends FRGatherer {
axeLibSource,
pageFunctions.getNodeDetailsString,
createAxeRuleResultArtifact,
runAxe,
],
});
}

// @ts-expect-error just sending 'em right through…
static runAxeForTest(...args) {
// @ts-expect-error
return runAxe(...args);
}
}

module.exports = Accessibility;

50 changes: 50 additions & 0 deletions lighthouse-core/test/gather/gatherers/accessibility-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,53 @@ describe('Accessibility gatherer', () => {
err => assert.ok(err.message.includes(error)));
});
});

describe('axe-run', () => {
/**
* @jest-environment jsdom
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this for eslint? does it actually mutate jest's environment for the entire file? just this describe?

*/
const fs = require('fs');
const jsdom = require('jsdom');
const pageFunctions = require('../../../lib/page-functions.js');

const jdomObj = new jsdom.JSDOM(`<!doctype html><meta charset="utf8"><title>hi</title>valid.`);
let axe;

beforeAll(() =>{
// Needed by axe-core
// https://github.com/dequelabs/axe-core/blob/581c441c/doc/examples/jsdom/test/a11y.js#L24
global.window = global.self = jdomObj.window;
global.document = jdomObj.window.document;
global.getNodeDetails = pageFunctions.getNodeDetails;

// axe-core must be required after the global polyfills
axe = require('axe-core'); // eslint-disable-line no-unused-vars
});

afterAll(() => {
global.window = undefined;
global.document = undefined;
global.getNodeDetails = undefined;
});

it('tests only the checks we have audits defined for', async () => {
// The color-contrast rule is manually disabled as jsdom doesn't support the APIs needed. https://github.com/dequelabs/axe-core/blob/581c441c/doc/examples/jsdom/test/a11y.js#L40
const axeResults = await AccessibilityGather.runAxeForTest(undefined, {
colorContrastEnabled: false,
});

const axeRuleIds = new Set();
for (const type of ['violations', 'incomplete', 'inapplicable', 'passes']) {
if (axeResults[type]) axeResults[type].forEach(result => axeRuleIds.add(result.id));
}
axeRuleIds.add('color-contrast');
const axeRuleIdsArr = Array.from(axeRuleIds).sort();

// Note: audit ids match their filenames, thx to the getAuditList test in runner-test.js
const filenames = fs.readdirSync(__dirname + '/../../../audits/accessibility/')
.map(f => f.replace('.js', '')).filter(f => f !== 'axe-audit' && f !== 'manual')
.sort();

expect(axeRuleIdsArr).toEqual(filenames);
});
});