diff --git a/.github/bin/extract-release-notes.sh b/.github/bin/extract-release-notes.sh new file mode 100755 index 000000000..409c89024 --- /dev/null +++ b/.github/bin/extract-release-notes.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +# Extracts the body of the most recent release section from CHANGELOG.md — +# everything between the latest version header and the previous one. The +# version header itself is not included. The result is written as a +# multiline `notes` step output to $GITHUB_OUTPUT when set (GitHub +# Actions), otherwise to stdout. +# +# The awk pattern /# \[?[0-9]/ matches version-header lines (`#`, space, +# optional `[`, digit), e.g. `### [4.11.4](...) (2026-04-23)`. The pattern +# is not anchored, so it matches the `# [4` substring inside `###`. On the +# first match it sets `found=1` and skips the line (so the header itself +# is not printed); on the next match (the previous release's header) it +# exits. `found{print}` prints every line in between. +# +# Walking through a typical CHANGELOG.md: +# +# Line v-header? found Action +# -------------------------------- --------- ------ ------- +# # Changelog no 0 nothing +# All notable changes... no 0 nothing +# ### [4.11.4](...) (2026-04-23) yes 0 -> 1 skip +# (blank) no 1 print +# ### Bug Fixes no 1 print +# - **commons/text:** ... no 1 print +# - **utils/getAncestry:** ... no 1 print +# ### [4.11.3](...) (2026-04-13) yes 1 exit + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +{ + echo 'notes<> "${GITHUB_OUTPUT:-/dev/stdout}" diff --git a/.github/bin/validate-package.mjs b/.github/bin/validate-package.mjs index c8afd16de..c1126b131 100755 --- a/.github/bin/validate-package.mjs +++ b/.github/bin/validate-package.mjs @@ -210,7 +210,13 @@ defined files in the \`files\` array of \`package.json\`. | File | Status | Version |\n|------|--------|--------| `; - const importTargets = [...pkg.files.map(file => `${pkg.name}/${file}`)]; + // these files are not part of the importable api + const nonImportableFiles = ['gather-internals.js']; + const importTargets = [ + ...pkg.files + .filter(file => !nonImportableFiles.includes(file)) + .map(file => `${pkg.name}/${file}`) + ]; let anyCaught = false; console.log('Validating package files are importable:'); @@ -284,6 +290,27 @@ defined files in the \`files\` array of \`package.json\`. } } + // check that the non-importable files are parsable + for (const file of nonImportableFiles) { + const target = `${pkg.name}/${file}`; + try { + const resolved = import.meta.resolve(target); + if (resolved.includes('node_modules')) { + console.error(`✗ ${target} resolves to node_modules`); + summary += `| \`${target}\` | ✗ Resolves to node_modules |\n`; + exitCode++; + continue; + } + execSync(`node --check "${fileURLToPath(resolved)}"`, { stdio: 'pipe' }); + console.info(`✓ ${target} (parse-only, browser-only)`); + summary += `| \`${target}\` | ✓ Parse OK (browser-only) | n/a |\n`; + } catch (error) { + console.error(`✗ ${target}: ${error.message}`); + summary += `| \`${target}\` | ✗ Parse failed (browser-only) | n/a |\n`; + exitCode++; + } + } + if (anyCaught) { exitCode++; } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 867f60a82..f482037a1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -166,16 +166,19 @@ jobs: contents: write # Required to create releases steps: - *checkout - - name: Install Release Helper - run: go install gopkg.in/aktau/github-release.v0@latest - - name: Download Release Script - run: curl https://raw.githubusercontent.com/dequelabs/attest-release-scripts/develop/src/node-github-release.sh -s -o ./node-github-release.sh - - name: Make Release Script Executable - run: chmod +x ./node-github-release.sh + - name: Extract release notes + id: release-notes + run: ./.github/bin/extract-release-notes.sh - name: Create GitHub Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./node-github-release.sh + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.prod-deploy.outputs.version }} + NOTES: ${{ steps.release-notes.outputs.notes }} + run: | + gh release create "v${VERSION}" \ + --title "Release ${VERSION}" \ + --target ${{ github.sha }} \ + --notes "$NOTES" validate-deploy: name: Validate Deployment needs: prod-deploy diff --git a/.gitignore b/.gitignore index 9aacdeb71..4f16ba245 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ test/integration/*/index.html # dist axe.js axe.*.js +/gather-internals.js # generated src file lib/core/base/metadata-function-map.js diff --git a/.vscode/launch.json b/.vscode/launch.json index e189436e9..25c4925c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,14 +4,13 @@ { "type": "chrome", "request": "attach", - "name": "Attach to Karma test:debug", + "name": "Attach to WTR test:debug", "address": "localhost", - "port": 9765, // keep in sync with debugPort in karma.conf.js + "port": 9765, // keep in sync with debugPort in wtr.config.mjs chrome-debug group "webRoot": "${workspaceFolder}", "sourceMaps": true, "sourceMapPathOverrides": { - "*": "${webRoot}/*", - "base/*": "${webRoot}/*" + "*": "${webRoot}/*" } } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fab22519..9d577f100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ 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.12.1](https://github.com/dequelabs/axe-core/compare/v4.12.0...v4.12.1) (2026-06-09) + +### Bug Fixes + +- **axe.d.ts:** make enabled property of RuleMetadata optional ([#5129](https://github.com/dequelabs/axe-core/issues/5129)) ([7eb3d2d](https://github.com/dequelabs/axe-core/commit/7eb3d2d7ce283926a786c8a0fbf2a52c81d5e694)) + +## [4.12.0](https://github.com/dequelabs/axe-core/compare/v4.11.4...v4.12.0) (2026-06-01) + +### Features + +- add gather-internals.js external script ([#5099](https://github.com/dequelabs/axe-core/issues/5099)) ([c61d58b](https://github.com/dequelabs/axe-core/commit/c61d58b40d87f81152526edcea67292aa7e3ae1d)), closes [#5080](https://github.com/dequelabs/axe-core/issues/5080) +- **aria-allowed/prohibited-attr, aria-required-parent/children:** partially support element internals role ([#5080](https://github.com/dequelabs/axe-core/issues/5080)) ([417b48a](https://github.com/dequelabs/axe-core/commit/417b48a0d60f0c01ce81e69cc50c2c59e45aa4de)), closes [#5039](https://github.com/dequelabs/axe-core/issues/5039) [#4259](https://github.com/dequelabs/axe-core/issues/4259) +- **axe.externalAPIs:** add public api for setting elementInternal data ([#5105](https://github.com/dequelabs/axe-core/issues/5105)) ([63bab8f](https://github.com/dequelabs/axe-core/commit/63bab8fec82817849a8e69b7cd00f1c1bf3ddf6e)) +- **core:** expose normalizeRunOptions ([#4998](https://github.com/dequelabs/axe-core/issues/4998)) ([b8e6a59](https://github.com/dequelabs/axe-core/commit/b8e6a5943f3d7613e770f36dd15fdb27621ca18c)) +- expose axe.resetLocale() to restore the default locale ([#5108](https://github.com/dequelabs/axe-core/issues/5108)) ([c2b5292](https://github.com/dequelabs/axe-core/commit/c2b5292397727e1f9d63ae1675db447a5cf58a23)), closes [#5107](https://github.com/dequelabs/axe-core/issues/5107) +- **getRules:** include rule enabled state in returned objects ([#5118](https://github.com/dequelabs/axe-core/issues/5118)) ([75bf772](https://github.com/dequelabs/axe-core/commit/75bf772d47ec1cc6027de55b47aaa63ffef171da)), closes [#5116](https://github.com/dequelabs/axe-core/issues/5116) +- **list,listitem:** support element internals role ([#5119](https://github.com/dequelabs/axe-core/issues/5119)) ([7d9d696](https://github.com/dequelabs/axe-core/commit/7d9d69678df257ee72b962d45371ae27e3aa82ca)) +- **new-rule:** check that aria-tab have an accessible name ([#5001](https://github.com/dequelabs/axe-core/issues/5001)) ([0d4e4e7](https://github.com/dequelabs/axe-core/commit/0d4e4e70aa9f46519eb6000744e043c058fd994e)), closes [#4842](https://github.com/dequelabs/axe-core/issues/4842) +- **rules:** deprecate landmark-complementary-is-top-level rules ([#4992](https://github.com/dequelabs/axe-core/issues/4992)) ([9e09139](https://github.com/dequelabs/axe-core/commit/9e091391189dba452ea485275609120e1e6ae8ba)), closes [#4950](https://github.com/dequelabs/axe-core/issues/4950) +- **utils:** add `getElementInternals` function ([#5077](https://github.com/dequelabs/axe-core/issues/5077)) ([1c15f82](https://github.com/dequelabs/axe-core/commit/1c15f8224a184d3c0da95942e99d9d73ad5645c0)) + +### Bug Fixes + +- **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](https://github.com/dequelabs/axe-core/issues/4974)) ([c6245e7](https://github.com/dequelabs/axe-core/commit/c6245e7aee824434fcdae3c77c24365493dbe4be)) +- **aria-conditional-attr:** add support for radio ([#5100](https://github.com/dequelabs/axe-core/issues/5100)) ([8223c98](https://github.com/dequelabs/axe-core/commit/8223c989ff4fd2b8002f4961a8ee005a371f39cc)) +- **aria-valid-attr-value:** handle multiple aria-errormessage IDs ([#4973](https://github.com/dequelabs/axe-core/issues/4973)) ([0489e30](https://github.com/dequelabs/axe-core/commit/0489e30aad3d80790d8fb9cf5b1807d7c3a2179f)) +- **aria:** prevent getOwnedVirtual from returning duplicate nodes ([#4987](https://github.com/dequelabs/axe-core/issues/4987)) ([48ca955](https://github.com/dequelabs/axe-core/commit/48ca9554e2f0400caeec55c09aa100cbb415422d)), closes [#4840](https://github.com/dequelabs/axe-core/issues/4840) +- **commons/text:** exclude natively hidden elements from aria-labelledby accessible name ([#5076](https://github.com/dequelabs/axe-core/issues/5076)) ([ea7202c](https://github.com/dequelabs/axe-core/commit/ea7202c6bf1a6166c878dbf19bb5454372b61fae)), closes [#4704](https://github.com/dequelabs/axe-core/issues/4704) +- **DqElement:** avoid calling constructors with cloneNode ([#5013](https://github.com/dequelabs/axe-core/issues/5013)) ([0281fa1](https://github.com/dequelabs/axe-core/commit/0281fa16f7110b793ac8b3b5b46f93e81be75ee4)) +- **existing-rule:** aria-busy now shows an error message for a use with unallowed children ([#5017](https://github.com/dequelabs/axe-core/issues/5017)) ([2067b87](https://github.com/dequelabs/axe-core/commit/2067b87195552daa3065be7aca1aa2a02c135f28)) +- **helpUrl:** ensure axe.configure always updates the help URLs ([#5114](https://github.com/dequelabs/axe-core/issues/5114)) ([c4f60ff](https://github.com/dequelabs/axe-core/commit/c4f60ffcd47eb64514e8cbafbc68ad357ce60e77)) +- **label-content-name-mismatch:** match visible text with aria-label and exclude invisible text ([#5096](https://github.com/dequelabs/axe-core/issues/5096)) ([3a012a1](https://github.com/dequelabs/axe-core/commit/3a012a141f56b76d6a58fcfb01598ba45e91a442)) +- **locale:** ensure all subtags are correctly set ([#5112](https://github.com/dequelabs/axe-core/issues/5112)) ([13005ed](https://github.com/dequelabs/axe-core/commit/13005eda098db154f3d78df3923ed85389344353)) +- **scrollable-region-focusable:** clarify the issue is in safari ([#4995](https://github.com/dequelabs/axe-core/issues/4995)) ([4ec5211](https://github.com/dequelabs/axe-core/commit/4ec52112b67b1b44f82b3eade1825789ee8cb659)), closes [WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870) [WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290) +- **scrollable-region-focusable:** do not fail scroll areas when all content is visible without scrolling ([#4993](https://github.com/dequelabs/axe-core/issues/4993)) ([838707a](https://github.com/dequelabs/axe-core/commit/838707a8f224907042221bbf6fb28d6ad59d7cb0)) +- **target-size:** determine offset using clientRects if target is display:inline ([#5012](https://github.com/dequelabs/axe-core/issues/5012)) ([a4b8091](https://github.com/dequelabs/axe-core/commit/a4b809183f43c4296a3ec57cd80d8a8f34743361)) +- **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](https://github.com/dequelabs/axe-core/issues/5066)) ([1229a6e](https://github.com/dequelabs/axe-core/commit/1229a6e7162768a283f5e2307024dee0d0566452)), closes [#5065](https://github.com/dequelabs/axe-core/issues/5065) +- **target-size:** ignore widgets that are inline with other inline elements ([#5000](https://github.com/dequelabs/axe-core/issues/5000)) ([a8dd81b](https://github.com/dequelabs/axe-core/commit/a8dd81be759c670203784acf7b1894257df5457c)) +- **utils/getAncestry:** escape node name ([#5079](https://github.com/dequelabs/axe-core/issues/5079)) ([d1fabaa](https://github.com/dequelabs/axe-core/commit/d1fabaad99b1b055b2436a0c3efc22cb66df3934)), closes [#5078](https://github.com/dequelabs/axe-core/issues/5078) +- **utils:** Add null check to parseCrossOriginStylesheet, closes [#5074](https://github.com/dequelabs/axe-core/issues/5074) ([#5075](https://github.com/dequelabs/axe-core/issues/5075)) ([f12ef32](https://github.com/dequelabs/axe-core/commit/f12ef32554deb116238ac29d854ad8e46baa9adb)) +- **utils:** update isShadowRoot to use spec-compliant custom element regex ([#5059](https://github.com/dequelabs/axe-core/issues/5059)) ([edc6ce2](https://github.com/dequelabs/axe-core/commit/edc6ce2815b79a976bdb654bd8062f28132a3cdd)), closes [#5030](https://github.com/dequelabs/axe-core/issues/5030) + ### [4.11.4](https://github.com/dequelabs/axe-core/compare/v4.11.3...v4.11.4) (2026-04-23) ### Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65031166..1719460ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ We expect all code to be 100% covered by tests. We don't have or want code cover Tests should be added to the `test` directory using the same file path and name of the source file the test is for. For example, the source file `lib/commons/text/sanitize.js` should have a test file at `test/commons/text/sanitize.js`. -Axe uses Karma / Mocha / Chai as its testing framework. +Axe uses Web Test Runner / Mocha / Chai / Sinon as its testing framework. ### Documentation and Comments @@ -202,13 +202,12 @@ If you need to debug the unit tests in a browser, you can run: npm run test:debug ``` -This will start the Karma server and open up the Chrome browser. Click the `Debug` button to start debugging the tests. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](./.vscode/launch.json) is provided. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. +This will start the Web Test Runner server. Press `D` to open the Chrome browser. Click the link to a test to start debugging. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](./.vscode/launch.json) is provided. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. -Because the amount of tests is so large, it's recommended to debug only a specific set of unit tests rather than the whole test suite. You can use the `testDirs` argument when using the debug command and pass a specific test directory. The test directory names are the same as those used for `test:unit:*`: +Because the amount of tests is so large, it's recommended to debug only a specific set of unit tests rather than the whole test suite. You can use the `files` argument when using the debug command and pass a test files glob pattern. ```console -# accepts a single directory or a comma-separated list of directories -npm run test:debug -- testDirs=core,commons +npm run test:debug -- --files 'test/core' ``` ## Using axe with TypeScript diff --git a/Gruntfile.js b/Gruntfile.js index 3b9872799..365674cce 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,7 +37,7 @@ module.exports = function (grunt) { // run tests only for affected files instead of all tests grunt.event.on('watch', function (action, filepath) { - grunt.config.set('watch.file', filepath); + grunt.option('changed-file', filepath); }); process.env.NODE_NO_HTTP2 = 1; // to hide node warning - (node:18740) ExperimentalWarning: The http2 module is an experimental API. @@ -46,7 +46,7 @@ module.exports = function (grunt) { pkg: grunt.file.readJSON('package.json'), clean: { core: ['dist', 'tmp/core', 'tmp/rules.js', 'axe.js', 'axe.*.js'], - tests: ['tmp/integration-tests.js'] + tests: ['tmp/integration-tests'] }, babel: { options: { @@ -109,6 +109,39 @@ module.exports = function (grunt) { cwd: 'lib/core', src: ['core.js'], dest: 'tmp/core' + }, + // build so we can test it by itself + { + expand: true, + cwd: 'lib/gather-internals', + src: ['walk-tree.js'], + dest: 'tmp/', + options: { + globalName: '_gatherInternals' + } + }, + { + expand: true, + src: ['lib/gather-internals/main.js'], + dest: './', + options: { + outfile: 'gather-internals.js', + // esbuild doesn't support returning from an iife so we'll have to do a bit of a hack to make it work + // @see https://github.com/evanw/esbuild/issues/2277 + banner: { + js: '(() => {' + }, + footer: { + js: `return elementInternalsMap; +})();` + }, + globalName: 'elementInternalsMap', + metafile: true + }, + validateImports: { + max: 10, + maxSize: 4000 + } } ] } @@ -231,9 +264,7 @@ module.exports = function (grunt) { } }, test: { - data: { - testFile: '<%= watch.file %>' - } + data: {} }, watch: { axe: { @@ -243,7 +274,7 @@ module.exports = function (grunt) { }, tests: { options: { spawn: false }, - files: ['test/**/*'], + files: ['test/**/*', '!test/integration/full/**/*'], tasks: ['test'] } }, diff --git a/axe.d.ts b/axe.d.ts index 20291d3e9..57d8ab446 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -147,6 +147,9 @@ declare namespace axe { performanceTimer?: boolean; pingWaitTime?: number; } + interface NormalizedRunOptions extends RunOptions { + runOnly?: RunOnly; + } interface PreloadOptions { assets: string[]; timeout?: number; @@ -331,7 +334,7 @@ declare namespace axe { matches?: string | ((node: Element, virtualNode: VirtualNode) => boolean); reviewOnFail?: boolean; actIds?: string[]; - metadata?: Omit; + metadata?: Omit; } interface AxePlugin { id: string; @@ -349,6 +352,7 @@ declare namespace axe { helpUrl: string; tags: string[]; actIds?: string[]; + enabled?: boolean; } interface SerialDqElement { source: string; @@ -499,6 +503,7 @@ declare namespace axe { offset?: number ) => string | Uint8Array | Array; nodeSerializer: NodeSerializer; + normalizeRunOptions: (options?: RunOptions) => NormalizedRunOptions; } interface Aria { @@ -616,13 +621,23 @@ declare namespace axe { * @param {Array} tags Optional array of tags * @return {Array} Array of rules */ - function getRules(tags?: string[]): RuleMetadata[]; + function getRules( + tags?: string[] + ): (Omit & { enabled: boolean })[]; /** * Restores the default axe configuration */ function reset(): void; + /** + * Restores the default locale that was active before any + * `axe.configure({ locale })` call. No-op if no non-default + * locale has ever been applied. Does not affect any other + * configuration. + */ + function resetLocale(): void; + /** * Function to register a plugin configuration in document and its subframes * @param {Object} plugin A plugin configuration object @@ -668,6 +683,31 @@ declare namespace axe { isDefault?: boolean ): void; + /** + * Run axe in the current window only + * @param {ElementContext} context Optional The `Context` specification object @see Context + * @param {RunOptions} options Optional Options passed into rules or checks, temporarily modifying them. + * @returns {Promise} Partial result, for use in axe.finishRun. + */ + function externalAPIs(params?: { + elementInternalsTimeout?: number; + getElementInternals?: () => Promise; + }): void; + + type ElementInternalsMap = Array<{ + ancestry: CrossTreeSelector; + internals: Record; + }>; + type InternalsData = string | InternalsDataIdref | InternalsDataIdrefs; + type InternalsDataIdref = { + type: 'HTMLElement'; + value: CrossTreeSelector; + }; + type InternalsDataIdrefs = { + type: 'NodeList'; + value: CrossTreeSelector[]; + }; + // axe.frameMessenger type FrameMessenger = { open: (topicHandler: TopicHandler) => Close | void; diff --git a/bower.json b/bower.json index 16da022c3..ee0f36409 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.11.4", + "version": "4.12.1", "deprecated": true, "contributors": [ { diff --git a/build/generate-integration-tests.js b/build/generate-integration-tests.js new file mode 100644 index 000000000..0143e28c7 --- /dev/null +++ b/build/generate-integration-tests.js @@ -0,0 +1,60 @@ +'use strict'; + +/** + * Generates test files for integration rule tests in tmp/integration-tests/. + * This replaces the Karma preprocessor (test/integration/rules/preprocessor.js) + * that combined *.json + *.html pairs into executable JS at serve-time. + * + * Output files are picked up by web-test-runner via the test:unit:integration script. + */ + +const path = require('path'); +const fs = require('fs'); +const { globSync } = require('glob'); + +const rootDir = path.join(__dirname, '..'); +const rulesDir = path.join(rootDir, 'test', 'integration', 'rules'); +const outDir = path.join(rootDir, 'tmp', 'integration-tests'); +const runnerTemplate = fs.readFileSync( + path.join(rulesDir, 'runner.js'), + 'utf-8' +); + +// Clean and recreate output directory +fs.rmSync(outDir, { recursive: true, force: true }); +fs.mkdirSync(outDir, { recursive: true }); + +const jsonFiles = globSync('**/*.json', { cwd: rulesDir }); + +let count = 0; +for (const relPath of jsonFiles) { + const jsonPath = path.join(rulesDir, relPath); + const htmlPath = jsonPath.replace(/\.json$/, '.html'); + + if (!fs.existsSync(htmlPath)) { + // Some JSON files may not have a sibling HTML (e.g. nested frame fixtures) + continue; + } + + let test; + try { + test = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); + } catch (e) { + throw new Error(`Unable to parse ${jsonPath}: ${e.message}`); + } + + const html = fs.readFileSync(htmlPath, 'utf-8'); + test.content = html; + + const outPath = path.join(outDir, relPath.replace(/\.json$/, '.test.js')); + const outDirForFile = path.dirname(outPath); + fs.mkdirSync(outDirForFile, { recursive: true }); + + const output = runnerTemplate.replace('{}; /*tests*/', JSON.stringify(test)); + fs.writeFileSync(outPath, output, 'utf-8'); + count++; +} + +console.log( + `Generated ${count} integration test files in tmp/integration-tests/` +); diff --git a/build/tasks/test.js b/build/tasks/test.js index c1f51a7b4..13e6a3d2b 100644 --- a/build/tasks/test.js +++ b/build/tasks/test.js @@ -1,5 +1,6 @@ const execSync = require('child_process').execSync; const chalk = require('chalk'); +const path = require('node:path'); /*eslint-env node */ ('use strict'); @@ -9,12 +10,37 @@ module.exports = function (grunt) { 'test', 'This task runs unit tests based on which file was changed', function () { - const testFile = this.data.testFile; + const testFile = grunt.option('changed-file'); console.log(`${chalk.green('>>')} File "${testFile}"`); + const files = []; - execSync(`npm run test:unit -- testFiles=${testFile}`, { - stdio: 'inherit' - }); + // build the integration tests before testing + if ( + (testFile.startsWith(path.join('test', 'integration', 'rules')) && + testFile.endsWith('.html')) || + testFile.endsWith('.json') + ) { + execSync('npm run build:integration-tests', { stdio: 'inherit' }); + const rule = testFile.split(path.sep)[3]; + files.push( + path.join('tmp', 'integration-tests', rule, rule + '.test.js') + ); + } + + if ( + testFile && + testFile.startsWith(`test${path.sep}`) && + testFile.endsWith('.js') + ) { + files.push(testFile); + } + + let cmd = 'npm run test:unit'; + if (files.length) { + cmd += ` -- --files "${files.join(',')}"`; + } + + execSync(cmd, { stdio: 'inherit' }); } ); }; diff --git a/doc/API.md b/doc/API.md index 18f4dbb9c..f85a53e50 100644 --- a/doc/API.md +++ b/doc/API.md @@ -10,6 +10,7 @@ 1. [API Name: axe.getRules](#api-name-axegetrules) 1. [API Name: axe.configure](#api-name-axeconfigure) 1. [API Name: axe.reset](#api-name-axereset) + 1. [API Name: axe.resetLocale](#api-name-axeresetlocale) 1. [API Name: axe.run](#api-name-axerun) 1. [Parameters axe.run](#parameters-axerun) 1. [Context Parameter](#context-parameter) @@ -24,6 +25,7 @@ 1. [API Name: axe.teardown](#api-name-axeteardown) 1. [API Name: axe.frameMessenger](#api-name-axeframemessenger) 1. [API name: axe.runPartial / axe.finishRun](#api-name-axerunpartial--axefinishrun) + 1. [API name: axe.externalAPIs](#api-name-axeexternal-apis) 1. [Virtual DOM Utilities](#virtual-dom-utilities) 1. [API Name: axe.utils.querySelectorAll](#api-name-axeutilsqueryselectorall) 1. [API Name: axe.utils.getRule](#api-name-axeutilsgetrule) @@ -135,7 +137,7 @@ Returns a list of all rules with their ID and description - `tags` - **optional** Array of tags used to filter returned rules. If omitted, it will return all rules. See [axe-core tags](#axe-core-tags). -**Returns:** Array of rules that match the input filter with each entry having a format of `{ruleId: , description: , helpUrl: , help: , tags: }` +**Returns:** Array of rules that match the input filter with each entry having a format of `{ruleId: , description: , helpUrl: , help: , tags: , enabled: }`. `enabled` is `true` for rules that run by default when `axe.run()` is called with no options, and `false` for rules that are disabled by default (i.e. experimental and deprecated rules). #### Example 1 @@ -161,7 +163,8 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr "section508", "section508.22.a" ], - actIds: ['c487ae'] + actIds: ['c487ae'], + enabled: true }, { description: "Ensure ARIA attributes are allowed for an element's role", @@ -172,7 +175,8 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr "cat.aria", "wcag2a", "wcag412" - ] + ], + enabled: true } … ] @@ -296,6 +300,24 @@ axe.reset(); None +### API Name: axe.resetLocale + +#### Description + +Restore the default locale that was active before any `axe.configure({ locale })` call, without touching the rest of the configuration. + +`axe.configure({ locale })` has no inverse, and `axe.reset()` also clears branding, rule enable/disable overrides, `frameMessenger`, and other configuration. `axe.resetLocale()` reverts only the locale (rule descriptions, check messages, failure summaries, `lang`) back to the default that was in effect before the first `applyLocale` call. It is a no-op if no non-default locale has ever been applied, and safe to call repeatedly. + +#### Synopsis + +```js +axe.resetLocale(); +``` + +#### Parameters + +None + ### API Name: axe.run #### Purpose @@ -840,6 +862,10 @@ Set up an alternative communication channel between parent and child frames. By Run axe without frame communication. This is the recommended way to run axe in browser drivers such as Selenium and Puppeteer. See [run-partial.md](run-partial.md) for details. +### API name: axe.externalAPIs + +Set external API data for axe-core to use during the run. See [external-apis.md](external-apis.md) for details. + ### Virtual DOM Utilities Note: If you’re writing rules or checks, you’ll have both the `node` and `virtualNode` passed in. diff --git a/doc/check-options.md b/doc/check-options.md index d35e9575a..532937e0b 100644 --- a/doc/check-options.md +++ b/doc/check-options.md @@ -517,7 +517,7 @@ h6:not([role]),
"passLength": 1
- Relative length, if the the candidate heading is X times or greater the length of the candidate paragraph, it will pass. + Relative length, if the candidate heading is X times or greater the length of the candidate paragraph, it will pass. @@ -526,7 +526,7 @@ h6:not([role]),
"failLength": 0.5
- Relative length, if the the candidate heading is X times or less the length of the candidate paragraph, it can fail. + Relative length, if the candidate heading is X times or less the length of the candidate paragraph, it can fail. diff --git a/doc/developer-guide.md b/doc/developer-guide.md index 51da6f5ac..a00f3f71a 100644 --- a/doc/developer-guide.md +++ b/doc/developer-guide.md @@ -43,7 +43,7 @@ To build axe.js, simply run `npm run build` in the root folder of the axe-core r You can watch for changes and automatically build axe and run relevant tests using `npm run develop`. Once run, any changes to files inside the [lib directory](../lib) will rebuild axe. After axe is built, it will try to run the relevant tests for the files changed. If you change a file inside the [test directory](../test) it will run the tests for the file changed. -Changes to files in the [full integration test directory](../test/integration/full) will not run the tests. This is because these tests require the browser to navigate to the page directly, which is something Mocha / Karma does not support. +Changes to files in the [full integration test directory](../test/integration/full) will not run the tests. This is because these tests require the browser to navigate to the page directly. **Note:** We are still working on knowing which tests are relevant to the changed file so this may not correctly run tests every time. In these cases you should run the tests manually. If you encounter a test that does not run when a relevant file is changed, please [open an issue](https://github.com/dequelabs/axe-core/issues). @@ -72,24 +72,20 @@ There are also a set of tests that are not considered unit tests that you can ru Additionally, you can [watch for changes](#watching-for-changes) to files and automatically run the relevant tests. -If you need to debug a test in a non-headless browser, you can run `npm run test:debug` which will run the Karma tests in non-headless Chrome. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](../.vscode/launch.json) is provided. You can also navigate to the newly opened page using any supported browser. +If you need to debug a test in a non-headless browser, you can run `npm run test:debug` which will start the Web Test Runner server. Press `D` to open a headed Chrome and run the tests. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](../.vscode/launch.json) is provided. You can also navigate to the newly opened page using any supported browser. -You can scope which set of tests to debug by passing the `testDirs` argument. Supported values are: - -- `core` -- `commons` -- `checks` -- `rule-matches` -- `integration` -- `virtual-rules` -- `api` +You can scope which set of tests to debug by passing the `files` argument and a path. Example: -- `npm run test:debug -- testDirs=core` +- `npm run test:debug -- files 'test/core/**/*.js'` Lastly, you can run the [full integration tests](../test/integration/full) by starting a local server by running `npm start`. Once started, you can open any supported browser and navigate to any test in the full integration tests directory. +### Using Mocha `.only` in Tests + +The use of `.only` for tests is supported. However Web Test Runner runs each test file in an isolated Mocha instance. This means using `.only` does not work across test files, just within the file it is used in. You can isolate a specific test by using the `files` argument. + ### API Reference [See API exposed on axe](./API.md#section-2-api-reference) diff --git a/doc/element-internals.md b/doc/element-internals.md new file mode 100644 index 000000000..19c101f4d --- /dev/null +++ b/doc/element-internals.md @@ -0,0 +1,47 @@ +# ElementInternals + +Axe-core supports CustomElements with attached ElementInternals or ARIA Properties. However, JavaScript does not provide an API for accessing ElementInternals information on a node so axe-core must rely on developers implementing a [community protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/element-internals-declaration.md) on their CustomElements in order to find this information. + +> [!Note] +> At this time, axe-core only supports the ElementInternals `role` property and no others (e.g. `ariaLabel`), though support for other properties is planned. Additionally axe-core will not validate the value of the ElementInternals `role`, and many rules will not run against the element (e.g. `aria-required-attr`). Rules that do run may only be partially supported (e.g. `aria-required-children`). +> +> Lastly, support for ElementInternals is behind a feature flag `axe._enableElementInternals`, which must manually be set to `true` before axe runs. + +```js +CustomElements.define( + 'my-custom-button', + class MyCustomButton extends HTMLElement { + constructor() { + super(); + + const internals = this.attachInternals(); + internals.role = 'button'; + + globalThis._elementInternals ??= new WeakMap(); + globalThis._elementInternals.set(this, internals); + } + } +); +``` + +In addition to the global WeakMap `globalThis._elementInternals`, axe-core also supports the following public properties or Symbols on the CustomElement: + +- `_internals` +- `internals` +- `internals_` +- `Symbol('internals')` +- `Symbol('privateInternals')` + +```js +CustomElements.define( + 'my-custom-button', + class MyCustomButton extends HTMLElement { + constructor() { + super(); + + this._internals = this.attachInternals(); + this._internals.role = 'button'; + } + } +); +``` diff --git a/doc/examples/puppeteer/axe-puppeteer.js b/doc/examples/puppeteer/axe-puppeteer.js index 64f657b33..08b4a6e4a 100644 --- a/doc/examples/puppeteer/axe-puppeteer.js +++ b/doc/examples/puppeteer/axe-puppeteer.js @@ -1,12 +1,15 @@ const puppeteer = require('puppeteer'); const axeCore = require('axe-core'); -const { parse: parseURL } = require('url'); const assert = require('assert'); // Cheap URL validation const isValidURL = input => { - const u = parseURL(input); - return u.protocol && u.host; + try { + const u = new URL(input); + return u.protocol && u.host; + } catch { + return false; + } }; (async () => { @@ -18,7 +21,15 @@ const isValidURL = input => { let results; try { // Setup Puppeteer - browser = await puppeteer.launch(); + browser = await puppeteer.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu' + ] + }); // Get new page const page = await browser.newPage(); diff --git a/doc/examples/puppeteer/set-content.js b/doc/examples/puppeteer/set-content.js index 60e85f8ef..97d746f06 100644 --- a/doc/examples/puppeteer/set-content.js +++ b/doc/examples/puppeteer/set-content.js @@ -3,7 +3,15 @@ const puppeteer = require('puppeteer'); const axe = require('axe-core'); (async () => { - const browser = await puppeteer.launch(); + const browser = await puppeteer.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu' + ] + }); const page = await browser.newPage(); await page.setContent(` diff --git a/doc/external-apis.md b/doc/external-apis.md new file mode 100644 index 000000000..4fd792f2a --- /dev/null +++ b/doc/external-apis.md @@ -0,0 +1,68 @@ +# External APIs + +Axe externalAPIs can be used to pass information to axe-core that it would be otherwise unable to obtain on its own (such as when it is running in an isolated JavaScript context of an extension). By default axe-core will try to gather the information if the external API for it is not set. If the external API is set axe-core will only rely on the passed in data and will not gather it itself. + +```js +axe.externalAPIs({ + // Pass ElementInternal data to axe-core + elementInternals() { + return Promise.resolve(internalData); + } +}); +``` + +## axe.externalAPIs({ elementInternals }) + +`elementInternals` is a function that can be used to pass ElementInternal data for the elements on the page. It should return a Promise with the ElementInternal data array. The data inside the ElementInternal array must have the following properties: + +```js +axe.externalAPIs({ + elementInternals() { + return Promise.resolve([ + { + // The CSS ancestry selector, which is either a string or an array of strings if the element is inside a ShadowDom tree. Generated using `axe.utils.getAncestry(node)` + ancestry: 'html > body > main > my-custom-button', + + // Object of each ElementInternals property name and value. The properties must be serialized so some serialization work is needed for element reference properties (idref(s)) + internals: { + // A simple String value + role: 'button', + + // An idref value. The type will always be "HTMLElement" and the value is a CSS ancestry selector + ariaActiveDescendantElement: { + type: 'HTMLElement', + value: 'html > body > main > my-custom-button > div:nth-child(1)' + }, + + // An idrefs value. The type will always be "NodeList" and the value is an array of CSS ancestry selectors for each node + ariaLabelledbyElements: { + type: 'NodeList', + value: ['html > body > div:nth-child(4)'] + } + } + } + ]); + } +}); +``` + +Axe-core provides a package script `gather-internals.js` that can be used to inject into the main context when working in an extension. The returned object from the script can be used directly as the returned value of the Promise for `elementInternals`. + +```js +const internals = await chrome.scripting.executeScript({ + target: { + tabId: tab.id + }, + files: ['/node_modules/axe-core/gather-internals.js'], + world: 'MAIN' +}); +``` + +See [element-internals.md](element-internals.md) for more information on what ElementInternal properties are supported. + +> [!Note] +> Support for ElementInternals is behind a feature flag `axe._enableElementInternals`, which must manually be set to `true` before axe runs, even when passing in `elementInternals` data. + +## axe.externalAPIs({ elementInternalsTimeout }) + +Since gathering ElementInternals data is an async operation, you can configure how long axe-core will wait for `elementInternals` promise to resolve. By default the timeout is set to 1 second. If the timeout occurs axe-core will not run and will throw an error. diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 472727b55..b08878b28 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -32,6 +32,7 @@ | [aria-required-children](https://dequeuniversity.com/rules/axe/4.11/aria-required-children?application=RuleDescription) | Ensure elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | | [aria-required-parent](https://dequeuniversity.com/rules/axe/4.11/aria-required-parent?application=RuleDescription) | Ensure elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | | [aria-roles](https://dequeuniversity.com/rules/axe/4.11/aria-roles?application=RuleDescription) | Ensure all elements with a role attribute use a valid value | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-tab-name](https://dequeuniversity.com/rules/axe/4.11/aria-tab-name?application=RuleDescription) | Ensure every ARIA tab node has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-7.1.1 | failure, needs review | | | [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-toggle-field-name?application=RuleDescription) | Ensure every ARIA toggle field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-7.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | | [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.11/aria-tooltip-name?application=RuleDescription) | Ensure every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | | | [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr-value?application=RuleDescription) | Ensure all ARIA attributes have valid values | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | @@ -100,38 +101,37 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r Rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience. -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.11/accesskeys?application=RuleDescription) | Ensure every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-role?application=RuleDescription) | Ensure role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | -| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.11/aria-dialog-name?application=RuleDescription) | Ensure every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-text](https://dequeuniversity.com/rules/axe/4.11/aria-text?application=RuleDescription) | Ensure role="text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.11/aria-treeitem-name?application=RuleDescription) | Ensure every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.11/empty-heading?application=RuleDescription) | Ensure headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | -| [empty-table-header](https://dequeuniversity.com/rules/axe/4.11/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | -| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.11/focus-order-semantics?application=RuleDescription) | Ensure elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, RGAAv4, RGAA-12.8.1, a11y-engine, a11y-engine-experimental, high-accuracy | failure | | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.11/frame-tested?application=RuleDescription) | Ensure <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | -| [heading-order-bp](https://dequeuniversity.com/rules/axe/4.11/heading-order-bp?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.structure, best-practice, a11y-engine | failure, needs review | | -| [hidden-content](https://dequeuniversity.com/rules/axe/4.11/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, review-item, a11y-engine, a11y-engine-experimental, high-accuracy | failure, needs review | | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.11/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | -| [label-title-only](https://dequeuniversity.com/rules/axe/4.11/label-title-only?application=RuleDescription) | Ensure that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | -| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-banner-is-top-level?application=RuleDescription) | Ensure the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-complementary-is-top-level?application=RuleDescription) | Ensure the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensure the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-main-is-top-level?application=RuleDescription) | Ensure the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-banner?application=RuleDescription) | Ensure the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensure the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-main?application=RuleDescription) | Ensure the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.11/landmark-one-main?application=RuleDescription) | Ensure the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.11/landmark-unique?application=RuleDescription) | Ensure landmarks are unique | Moderate | cat.semantics, best-practice | failure | | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.11/meta-viewport-large?application=RuleDescription) | Ensure <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | -| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.11/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | -| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.11/presentation-role-conflict?application=RuleDescription) | Ensure elements marked as presentational do not have global ARIA or tabindex so that all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | -| [region](https://dequeuniversity.com/rules/axe/4.11/region?application=RuleDescription) | Ensure all page content is contained by landmarks | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-9.2.1 | failure | | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.11/scope-attr-valid?application=RuleDescription) | Ensure the scope attribute is used correctly on tables | Moderate | cat.tables, best-practice | failure | | -| [skip-link](https://dequeuniversity.com/rules/axe/4.11/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-12.7.1 | failure, needs review | | -| [tabindex](https://dequeuniversity.com/rules/axe/4.11/tabindex?application=RuleDescription) | Ensure tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.11/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice, RGAAv4, RGAA-5.2.1 | failure, needs review | | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.11/accesskeys?application=RuleDescription) | Ensure every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-role?application=RuleDescription) | Ensure role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | +| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.11/aria-dialog-name?application=RuleDescription) | Ensure every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-text](https://dequeuniversity.com/rules/axe/4.11/aria-text?application=RuleDescription) | Ensure role="text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.11/aria-treeitem-name?application=RuleDescription) | Ensure every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.11/empty-heading?application=RuleDescription) | Ensure headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | +| [empty-table-header](https://dequeuniversity.com/rules/axe/4.11/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.11/focus-order-semantics?application=RuleDescription) | Ensure elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, RGAAv4, RGAA-12.8.1, a11y-engine, a11y-engine-experimental, high-accuracy | failure | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.11/frame-tested?application=RuleDescription) | Ensure <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | +| [heading-order-bp](https://dequeuniversity.com/rules/axe/4.11/heading-order-bp?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.structure, best-practice, a11y-engine | failure, needs review | | +| [hidden-content](https://dequeuniversity.com/rules/axe/4.11/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, review-item, a11y-engine, a11y-engine-experimental, high-accuracy | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.11/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | +| [label-title-only](https://dequeuniversity.com/rules/axe/4.11/label-title-only?application=RuleDescription) | Ensure that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | +| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-banner-is-top-level?application=RuleDescription) | Ensure the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensure the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-main-is-top-level?application=RuleDescription) | Ensure the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-banner?application=RuleDescription) | Ensure the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensure the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-main?application=RuleDescription) | Ensure the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.11/landmark-one-main?application=RuleDescription) | Ensure the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.11/landmark-unique?application=RuleDescription) | Ensure landmarks are unique | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.11/meta-viewport-large?application=RuleDescription) | Ensure <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | +| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.11/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | +| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.11/presentation-role-conflict?application=RuleDescription) | Ensure elements marked as presentational do not have global ARIA or tabindex so that all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | +| [region](https://dequeuniversity.com/rules/axe/4.11/region?application=RuleDescription) | Ensure all page content is contained by landmarks | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-9.2.1 | failure | | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.11/scope-attr-valid?application=RuleDescription) | Ensure the scope attribute is used correctly on tables | Moderate | cat.tables, best-practice | failure | | +| [skip-link](https://dequeuniversity.com/rules/axe/4.11/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-12.7.1 | failure, needs review | | +| [tabindex](https://dequeuniversity.com/rules/axe/4.11/tabindex?application=RuleDescription) | Ensure tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.11/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice, RGAAv4, RGAA-5.2.1 | failure, needs review | | ## WCAG 2.x level AAA rules @@ -154,9 +154,10 @@ _There are no matching rules_ Deprecated rules are disabled by default and will be removed in the next major release. -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | -| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.11/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, deprecated | failure, needs review | | -| [audio-caption](https://dequeuniversity.com/rules/axe/4.11/audio-caption?application=RuleDescription) | Ensure <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, EN-301-549, EN-9.1.2.1, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | -| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-active?application=RuleDescription) | Ensure every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id](https://dequeuniversity.com/rules/axe/4.11/duplicate-id?application=RuleDescription) | Ensure every id attribute value is unique | Minor | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | +| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.11/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, deprecated | failure, needs review | | +| [audio-caption](https://dequeuniversity.com/rules/axe/4.11/audio-caption?application=RuleDescription) | Ensure <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, EN-301-549, EN-9.1.2.1, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | +| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-active?application=RuleDescription) | Ensure every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [duplicate-id](https://dequeuniversity.com/rules/axe/4.11/duplicate-id?application=RuleDescription) | Ensure every id attribute value is unique | Minor | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-complementary-is-top-level?application=RuleDescription) | Ensure the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice, deprecated | failure | | diff --git a/eslint.config.js b/eslint.config.js index 495c1d5a2..3e25970f2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -124,7 +124,7 @@ module.exports = [ }, { // disallow imports from node modules - ignores: ['lib/core/imports/**/*.js'], + ignores: ['lib/core/imports/**/*.js', 'test/**'], rules: { 'no-restricted-imports': [ 'error', diff --git a/lib/checks/aria/aria-conditional-attr-evaluate.js b/lib/checks/aria/aria-conditional-attr-evaluate.js index 92fca6752..c772c9a25 100644 --- a/lib/checks/aria/aria-conditional-attr-evaluate.js +++ b/lib/checks/aria/aria-conditional-attr-evaluate.js @@ -1,10 +1,12 @@ import getRole from '../../commons/aria/get-role'; import ariaConditionalCheckboxAttr from './aria-conditional-checkbox-attr-evaluate'; +import ariaConditionalRadioAttr from './aria-conditional-radio-attr-evaluate'; import ariaConditionalRowAttr from './aria-conditional-row-attr-evaluate'; const conditionalRoleMap = { row: ariaConditionalRowAttr, - checkbox: ariaConditionalCheckboxAttr + checkbox: ariaConditionalCheckboxAttr, + radio: ariaConditionalRadioAttr }; export default function ariaConditionalAttrEvaluate( diff --git a/lib/checks/aria/aria-conditional-attr.json b/lib/checks/aria/aria-conditional-attr.json index c55829016..5f395ba63 100644 --- a/lib/checks/aria/aria-conditional-attr.json +++ b/lib/checks/aria/aria-conditional-attr.json @@ -15,6 +15,7 @@ "pass": "ARIA attribute is allowed", "fail": { "checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state", + "radio": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real radio state", "rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}", "rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}" } diff --git a/lib/checks/aria/aria-conditional-radio-attr-evaluate.js b/lib/checks/aria/aria-conditional-radio-attr-evaluate.js new file mode 100644 index 000000000..4f6897c8f --- /dev/null +++ b/lib/checks/aria/aria-conditional-radio-attr-evaluate.js @@ -0,0 +1,32 @@ +export default function ariaConditionalRadioAttr(node, options, virtualNode) { + const { nodeName, type } = virtualNode.props; + const ariaChecked = normalizeAriaChecked(virtualNode.attr('aria-checked')); + if (nodeName !== 'input' || type !== 'radio' || !ariaChecked) { + return true; + } + + const checkState = getCheckState(virtualNode); + if (ariaChecked === checkState) { + return true; + } + this.data({ + messageKey: 'radio', + checkState + }); + return false; +} + +function getCheckState(vNode) { + return vNode.props.checked ? 'true' : 'false'; +} + +function normalizeAriaChecked(ariaCheckedVal) { + if (!ariaCheckedVal) { + return ''; + } + ariaCheckedVal = ariaCheckedVal.toLowerCase(); + if (ariaCheckedVal === 'true') { + return ariaCheckedVal; + } + return 'false'; +} diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index 403cd4979..0330d2a86 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -103,7 +103,8 @@ function getOwnedRoles(virtualNode, required) { ) { ownedVirtual.push(...vNode.children); } else if (role || hasGlobalAriaOrFocusable) { - const attr = globalAriaAttr || 'tabindex'; + const attr = + globalAriaAttr || (vNode.hasAttr('tabindex') ? 'tabindex' : undefined); ownedRoles.push({ role, attr, vNode }); } } diff --git a/lib/checks/lists/invalid-children-evaluate.js b/lib/checks/lists/invalid-children-evaluate.js index 684e2eb65..633a64bb9 100644 --- a/lib/checks/lists/invalid-children-evaluate.js +++ b/lib/checks/lists/invalid-children-evaluate.js @@ -1,5 +1,5 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; -import { getExplicitRole } from '../../commons/aria'; +import { getExplicitRole, getRole } from '../../commons/aria'; export default function invalidChildrenEvaluate( node, @@ -58,11 +58,16 @@ function getInvalidSelector( return false; } - const role = getExplicitRole(vChild); - if (role) { - return validRoles.includes(role) ? false : selector + `[role=${role}]`; + const explicitRole = getExplicitRole(vChild); + const role = getRole(vChild); + if (explicitRole) { + return validRoles.includes(explicitRole) + ? false + : selector + `[role=${role}]`; } else { - return validNodeNames.includes(nodeName) ? false : selector + nodeName; + return validNodeNames.includes(nodeName) || validRoles.includes(role) + ? false + : selector + nodeName; } } diff --git a/lib/checks/lists/listitem-evaluate.js b/lib/checks/lists/listitem-evaluate.js index dde5dcb4f..630056c49 100644 --- a/lib/checks/lists/listitem-evaluate.js +++ b/lib/checks/lists/listitem-evaluate.js @@ -1,4 +1,4 @@ -import { isValidRole, getExplicitRole } from '../../commons/aria'; +import { isValidRole, getExplicitRole, getRole } from '../../commons/aria'; export default function listitemEvaluate(node, options, virtualNode) { const { parent } = virtualNode; @@ -7,18 +7,17 @@ export default function listitemEvaluate(node, options, virtualNode) { return undefined; } - const parentNodeName = parent.props.nodeName; - const parentRole = getExplicitRole(parent); + const parentExplicitRole = getExplicitRole(parent); + const parentRole = getRole(parent); if (['presentation', 'none', 'list'].includes(parentRole)) { return true; } - if (parentRole && isValidRole(parentRole)) { + if (parentExplicitRole && isValidRole(parentExplicitRole)) { this.data({ messageKey: 'roleNotValid' }); - return false; } - return ['ul', 'ol', 'menu'].includes(parentNodeName); + return false; } diff --git a/lib/commons/aria/implicit-role.js b/lib/commons/aria/implicit-role.js index 509d77217..72bf1e3fd 100644 --- a/lib/commons/aria/implicit-role.js +++ b/lib/commons/aria/implicit-role.js @@ -1,7 +1,6 @@ import implicitHtmlRoles from '../standards/implicit-html-roles'; -import { getNodeFromTree } from '../../core/utils'; +import { nodeLookup } from '../../core/utils'; import getElementSpec from '../standards/get-element-spec'; -import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node'; /** * Get the implicit role for a given node @@ -12,20 +11,22 @@ import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-no * @return {Mixed} Either the role or `null` if there is none */ function implicitRole(node, { chromium } = {}) { - const vNode = - node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); - node = vNode.actualNode; + const { vNode } = nodeLookup(node); // this error is only thrown if the virtual tree is not a - // complete tree, which only happens in linting and if a - // user used `getFlattenedTree` manually on a subset of the - // DOM tree + // complete tree, which only happens in certain scenarios, + // such as if a user used `getFlattenedTree` manually on a + // subset of the DOM tree if (!vNode) { throw new ReferenceError( 'Cannot get implicit role of a node outside the current scope.' ); } + if (vNode.elementInternals?.role) { + return vNode.elementInternals.role; + } + const nodeName = vNode.props.nodeName; const role = implicitHtmlRoles[nodeName]; diff --git a/lib/commons/text/visible-virtual.js b/lib/commons/text/visible-virtual.js index 6e5453aae..fbfa5ccd1 100644 --- a/lib/commons/text/visible-virtual.js +++ b/lib/commons/text/visible-virtual.js @@ -1,7 +1,8 @@ -import sanitize from './sanitize'; +import { nodeLookup } from '../../core/utils'; import isVisibleOnScreen from '../dom/is-visible-on-screen'; import isVisibleToScreenReaders from '../dom/is-visible-to-screenreader'; -import { nodeLookup } from '../../core/utils'; +import isIconLigature from './is-icon-ligature'; +import sanitize from './sanitize'; /** * Returns the visible text of the virtual node @@ -15,9 +16,13 @@ import { nodeLookup } from '../../core/utils'; * @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader * @param {Boolean} noRecursing When False, the result will contain text from the element and it's children. * When True, the result will only contain text from the element + * @param {Object} [options] + * @param {Boolean} [options.ignoreIconLigature] When true, icon ligature text nodes are excluded + * @param {number} [options.pixelThreshold] Pixel threshold for icon ligature detection + * @param {number} [options.occurrenceThreshold] Occurrence threshold for icon ligature detection * @return {String} */ -function visibleVirtual(element, screenReader, noRecursing) { +function visibleVirtual(element, screenReader, noRecursing, options = {}) { const { vNode } = nodeLookup(element); const visibleMethod = screenReader ? isVisibleToScreenReaders @@ -28,16 +33,29 @@ function visibleVirtual(element, screenReader, noRecursing) { const visible = !element.actualNode || (element.actualNode && visibleMethod(element)); + const { ignoreIconLigature, pixelThreshold, occurrenceThreshold } = options; + const result = vNode.children .map(child => { - const { nodeType, nodeValue } = child.props; + const { nodeType, nodeValue, nodeName } = child.props; if (nodeType === 3) { // filter on text nodes - if (nodeValue && visible) { - return nodeValue; + if (!nodeValue || !visible) { + return ''; + } + if ( + ignoreIconLigature && + isIconLigature(child, pixelThreshold, occurrenceThreshold) + ) { + return ''; } - } else if (!noRecursing) { - return visibleVirtual(child, screenReader); + return nodeValue; + } + if (nodeName === 'br') { + return ' '; + } + if (!noRecursing) { + return visibleVirtual(child, screenReader, false, options); } }) .join(''); diff --git a/lib/core/_exposed-for-testing.js b/lib/core/_exposed-for-testing.js index 7957e2dcd..8c7b00282 100644 --- a/lib/core/_exposed-for-testing.js +++ b/lib/core/_exposed-for-testing.js @@ -16,6 +16,7 @@ import failureSummary from './reporters/helpers/failure-summary'; import incompleteFallbackMessage from './reporters/helpers/incomplete-fallback-msg'; import processAggregate from './reporters/helpers/process-aggregate'; +import { external } from './public/external-apis'; import { setDefaultFrameMessenger } from './utils/frame-messenger'; import { cacheNodeSelectors, @@ -48,7 +49,8 @@ const _thisWillBeDeletedDoNotUse = { metadataFunctionMap }, public: { - reporters + reporters, + external }, helpers: { failureSummary, diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index 3353545af..e0d51b4aa 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -10,10 +10,12 @@ import { findBy, ruleShouldRun, performanceTimer, - serializeError + serializeError, + normalizeRunOptions } from '../utils'; import { doT } from '../imports'; import constants from '../constants'; +import { external } from '../public/external-apis'; const dotRegex = /\{\{.+?\}\}/g; @@ -135,7 +137,7 @@ export default class Audit { /** * Apply the given `locale`. * - * @param {axe.Locale} + * @param {axe.Locale} locale */ applyLocale(locale) { this._setDefaultLocale(); @@ -271,91 +273,99 @@ export default class Audit { * @param {Function} reject Callback function to fire when audit experiences an error */ run(context, options, resolve, reject) { - this.normalizeOptions(options); + normalizeRunOptions(options); DqElement.setRunOptions(options); - // TODO: es-modules_selectCache - axe._selectCache = []; - // get a list of rules to run NOW vs. LATER (later are preload assets dependent rules) - const allRulesToRun = getRulesToRun(this.rules, context, options); - const runNowRules = allRulesToRun.now; - const runLaterRules = allRulesToRun.later; - // init a NOW queue for rules to run immediately - const nowRulesQueue = queue(); - // construct can run NOW rules into NOW queue - runNowRules.forEach(rule => { - nowRulesQueue.defer(getDefferedRule(rule, context, options)); - }); - // init a PRELOADER queue to start preloading assets - const preloaderQueue = queue(); - // defer preload if preload dependent rules exist - if (runLaterRules.length) { - preloaderQueue.defer(res => { - // handle both success and fail of preload - // and resolve, to allow to run all checks - preload(options) - .then(assets => res(assets)) - .catch(err => { - /** - * Note: - * we do not reject, to allow other (non-preload) rules to `run` - * -> instead we resolve as `undefined` - */ - console.warn(`Couldn't load preload assets: `, err); - res(undefined); + const internalsQueue = queue(); + internalsQueue.defer(external.loadElementInternals()); + + // if there is no external api function the queue resolves immediately + internalsQueue + .then(() => { + // TODO: es-modules_selectCache + axe._selectCache = []; + // get a list of rules to run NOW vs. LATER (later are preload assets dependent rules) + const allRulesToRun = getRulesToRun(this.rules, context, options); + const runNowRules = allRulesToRun.now; + const runLaterRules = allRulesToRun.later; + // init a NOW queue for rules to run immediately + const nowRulesQueue = queue(); + // construct can run NOW rules into NOW queue + runNowRules.forEach(rule => { + nowRulesQueue.defer(getDefferedRule(rule, context, options)); + }); + // init a PRELOADER queue to start preloading assets + const preloaderQueue = queue(); + // defer preload if preload dependent rules exist + if (runLaterRules.length) { + preloaderQueue.defer(res => { + // handle both success and fail of preload + // and resolve, to allow to run all checks + preload(options) + .then(assets => res(assets)) + .catch(err => { + /** + * Note: + * we do not reject, to allow other (non-preload) rules to `run` + * -> instead we resolve as `undefined` + */ + console.warn(`Couldn't load preload assets: `, err); + res(undefined); + }); }); - }); - } - // defer now and preload queue to run immediately - const queueForNowRulesAndPreloader = queue(); - queueForNowRulesAndPreloader.defer(nowRulesQueue); - queueForNowRulesAndPreloader.defer(preloaderQueue); - // invoke the now queue - queueForNowRulesAndPreloader - .then(nowRulesAndPreloaderResults => { - // interpolate results into separate variables - const assetsFromQueue = nowRulesAndPreloaderResults.pop(); - if (assetsFromQueue && assetsFromQueue.length) { - // result is a queue (again), hence the index resolution - // assets is either an object of key value pairs of asset type and values - // eg: cssom: [stylesheets] - // or undefined if preload failed - const assets = assetsFromQueue[0]; - // extend context with preloaded assets - if (assets) { - context = { - ...context, - ...assets - }; - } } - // the reminder of the results are RuleResults - const nowRulesResults = nowRulesAndPreloaderResults[0]; - // if there are no rules to run LATER - resolve with rule results - if (!runLaterRules.length) { - // remove the cache - axe._selectCache = undefined; - // resolve - resolve(nowRulesResults.filter(result => !!result)); - return; - } - // init a LATER queue for rules that are dependant on preloaded assets - const laterRulesQueue = queue(); - runLaterRules.forEach(rule => { - const deferredRule = getDefferedRule(rule, context, options); - laterRulesQueue.defer(deferredRule); - }); - // invoke the later queue - laterRulesQueue - .then(laterRuleResults => { - // remove the cache - axe._selectCache = undefined; - // resolve - resolve( - nowRulesResults - .concat(laterRuleResults) - .filter(result => !!result) - ); + // defer now and preload queue to run immediately + const queueForNowRulesAndPreloader = queue(); + queueForNowRulesAndPreloader.defer(nowRulesQueue); + queueForNowRulesAndPreloader.defer(preloaderQueue); + // invoke the now queue + queueForNowRulesAndPreloader + .then(nowRulesAndPreloaderResults => { + // interpolate results into separate variables + const assetsFromQueue = nowRulesAndPreloaderResults.pop(); + if (assetsFromQueue && assetsFromQueue.length) { + // result is a queue (again), hence the index resolution + // assets is either an object of key value pairs of asset type and values + // eg: cssom: [stylesheets] + // or undefined if preload failed + const assets = assetsFromQueue[0]; + // extend context with preloaded assets + if (assets) { + context = { + ...context, + ...assets + }; + } + } + // the reminder of the results are RuleResults + const nowRulesResults = nowRulesAndPreloaderResults[0]; + // if there are no rules to run LATER - resolve with rule results + if (!runLaterRules.length) { + // remove the cache + axe._selectCache = undefined; + // resolve + resolve(nowRulesResults.filter(result => !!result)); + return; + } + // init a LATER queue for rules that are dependant on preloaded assets + const laterRulesQueue = queue(); + runLaterRules.forEach(rule => { + const deferredRule = getDefferedRule(rule, context, options); + laterRulesQueue.defer(deferredRule); + }); + // invoke the later queue + laterRulesQueue + .then(laterRuleResults => { + // remove the cache + axe._selectCache = undefined; + // resolve + resolve( + nowRulesResults + .concat(laterRuleResults) + .filter(result => !!result) + ); + }) + .catch(reject); }) .catch(reject); }) @@ -397,95 +407,12 @@ export default class Audit { getRule(ruleId) { return this.rules.find(rule => rule.id === ruleId); } - /** - * Ensure all rules that are expected to run exist - * @throws {Error} If any tag or rule specified in options is unknown - * @param {Object} options Options object - * @return {Object} Validated options object - */ - normalizeOptions(options) { - const audit = this; - const tags = []; - const ruleIds = []; - audit.rules.forEach(rule => { - ruleIds.push(rule.id); - rule.tags.forEach(tag => { - if (!tags.includes(tag)) { - tags.push(tag); - } - }); - }); - // Validate runOnly - if (['object', 'string'].includes(typeof options.runOnly)) { - if (typeof options.runOnly === 'string') { - options.runOnly = [options.runOnly]; - } - if (Array.isArray(options.runOnly)) { - const hasTag = options.runOnly.find(value => tags.includes(value)); - const hasRule = options.runOnly.find(value => ruleIds.includes(value)); - if (hasTag && hasRule) { - throw new Error('runOnly cannot be both rules and tags'); - } - if (hasRule) { - options.runOnly = { - type: 'rule', - values: options.runOnly - }; - } else { - options.runOnly = { - type: 'tag', - values: options.runOnly - }; - } - } - const only = options.runOnly; - if (only.value && !only.values) { - only.values = only.value; - delete only.value; - } - if (!Array.isArray(only.values) || only.values.length === 0) { - throw new Error('runOnly.values must be a non-empty array'); - } - // Check if every value in options.runOnly is a known rule ID - if (['rule', 'rules'].includes(only.type)) { - only.type = 'rule'; - only.values.forEach(ruleId => { - if (!ruleIds.includes(ruleId)) { - throw new Error('unknown rule `' + ruleId + '` in options.runOnly'); - } - }); - // Validate 'tags' (e.g. anything not 'rule') - } else if (['tag', 'tags', undefined].includes(only.type)) { - only.type = 'tag'; - const unmatchedTags = only.values.filter( - tag => !tags.includes(tag) && !/wcag2[1-3]a{1,3}/.test(tag) - ); - if (unmatchedTags.length !== 0) { - axe.log('Could not find tags `' + unmatchedTags.join('`, `') + '`'); - } - } else { - throw new Error(`Unknown runOnly type '${only.type}'`); - } - } - if (typeof options.rules === 'object') { - Object.keys(options.rules).forEach(ruleId => { - if (!ruleIds.includes(ruleId)) { - throw new Error('unknown rule `' + ruleId + '` in options.rules'); - } - }); - } - return options; - } /* * Updates the default options and then applies them - * @param {Mixed} options Options object + * @param {Mixed} branding */ setBranding(branding) { - const previous = { - brand: this.brand, - application: this.application - }; if (typeof branding === 'string') { this.application = branding; } @@ -505,7 +432,6 @@ export default class Audit { ) { this.application = branding.application; } - this._constructHelpUrls(previous); } _constructHelpUrls(previous = null) { // TODO: es-modules-version diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index 4a19c8b02..e0c7e5da4 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -1,11 +1,11 @@ import AbstractVirtualNode from './abstract-virtual-node'; -import { isXHTML, validInputTypes } from '../../utils'; +import { isXHTML, validInputTypes, getElementInternals } from '../../utils'; import { isFocusable, getTabbableElements } from '../../../commons/dom'; import cache from '../cache'; let nodeIndex = 0; -class VirtualNode extends AbstractVirtualNode { +export default class VirtualNode extends AbstractVirtualNode { /** * Wrap the real node and provide list of the flattened children * @param {Node} node the node in question @@ -189,6 +189,26 @@ class VirtualNode extends AbstractVirtualNode { } return this._cache.boundingClientRect; } -} -export default VirtualNode; + /** + * Return the element internals for this element and cache the result. + * @return {ElementInternals} + */ + get elementInternals() { + // feature flag to enable internals. uses globalThis.axe as it can be run outside of axe context + // TODO: remove when feature is fully enabled + if (!axe._enableElementInternals) { + return; + } + + if (!this._cache.hasOwnProperty('elementInternals')) { + this._cache.elementInternals = getElementInternals(this.actualNode); + } + + return this._cache.elementInternals; + } + + set elementInternals(value) { + this._cache.elementInternals = value; + } +} diff --git a/lib/core/core.js b/lib/core/core.js index 19dedc20f..8994ad3d5 100644 --- a/lib/core/core.js +++ b/lib/core/core.js @@ -1,5 +1,5 @@ import constants from './constants'; -import log from './log'; +import log, { setLogger } from './log'; import AbstractVirtualNode from './base/virtual-node/abstract-virtual-node'; import SerialVirtualNode from './base/virtual-node/serial-virtual-node'; @@ -10,12 +10,14 @@ import * as imports from './imports'; import cleanup from './public/cleanup'; import configure from './public/configure'; +import externalAPIs from './public/external-apis'; import frameMessenger from './public/frame-messenger'; import getRules from './public/get-rules'; import load from './public/load'; import registerPlugin from './public/plugins'; import { hasReporter, getReporter, addReporter } from './public/reporter'; import reset from './public/reset'; +import resetLocale from './public/reset-locale'; import runRules from './public/run-rules'; import runVirtualRule from './public/run-virtual-rule'; import run from './public/run'; @@ -40,6 +42,9 @@ axe._thisWillBeDeletedDoNotUse = _thisWillBeDeletedDoNotUse; axe.constants = constants; axe.log = log; +// @private, for tests only +axe._setLogger = setLogger; + axe.AbstractVirtualNode = AbstractVirtualNode; axe.SerialVirtualNode = SerialVirtualNode; axe.VirtualNode = VirtualNode; @@ -49,6 +54,7 @@ axe.imports = imports; axe.cleanup = cleanup; axe.configure = configure; +axe.externalAPIs = externalAPIs; axe.frameMessenger = frameMessenger; axe.getRules = getRules; axe._load = load; @@ -58,6 +64,7 @@ axe.hasReporter = hasReporter; axe.getReporter = getReporter; axe.addReporter = addReporter; axe.reset = reset; +axe.resetLocale = resetLocale; axe._runRules = runRules; axe.runVirtualRule = runVirtualRule; axe.run = run; diff --git a/lib/core/log.js b/lib/core/log.js index 3460bb7bd..47f504afd 100644 --- a/lib/core/log.js +++ b/lib/core/log.js @@ -1,13 +1,20 @@ -/*eslint no-console: 0 */ +let logger; /** * Logs a message to the developer console (if it exists and is active). */ -function log() { - if (typeof console === 'object' && console.log) { - // IE does not support console.log.apply - Function.prototype.apply.call(console.log, console, arguments); +export default function log(...args) { + if (logger) { + logger(...args); + } else if (typeof console === 'object' && console.log) { + console.log(...args); } } -export default log; +/** + * Set the logger. Exposed for testing. + * @private + */ +export function setLogger(fn) { + logger = fn; +} diff --git a/lib/core/public/configure.js b/lib/core/public/configure.js index 1cf73d642..587f617fb 100644 --- a/lib/core/public/configure.js +++ b/lib/core/public/configure.js @@ -90,10 +90,13 @@ function configure(spec) { }); } + const previousHelpUrlSettings = { + brand: audit.brand, + application: audit.application, + lang: audit.lang + }; if (typeof spec.branding !== 'undefined') { audit.setBranding(spec.branding); - } else { - audit._constructHelpUrls(); } if (spec.tagExclude) { @@ -105,6 +108,8 @@ function configure(spec) { audit.applyLocale(spec.locale); } + audit._constructHelpUrls(previousHelpUrlSettings); + if (spec.standards) { configureStandards(spec.standards); } diff --git a/lib/core/public/external-apis.js b/lib/core/public/external-apis.js new file mode 100644 index 000000000..ea4668c13 --- /dev/null +++ b/lib/core/public/external-apis.js @@ -0,0 +1,194 @@ +import { shadowSelect, getNodeFromTree, clone, assert } from '../utils'; +import log from '../log'; + +const ELEMENT_INTERNALS_DEFAULT_TIMEOUT = 1000; + +let getElementInternals; +let elementInternalsTimeout; + +export default function externalAPIs({ + elementInternalsTimeout: internalsTimeout, + getElementInternals: getInternals +} = {}) { + if (isNotNullOrUndefined(internalsTimeout)) { + assert( + typeof internalsTimeout === 'number', + 'elementInternalsTimeout must be a number' + ); + elementInternalsTimeout = internalsTimeout; + } + // reset if set to null + else if (internalsTimeout === null) { + elementInternalsTimeout = ELEMENT_INTERNALS_DEFAULT_TIMEOUT; + } + + if (isNotNullOrUndefined(getInternals)) { + assert( + typeof getInternals === 'function', + 'getElementInternals must be a function that returns a Promise' + ); + getElementInternals = getInternals; + } + // reset if set to null + else if (getInternals === null) { + getElementInternals = null; + } +} + +function isNotNullOrUndefined(val) { + return val !== undefined && val !== null; +} + +/** + * Async setTimeout + */ +async function asyncTimeout(ms) { + return new Promise(res => setTimeout(res, ms, 'timeout')); +} + +/** + * load element internals map data provided by the user to elementInternals property on vNodes + */ +async function loadElementInternals(logger = log) { + if (!getElementInternals) { + return; + } + + const promiseValue = await Promise.race([ + asyncTimeout(elementInternalsTimeout), + getElementInternals() + ]); + + assert(promiseValue !== 'timeout', 'Timeout called for elementInternals'); + + const internalsMap = clone(promiseValue); + + // validate the map structure + if (!internalsMap || !Array.isArray(internalsMap)) { + logger('externalAPIs.getElementInternals() did not return an array'); + return; + } + + for (let i = 0; i < internalsMap.length; i++) { + // validate we can destructure the item + if (!internalsMap[i] || typeof internalsMap[i] !== 'object') { + logger(`externalAPIs.getElementInternals()[${i}] is not an object`); + continue; + } + + const { internals, ancestry } = internalsMap[i]; + + // skip missing or malformed properties + if (!internals || typeof internals !== 'object') { + logger( + `externalAPIs.getElementInternals()[${i}].internals is not an object` + ); + continue; + } + + if ( + !ancestry || + !(Array.isArray(ancestry) || typeof ancestry === 'string') + ) { + logger( + `externalAPIs.getElementInternals()[${i}].ancestry is not a string or an array of strings` + ); + continue; + } + + const node = shadowSelect(ancestry); + const vNode = getNodeFromTree(node); + + if (!vNode) { + logger( + `Unable to locate node using selector ${ancestry} from externalAPIs.getElementInternals()[${i}]` + ); + continue; + } + + // convert idref(s) ancestries back to nodes + for (const [key, val] of Object.entries(internals)) { + if (typeof val === 'string') { + continue; + } + + const { type, value } = val; + if (!type) { + logger( + `externalAPIs.getElementInternals()[${i}].internals.${key} is an object but has no "type" property` + ); + } + if (!value) { + logger( + `externalAPIs.getElementInternals()[${i}].internals.${key} is an object but has no "value" property` + ); + } + + if (type === 'HTMLElement') { + setHTMLElement(internals, key, value); + } else if (type === 'NodeList') { + setNodeList(internals, key, value); + } + } + + // set internals directly onto the vNode + vNode.elementInternals = internals; + } +} + +export const external = { + loadElementInternals +}; + +/** + * Find an HTMLElement idref on the page and set it on the internal object or set a getter that will error on access so rules will incomplete. + * @param {Object} internals + * @param {String} key + * @param {String|String[]} value + */ +function setHTMLElement(internals, key, value) { + const node = shadowSelect(value); + if (node) { + internals[key] = node; + } else { + // only throw when we access it so rules can incomplete + Object.defineProperty(internals, key, { + get() { + throw new Error(`Unable to locate node using selector: ${value}`); + } + }); + } +} + +/** + * Find a NodeList idrefs on the page and set it on the internal object or set a getter that will error on access so rules will incomplete. + * @param {Object} internals + * @param {String} key + * @param {String|String[]} value + */ +function setNodeList(internals, key, value) { + const nodes = []; + const errorSelectors = []; + + for (const selector of value) { + const node = shadowSelect(selector); + if (node) { + nodes.push(node); + } else { + errorSelectors.push(selector); + } + } + + if (errorSelectors.length === 0) { + internals[key] = nodes; + } else { + // only throw when we access it so rules can incomplete + Object.defineProperty(internals, key, { + get() { + throw new Error( + `Unable to locate nodes using selectors: ${errorSelectors.join(',')}` + ); + } + }); + } +} diff --git a/lib/core/public/finish-run.js b/lib/core/public/finish-run.js index 31e6417c2..83f3ca8f1 100644 --- a/lib/core/public/finish-run.js +++ b/lib/core/public/finish-run.js @@ -4,7 +4,8 @@ import { publishMetaData, finalizeRuleResult, nodeSerializer, - clone + clone, + normalizeRunOptions } from '../utils'; export default function finishRun(partialResults, options = {}) { @@ -12,7 +13,7 @@ export default function finishRun(partialResults, options = {}) { const { environmentData } = partialResults.find(r => r.environmentData) || {}; // normalize the runOnly option for the output of reporters toolOptions - axe._audit.normalizeOptions(options); + normalizeRunOptions(options); options.reporter = options.reporter ?? axe._audit?.reporter ?? 'v1'; setFrameSpec(partialResults); diff --git a/lib/core/public/get-rules.js b/lib/core/public/get-rules.js index fa23a3b5b..e0870e5a3 100644 --- a/lib/core/public/get-rules.js +++ b/lib/core/public/get-rules.js @@ -5,15 +5,17 @@ */ function getRules(tags) { tags = tags || []; + const { rules, data, tagExclude } = axe._audit; + const matchingRules = !tags.length - ? axe._audit.rules - : axe._audit.rules.filter(item => { + ? rules + : rules.filter(item => { return !!tags.filter(tag => { return item.tags.indexOf(tag) !== -1; }).length; }); - const ruleData = axe._audit.data.rules || {}; + const ruleData = data.rules || {}; return matchingRules.map(matchingRule => { const rd = ruleData[matchingRule.id] || {}; return { @@ -22,7 +24,10 @@ function getRules(tags) { help: rd.help, helpUrl: rd.helpUrl, tags: matchingRule.tags, - actIds: matchingRule.actIds + actIds: matchingRule.actIds, + enabled: + matchingRule.enabled && + !matchingRule.tags.some(tag => tagExclude.includes(tag)) }; }); } diff --git a/lib/core/public/reset-locale.js b/lib/core/public/reset-locale.js new file mode 100644 index 000000000..c82b4ba68 --- /dev/null +++ b/lib/core/public/reset-locale.js @@ -0,0 +1,16 @@ +/** + * Restore the default locale that was active before any + * `axe.configure({ locale })` call. No-op if no non-default + * locale has ever been applied. Does not affect any other + * configuration; for a full reset, use `axe.reset()`. + */ +function resetLocale() { + const audit = axe._audit; + + if (!audit) { + throw new Error('No audit configured'); + } + audit._resetLocale(); +} + +export default resetLocale; diff --git a/lib/core/public/run-partial.js b/lib/core/public/run-partial.js index 50ab6465d..7c2e0d3e5 100644 --- a/lib/core/public/run-partial.js +++ b/lib/core/public/run-partial.js @@ -4,7 +4,8 @@ import { nodeSerializer, getSelectorData, assert, - getEnvironmentData + getEnvironmentData, + performanceTimer } from '../utils'; import normalizeRunParams from './run/normalize-run-params'; @@ -27,9 +28,17 @@ export default function runPartial(...args) { return ( new Promise((res, rej) => { + if (options.performanceTimer) { + performanceTimer.auditStart(); + } + axe._audit.run(contextObj, options, res, rej); }) .then(results => { + if (options.performanceTimer) { + performanceTimer.auditEnd(); + } + results = nodeSerializer.mapRawResults(results); const frames = contextObj.frames.map(({ node }) => { return nodeSerializer.toSpec(node); diff --git a/lib/core/utils/get-element-internals.js b/lib/core/utils/get-element-internals.js new file mode 100644 index 000000000..fcfbd414e --- /dev/null +++ b/lib/core/utils/get-element-internals.js @@ -0,0 +1,73 @@ +import isValidCustomElementName from './is-valid-custom-element-name'; + +/** + * community protocols that axe-core supports to find element internals: + * - globalThis._elementInternals.get(node) + * - node._internals (recommended) + * - node.internals + * - node.internals_ + * - node[Symbol('internals')] + * - node[Symbol('privateInternals')] + **/ +const propNames = ['_internals', 'internals', 'internals_']; +const symbolNames = ['internals', 'privateInternals']; + +/** + * Get the ElementInternals object for a custom element using a community protocol. + * @example + * // use the global map + * const internals = node.attachInternals() + * globalThis._elementInternals ??= new WeakMap(); + * globalThis._elementInternals.set(node, internals); + * @example + * // set property + * const internals = node.attachInternals() + * node._internals = internals; + * @param {HTMLElement} node + * @return {ElementInternals|undefined} + */ +export default function getElementInternals(node) { + // internals can only be attached to custom-elements + // trying to do otherwise results in an error "Cannot attach ElementInternals to a customized built-in or non-custom element" + if (!isValidCustomElementName(node.nodeName.toLowerCase())) { + return; + } + + // support finding internals from an optional global map. we assume that if a node is set here the value will be an ElementInternals object + const mapInternals = globalThis._elementInternals?.get(node); + if (mapInternals) { + return mapInternals; + } + + // ie11 guard + if (!('ElementInternals' in window)) { + return; + } + + for (const propName of propNames) { + if (Object.getOwnPropertyDescriptor(node, propName)?.get) { + continue; + } + if (node[propName] instanceof window.ElementInternals) { + return node[propName]; + } + } + + const ownSymbols = Object.getOwnPropertySymbols(node); + if (!ownSymbols.length) { + return; + } + + for (const symbolName of symbolNames) { + const symbol = ownSymbols.find(s => s.description === symbolName); + + if (symbol) { + if (Object.getOwnPropertyDescriptor(node, symbol)?.get) { + continue; + } + if (node[symbol] instanceof window.ElementInternals) { + return node[symbol]; + } + } + } +} diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index 4cf4aade9..cb0bf3d74 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -42,6 +42,7 @@ export { default as getStandards } from './get-standards'; export { default as getStyleSheetFactory } from './get-stylesheet-factory'; export { default as getXpath } from './get-xpath'; export { default as getAncestry } from './get-ancestry'; +export { default as getElementInternals } from './get-element-internals'; export { default as getElementSource } from './get-element-source'; export { default as injectStyle } from './inject-style'; export { default as isArrayLike } from './is-array-like'; @@ -52,6 +53,7 @@ export { isLabelledShadowDomSelector, isLabelledFramesSelector } from './is-context'; +export { default as isValidCustomElementName } from './is-valid-custom-element-name'; export { default as isHidden } from './is-hidden'; export { default as isHtmlElement } from './is-html-element'; export { default as isNodeInContext } from './is-node-in-context'; @@ -101,3 +103,4 @@ export { default as validInputTypes } from './valid-input-type'; export { default as isValidLang, validLangs } from './valid-langs'; export { default as detectModalStack } from './detect-modal-stack'; export { default as getOverlayFromStack } from './get-overlay-from-stack'; +export { default as normalizeRunOptions } from './normalize-run-options'; diff --git a/lib/core/utils/is-html-element.js b/lib/core/utils/is-html-element.js index a2f3f112f..678525729 100644 --- a/lib/core/utils/is-html-element.js +++ b/lib/core/utils/is-html-element.js @@ -7,7 +7,7 @@ import standards from '../../standards'; * @param {HTMLElement|VirtualNode} node node to check if valid * @return {Boolean} true/ false */ -function isHtmlElement(node) { +export default function isHtmlElement(node) { const nodeName = node.props?.nodeName ?? node.nodeName.toLowerCase(); if (node.namespaceURI === 'http://www.w3.org/2000/svg') { @@ -16,5 +16,3 @@ function isHtmlElement(node) { return !!standards.htmlElms[nodeName]; } - -export default isHtmlElement; diff --git a/lib/core/utils/is-shadow-root.js b/lib/core/utils/is-shadow-root.js index ab8aedd00..0bd6a9fd3 100644 --- a/lib/core/utils/is-shadow-root.js +++ b/lib/core/utils/is-shadow-root.js @@ -1,3 +1,5 @@ +import isValidCustomElementName from './is-valid-custom-element-name'; + const possibleShadowRoots = [ 'article', 'aside', @@ -18,6 +20,7 @@ const possibleShadowRoots = [ 'section', 'span' ]; + /** * Test a node to see if it has a spec-conforming shadow root * @@ -29,7 +32,7 @@ function isShadowRoot(node) { const nodeName = node.nodeName.toLowerCase(); if ( possibleShadowRoots.includes(nodeName) || - /^[a-z][a-z0-9_.-]*-[a-z0-9_.-]*$/.test(nodeName) + isValidCustomElementName(nodeName) ) { return true; } diff --git a/lib/core/utils/is-valid-custom-element-name.js b/lib/core/utils/is-valid-custom-element-name.js new file mode 100644 index 000000000..63e15ea5e --- /dev/null +++ b/lib/core/utils/is-valid-custom-element-name.js @@ -0,0 +1,37 @@ +// reserved names that match the custom element regex but are not valid custom elements +// @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +const reservedNames = [ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph' +]; + +// valid localname regex +// @see https://dom.spec.whatwg.org/#valid-element-local-name +const validLocalNameRegex = + /^(?:[A-Za-z][^\0\t\n\f\r\u0020/>]*|[:_\u0080-\u{10FFFF}][A-Za-z0-9-.:_\u0080-\u{10FFFF}]*)$/u; +const alphaLowerRegex = /[a-z]/; +const alphaUpperRegex = /[A-Z]/; + +/** + * Verifies that the passed in string is a valid custom element name + * @method isValidCustomElementName + * @memberof axe.utils + * @param {string} nodeName nodeName to validate + * @return {Boolean} true / false + */ +export default function isValidCustomElementName(nodeName) { + // @see https://html.spec.whatwg.org/#valid-custom-element-name + return ( + !reservedNames.includes(nodeName) && + validLocalNameRegex.test(nodeName) && + alphaLowerRegex.test(nodeName[0]) && + !alphaUpperRegex.test(nodeName) && + nodeName.includes('-') + ); +} diff --git a/lib/core/utils/normalize-run-options.js b/lib/core/utils/normalize-run-options.js new file mode 100644 index 000000000..1c65d518c --- /dev/null +++ b/lib/core/utils/normalize-run-options.js @@ -0,0 +1,79 @@ +/** + * Ensure all rules that are expected to run exist + * @throws {Error} If any tag or rule specified in options is unknown + * @param {Object} options Options object + * @return {Object} Validated options object + */ +export default function normalizeRunOptions(options = {}) { + const tags = []; + const ruleIds = []; + axe._audit.rules.forEach(rule => { + ruleIds.push(rule.id); + rule.tags.forEach(tag => { + if (!tags.includes(tag)) { + tags.push(tag); + } + }); + }); + // Validate runOnly + if (['object', 'string'].includes(typeof options.runOnly)) { + if (typeof options.runOnly === 'string') { + options.runOnly = [options.runOnly]; + } + if (Array.isArray(options.runOnly)) { + const hasTag = options.runOnly.find(value => tags.includes(value)); + const hasRule = options.runOnly.find(value => ruleIds.includes(value)); + if (hasTag && hasRule) { + throw new Error('runOnly cannot be both rules and tags'); + } + if (hasRule) { + options.runOnly = { + type: 'rule', + values: options.runOnly + }; + } else { + options.runOnly = { + type: 'tag', + values: options.runOnly + }; + } + } + const only = options.runOnly; + if (only.value && !only.values) { + only.values = only.value; + delete only.value; + } + if (!Array.isArray(only.values) || only.values.length === 0) { + throw new Error('runOnly.values must be a non-empty array'); + } + // Check if every value in options.runOnly is a known rule ID + if (['rule', 'rules'].includes(only.type)) { + only.type = 'rule'; + only.values.forEach(ruleId => { + if (!ruleIds.includes(ruleId)) { + throw new Error('unknown rule `' + ruleId + '` in options.runOnly'); + } + }); + // Validate 'tags' (e.g. anything not 'rule') + } else if (['tag', 'tags', undefined].includes(only.type)) { + only.type = 'tag'; + + const unmatchedTags = only.values.filter( + tag => !tags.includes(tag) && !/wcag2[1-3]a{1,3}/.test(tag) + ); + if (unmatchedTags.length !== 0) { + axe.log('Could not find tags `' + unmatchedTags.join('`, `') + '`'); + } + } else { + throw new Error(`Unknown runOnly type '${only.type}'`); + } + } + if (typeof options.rules === 'object') { + Object.keys(options.rules).forEach(ruleId => { + if (!ruleIds.includes(ruleId)) { + throw new Error('unknown rule `' + ruleId + '` in options.rules'); + } + }); + } + return options; +} diff --git a/lib/core/utils/parse-crossorigin-stylesheet.js b/lib/core/utils/parse-crossorigin-stylesheet.js index 3ec44fa52..825ba31a9 100644 --- a/lib/core/utils/parse-crossorigin-stylesheet.js +++ b/lib/core/utils/parse-crossorigin-stylesheet.js @@ -20,6 +20,10 @@ function parseCrossOriginStylesheet( importedUrls, isCrossOrigin ) { + if (url === null || url === undefined) { + return Promise.resolve(); + } + /** * Add `url` to `importedUrls` */ diff --git a/lib/gather-internals/main.js b/lib/gather-internals/main.js new file mode 100644 index 000000000..cc9063449 --- /dev/null +++ b/lib/gather-internals/main.js @@ -0,0 +1,4 @@ +import { walkTree, elementInternalsMap } from './walk-tree'; + +walkTree(); +module.exports = elementInternalsMap; diff --git a/lib/gather-internals/walk-tree.js b/lib/gather-internals/walk-tree.js new file mode 100644 index 000000000..c1cf0fe12 --- /dev/null +++ b/lib/gather-internals/walk-tree.js @@ -0,0 +1,80 @@ +// importing from the index file results in esbuild processing all utils files and not tree shaking them correctly +import getElementInternals from '../core/utils/get-element-internals'; +import getAncestry from '../core/utils/get-ancestry'; +import isShadowRoot from '../core/utils/is-shadow-root'; + +export const elementInternalsMap = []; +const ariaPropRegex = /^aria[A-Z]/; +const propsToCapture = ['role', 'labels', 'form']; + +export function walkTree(tree = document.body) { + const treeWalker = document.createTreeWalker( + tree, + globalThis.NodeFilter.SHOW_ELEMENT, + null, + false + ); + + let node = treeWalker.currentNode; + while (node) { + const elementInternals = getElementInternals(node); + if (elementInternals) { + const ancestry = getAncestry(node); + const internals = {}; + + // can't spread internals so have to loop over the props instead + for (const prop in elementInternals) { + if (!ariaPropRegex.test(prop) && !propsToCapture.includes(prop)) { + continue; + } + + // trying to read internals.form will throw if the custom element isn't a form associated element :( + try { + const value = elementInternals[prop]; + if (value === null) { + continue; + } + + // convert idref to node ancestry + // aria props can use unattached nodes but those won't be used by the browser to calculate the prop value + if (value instanceof globalThis.HTMLElement) { + internals[prop] = value.isConnected + ? { type: 'HTMLElement', value: getAncestry(value) } + : undefined; + } + // convert idrefs to node ancestry. `labels` is a NodeList while idrefs aria props are arrays + else if ( + Array.isArray(value) || + value instanceof globalThis.NodeList + ) { + const array = Array.from(value).filter(n => n.isConnected); + internals[prop] = array.length + ? { type: 'NodeList', value: array.map(n => getAncestry(n)) } + : undefined; + } else if (typeof value === 'string') { + internals[prop] = value; + } + } catch { + // do nothing + } + } + + elementInternalsMap.push({ + ancestry, + internals + }); + } + + if (isShadowRoot(node)) { + walkTree(node.shadowRoot); + } + + node = treeWalker.nextNode(); + } +} + +// exposed for testing +export function _reset() { + elementInternalsMap.length = 0; +} +export { getAncestry }; diff --git a/lib/rules/aria-tab-name.json b/lib/rules/aria-tab-name.json new file mode 100644 index 000000000..97a959a79 --- /dev/null +++ b/lib/rules/aria-tab-name.json @@ -0,0 +1,30 @@ +{ + "id": "aria-tab-name", + "impact": "serious", + "selector": "[role=\"tab\"]", + "matches": "no-naming-method-matches", + "tags": [ + "cat.aria", + "wcag2a", + "wcag412", + "TTv5", + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT", + "RGAAv4", + "RGAA-7.1.1" + ], + "metadata": { + "description": "Ensure every ARIA tab node has an accessible name", + "help": "ARIA tab nodes must have an accessible name" + }, + "all": [], + "any": [ + "has-visible-text", + "aria-label", + "aria-labelledby", + "non-empty-title" + ], + "none": [] +} diff --git a/lib/rules/landmark-complementary-is-top-level.json b/lib/rules/landmark-complementary-is-top-level.json index 638e392f1..60f7e319e 100644 --- a/lib/rules/landmark-complementary-is-top-level.json +++ b/lib/rules/landmark-complementary-is-top-level.json @@ -2,7 +2,8 @@ "id": "landmark-complementary-is-top-level", "impact": "moderate", "selector": "aside:not([role]), [role=complementary]", - "tags": ["cat.semantics", "best-practice"], + "tags": ["cat.semantics", "best-practice", "deprecated"], + "enabled": false, "metadata": { "description": "Ensure the complementary landmark or aside is at top level", "help": "Aside should not be contained in another landmark" diff --git a/locales/_template.json b/locales/_template.json index 7e3304e21..f34a1cf61 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -81,6 +81,10 @@ "description": "Ensure all elements with a role attribute use a valid value", "help": "ARIA roles used must conform to valid values" }, + "aria-tab-name": { + "description": "Ensure every ARIA tab node has an accessible name", + "help": "ARIA tab nodes must have an accessible name" + }, "aria-text": { "description": "Ensure role=\"text\" is used on elements with no focusable descendants", "help": "\"role=text\" should have no focusable descendants" @@ -460,6 +464,7 @@ "pass": "ARIA attribute is allowed", "fail": { "checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state", + "radio": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real radio state", "rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}", "rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}" } diff --git a/locales/ja.json b/locales/ja.json index 19caad999..e07bf095c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -453,6 +453,7 @@ "pass": "ARIA 属性が許可されています", "fail": { "checkbox": "aria-checkedを削除するか\"${data.checkState}\"を設定して、チェックボックスの実際の状態と合うようにしましょう", + "radio": "aria-checkedを削除するか\"${data.checkState}\"を設定して、ラジオボタンの実際の状態と合うようにしましょう", "rowSingular": "treegridの行でサポートされている属性ですが、${data.ownerRole}: ${data.invalidAttrs}の場合はこの限りではありません", "rowPlural": "treegridの行でサポートされている属性ですが、${data.ownerRole}: ${data.invalidAttrs}の場合はこの限りではありません" } diff --git a/locales/no_NB.json b/locales/nb.json similarity index 99% rename from locales/no_NB.json rename to locales/nb.json index 4dbf8dbe9..e5948942a 100644 --- a/locales/no_NB.json +++ b/locales/nb.json @@ -1,5 +1,5 @@ { - "lang": "nb-no", + "lang": "nb", "rules": { "accesskeys": { "description": "", diff --git a/locales/pt_BR.json b/locales/pt_BR.json index 404d5ab29..69e1d5c9f 100644 --- a/locales/pt_BR.json +++ b/locales/pt_BR.json @@ -1,5 +1,5 @@ { - "lang": "pt_BR", + "lang": "pt-BR", "rules": { "accesskeys": { "description": "Certifique-se de que cada valor do atributo 'acesskey' é único", diff --git a/locales/pt_PT.json b/locales/pt_PT.json index 47ee235c2..c013211cc 100644 --- a/locales/pt_PT.json +++ b/locales/pt_PT.json @@ -1,5 +1,5 @@ { - "lang": "pt_PT", + "lang": "pt-PT", "rules": { "accesskeys": { "description": "Garantir que cada valor do atributo 'accesskey' seja único", diff --git a/locales/zh_CN.json b/locales/zh_CN.json index ef5e6876d..961e05010 100644 --- a/locales/zh_CN.json +++ b/locales/zh_CN.json @@ -1,5 +1,5 @@ { - "lang": "zh_CN", + "lang": "zh-CN", "rules": { "accesskeys": { "description": "确保每个 accesskey 属性值都是唯一的", diff --git a/locales/zh_TW.json b/locales/zh_TW.json index 37a9d3262..f62a51597 100644 --- a/locales/zh_TW.json +++ b/locales/zh_TW.json @@ -1,5 +1,5 @@ { - "lang": "zh_TW", + "lang": "zh-TW", "rules": { "accesskeys": { "description": "確保每個 accesskey 屬性值都是唯一的", diff --git a/sri-history.json b/sri-history.json index c9cab782f..2c1505a99 100644 --- a/sri-history.json +++ b/sri-history.json @@ -414,5 +414,13 @@ "4.11.4": { "axe.js": "sha256-MFYbxYKg+pR6osY47p1hwmvMOb7f5kMTHtPU7APqp/A=", "axe.min.js": "sha256-+4OkN42Xjst9La5Io6N3ioTJcaxpLzr9RdcU+6icDw0=" + }, + "4.12.0": { + "axe.js": "sha256-oNBNtCwIenGwI5OepWKyxcy5/OyV+O6pYjPmYhZrVvU=", + "axe.min.js": "sha256-oK/ECKvsvgb/dnUDPn6ixARmGBYhZ6qkzhAwsQlj3s4=" + }, + "4.12.1": { + "axe.js": "sha256-FRl8Y/9oP3b35USiZrI5x7sAP6jiyQTDJDTQ6f9ldd4=", + "axe.min.js": "sha256-ZqiqqVqLBEp/10pUNYc78E/2WhynVWfJIbdQl0IIWhQ=" } } diff --git a/test/checks/aria/aria-allowed-attr-elm.js b/test/checks/aria/aria-allowed-attr-elm.js index 2d90568e2..e51ad4098 100644 --- a/test/checks/aria/aria-allowed-attr-elm.js +++ b/test/checks/aria/aria-allowed-attr-elm.js @@ -1,6 +1,4 @@ describe('aria-allowed-attr-elm', () => { - 'use strict'; - const queryFixture = axe.testUtils.queryFixture; const checkContext = axe.testUtils.MockCheckContext(); diff --git a/test/checks/aria/aria-allowed-attr.js b/test/checks/aria/aria-allowed-attr.js index b25b9df4a..224240766 100644 --- a/test/checks/aria/aria-allowed-attr.js +++ b/test/checks/aria/aria-allowed-attr.js @@ -1,6 +1,4 @@ describe('aria-allowed-attr', () => { - 'use strict'; - const queryFixture = axe.testUtils.queryFixture; const checkContext = axe.testUtils.MockCheckContext(); diff --git a/test/checks/aria/aria-allowed-role.js b/test/checks/aria/aria-allowed-role.js index 7c062b89a..43090a546 100644 --- a/test/checks/aria/aria-allowed-role.js +++ b/test/checks/aria/aria-allowed-role.js @@ -1,36 +1,36 @@ -describe('aria-allowed-role', function () { - 'use strict'; +describe('aria-allowed-role', () => { + const html = axe.testUtils.html; - var queryFixture = axe.testUtils.queryFixture; - var checkContext = axe.testUtils.MockCheckContext(); + const queryFixture = axe.testUtils.queryFixture; + const checkContext = axe.testUtils.MockCheckContext(); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('returns true if given element is an ignoredTag in options', function () { - var vNode = queryFixture( + it('returns true if given element is an ignoredTag in options', () => { + const vNode = queryFixture( '' ); - var options = { + const options = { ignoredTags: ['article'] }; - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, options, vNode); - var expected = true; + const expected = true; assert.equal(actual, expected); assert.isNull(checkContext._data, null); }); - it('returns false with implicit role of row for TR when allowImplicit is set to false via options', function () { - var vNode = queryFixture( + it('returns false with implicit role of row for TR when allowImplicit is set to false via options', () => { + const vNode = queryFixture( '
' ); - var options = { + const options = { allowImplicit: false }; - var outcome = axe.testUtils + const outcome = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, options, vNode); @@ -38,32 +38,40 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['row']); }); - it('returns undefined (needs review) when element is hidden and has unallowed role', function () { - var vNode = queryFixture( - '' - ); - var actual = axe.testUtils + it('returns undefined (needs review) when element is hidden and has unallowed role', () => { + const vNode = queryFixture(html` + + `); + const actual = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, null, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) when element is with in hidden parent and has unallowed role', function () { - var vNode = queryFixture( - '
' + - '' + - '
' - ); - var actual = axe.testUtils + it('returns undefined (needs review) when element is with in hidden parent and has unallowed role', () => { + const vNode = queryFixture(html` +
+ +
+ `); + const actual = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, null, vNode); assert.isUndefined(actual); }); - it('returns true when BUTTON has type menu and role as menuitem', function () { - var vNode = queryFixture( + it('returns true when BUTTON has type menu and role as menuitem', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -73,8 +81,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when img has no alt and role="presentation"', function () { - var vNode = queryFixture(''); + it('returns true when img has no alt and role="presentation"', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -83,8 +91,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, null); }); - it('returns true when img has no alt and role="none"', function () { - var vNode = queryFixture(''); + it('returns true when img has no alt and role="none"', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -93,8 +101,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, null); }); - it('returns true when img has empty alt and role="presentation"', function () { - var vNode = queryFixture(''); + it('returns true when img has empty alt and role="presentation"', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -103,8 +111,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, null); }); - it('returns true when img has empty alt and role="none"', function () { - var vNode = queryFixture(''); + it('returns true when img has empty alt and role="none"', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -113,8 +121,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, null); }); - it('returns false when img has alt and role="presentation"', function () { - var vNode = queryFixture( + it('returns false when img has alt and role="presentation"', () => { + const vNode = queryFixture( 'not empty' ); assert.isFalse( @@ -125,8 +133,10 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['presentation']); }); - it('returns false when img has alt and role="none"', function () { - var vNode = queryFixture('not empty'); + it('returns false when img has alt and role="none"', () => { + const vNode = queryFixture( + 'not empty' + ); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -135,8 +145,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['none']); }); - it('returns true when img has aria-label and a valid role, role="button"', function () { - var vNode = queryFixture( + it('returns true when img has aria-label and a valid role, role="button"', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -147,8 +157,8 @@ describe('aria-allowed-role', function () { assert.isNull(checkContext._data, null); }); - it('returns false when img has aria-label and a invalid role, role="alert"', function () { - var vNode = queryFixture( + it('returns false when img has aria-label and a invalid role, role="alert"', () => { + const vNode = queryFixture( '' ); assert.isFalse( @@ -159,11 +169,11 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['alert']); }); - it('returns true when img has aria-labelledby and a valid role, role="menuitem"', function () { - var vNode = queryFixture( - '
hello world
' + - '' - ); + it('returns true when img has aria-labelledby and a valid role, role="menuitem"', () => { + const vNode = queryFixture(html` +
hello world
+ + `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -172,11 +182,11 @@ describe('aria-allowed-role', function () { assert.isNull(checkContext._data, null); }); - it('returns false when img has aria-labelledby and a invalid role, role="rowgroup"', function () { - var vNode = queryFixture( - '
hello world
' + - '' - ); + it('returns false when img has aria-labelledby and a invalid role, role="rowgroup"', () => { + const vNode = queryFixture(html` +
hello world
+ + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -185,11 +195,11 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['rowgroup']); }); - it('returns true when img has title and a valid role, role="link"', function () { - var vNode = queryFixture( - '
hello world
' + - '' - ); + it('returns true when img has title and a valid role, role="link"', () => { + const vNode = queryFixture(html` +
hello world
+ + `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -198,11 +208,11 @@ describe('aria-allowed-role', function () { assert.isNull(checkContext._data, null); }); - it('returns false when img has title and a invalid role, role="radiogroup"', function () { - var vNode = queryFixture( - '
hello world
' + - '' - ); + it('returns false when img has title and a invalid role, role="radiogroup"', () => { + const vNode = queryFixture(html` +
hello world
+ + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -211,8 +221,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['radiogroup']); }); - it('returns true when input of type image and no role', function () { - var vNode = queryFixture(''); + it('returns true when input of type image and no role', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -221,8 +231,8 @@ describe('aria-allowed-role', function () { assert.isNull(checkContext._data, null); }); - it('returns true when INPUT type is checkbox and has aria-pressed attribute', function () { - var vNode = queryFixture( + it('returns true when INPUT type is checkbox and has aria-pressed attribute', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -232,8 +242,10 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is text with role combobox', function () { - var vNode = queryFixture(''); + it('returns true when INPUT type is text with role combobox', () => { + const vNode = queryFixture( + '' + ); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -241,8 +253,10 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is tel with role combobox', function () { - var vNode = queryFixture(''); + it('returns true when INPUT type is tel with role combobox', () => { + const vNode = queryFixture( + '' + ); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -250,8 +264,10 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is url with role combobox', function () { - var vNode = queryFixture(''); + it('returns true when INPUT type is url with role combobox', () => { + const vNode = queryFixture( + '' + ); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -259,8 +275,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is search with role combobox', function () { - var vNode = queryFixture( + it('returns true when INPUT type is search with role combobox', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -270,8 +286,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is email with role combobox', function () { - var vNode = queryFixture( + it('returns true when INPUT type is email with role combobox', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -281,8 +297,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is text with role spinbutton', function () { - var vNode = queryFixture( + it('returns true when INPUT type is text with role spinbutton', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -292,8 +308,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is number with role spinbutton', function () { - var vNode = queryFixture( + it('returns true when INPUT type is number with role spinbutton', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -303,8 +319,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is tel with role spinbutton', function () { - var vNode = queryFixture( + it('returns true when INPUT type is tel with role spinbutton', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -314,8 +330,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true when INPUT type is text with role searchbox', function () { - var vNode = queryFixture( + it('returns true when INPUT type is text with role searchbox', () => { + const vNode = queryFixture( '' ); assert.isTrue( @@ -325,8 +341,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns false when a role is set on an element that does not allow any role', function () { - var vNode = queryFixture('
'); + it('returns false when a role is set on an element that does not allow any role', () => { + const vNode = queryFixture('
'); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -335,8 +351,8 @@ describe('aria-allowed-role', function () { assert.deepEqual(checkContext._data, ['link']); }); - it('returns true when a role is set on an element that can have any role', function () { - var vNode = queryFixture('
'); + it('returns true when a role is set on an element that can have any role', () => { + const vNode = queryFixture('
'); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -344,8 +360,8 @@ describe('aria-allowed-role', function () { ); }); - it('returns true an without a href to have any role', function () { - var vNode = queryFixture(''); + it('returns true an without a href to have any role', () => { + const vNode = queryFixture(''); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -353,16 +369,18 @@ describe('aria-allowed-role', function () { ); }); - it('returns true with a empty href to have any valid role', function () { - var vNode = queryFixture(''); - var actual = axe.testUtils + it('returns true with a empty href to have any valid role', () => { + const vNode = queryFixture(''); + const actual = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, null, vNode); assert.isTrue(actual); }); - it('returns true with a non-empty alt', function () { - var vNode = queryFixture('some text'); + it('returns true with a non-empty alt', () => { + const vNode = queryFixture( + 'some text' + ); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -370,8 +388,8 @@ describe('aria-allowed-role', function () { ); }); - it('should allow '); + it('should allow '); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -380,17 +398,17 @@ describe('aria-allowed-role', function () { assert.isNull(checkContext._data, null); }); - it('returns true custom element with a role of navigation', function () { - var vNode = queryFixture(''); - var actual = axe.testUtils + it('returns true custom element with a role of navigation', () => { + const vNode = queryFixture(''); + const actual = axe.testUtils .getCheckEvaluate('aria-allowed-role') .call(checkContext, null, null, vNode); assert.isTrue(actual); assert.isNull(checkContext._data, null); }); - it('returns false if a dpub role’s type is not the element’s implicit role', function () { - var vNode = queryFixture('
'); + it('returns false if a dpub role’s type is not the element’s implicit role', () => { + const vNode = queryFixture('
'); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-allowed-role') @@ -398,8 +416,10 @@ describe('aria-allowed-role', function () { ); }); - it('returns true if a dpub role’s type is the element’s implicit role', function () { - var vNode = queryFixture(''); + it('returns true if a dpub role’s type is the element’s implicit role', () => { + const vNode = queryFixture( + '' + ); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-role') diff --git a/test/checks/aria/aria-busy.js b/test/checks/aria/aria-busy.js index 166faec83..aad9566c7 100644 --- a/test/checks/aria/aria-busy.js +++ b/test/checks/aria/aria-busy.js @@ -1,28 +1,26 @@ -describe('aria-busy', function () { - 'use strict'; +describe('aria-busy', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const checkEvaluate = axe.testUtils.getCheckEvaluate('aria-busy'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var checkEvaluate = axe.testUtils.getCheckEvaluate('aria-busy'); - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return false if no aria-busy tag on element', function () { - var params = checkSetup('
'); + it('should return false if no aria-busy tag on element', () => { + const params = checkSetup('
'); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should return false if aria-busy is set to false', function () { - var params = checkSetup( + it('should return false if aria-busy is set to false', () => { + const params = checkSetup( '
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should return true if aria-busy is set to true', function () { - var params = checkSetup( + it('should return true if aria-busy is set to true', () => { + const params = checkSetup( '
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); diff --git a/test/checks/aria/aria-conditional-attr.js b/test/checks/aria/aria-conditional-attr.js index 2a3f6dc4e..68be521c2 100644 --- a/test/checks/aria/aria-conditional-attr.js +++ b/test/checks/aria/aria-conditional-attr.js @@ -1,4 +1,5 @@ describe('aria-conditional-attr', () => { + const html = axe.testUtils.html; const { checkSetup, getCheckEvaluate } = axe.testUtils; const checkContext = axe.testUtils.MockCheckContext(); const ariaConditionalCheck = getCheckEvaluate('aria-conditional-attr'); @@ -10,7 +11,7 @@ describe('aria-conditional-attr', () => { it('is true for non-conditional roles', () => { const roles = ['main', 'button', 'radiogroup', 'tree', 'none']; for (const role of roles) { - const params = checkSetup(`
`); + const params = checkSetup(html`
`); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); } }); @@ -25,8 +26,13 @@ describe('aria-conditional-attr', () => { it('returns true when valid ARIA props are used on table', () => { const params = checkSetup( - `
-
+ html`
+
` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); @@ -35,7 +41,7 @@ describe('aria-conditional-attr', () => { it('returns true when treegrid row props are used on a treegrid row', () => { const params = checkSetup( - `
+ html`
` ); @@ -45,7 +51,7 @@ describe('aria-conditional-attr', () => { it('returns true when the row is not in a table, grid, or treegrid', () => { const params = checkSetup( - `
` + html`
` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); assert.isNull(checkContext._data); @@ -54,7 +60,7 @@ describe('aria-conditional-attr', () => { it('returns false when treegrid row props are used on an ARIA table row', () => { for (const prop of treeGridRowProps) { const params = checkSetup( - `
+ html`
` ); @@ -70,7 +76,7 @@ describe('aria-conditional-attr', () => { it('returns false when treegrid row props are used on a grid row', () => { for (const prop of treeGridRowProps) { const params = checkSetup( - `
+ html`
` ); @@ -86,7 +92,11 @@ describe('aria-conditional-attr', () => { it('returns false when treegrid row props are used on a native table row', () => { for (const prop of treeGridRowProps) { const params = checkSetup( - `
` + html` + + + +
` ); assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); assert.deepEqual(checkContext._data, { @@ -99,8 +109,13 @@ describe('aria-conditional-attr', () => { it('sets messageKey to rowPlural with multiple bad attributes', () => { const params = checkSetup( - `
- + html`
+
` ); assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); @@ -111,11 +126,15 @@ describe('aria-conditional-attr', () => { }); }); - describe('options.invalidTableRowAttrs', function () { + describe('options.invalidTableRowAttrs', () => { it('returns false for removed attribute', () => { const options = { invalidTableRowAttrs: ['aria-rowindex'] }; const params = checkSetup( - `
`, + html` + + + +
`, options ); assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); @@ -124,8 +143,15 @@ describe('aria-conditional-attr', () => { it('returns true for additional attribute', () => { const options = { invalidTableRowAttrs: ['aria-level'] }; const params = checkSetup( - ` - + html`
+ + +
`, options ); @@ -137,7 +163,7 @@ describe('aria-conditional-attr', () => { describe('ariaConditionalCheckboxAttr', () => { it('returns true for non-native checkbox', () => { const params = checkSetup( - `` + html`` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); assert.isNull(checkContext._data); @@ -146,7 +172,7 @@ describe('aria-conditional-attr', () => { it('returns true for checkbox without aria-checked value', () => { for (const prop of ['', 'aria-checked', 'aria-checked=""']) { const params = checkSetup( - `` + html`` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); assert.isNull(checkContext._data); @@ -157,7 +183,11 @@ describe('aria-conditional-attr', () => { const fixture = document.querySelector('#fixture'); it('returns true for aria-checked="true" on a [checked] checkbox', () => { - fixture.innerHTML = ``; + fixture.innerHTML = html``; const root = axe.setup(fixture); const vNode = axe.utils.querySelectorAll(root, 'input')[0]; @@ -168,7 +198,7 @@ describe('aria-conditional-attr', () => { }); it('returns true for aria-checked="true" on a clicked checkbox', () => { - fixture.innerHTML = ``; + fixture.innerHTML = html``; fixture.firstChild.click(); // set checked state const root = axe.setup(fixture); const vNode = axe.utils.querySelectorAll(root, 'input')[0]; @@ -182,7 +212,12 @@ describe('aria-conditional-attr', () => { it('returns false for other aria-checked values', () => { for (const prop of [' ', 'false', 'mixed', 'incorrect', ' true ']) { const params = checkSetup( - `` + html`` ); assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); assert.deepEqual(checkContext._data, { @@ -196,7 +231,7 @@ describe('aria-conditional-attr', () => { describe('unchecked state', () => { it('returns true for aria-checked="false"', () => { const params = checkSetup( - `` + html`` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); assert.isNull(checkContext._data); @@ -205,7 +240,7 @@ describe('aria-conditional-attr', () => { it('returns true for aria-checked with an invalid value', () => { for (const prop of [' ', 'invalid', 'FALSE', 'nope']) { const params = checkSetup( - `` + html`` ); assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); assert.isNull(checkContext._data); @@ -215,7 +250,7 @@ describe('aria-conditional-attr', () => { it('returns false for other aria-checked values', () => { for (const prop of ['true', 'TRUE', 'mixed', 'MiXeD']) { const params = checkSetup( - `` + html`` ); assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); assert.deepEqual(checkContext._data, { @@ -227,9 +262,9 @@ describe('aria-conditional-attr', () => { }); describe('indeterminate state', () => { - function asIndeterminateVirtualNode(html) { + function asIndeterminateVirtualNode(markup) { const fixture = document.querySelector('#fixture'); - fixture.innerHTML = html; + fixture.innerHTML = markup; fixture.querySelector('input').indeterminate = true; const root = axe.setup(fixture); return axe.utils.querySelectorAll(root, 'input')[0]; @@ -237,7 +272,7 @@ describe('aria-conditional-attr', () => { it('returns true for aria-checked="mixed"', () => { const vNode = asIndeterminateVirtualNode( - `` + html`` ); assert.isTrue( ariaConditionalCheck.call(checkContext, null, null, vNode) @@ -247,7 +282,7 @@ describe('aria-conditional-attr', () => { it('returns false for other aria-checked values', () => { for (const prop of ['true', 'TRUE', 'false', 'invalid']) { const vNode = asIndeterminateVirtualNode( - `` + html`` ); assert.isFalse( ariaConditionalCheck.call(checkContext, null, null, vNode) @@ -261,4 +296,95 @@ describe('aria-conditional-attr', () => { }); }); }); + + describe('ariaConditionalRadioAttr', () => { + it('returns true for non-native radio', () => { + const params = checkSetup( + `` + ); + assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); + assert.isNull(checkContext._data); + }); + + it('returns true for radio without aria-checked value', () => { + for (const prop of ['', 'aria-checked', 'aria-checked=""']) { + const params = checkSetup(``); + assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); + assert.isNull(checkContext._data); + } + }); + + describe('checked state', () => { + const fixture = document.querySelector('#fixture'); + + it('returns true for aria-checked="true" on a [checked] radio', () => { + fixture.innerHTML = ``; + const root = axe.setup(fixture); + const vNode = axe.utils.querySelectorAll(root, 'input')[0]; + + assert.isTrue( + ariaConditionalCheck.call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data); + }); + + it('returns true for aria-checked="true" on a clicked radio', () => { + fixture.innerHTML = ``; + fixture.firstChild.click(); // set checked state + const root = axe.setup(fixture); + const vNode = axe.utils.querySelectorAll(root, 'input')[0]; + + assert.isTrue( + ariaConditionalCheck.call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data); + }); + + it('returns false for other aria-checked values', () => { + for (const prop of [' ', 'false', 'mixed', 'incorrect', ' true ']) { + const params = checkSetup( + `` + ); + assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'radio', + checkState: 'true' + }); + } + }); + }); + + describe('unchecked state', () => { + it('returns true for aria-checked="false"', () => { + const params = checkSetup( + `` + ); + assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); + assert.isNull(checkContext._data); + }); + + it('returns true for aria-checked with an invalid value', () => { + for (const prop of [' ', 'invalid', 'FALSE', 'nope']) { + const params = checkSetup( + `` + ); + assert.isTrue(ariaConditionalCheck.apply(checkContext, params)); + assert.isNull(checkContext._data); + } + }); + + it('returns false for other aria-checked values', () => { + for (const prop of ['true', 'TRUE']) { + const params = checkSetup( + `` + ); + assert.isFalse(ariaConditionalCheck.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'radio', + checkState: 'false' + }); + } + }); + }); + }); }); diff --git a/test/checks/aria/aria-hidden-body.js b/test/checks/aria/aria-hidden-body.js index a84f86e57..fc8b9a138 100644 --- a/test/checks/aria/aria-hidden-body.js +++ b/test/checks/aria/aria-hidden-body.js @@ -1,15 +1,13 @@ -describe('aria-hidden', function () { - 'use strict'; - - var checkContext = axe.testUtils.MockCheckContext(); - var body = document.body; - afterEach(function () { +describe('aria-hidden', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const body = document.body; + afterEach(() => { checkContext.reset(); body.removeAttribute('aria-hidden'); }); - it('should not be present on document.body', function () { - var tree = axe.testUtils.flatTreeSetup(body); + it('should not be present on document.body', () => { + const tree = axe.testUtils.flatTreeSetup(body); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-hidden-body') @@ -17,9 +15,9 @@ describe('aria-hidden', function () { ); }); - it('fails appropriately if aria-hidden=true on document.body', function () { + it('fails appropriately if aria-hidden=true on document.body', () => { body.setAttribute('aria-hidden', true); - var tree = axe.testUtils.flatTreeSetup(body); + const tree = axe.testUtils.flatTreeSetup(body); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-hidden-body') @@ -27,9 +25,9 @@ describe('aria-hidden', function () { ); }); - it('passes if aria-hidden=false on document.body', function () { + it('passes if aria-hidden=false on document.body', () => { body.setAttribute('aria-hidden', 'false'); - var tree = axe.testUtils.flatTreeSetup(body); + const tree = axe.testUtils.flatTreeSetup(body); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-hidden-body') diff --git a/test/checks/aria/aria-level.js b/test/checks/aria/aria-level.js index 6ac88a4ea..53db0b316 100644 --- a/test/checks/aria/aria-level.js +++ b/test/checks/aria/aria-level.js @@ -1,36 +1,36 @@ -describe('aria-prohibited-attr', function () { - 'use strict'; +describe('aria-prohibited-attr', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const checkEvaluate = axe.testUtils.getCheckEvaluate('aria-level'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var checkEvaluate = axe.testUtils.getCheckEvaluate('aria-level'); - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true if aria-level is less than 6', function () { - var params = checkSetup('
Contents
'); + it('should return true if aria-level is less than 6', () => { + const params = checkSetup('
Contents
'); assert.isTrue(checkEvaluate.apply(checkContext, params)); }); - it('should return true if aria-level is 6', function () { - var params = checkSetup('
Contents
'); + it('should return true if aria-level is 6', () => { + const params = checkSetup('
Contents
'); assert.isTrue(checkEvaluate.apply(checkContext, params)); }); - it('should return true if aria-level is negative', function () { - var params = checkSetup('
Contents
'); + it('should return true if aria-level is negative', () => { + const params = checkSetup( + '
Contents
' + ); assert.isTrue(checkEvaluate.apply(checkContext, params)); }); - it('should return true if there is no aria-level', function () { - var params = checkSetup('
Contents
'); + it('should return true if there is no aria-level', () => { + const params = checkSetup('
Contents
'); assert.isTrue(checkEvaluate.apply(checkContext, params)); }); - it('should return undefined if aria-level is greater than 6', function () { - var params = checkSetup('
Contents
'); + it('should return undefined if aria-level is greater than 6', () => { + const params = checkSetup('
Contents
'); assert.isUndefined(checkEvaluate.apply(checkContext, params)); }); }); diff --git a/test/checks/aria/aria-prohibited-attr.js b/test/checks/aria/aria-prohibited-attr.js index 75fbaf9a8..3895ac694 100644 --- a/test/checks/aria/aria-prohibited-attr.js +++ b/test/checks/aria/aria-prohibited-attr.js @@ -1,5 +1,5 @@ describe('aria-prohibited-attr', () => { - 'use strict'; + const html = axe.testUtils.html; const checkContext = axe.testUtils.MockCheckContext(); const checkSetup = axe.testUtils.checkSetup; @@ -143,7 +143,7 @@ describe('aria-prohibited-attr', () => { assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should not allow aria-label on divs that have an invalid role', function () { + it('should not allow aria-label on divs that have an invalid role', () => { const params = checkSetup( '
' ); @@ -156,14 +156,14 @@ describe('aria-prohibited-attr', () => { }); }); - it('should allow aria-label on divs with a valid fallback role', function () { + it('should allow aria-label on divs with a valid fallback role', () => { const params = checkSetup( '
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should not allow aria-label on divs with no valid fallback roles', function () { + it('should not allow aria-label on divs with no valid fallback roles', () => { const params = checkSetup( '
' ); @@ -178,7 +178,7 @@ describe('aria-prohibited-attr', () => { describe('widget ancestor', () => { it('should allow aria-label', () => { - const params = checkSetup(` + const params = checkSetup(html`
' ); - var actual = axe.testUtils.getCheckEvaluate('aria-roledescription').call( + const actual = axe.testUtils.getCheckEvaluate('aria-roledescription').call( checkContext, null, { @@ -24,11 +22,11 @@ describe('aria-roledescription', function () { assert.isNull(checkContext._data, null); }); - it('returns true for elements with an explicit supported role', function () { - var vNode = queryFixture( + it('returns true for elements with an explicit supported role', () => { + const vNode = queryFixture( '' ); - var actual = axe.testUtils.getCheckEvaluate('aria-roledescription').call( + const actual = axe.testUtils.getCheckEvaluate('aria-roledescription').call( checkContext, null, { @@ -40,45 +38,45 @@ describe('aria-roledescription', function () { assert.isNull(checkContext._data, null); }); - it('returns undefined for elements with an unsupported role', function () { - var vNode = queryFixture( + it('returns undefined for elements with an unsupported role', () => { + const vNode = queryFixture( '
The main element
' ); - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, null, {}, vNode); assert.equal(actual, undefined); assert.isNull(checkContext._data, null); }); - it('returns false for elements without role', function () { - var vNode = queryFixture( + it('returns false for elements without role', () => { + const vNode = queryFixture( '
The main element
' ); - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, null, {}, vNode); assert.equal(actual, false); assert.isNull(checkContext._data, null); }); - it('returns false for elements with role=presentation', function () { - var vNode = queryFixture( + it('returns false for elements with role=presentation', () => { + const vNode = queryFixture( '' ); - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, null, {}, vNode); assert.equal(actual, false); assert.isNull(checkContext._data, null); }); - it('returns false for elements with role=none', function () { - var vNode = queryFixture( + it('returns false for elements with role=none', () => { + const vNode = queryFixture( '
The main element
' ); - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, null, {}, vNode); assert.equal(actual, false); diff --git a/test/checks/aria/braille-label-equivalent.js b/test/checks/aria/braille-label-equivalent.js index b98439bc2..d63b24baa 100644 --- a/test/checks/aria/braille-label-equivalent.js +++ b/test/checks/aria/braille-label-equivalent.js @@ -1,4 +1,5 @@ describe('braille-label-equivalent tests', () => { + const html = axe.testUtils.html; const { checkSetup, getCheckEvaluate } = axe.testUtils; const checkContext = axe.testUtils.MockCheckContext(); const checkEvaluate = getCheckEvaluate('braille-label-equivalent'); @@ -28,7 +29,7 @@ describe('braille-label-equivalent tests', () => { describe('when aria-braillelabel has text', () => { it('returns false when the accessible name is empty', () => { - const params = checkSetup(` + const params = checkSetup(html` `); assert.isFalse(checkEvaluate.apply(checkContext, params)); @@ -42,7 +43,7 @@ describe('braille-label-equivalent tests', () => { }); it('returns true when the accessible name is not empty', () => { - const params = checkSetup(` + const params = checkSetup(html` foo `); assert.isTrue(checkEvaluate.apply(checkContext, params)); diff --git a/test/checks/aria/braille-roledescription-equivalent.js b/test/checks/aria/braille-roledescription-equivalent.js index 3dda24c32..2645c5c95 100644 --- a/test/checks/aria/braille-roledescription-equivalent.js +++ b/test/checks/aria/braille-roledescription-equivalent.js @@ -1,4 +1,5 @@ describe('braille-roledescription-equivalent tests', () => { + const html = axe.testUtils.html; const { checkSetup, getCheckEvaluate } = axe.testUtils; const checkContext = axe.testUtils.MockCheckContext(); const checkEvaluate = getCheckEvaluate('braille-roledescription-equivalent'); @@ -28,18 +29,15 @@ describe('braille-roledescription-equivalent tests', () => { describe('when aria-brailleroledescription has text', () => { it('returns false without aria-roledescription', () => { - const params = checkSetup(` -
+ const params = checkSetup(html` +
`); assert.isFalse(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { messageKey: 'noRoleDescription' }); }); it('returns false when aria-roledescription is empty', () => { - const params = checkSetup(` + const params = checkSetup(html`
{ }); it('returns true when aria-roledescription is not empty', () => { - const params = checkSetup(` + const params = checkSetup(html`
{ + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const checkEvaluate = axe.testUtils.getCheckEvaluate('deprecatedrole'); + afterEach(() => { checkContext.reset(); axe.reset(); }); - it('returns true if applied to a deprecated role', function () { + it('returns true if applied to a deprecated role', () => { axe.configure({ standards: { ariaRoles: { @@ -20,12 +18,12 @@ describe('deprecatedrole', function () { } } }); - var params = checkSetup('
Contents
'); + const params = checkSetup('
Contents
'); assert.isTrue(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, 'melon'); }); - it('returns true if applied to a deprecated DPUB role', function () { + it('returns true if applied to a deprecated DPUB role', () => { axe.configure({ standards: { ariaRoles: { @@ -36,31 +34,31 @@ describe('deprecatedrole', function () { } } }); - var params = checkSetup( + const params = checkSetup( '
Contents
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, 'doc-fizzbuzz'); }); - it('returns false if applied to a non-deprecated role', function () { - var params = checkSetup('
Contents
'); + it('returns false if applied to a non-deprecated role', () => { + let params = checkSetup('
Contents
'); assert.isFalse(checkEvaluate.apply(checkContext, params)); assert.isNull(checkContext._data); - var params = checkSetup(''); + params = checkSetup(''); assert.isFalse(checkEvaluate.apply(checkContext, params)); assert.isNull(checkContext._data); }); - it('returns false if applied to an invalid role', function () { - var params = checkSetup(''); + it('returns false if applied to an invalid role', () => { + const params = checkSetup(''); assert.isFalse(checkEvaluate.apply(checkContext, params)); assert.isNull(checkContext._data); }); - describe('with fallback roles', function () { - it('returns true if the deprecated role is the first valid role', function () { + describe('with fallback roles', () => { + it('returns true if the deprecated role is the first valid role', () => { axe.configure({ standards: { ariaRoles: { @@ -71,14 +69,14 @@ describe('deprecatedrole', function () { } } }); - var params = checkSetup( + const params = checkSetup( '
Contents
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, 'melon'); }); - it('returns false if the deprecated role is not the first valid role', function () { + it('returns false if the deprecated role is not the first valid role', () => { axe.configure({ standards: { ariaRoles: { @@ -89,7 +87,7 @@ describe('deprecatedrole', function () { } } }); - var params = checkSetup( + const params = checkSetup( '
Contents
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); diff --git a/test/checks/aria/errormessage.js b/test/checks/aria/errormessage.js index cef7ea31f..ebfcefb4d 100644 --- a/test/checks/aria/errormessage.js +++ b/test/checks/aria/errormessage.js @@ -1,20 +1,20 @@ -describe('aria-errormessage', function () { - 'use strict'; +describe('aria-errormessage', () => { + const html = axe.testUtils.html; - var queryFixture = axe.testUtils.queryFixture; - var shadowSupported = axe.testUtils.shadowSupport.v1; - var shadowCheckSetup = axe.testUtils.shadowCheckSetup; - var checkContext = axe.testUtils.MockCheckContext(); + const queryFixture = axe.testUtils.queryFixture; + const shadowCheckSetup = axe.testUtils.shadowCheckSetup; + const checkContext = axe.testUtils.MockCheckContext(); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return false if aria-errormessage value is invalid', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('should return false if aria-errormessage value is invalid', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -22,11 +22,12 @@ describe('aria-errormessage', function () { ); }); - it('should return undefined if aria-errormessage references an element that does not exist', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('should return undefined if aria-errormessage references an element that does not exist', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isUndefined( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -34,11 +35,12 @@ describe('aria-errormessage', function () { ); }); - it('should return true if aria-errormessage id is alert', function () { - var vNode = queryFixture( - '
' + - '' - ); + it('should return true if aria-errormessage id is alert', () => { + const vNode = queryFixture(html` +
+ +
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -46,11 +48,12 @@ describe('aria-errormessage', function () { ); }); - it('should return true if aria-errormessage id is aria-live=assertive', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('should return true if aria-errormessage id is aria-live=assertive', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -58,11 +61,17 @@ describe('aria-errormessage', function () { ); }); - it('should return true if aria-errormessage id is aria-describedby', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('should return true if aria-errormessage id is aria-describedby', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -70,12 +79,17 @@ describe('aria-errormessage', function () { ); }); - it('should return false if aria-errormessage has multiple ids (unsupported)', function () { - var vNode = queryFixture( - '' + - '
Error 1
' + - '
Error 2
' - ); + it('should return false if aria-errormessage has multiple ids (unsupported)', () => { + const vNode = queryFixture(html` + +
Error 1
+
Error 2
+ `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -87,12 +101,17 @@ describe('aria-errormessage', function () { }); }); - it('should return false if aria-errormessage has multiple ids even when one is in aria-describedby', function () { - var vNode = queryFixture( - '' + - '
Error 1
' + - '
Error 2
' - ); + it('should return false if aria-errormessage has multiple ids even when one is in aria-describedby', () => { + const vNode = queryFixture(html` + +
Error 1
+
Error 2
+ `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -104,13 +123,18 @@ describe('aria-errormessage', function () { }); }); - it('should return false if aria-errormessage has multiple ids even when none are in aria-describedby', function () { - var vNode = queryFixture( - '' + - '
Other
' + - '
Error 1
' + - '
Error 2
' - ); + it('should return false if aria-errormessage has multiple ids even when none are in aria-describedby', () => { + const vNode = queryFixture(html` + +
Other
+
Error 1
+
Error 2
+ `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -122,11 +146,12 @@ describe('aria-errormessage', function () { }); }); - it('sets an unsupported message when aria-errormessage contains multiple ids', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('sets an unsupported message when aria-errormessage contains multiple ids', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -138,7 +163,7 @@ describe('aria-errormessage', function () { }); }); - it('returns true when aria-errormessage is empty, if that is allowed', function () { + it('returns true when aria-errormessage is empty, if that is allowed', () => { axe.configure({ standards: { ariaAttrs: { @@ -148,7 +173,7 @@ describe('aria-errormessage', function () { } } }); - var vNode = queryFixture( + const vNode = queryFixture( '
' ); assert.isTrue( @@ -158,10 +183,12 @@ describe('aria-errormessage', function () { ); }); - it('should return true when aria-invalid is not set', function () { - var vNode = queryFixture( - '
' + '
' - ); + it('should return true when aria-invalid is not set', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -169,11 +196,12 @@ describe('aria-errormessage', function () { ); }); - it('should return true when aria-invalid=false', function () { - var vNode = queryFixture( - '
' + - '
' - ); + it('should return true when aria-invalid=false', () => { + const vNode = queryFixture(html` +
+
+
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -181,7 +209,7 @@ describe('aria-errormessage', function () { ); }); - it('returns false when aria-errormessage is empty, if that is not allowed', function () { + it('returns false when aria-errormessage is empty, if that is not allowed', () => { axe.configure({ standards: { ariaAttrs: { @@ -191,7 +219,7 @@ describe('aria-errormessage', function () { } } }); - var vNode = queryFixture( + const vNode = queryFixture( '
' ); assert.isFalse( @@ -201,11 +229,16 @@ describe('aria-errormessage', function () { ); }); - it('should return false when hidden attribute is used', function () { - var vNode = queryFixture( - '' + - '' - ); + it('should return false when hidden attribute is used', () => { + const vNode = queryFixture(html` + + + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -217,11 +250,16 @@ describe('aria-errormessage', function () { }); }); - it('should return false when display: "none" is used', function () { - var vNode = queryFixture( - '' + - '' - ); + it('should return false when display: "none" is used', () => { + const vNode = queryFixture(html` + + + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -233,11 +271,16 @@ describe('aria-errormessage', function () { }); }); - it('should return false when visibility: "hidden" is used', function () { - var vNode = queryFixture( - '' + - '' - ); + it('should return false when visibility: "hidden" is used', () => { + const vNode = queryFixture(html` + + + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -249,11 +292,16 @@ describe('aria-errormessage', function () { }); }); - it('should return false when aria-hidden=true is used', function () { - var vNode = queryFixture( - '' + - '' - ); + it('should return false when aria-hidden=true is used', () => { + const vNode = queryFixture(html` + + + `); assert.isFalse( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -265,11 +313,18 @@ describe('aria-errormessage', function () { }); }); - it('should return true when aria-hidden=false is used', function () { - var vNode = queryFixture( - '' + - '
Error message 1
' - ); + it('should return true when aria-hidden=false is used', () => { + const vNode = queryFixture(html` + +
+ Error message 1 +
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -277,11 +332,16 @@ describe('aria-errormessage', function () { ); }); - it('should return true when no hidden functionality is used', function () { - var vNode = queryFixture( - '' + - '
Error message 1
' - ); + it('should return true when no hidden functionality is used', () => { + const vNode = queryFixture(html` + +
Error message 1
+ `); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-errormessage') @@ -289,56 +349,51 @@ describe('aria-errormessage', function () { ); }); - (shadowSupported ? it : xit)( - 'should return undefined if aria-errormessage value crosses shadow boundary', - function () { - var params = shadowCheckSetup( - '
', - '
' - ); - assert.isUndefined( - axe.testUtils - .getCheckEvaluate('aria-errormessage') - .apply(checkContext, params) - ); - } - ); + it('should return undefined if aria-errormessage value crosses shadow boundary', () => { + const params = shadowCheckSetup( + '
', + '
' + ); + assert.isUndefined( + axe.testUtils + .getCheckEvaluate('aria-errormessage') + .apply(checkContext, params) + ); + }); - (shadowSupported ? it : xit)( - 'should return false if aria-errormessage and invalid reference are both inside shadow dom', - function () { - var params = shadowCheckSetup( - '
', - '
' + - '
' - ); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('aria-errormessage') - .apply(checkContext, params) - ); - } - ); + it('should return false if aria-errormessage and invalid reference are both inside shadow dom', () => { + const params = shadowCheckSetup( + '
', + html` +
+
+ ` + ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-errormessage') + .apply(checkContext, params) + ); + }); - (shadowSupported ? it : xit)( - 'should return true if aria-errormessage and valid reference are both inside shadow dom', - function () { - var params = shadowCheckSetup( - '
', - '
' + - '
' - ); - assert.isTrue( - axe.testUtils - .getCheckEvaluate('aria-errormessage') - .apply(checkContext, params) - ); - } - ); + it('should return true if aria-errormessage and valid reference are both inside shadow dom', () => { + const params = shadowCheckSetup( + '
', + html` +
+
+ ` + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-errormessage') + .apply(checkContext, params) + ); + }); - describe('SerialVirtualNode', function () { - it('should return undefined', function () { - var vNode = new axe.SerialVirtualNode({ + describe('SerialVirtualNode', () => { + it('should return undefined', () => { + const vNode = new axe.SerialVirtualNode({ nodeName: 'div', attributes: { 'aria-invalid': 'true', diff --git a/test/checks/aria/fallbackrole.js b/test/checks/aria/fallbackrole.js index b40a29e36..3a6498de0 100644 --- a/test/checks/aria/fallbackrole.js +++ b/test/checks/aria/fallbackrole.js @@ -1,15 +1,13 @@ -describe('fallbackrole', function () { - 'use strict'; +describe('fallbackrole', () => { + const fixture = document.getElementById('fixture'); + const queryFixture = axe.testUtils.queryFixture; - var fixture = document.getElementById('fixture'); - var queryFixture = axe.testUtils.queryFixture; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should return true if fallback role is used', function () { - var virtualNode = queryFixture( + it('should return true if fallback role is used', () => { + const virtualNode = queryFixture( '
Foo
' ); assert.isTrue( @@ -17,29 +15,35 @@ describe('fallbackrole', function () { ); }); - it('should return false if fallback role is not used', function () { - var virtualNode = queryFixture('
Foo
'); + it('should return false if fallback role is not used', () => { + const virtualNode = queryFixture( + '
Foo
' + ); assert.isFalse( checks.fallbackrole.evaluate(virtualNode.actualNode, null, virtualNode) ); }); - it('should return false if applied to an invalid role', function () { - var virtualNode = queryFixture('
Foo
'); + it('should return false if applied to an invalid role', () => { + const virtualNode = queryFixture( + '
Foo
' + ); assert.isFalse( checks.fallbackrole.evaluate(virtualNode.actualNode, null, virtualNode) ); }); - it('should return false if applied to an invalid role', function () { - var virtualNode = queryFixture('
Foo
'); + it('should return false if applied to an invalid role', () => { + const virtualNode = queryFixture( + '
Foo
' + ); assert.isFalse( checks.fallbackrole.evaluate(virtualNode.actualNode, null, virtualNode) ); }); - it('should return undefined/needs review if an element with no implicit role uses both none and presentation', function () { - var virtualNode = queryFixture( + it('should return undefined/needs review if an element with no implicit role uses both none and presentation', () => { + const virtualNode = queryFixture( '
Foo
' ); assert.isUndefined( @@ -47,8 +51,8 @@ describe('fallbackrole', function () { ); }); - it('should return undefined/needs review if an element with no implicit role uses both presentation and none', function () { - var virtualNode = queryFixture( + it('should return undefined/needs review if an element with no implicit role uses both presentation and none', () => { + const virtualNode = queryFixture( '
Foo
' ); assert.isUndefined( @@ -56,8 +60,8 @@ describe('fallbackrole', function () { ); }); - it('should return true if an element with an implicit role uses both presentation and none', function () { - var virtualNode = queryFixture( + it('should return true if an element with an implicit role uses both presentation and none', () => { + const virtualNode = queryFixture( '' ); assert.isTrue( diff --git a/test/checks/aria/has-global-aria-attribute.js b/test/checks/aria/has-global-aria-attribute.js index 199ebbe89..019f0b27d 100755 --- a/test/checks/aria/has-global-aria-attribute.js +++ b/test/checks/aria/has-global-aria-attribute.js @@ -1,23 +1,21 @@ -describe('has-global-aria-attribute', function () { - 'use strict'; - - var checkSetup = axe.testUtils.checkSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var hasGlobalAriaAttribute = axe.testUtils.getCheckEvaluate( +describe('has-global-aria-attribute', () => { + const checkSetup = axe.testUtils.checkSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const hasGlobalAriaAttribute = axe.testUtils.getCheckEvaluate( 'has-global-aria-attribute' ); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true if any global ARIA attributes are found', function () { - var params = checkSetup('
'); + it('should return true if any global ARIA attributes are found', () => { + const params = checkSetup('
'); assert.isTrue(hasGlobalAriaAttribute.apply(checkContext, params)); }); - it('should return false if no valid ARIA attributes are found', function () { - var params = checkSetup('
'); + it('should return false if no valid ARIA attributes are found', () => { + const params = checkSetup('
'); assert.isFalse(hasGlobalAriaAttribute.apply(checkContext, params)); }); }); diff --git a/test/checks/aria/is-element-focusable.js b/test/checks/aria/is-element-focusable.js index 740270716..0f2cc62d9 100755 --- a/test/checks/aria/is-element-focusable.js +++ b/test/checks/aria/is-element-focusable.js @@ -1,21 +1,19 @@ -describe('is-element-focusable', function () { - 'use strict'; +describe('is-element-focusable', () => { + const checkSetup = axe.testUtils.checkSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const isFocusable = axe.testUtils.getCheckEvaluate('is-element-focusable'); - var checkSetup = axe.testUtils.checkSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var isFocusable = axe.testUtils.getCheckEvaluate('is-element-focusable'); - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true for div with a tabindex', function () { - var params = checkSetup('
'); + it('should return true for div with a tabindex', () => { + const params = checkSetup('
'); assert.isTrue(isFocusable.apply(checkContext, params)); }); - it('should return false for natively unfocusable element', function () { - var params = checkSetup(''); + it('should return false for natively unfocusable element', () => { + const params = checkSetup(''); assert.isFalse(isFocusable.apply(checkContext, params)); }); }); diff --git a/test/checks/aria/no-implicit-explicit-label.js b/test/checks/aria/no-implicit-explicit-label.js index 62d860deb..b725dbb0d 100644 --- a/test/checks/aria/no-implicit-explicit-label.js +++ b/test/checks/aria/no-implicit-explicit-label.js @@ -1,51 +1,49 @@ -describe('no-implicit-explicit-label', function () { - 'use strict'; +describe('no-implicit-explicit-label', () => { + const fixture = document.getElementById('fixture'); + const queryFixture = axe.testUtils.queryFixture; + const check = checks['no-implicit-explicit-label']; + const checkContext = axe.testUtils.MockCheckContext(); - var fixture = document.getElementById('fixture'); - var queryFixture = axe.testUtils.queryFixture; - var check = checks['no-implicit-explicit-label']; - var checkContext = axe.testUtils.MockCheckContext(); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; checkContext.reset(); }); - it('returns false when there is no label text or accessible text', function () { - var vNode = queryFixture( + it('returns false when there is no label text or accessible text', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate.call(checkContext, null, {}, vNode); + const actual = check.evaluate.call(checkContext, null, {}, vNode); assert.isFalse(actual); }); - it('returns undefined when there is no accessible text', function () { - var vNode = queryFixture( + it('returns undefined when there is no accessible text', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate.call(checkContext, null, {}, vNode); + const actual = check.evaluate.call(checkContext, null, {}, vNode); assert.isUndefined(actual); }); - it('returns undefined when accessible text does not contain label text', function () { - var vNode = queryFixture( + it('returns undefined when accessible text does not contain label text', () => { + const vNode = queryFixture( '
England
' ); - var actual = check.evaluate.call(checkContext, null, {}, vNode); + const actual = check.evaluate.call(checkContext, null, {}, vNode); assert.isUndefined(actual); }); - it('returns false when accessible text contains label text', function () { - var vNode = queryFixture( + it('returns false when accessible text contains label text', () => { + const vNode = queryFixture( '
England
' ); - var actual = check.evaluate.call(checkContext, null, {}, vNode); + const actual = check.evaluate.call(checkContext, null, {}, vNode); assert.isFalse(actual); }); - describe('SerialVirtualNode', function () { - it('should return false if there is no parent', function () { - var serialNode = new axe.SerialVirtualNode({ + describe('SerialVirtualNode', () => { + it('should return false if there is no parent', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'div', attributes: { role: 'combobox', @@ -54,12 +52,12 @@ describe('no-implicit-explicit-label', function () { }); serialNode.parent = null; - var actual = check.evaluate.call(checkContext, null, {}, serialNode); + const actual = check.evaluate.call(checkContext, null, {}, serialNode); assert.isFalse(actual); }); - it('should return undefined if incomplete tree', function () { - var serialNode = new axe.SerialVirtualNode({ + it('should return undefined if incomplete tree', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'div', attributes: { role: 'combobox', @@ -67,7 +65,7 @@ describe('no-implicit-explicit-label', function () { } }); - var actual = check.evaluate.call(checkContext, null, {}, serialNode); + const actual = check.evaluate.call(checkContext, null, {}, serialNode); assert.isUndefined(actual); }); }); diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index 8cfab3a17..ec0b109bd 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -1,6 +1,6 @@ describe('aria-required-children', () => { + const html = axe.testUtils.html; const fixture = document.getElementById('fixture'); - const shadowSupported = axe.testUtils.shadowSupport.v1; const checkContext = axe.testUtils.MockCheckContext(); const checkSetup = axe.testUtils.checkSetup; const requiredChildrenCheck = axe.testUtils.getCheckEvaluate( @@ -8,8 +8,6 @@ describe('aria-required-children', () => { ); afterEach(() => { - fixture.innerHTML = ''; - axe._tree = undefined; checkContext.reset(); }); @@ -22,23 +20,20 @@ describe('aria-required-children', () => { assert.deepEqual(checkContext._data, ['listitem']); }); - (shadowSupported ? it : xit)( - 'should detect missing sole required child in shadow tree', - () => { - fixture.innerHTML = '
'; + it('should detect missing sole required child in shadow tree', () => { + fixture.innerHTML = '
'; - const target = document.querySelector('#target'); - const shadowRoot = target.attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = '

Nothing here.

'; + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '

Nothing here.

'; - axe.testUtils.flatTreeSetup(fixture); - const virtualTarget = axe.utils.getNodeFromTree(target); + axe.testUtils.flatTreeSetup(fixture); + const virtualTarget = axe.utils.getNodeFromTree(target); - const params = [target, undefined, virtualTarget]; - assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); - assert.deepEqual(checkContext._data, ['listitem']); - } - ); + const params = [target, undefined, virtualTarget]; + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + assert.deepEqual(checkContext._data, ['listitem']); + }); it('should detect multiple missing required children when one required', () => { const params = checkSetup( @@ -49,27 +44,24 @@ describe('aria-required-children', () => { assert.deepEqual(checkContext._data, ['rowgroup', 'row']); }); - (shadowSupported ? it : xit)( - 'should detect missing multiple required children in shadow tree when one required', - () => { - fixture.innerHTML = '
'; + it('should detect missing multiple required children in shadow tree when one required', () => { + fixture.innerHTML = '
'; - const target = document.querySelector('#target'); - const shadowRoot = target.attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = '

Nothing here.

'; + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '

Nothing here.

'; - axe.testUtils.flatTreeSetup(fixture); - const virtualTarget = axe.utils.getNodeFromTree(target); + axe.testUtils.flatTreeSetup(fixture); + const virtualTarget = axe.utils.getNodeFromTree(target); - const params = [target, undefined, virtualTarget]; - assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); - assert.deepEqual(checkContext._data, ['rowgroup', 'row']); - } - ); + const params = [target, undefined, virtualTarget]; + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + assert.deepEqual(checkContext._data, ['rowgroup', 'row']); + }); it('should pass all existing required children when all required', () => { const params = checkSetup( - `