diff --git a/.circleci/config.yml b/.circleci/config.yml index a37b61a524..c7b1f920a4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,6 +114,8 @@ jobs: steps: - checkout - <<: *restore_dependency_cache_unix + # Always run on the latest master branch, regardless of cache + - run: npm install act-rules/act-rules.github.io#master - run: npm run build - run: npm run test:act diff --git a/.eslintrc.js b/.eslintrc.js index a6617baecd..45e80130bd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + root: true, extends: ['prettier'], parserOptions: { ecmaVersion: 2021 @@ -70,6 +71,10 @@ module.exports = { overrides: [ { files: ['lib/**/*.js'], + excludedFiles: [ + 'lib/core/reporters/**/*.js', + 'lib/**/*-after.js' + ], parserOptions: { sourceType: 'module' }, @@ -87,6 +92,23 @@ module.exports = { 'no-use-before-define': 'off' } }, + { + // after functions and reporters will not be run inside the same context as axe.run so should not access browser globals that require context specific information (window.location, window.getComputedStyles, etc.) + files: [ + 'lib/**/*-after.js', + 'lib/core/reporters/**/*.js' + ], + parserOptions: { + sourceType: 'module' + }, + env: {}, + globals: {}, + rules: { + 'func-names': [2, 'as-needed'], + 'prefer-const': 2, + 'no-use-before-define': 'off' + } + }, { files: ['test/**/*.js'], parserOptions: { diff --git a/CHANGELOG.md b/CHANGELOG.md index a17e292c80..2623d61617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [4.3.3](https://github.com/dequelabs/axe-core/compare/v4.3.2...v4.3.3) (2021-08-24) + +### Bug Fixes + +- **aria-allowed-role:** Update allowed roles based on ARIA spec updates ([#3124](https://github.com/dequelabs/axe-core/issues/3124)) ([a1f637f](https://github.com/dequelabs/axe-core/commit/a1f637f3f5ebf0e483fd21865bd2191c24ccb87a)) +- **d.ts:** Add PartialResults type ([#3126](https://github.com/dequelabs/axe-core/issues/3126)) ([5cdaf01](https://github.com/dequelabs/axe-core/commit/5cdaf012a2f09834d8b7e5f3a645a40e61d47ea9)) +- **reporter:** Run inside isolated contexts ([#3129](https://github.com/dequelabs/axe-core/issues/3129)) ([98066f8](https://github.com/dequelabs/axe-core/commit/98066f8864d4ef09b4b3de12456992d3ca3207b4)) + ### [4.3.2](https://github.com/dequelabs/axe-core/compare/v4.3.1...v4.3.2) (2021-07-27) ### Bug Fixes diff --git a/axe.d.ts b/axe.d.ts index 9d50626755..f0ba24b635 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -96,13 +96,8 @@ declare namespace axe { preload?: boolean; performanceTimer?: boolean; } - interface AxeResults { + interface AxeResults extends EnvironmentData { toolOptions: RunOptions; - testEngine: TestEngine; - testRunner: TestRunner; - testEnvironment: TestEnvironment; - url: string; - timestamp: string; passes: Result[]; violations: Result[]; incomplete: Result[]; @@ -262,7 +257,9 @@ declare namespace axe { interface PartialResult { frames: SerialDqElement[]; results: PartialRuleResult[]; + environmentData?: EnvironmentData; } + type PartialResults = Array interface FrameContext { frameSelector: CrossTreeSelector; frameContext: ContextObject; @@ -271,6 +268,13 @@ declare namespace axe { getFrameContexts: (context?: ElementContext) => FrameContext[]; shadowSelect: (selector: CrossTreeSelector) => Element | null; } + interface EnvironmentData { + testEngine: TestEngine; + testRunner: TestRunner; + testEnvironment: TestEnvironment; + url: string; + timestamp: string; + } let version: string; let plugins: any; @@ -333,7 +337,7 @@ declare namespace axe { * @param {RunOptions} options Optional Options passed into rules or checks, temporarily modifying them. */ function finishRun( - partialResults: Array, + partialResults: PartialResults, options: RunOptions ): Promise; diff --git a/bower.json b/bower.json index f3cc9089d7..9db9259b71 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.3.2", + "version": "4.3.3", "contributors": [ { "name": "David Sturley", diff --git a/build/tasks/aria-supported.js b/build/tasks/aria-supported.js index 81de153d75..cccf92ebbf 100644 --- a/build/tasks/aria-supported.js +++ b/build/tasks/aria-supported.js @@ -37,16 +37,17 @@ module.exports = function(grunt) { attributesMdTableHeader: ['aria-attribute', 'axe-core support'] }; + const { ariaRoles, ariaAttrs } = axe.utils.getStandards(); const { diff: rolesTable, notes: rolesFootnotes } = getDiff( roles, - axe.commons.aria.lookupTable.role, + ariaRoles, listType ); const ariaQueryAriaAttributes = getAriaQueryAttributes(); const { diff: attributesTable, notes: attributesFootnotes } = getDiff( ariaQueryAriaAttributes, - axe.commons.aria.lookupTable.attributes, + ariaAttrs, listType ); const attributesTableMarkdown = mdTable([ diff --git a/doc/run-partial.md b/doc/run-partial.md index 625aa790d9..011edf4a85 100644 --- a/doc/run-partial.md +++ b/doc/run-partial.md @@ -81,6 +81,13 @@ The `axe.utils.getFrameContexts` method takes any valid context, and returns an - `frameSelector`: This is a CSS selector, or array of CSS selectors in case of nodes in a shadow DOM tree to locate the frame element to be tested. - `frameContext`: This is an object is a context object that should be tested in the particular frame. + +## Custom Rulesets and Reporters + +Because `axe.finishRun` does not run inside the page, the `reporter` and `after` methods do not have access to the top-level `window` and `document` objects, and might not have access to common browser APIs. Axe-core reporter use the `environmentData` property that is set on the partialResult object of the initiator. + +Because of this constraint, custom reporters, and custom rulesets that add `after` methods must not rely on browser APIs or globals. Any data needed for either should either be taken from the `environmentData` property, or collected in an `evaluate` method of a check, and stored using its `.data()` method. + ## Recommendations When building integrations with browser drivers using axe-core, it is safer and more stable to use `axe.runPartial` and `axe.finishRun` then to use `axe.run`. These two methods ensure that no information from one frame is ever handed off to another. That way if any script in a frame interferes with the `axe` object, or with `window.postMessage`, other frames will not be affected. diff --git a/lib/core/public/finish-run.js b/lib/core/public/finish-run.js index c9c7d0d1dd..63228d0bb6 100644 --- a/lib/core/public/finish-run.js +++ b/lib/core/public/finish-run.js @@ -9,6 +9,7 @@ import { export default function finishRun(partialResults, options = {}) { options = clone(options); + const { environmentData } = partialResults.find(r => r.environmentData) || {} // normalize the runOnly option for the output of reporters toolOptions axe._audit.normalizeOptions(options); @@ -20,7 +21,7 @@ export default function finishRun(partialResults, options = {}) { results.forEach(publishMetaData); results = results.map(finalizeRuleResult); - return createReport(results, options); + return createReport(results, { environmentData, ...options }); } function setFrameSpec(partialResults) { diff --git a/lib/core/public/run-partial.js b/lib/core/public/run-partial.js index 50dc14722c..e543397933 100644 --- a/lib/core/public/run-partial.js +++ b/lib/core/public/run-partial.js @@ -1,6 +1,6 @@ import Context from '../base/context'; import teardown from './teardown'; -import { DqElement, getSelectorData, assert } from '../utils'; +import { DqElement, getSelectorData, assert, getEnvironmentData } from '../utils'; import normalizeRunParams from './run/normalize-run-params'; export default function runPartial(...args) { @@ -28,7 +28,12 @@ export default function runPartial(...args) { const frames = contextObj.frames.map(({ node }) => { return new DqElement(node, options).toJSON(); }); - return { results, frames }; + let environmentData; + if (contextObj.initiator) { + environmentData = getEnvironmentData(); + } + + return { results, frames, environmentData }; }) .finally(() => { axe._running = false; diff --git a/lib/core/public/run-virtual-rule.js b/lib/core/public/run-virtual-rule.js index 1477d3796c..d474dcb112 100644 --- a/lib/core/public/run-virtual-rule.js +++ b/lib/core/public/run-virtual-rule.js @@ -5,6 +5,7 @@ import { publishMetaData, finalizeRuleResult, aggregateResult, + getEnvironmentData, getRule } from '../utils'; @@ -54,7 +55,7 @@ function runVirtualRule(ruleId, vNode, options = {}) { ); return { - ...helpers.getEnvironmentData(), + ...getEnvironmentData(), ...results, toolOptions: options }; diff --git a/lib/core/reporters/helpers/get-environment-data.js b/lib/core/reporters/helpers/get-environment-data.js deleted file mode 100644 index 986aa55d27..0000000000 --- a/lib/core/reporters/helpers/get-environment-data.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Add information about the environment axe was run in. - * @return {Object} - */ -function getEnvironmentData(win = window) { - // TODO: remove parameter once we are testing axe-core in jsdom and other - // supported environments - const { - screen = {}, - navigator = {}, - location = {}, - innerHeight, - innerWidth - } = win; - - const orientation = - screen.msOrientation || screen.orientation || screen.mozOrientation || {}; - - return { - testEngine: { - name: 'axe-core', - version: axe.version - }, - testRunner: { - name: axe._audit.brand - }, - testEnvironment: { - userAgent: navigator.userAgent, - windowWidth: innerWidth, - windowHeight: innerHeight, - orientationAngle: orientation.angle, - orientationType: orientation.type - }, - timestamp: new Date().toISOString(), - url: location.href - }; -} - -export default getEnvironmentData; diff --git a/lib/core/reporters/helpers/index.js b/lib/core/reporters/helpers/index.js index a0d0736c1e..dbde573bcb 100644 --- a/lib/core/reporters/helpers/index.js +++ b/lib/core/reporters/helpers/index.js @@ -1,5 +1,4 @@ import failureSummary from './failure-summary'; -import getEnvironmentData from './get-environment-data'; import incompleteFallbackMessage from './incomplete-fallback-msg'; import processAggregate from './process-aggregate'; @@ -8,14 +7,12 @@ import processAggregate from './process-aggregate'; axe._thisWillBeDeletedDoNotUse = axe._thisWillBeDeletedDoNotUse || {}; axe._thisWillBeDeletedDoNotUse.helpers = { failureSummary, - getEnvironmentData, incompleteFallbackMessage, processAggregate }; export { failureSummary, - getEnvironmentData, incompleteFallbackMessage, processAggregate }; diff --git a/lib/core/reporters/na.js b/lib/core/reporters/na.js index 4723364b99..edf76d0fc6 100644 --- a/lib/core/reporters/na.js +++ b/lib/core/reporters/na.js @@ -1,23 +1,20 @@ -import { processAggregate, getEnvironmentData } from './helpers'; +import { processAggregate } from './helpers'; +import { getEnvironmentData } from '../utils'; const naReporter = (results, options, callback) => { console.warn( '"na" reporter will be deprecated in axe v4.0. Use the "v2" reporter instead.' ); - if (typeof options === 'function') { callback = options; options = {}; } - var out = processAggregate(results, options); + const { environmentData, ...toolOptions } = options; callback({ - ...getEnvironmentData(), - toolOptions: options, - violations: out.violations, - passes: out.passes, - incomplete: out.incomplete, - inapplicable: out.inapplicable + ...getEnvironmentData(environmentData), + toolOptions, + ...processAggregate(results, options) }); }; diff --git a/lib/core/reporters/no-passes.js b/lib/core/reporters/no-passes.js index 91b9c49b34..3cc3cbe451 100644 --- a/lib/core/reporters/no-passes.js +++ b/lib/core/reporters/no-passes.js @@ -1,19 +1,21 @@ -import { processAggregate, getEnvironmentData } from './helpers'; +import { processAggregate } from './helpers'; +import { getEnvironmentData } from '../utils'; const noPassesReporter = (results, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } + const { environmentData, ...toolOptions } = options; // limit result processing to types we want to include in the output options.resultTypes = ['violations']; - var out = processAggregate(results, options); + var { violations } = processAggregate(results, options); callback({ - ...getEnvironmentData(), - toolOptions: options, - violations: out.violations + ...getEnvironmentData(environmentData), + toolOptions, + violations }); }; diff --git a/lib/core/reporters/raw-env.js b/lib/core/reporters/raw-env.js index becd34ef3a..cda4904b37 100644 --- a/lib/core/reporters/raw-env.js +++ b/lib/core/reporters/raw-env.js @@ -1,4 +1,4 @@ -import { getEnvironmentData } from './helpers'; +import { getEnvironmentData } from '../utils'; import rawReporter from './raw'; const rawEnvReporter = (results, options, callback) => { @@ -6,12 +6,11 @@ const rawEnvReporter = (results, options, callback) => { callback = options; options = {}; } - function rawCallback(raw) { - const env = getEnvironmentData(); + const { environmentData, ...toolOptions } = options; + rawReporter(results, toolOptions, (raw) => { + const env = getEnvironmentData(environmentData); callback({ raw, env }); - } - - rawReporter(results, options, rawCallback); + }); }; export default rawEnvReporter; diff --git a/lib/core/reporters/v1.js b/lib/core/reporters/v1.js index 3375ce5978..d83f6d3b72 100644 --- a/lib/core/reporters/v1.js +++ b/lib/core/reporters/v1.js @@ -1,15 +1,13 @@ -import { - processAggregate, - failureSummary, - getEnvironmentData -} from './helpers'; +import { processAggregate, failureSummary } from './helpers'; +import { getEnvironmentData } from '../utils' const v1Reporter = (results, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; - } - var out = processAggregate(results, options); + }; + const { environmentData, ...toolOptions } = options; + const out = processAggregate(results, options); const addFailureSummaries = result => { result.nodes.forEach(nodeResult => { @@ -21,12 +19,9 @@ const v1Reporter = (results, options, callback) => { out.violations.forEach(addFailureSummaries); callback({ - ...getEnvironmentData(), - toolOptions: options, - violations: out.violations, - passes: out.passes, - incomplete: out.incomplete, - inapplicable: out.inapplicable + ...getEnvironmentData(environmentData), + toolOptions, + ...out }); }; diff --git a/lib/core/reporters/v2.js b/lib/core/reporters/v2.js index 0c6102a81e..f4a63333ee 100644 --- a/lib/core/reporters/v2.js +++ b/lib/core/reporters/v2.js @@ -1,18 +1,17 @@ -import { processAggregate, getEnvironmentData } from './helpers'; +import { processAggregate } from './helpers'; +import { getEnvironmentData } from '../utils'; const v2Reporter = (results, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } + const { environmentData, ...toolOptions } = options; var out = processAggregate(results, options); callback({ - ...getEnvironmentData(), - toolOptions: options, - violations: out.violations, - passes: out.passes, - incomplete: out.incomplete, - inapplicable: out.inapplicable + ...getEnvironmentData(environmentData), + toolOptions, + ...out }); }; diff --git a/lib/core/utils/get-environment-data.js b/lib/core/utils/get-environment-data.js new file mode 100644 index 0000000000..fc99f56796 --- /dev/null +++ b/lib/core/utils/get-environment-data.js @@ -0,0 +1,47 @@ +/** + * Add information about the environment axe was run in. + * @return {EnvironmentData} + */ +export default function getEnvironmentData(metadata = null, win = window) { + if (metadata && typeof metadata === 'object') { + return metadata; + } else if (typeof win !== 'object') { + return {} + } + + return { + testEngine: { + name: 'axe-core', + version: axe.version + }, + testRunner: { + name: axe._audit.brand + }, + testEnvironment: getTestEnvironment(win), + timestamp: new Date().toISOString(), + url: win.location?.href + }; +} + +function getTestEnvironment(win) { + if (!win.navigator || typeof win.navigator !== 'object') { + return {} + } + const { navigator, innerHeight, innerWidth } = win; + const { angle, type } = getOrientation(win) || {} + return { + userAgent: navigator.userAgent, + windowWidth: innerWidth, + windowHeight: innerHeight, + orientationAngle: angle, + orientationType: type + } +} + +function getOrientation({ screen }) { + return ( + screen.orientation || + screen.msOrientation || + screen.mozOrientation + ); +} diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index 5fc50f7eb9..d1004457b0 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -29,6 +29,7 @@ export { default as getAllChecks } from './get-all-checks'; export { default as getBaseLang } from './get-base-lang'; export { default as getCheckMessage } from './get-check-message'; export { default as getCheckOption } from './get-check-option'; +export { default as getEnvironmentData } from './get-environment-data'; export { default as getFrameContexts } from './get-frame-contexts'; export { default as getFriendlyUriEnd } from './get-friendly-uri-end'; export { default as getNodeAttributes } from './get-node-attributes'; diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 7d889a26f8..3b854e1b71 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -98,7 +98,7 @@ const htmlElms = { }, b: { contentTypes: ['phrasing', 'flow'], - allowedRoles: false + allowedRoles: true }, base: { allowedRoles: false, @@ -612,7 +612,14 @@ const htmlElms = { }, nav: { contentTypes: ['sectioning', 'flow'], - allowedRoles: ['doc-index', 'doc-pagelist', 'doc-toc'], + allowedRoles: [ + 'doc-index', + 'doc-pagelist', + 'doc-toc', + 'menu', + 'menubar', + 'tablist' + ], shadowRoot: true }, noscript: { @@ -685,7 +692,7 @@ const htmlElms = { }, progress: { contentTypes: ['phrasing', 'flow'], - allowedRoles: true, + allowedRoles: false, implicitAttrs: { 'aria-valuemax': '100', 'aria-valuemin': '0', @@ -822,7 +829,7 @@ const htmlElms = { }, svg: { contentTypes: ['embedded', 'phrasing', 'flow'], - allowedRoles: ['application', 'document', 'img'], + allowedRoles: true, chromiumRole: 'SVGRoot', namingMethods: ['svgTitleText'] }, diff --git a/locales/fr.json b/locales/fr.json index a6054530c4..a1cba7785d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -17,6 +17,14 @@ "description": "Vérifier que l’attribut role a une valeur valide pour cet élément", "help": "Le rôle ARIA doit être valide pour cet élément" }, + "aria-command-name": { + "description": "Vérifier que chaque \"button\", \"link\" et \"menuitem\" ARIA a un nom accessible", + "help": "Les commandes ARIA doivent avoir un nom accessible" + }, + "aria-dialog-name": { + "description": "Vérifier que chaque nœud ARIA \"dialog\" et \"alertdialog\" a un nom accessible", + "help": "Les nœuds ARIA \"dialog\" and \"alertdialog\" doivent avoir un nom accessible" + }, "aria-hidden-body": { "description": "Vérifier qu’aria-hidden='true' n’est pas présent sur le corps du document (élément body)", "help": "aria-hidden='true' ne doit pas être présent sur " @@ -29,6 +37,14 @@ "description": "Vérifier que chaque champ de formulaire avec ARIA est doté d’un intitulé accessible", "help": "Les champs de formulaire ARIA ont un intitulé accessible" }, + "aria-meter-name": { + "description": "Vérifier que chaque nœud ARIA \"meter\" a un nom accessible", + "help": "Les nœuds ARIA \"meter\" doivent avoir un nom accessible" + }, + "aria-progressbar-name": { + "description": "Vérifier que chaque nœud ARIA \"progressbar\" a un nom accessible", + "help": "Les nœuds ARIA \"progressbar\" doivent avoir un nom accessible" + }, "aria-required-attr": { "description": "Vérifier que les éléments avec des rôles ARIA ont les attributs ARIA requis", "help": "Les attributs ARIA requis doivent être présents" @@ -49,10 +65,22 @@ "description": "Vérifier que les éléments avec un attribut role utilisent une valeur valide", "help": "Les rôles ARIA doivent se conformer aux valeurs valides" }, + "aria-text": { + "description": "Vérifier que \"role=text\" est uniquement utilisé sur des éléments sans descendants focalisables", + "help": "\"role=text\" ne doit pas avoir de descendant focalisable" + }, "aria-toggle-field-name": { "description": "Vérifier que chaque champ de basculement ARIA a un libellé accessible", "help": "Les champs de basculement ARIA ont un libellé accessible" }, + "aria-tooltip-name": { + "description": "Vérifier que chaque nœud ARIA \"tooltip\" a un nom accessible", + "help": "Les nœuds ARIA \"tooltip\" doivent avoir un nom accessible" + }, + "aria-treeitem-name": { + "description": "Vérifier que chaque nœud ARIA \"treeitem\" a un nom accessible", + "help": "Les nœuds ARIA \"treeitem\" doivent avoir un nom accessible" + }, "aria-valid-attr-value": { "description": "Vérifier que tous les attributs ARIA comportent des valeurs valides", "help": "Les attributs ARIA doivent comporter des valeurs valides" @@ -121,6 +149,10 @@ "description": "Vérifier que les niveaux de titre ont un texte perceptible", "help": "Les niveaux de titre ne doivent pas être vides" }, + "empty-table-header": { + "description": "Vérifier que les entêtes de tableaux ont un texte perceptible", + "help": "Les textes d’entêtes de tableaux ne doivent pas être vides" + }, "focus-order-semantics": { "description": "Vérifier que les éléments dans le parcours du focus ont un rôle approprié", "help": "Les éléments dans le parcours du focus doivent avoir un rôle approprié pour le contenu interactif" @@ -129,6 +161,10 @@ "description": "Vérifier que le champ de formulaire n’a pas plusieurs éléments d’étiquettes", "help": "Le champ de formulaire ne devrait pas comporter plusieurs éléments d’étiquettes" }, + "frame-focusable-content": { + "description": "Vérifier que les éléments et