UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

215 lines (195 loc) • 7.24 kB
/** * @license * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /* global getNodeDetails */ import BaseGatherer from '../base-gatherer.js'; import {axeSource} from '../../lib/axe.js'; import {pageFunctions} from '../../lib/page-functions.js'; /** * @return {Promise<LH.Artifacts.Accessibility>} */ /* c8 ignore start */ async function runA11yChecks() { /** @type {import('axe-core/axe')} */ // @ts-expect-error - axe defined by axeLibSource const axe = window.axe; const application = `lighthouse-${Math.random()}`; axe.configure({ branding: { application, }, noHtml: true, }); const axeResults = await axe.run(document, { elementRef: true, runOnly: { type: 'tag', values: [ 'wcag2a', 'wcag2aa', ], }, // resultTypes doesn't limit the output of the axeResults object. Instead, if it's defined, // some expensive element identification is done only for the respective types. https://github.com/dequelabs/axe-core/blob/f62f0cf18f7b69b247b0b6362cf1ae71ffbf3a1b/lib/core/reporters/helpers/process-aggregate.js#L61-L97 resultTypes: ['violations', 'inapplicable'], rules: { // Consider http://go/prcpg for expert review of the aXe rules. 'accesskeys': {enabled: true}, 'area-alt': {enabled: false}, 'aria-allowed-role': {enabled: true}, 'aria-braille-equivalent': {enabled: false}, 'aria-conditional-attr': {enabled: true}, 'aria-deprecated-role': {enabled: true}, 'aria-dialog-name': {enabled: true}, 'aria-prohibited-attr': {enabled: true}, 'aria-roledescription': {enabled: false}, 'aria-treeitem-name': {enabled: true}, 'aria-text': {enabled: true}, 'audio-caption': {enabled: false}, 'blink': {enabled: false}, 'duplicate-id': {enabled: false}, 'empty-heading': {enabled: true}, 'frame-focusable-content': {enabled: false}, 'frame-title-unique': {enabled: false}, 'heading-order': {enabled: true}, 'html-xml-lang-mismatch': {enabled: true}, 'identical-links-same-purpose': {enabled: true}, 'image-redundant-alt': {enabled: true}, 'input-button-name': {enabled: true}, 'label-content-name-mismatch': {enabled: true}, 'landmark-one-main': {enabled: true}, 'link-in-text-block': {enabled: true}, 'marquee': {enabled: false}, 'meta-viewport': {enabled: true}, // https://github.com/dequelabs/axe-core/issues/2958 'nested-interactive': {enabled: false}, 'no-autoplay-audio': {enabled: false}, 'role-img-alt': {enabled: false}, 'scrollable-region-focusable': {enabled: false}, 'select-name': {enabled: true}, 'server-side-image-map': {enabled: false}, 'skip-link': {enabled: true}, // https://github.com/GoogleChrome/lighthouse/issues/16163 'summary-name': {enabled: false}, 'svg-img-alt': {enabled: false}, 'tabindex': {enabled: true}, 'table-duplicate-name': {enabled: true}, 'table-fake-caption': {enabled: true}, 'target-size': {enabled: true}, 'td-has-header': {enabled: true}, }, }); // 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; return { violations: axeResults.violations.map(createAxeRuleResultArtifact), incomplete: axeResults.incomplete.map(createAxeRuleResultArtifact), notApplicable: axeResults.inapplicable.map(result => ({id: result.id})), // FYI: inapplicable => notApplicable! passes: axeResults.passes.map(result => ({id: result.id})), version: axeResults.testEngine.version, }; } async function runA11yChecksAndResetScroll() { const originalScrollPosition = { x: window.scrollX, y: window.scrollY, }; try { return await runA11yChecks(); } finally { window.scrollTo(originalScrollPosition.x, originalScrollPosition.y); } } /** * @param {import('axe-core/axe').Result} result * @return {LH.Artifacts.AxeRuleResult} */ function createAxeRuleResultArtifact(result) { // Simplify `nodes` and collect nodeDetails for each. const nodes = result.nodes.map(node => { const {target, failureSummary, element} = node; // TODO: with `elementRef: true`, `element` _should_ always be defined, but need to verify. // @ts-expect-error - getNodeDetails put into scope via stringification const nodeDetails = getNodeDetails(/** @type {HTMLElement} */ (element)); /** @type {Set<HTMLElement>} */ const relatedNodeElements = new Set(); /** @param {import('axe-core/axe').ImpactValue} impact */ const impactToNumber = (impact) => [null, 'minor', 'moderate', 'serious', 'critical'].indexOf(impact); const checkResults = [...node.any, ...node.all, ...node.none] // @ts-expect-error CheckResult.impact is a string, even though ImpactValue is a thing. .sort((a, b) => impactToNumber(b.impact) - impactToNumber(a.impact)); for (const checkResult of checkResults) { for (const relatedNode of checkResult.relatedNodes || []) { /** @type {HTMLElement} */ // @ts-expect-error - should always exist, just being cautious. const relatedElement = relatedNode.element; // Prevent overloading the report with way too many nodes. if (relatedNodeElements.size >= 3) break; // Should always exist, just being cautious. if (!relatedElement) continue; if (element === relatedElement) continue; relatedNodeElements.add(relatedElement); } } // @ts-expect-error - getNodeDetails put into scope via stringification const relatedNodeDetails = [...relatedNodeElements].map(getNodeDetails); return { target, failureSummary, node: nodeDetails, relatedNodes: relatedNodeDetails, }; }); // Ensure errors can be serialized over the protocol. /** @type {Error | undefined} */ // @ts-expect-error - when rules throw an error, axe saves it here. // see https://github.com/dequelabs/axe-core/blob/eeff122c2de11dd690fbad0e50ba2fdb244b50e8/lib/core/base/audit.js#L684-L693 const resultError = result.error; let error; if (resultError instanceof Error) { error = { name: resultError.name, message: resultError.message, }; } return { id: result.id, impact: result.impact || undefined, tags: result.tags, nodes, error, }; } /* c8 ignore stop */ class Accessibility extends BaseGatherer { /** @type {LH.Gatherer.GathererMeta} */ meta = { supportedModes: ['snapshot', 'navigation'], }; static pageFns = { runA11yChecks, createAxeRuleResultArtifact, }; /** * @param {LH.Gatherer.Context} passContext * @return {Promise<LH.Artifacts.Accessibility>} */ getArtifact(passContext) { const driver = passContext.driver; return driver.executionContext.evaluate(runA11yChecksAndResetScroll, { args: [], useIsolation: true, deps: [ axeSource, pageFunctions.getNodeDetails, createAxeRuleResultArtifact, runA11yChecks, ], }); } } export default Accessibility;