diff --git a/.github/bin/extract-release-notes.sh b/.github/bin/extract-release-notes.sh new file mode 100755 index 0000000000..409c890240 --- /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 c8afd16de8..c1126b1317 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 867f60a824..f482037a18 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/.github/workflows/test.yml b/.github/workflows/test.yml index 4e484e9a73..a7e082feb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: run: | npm run prepare npm run build - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: axe-core path: axe.js diff --git a/.gitignore b/.gitignore index 3331c6fdbb..0eb44dfdfb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,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/CHANGELOG.md b/CHANGELOG.md index 8fab225196..fad7c19040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ 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.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/Gruntfile.js b/Gruntfile.js index 64cfb9ab1d..53fb779601 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -108,6 +108,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 + } } ] } diff --git a/axe.d.ts b/axe.d.ts index 03cd835b32..65a734eb3e 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; @@ -501,6 +505,7 @@ declare namespace axe { offset?: number ) => string | Uint8Array | Array; nodeSerializer: NodeSerializer; + normalizeRunOptions: (options?: RunOptions) => NormalizedRunOptions; } interface Aria { @@ -625,6 +630,14 @@ declare namespace axe { */ 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 @@ -670,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 16da022c3e..87e17031c3 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.11.4", + "version": "4.12.0", "deprecated": true, "contributors": [ { diff --git a/build/tasks/esbuild.js b/build/tasks/esbuild.js index 30dbeb4ef6..d157766e39 100644 --- a/build/tasks/esbuild.js +++ b/build/tasks/esbuild.js @@ -1,5 +1,6 @@ const { build } = require('esbuild'); const path = require('path'); +const assert = require('assert'); module.exports = function (grunt) { grunt.registerMultiTask( @@ -12,6 +13,7 @@ module.exports = function (grunt) { files.forEach(file => { const src = Array.isArray(file.src) ? file.src : [file.src]; const dest = file.dest; + const options = file.options || {}; src.forEach(entry => { const name = path.basename(entry); @@ -23,9 +25,29 @@ module.exports = function (grunt) { entryPoints: [entry], outfile: path.join(dest, name), minify: false, - bundle: true + bundle: true, + ...options }) - .then(done) + .then(result => { + if (options.metafile && file.validateImports) { + const { max, maxSize } = file.validateImports; + const { inputs } = result.metafile; + const entries = Object.entries(inputs); + + assert( + entries.length <= max, + `${entry} imported too many files (max: ${max}): ${entries.length}` + ); + for (const [key, value] of entries) { + assert( + value.bytes <= maxSize, + `${key} import size too large (max: ${maxSize}): ${value.bytes}` + ); + } + } + + done(result); + }) .catch(e => { grunt.fail.fatal(e); done(); diff --git a/doc/API.md b/doc/API.md index 18f4dbb9c8..2d62ac9033 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. #### 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 d35e9575a2..532937e0b4 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/element-internals.md b/doc/element-internals.md new file mode 100644 index 0000000000..19c101f4d5 --- /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/external-apis.md b/doc/external-apis.md new file mode 100644 index 0000000000..4fd792f2a7 --- /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 a211557441..0714b46f17 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -16,72 +16,73 @@ | Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | | :-------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [area-alt](https://dequeuniversity.com/rules/axe/4.11/area-alt?application=RuleDescription) | Ensure <area> elements of image maps have alternative text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.2 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-attr?application=RuleDescription) | Ensure an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.11/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | | -| [aria-command-name](https://dequeuniversity.com/rules/axe/4.11/aria-command-name?application=RuleDescription) | Ensure every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.11/aria-conditional-attr?application=RuleDescription) | Ensure ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.11/aria-deprecated-role?application=RuleDescription) | Ensure elements do not use deprecated roles | Minor | 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-hidden-body](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-body?application=RuleDescription) | Ensure aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.1.3.1, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure | | -| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-focus?application=RuleDescription) | Ensure aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | -| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-input-field-name?application=RuleDescription) | Ensure every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.11/aria-meter-name?application=RuleDescription) | Ensure every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | -| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.11/aria-progressbar-name?application=RuleDescription) | Ensure every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | -| [aria-prohibited-attr](https://dequeuniversity.com/rules/axe/4.11/aria-prohibited-attr?application=RuleDescription) | Ensure ARIA attributes are not prohibited for an element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.11/aria-required-attr?application=RuleDescription) | Ensure elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | -| [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-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) | -| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr?application=RuleDescription) | Ensure attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | -| [blink](https://dequeuniversity.com/rules/axe/4.11/blink?application=RuleDescription) | Ensure <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | -| [button-name](https://dequeuniversity.com/rules/axe/4.11/button-name?application=RuleDescription) | Ensure buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1), [m6b1q3](https://act-rules.github.io/rules/m6b1q3) | -| [bypass](https://dequeuniversity.com/rules/axe/4.11/bypass?application=RuleDescription) | Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a, EN-301-549, EN-9.2.4.1, RGAAv4, RGAA-12.7.1 | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | -| [color-contrast](https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, TTv5, TT13.c, EN-301-549, EN-9.1.4.3, ACT, RGAAv4, RGAA-3.2.1 | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [definition-list](https://dequeuniversity.com/rules/axe/4.11/definition-list?application=RuleDescription) | Ensure <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | -| [dlitem](https://dequeuniversity.com/rules/axe/4.11/dlitem?application=RuleDescription) | Ensure <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | -| [document-title](https://dequeuniversity.com/rules/axe/4.11/document-title?application=RuleDescription) | Ensure each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, TTv5, TT12.a, EN-301-549, EN-9.2.4.2, ACT, RGAAv4, RGAA-8.5.1 | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | -| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-aria?application=RuleDescription) | Ensure every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-8.2.1 | needs review | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.11/form-field-multiple-labels?application=RuleDescription) | Ensure form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c, EN-301-549, EN-9.3.3.2, RGAAv4, RGAA-11.2.1 | needs review | | -| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.11/frame-focusable-content?application=RuleDescription) | Ensure <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-7.3.2 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.11/frame-title-unique?application=RuleDescription) | Ensure <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag2a, wcag412, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.2.1 | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | -| [frame-title](https://dequeuniversity.com/rules/axe/4.11/frame-title?application=RuleDescription) | Ensure <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.1.1 | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | -| [html-has-lang](https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=RuleDescription) | Ensure every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | -| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.11/html-lang-valid?application=RuleDescription) | Ensure the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.4.1 | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | -| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.11/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | -| [image-alt](https://dequeuniversity.com/rules/axe/4.11/image-alt?application=RuleDescription) | Ensure <img> elements have alternative text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, TT7.b, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [input-button-name](https://dequeuniversity.com/rules/axe/4.11/input-button-name?application=RuleDescription) | Ensure input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [input-image-alt](https://dequeuniversity.com/rules/axe/4.11/input-image-alt?application=RuleDescription) | Ensure <input type="image"> elements have alternative text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.3 | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | -| [label](https://dequeuniversity.com/rules/axe/4.11/label?application=RuleDescription) | Ensure every form element has a label | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.11/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a, EN-301-549, EN-9.1.4.1, RGAAv4, RGAA-10.6.1 | failure, needs review | | -| [link-name](https://dequeuniversity.com/rules/axe/4.11/link-name?application=RuleDescription) | Ensure links have discernible text | Serious | cat.name-role-value, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-6.2.1 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [list](https://dequeuniversity.com/rules/axe/4.11/list?application=RuleDescription) | Ensure that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | -| [listitem](https://dequeuniversity.com/rules/axe/4.11/listitem?application=RuleDescription) | Ensure <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | -| [marquee](https://dequeuniversity.com/rules/axe/4.11/marquee?application=RuleDescription) | Ensure <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | -| [meta-refresh](https://dequeuniversity.com/rules/axe/4.11/meta-refresh?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a, EN-301-549, EN-9.2.2.1, RGAAv4, RGAA-13.1.2 | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | -| [meta-viewport](https://dequeuniversity.com/rules/axe/4.11/meta-viewport?application=RuleDescription) | Ensure <meta name="viewport"> does not disable text scaling and zooming | Moderate | cat.sensory-and-visual-cues, wcag2aa, wcag144, EN-301-549, EN-9.1.4.4, ACT, RGAAv4, RGAA-10.4.2 | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | -| [nested-interactive](https://dequeuniversity.com/rules/axe/4.11/nested-interactive?application=RuleDescription) | Ensure interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | -| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.11/no-autoplay-audio?application=RuleDescription) | Ensure <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, TTv5, TT2.a, EN-301-549, EN-9.1.4.2, ACT, RGAAv4, RGAA-4.10.1 | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | -| [object-alt](https://dequeuniversity.com/rules/axe/4.11/object-alt?application=RuleDescription) | Ensure <object> elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-1.1.6 | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | -| [role-img-alt](https://dequeuniversity.com/rules/axe/4.11/role-img-alt?application=RuleDescription) | Ensure [role="img"] elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.11/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard in Safari | Serious | cat.keyboard, wcag2a, wcag211, wcag213, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, EN-9.2.1.3, RGAAv4, RGAA-7.3.2 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | -| [select-name](https://dequeuniversity.com/rules/axe/4.11/select-name?application=RuleDescription) | Ensure select element has an accessible name | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.11/server-side-image-map?application=RuleDescription) | Ensure that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-1.1.4 | needs review | | -| [summary-name](https://dequeuniversity.com/rules/axe/4.11/summary-name?application=RuleDescription) | Ensure summary elements have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | | -| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.11/svg-img-alt?application=RuleDescription) | Ensure <svg> elements with an img, graphics-document or graphics-symbol role have accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.5 | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | -| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.11/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | -| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.1 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | -| [valid-lang](https://dequeuniversity.com/rules/axe/4.11/valid-lang?application=RuleDescription) | Ensure lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT, RGAAv4, RGAA-8.8.1 | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | -| [video-caption](https://dequeuniversity.com/rules/axe/4.11/video-caption?application=RuleDescription) | Ensure <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2, RGAAv4, RGAA-4.3.1 | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | +| [area-alt](https://dequeuniversity.com/rules/axe/4.12/area-alt?application=RuleDescription) | Ensure <area> elements of image maps have alternative text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.2 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.12/aria-allowed-attr?application=RuleDescription) | Ensure an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.12/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | | +| [aria-command-name](https://dequeuniversity.com/rules/axe/4.12/aria-command-name?application=RuleDescription) | Ensure every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.12/aria-conditional-attr?application=RuleDescription) | Ensure ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.12/aria-deprecated-role?application=RuleDescription) | Ensure elements do not use deprecated roles | Minor | 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-hidden-body](https://dequeuniversity.com/rules/axe/4.12/aria-hidden-body?application=RuleDescription) | Ensure aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.1.3.1, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure | | +| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.12/aria-hidden-focus?application=RuleDescription) | Ensure aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | +| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.12/aria-input-field-name?application=RuleDescription) | Ensure every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.12/aria-meter-name?application=RuleDescription) | Ensure every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | +| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.12/aria-progressbar-name?application=RuleDescription) | Ensure every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | +| [aria-prohibited-attr](https://dequeuniversity.com/rules/axe/4.12/aria-prohibited-attr?application=RuleDescription) | Ensure ARIA attributes are not prohibited for an element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.12/aria-required-attr?application=RuleDescription) | Ensure elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | +| [aria-required-children](https://dequeuniversity.com/rules/axe/4.12/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.12/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.12/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.12/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.12/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.12/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.12/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) | +| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.12/aria-valid-attr?application=RuleDescription) | Ensure attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | +| [blink](https://dequeuniversity.com/rules/axe/4.12/blink?application=RuleDescription) | Ensure <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | +| [button-name](https://dequeuniversity.com/rules/axe/4.12/button-name?application=RuleDescription) | Ensure buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1), [m6b1q3](https://act-rules.github.io/rules/m6b1q3) | +| [bypass](https://dequeuniversity.com/rules/axe/4.12/bypass?application=RuleDescription) | Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a, EN-301-549, EN-9.2.4.1, RGAAv4, RGAA-12.7.1 | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | +| [color-contrast](https://dequeuniversity.com/rules/axe/4.12/color-contrast?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, TTv5, TT13.c, EN-301-549, EN-9.1.4.3, ACT, RGAAv4, RGAA-3.2.1 | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [definition-list](https://dequeuniversity.com/rules/axe/4.12/definition-list?application=RuleDescription) | Ensure <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | +| [dlitem](https://dequeuniversity.com/rules/axe/4.12/dlitem?application=RuleDescription) | Ensure <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | +| [document-title](https://dequeuniversity.com/rules/axe/4.12/document-title?application=RuleDescription) | Ensure each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, TTv5, TT12.a, EN-301-549, EN-9.2.4.2, ACT, RGAAv4, RGAA-8.5.1 | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | +| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.12/duplicate-id-aria?application=RuleDescription) | Ensure every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-8.2.1 | needs review | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.12/form-field-multiple-labels?application=RuleDescription) | Ensure form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c, EN-301-549, EN-9.3.3.2, RGAAv4, RGAA-11.2.1 | needs review | | +| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.12/frame-focusable-content?application=RuleDescription) | Ensure <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-7.3.2 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.12/frame-title-unique?application=RuleDescription) | Ensure <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag2a, wcag412, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.2.1 | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | +| [frame-title](https://dequeuniversity.com/rules/axe/4.12/frame-title?application=RuleDescription) | Ensure <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.1.1 | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | +| [html-has-lang](https://dequeuniversity.com/rules/axe/4.12/html-has-lang?application=RuleDescription) | Ensure every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | +| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.12/html-lang-valid?application=RuleDescription) | Ensure the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.4.1 | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | +| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.12/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | +| [image-alt](https://dequeuniversity.com/rules/axe/4.12/image-alt?application=RuleDescription) | Ensure <img> elements have alternative text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, TT7.b, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [input-button-name](https://dequeuniversity.com/rules/axe/4.12/input-button-name?application=RuleDescription) | Ensure input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [input-image-alt](https://dequeuniversity.com/rules/axe/4.12/input-image-alt?application=RuleDescription) | Ensure <input type="image"> elements have alternative text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.3 | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | +| [label](https://dequeuniversity.com/rules/axe/4.12/label?application=RuleDescription) | Ensure every form element has a label | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.12/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a, EN-301-549, EN-9.1.4.1, RGAAv4, RGAA-10.6.1 | failure, needs review | | +| [link-name](https://dequeuniversity.com/rules/axe/4.12/link-name?application=RuleDescription) | Ensure links have discernible text | Serious | cat.name-role-value, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-6.2.1 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [list](https://dequeuniversity.com/rules/axe/4.12/list?application=RuleDescription) | Ensure that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | +| [listitem](https://dequeuniversity.com/rules/axe/4.12/listitem?application=RuleDescription) | Ensure <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | +| [marquee](https://dequeuniversity.com/rules/axe/4.12/marquee?application=RuleDescription) | Ensure <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | +| [meta-refresh](https://dequeuniversity.com/rules/axe/4.12/meta-refresh?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a, EN-301-549, EN-9.2.2.1, RGAAv4, RGAA-13.1.2 | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | +| [meta-viewport](https://dequeuniversity.com/rules/axe/4.12/meta-viewport?application=RuleDescription) | Ensure <meta name="viewport"> does not disable text scaling and zooming | Moderate | cat.sensory-and-visual-cues, wcag2aa, wcag144, EN-301-549, EN-9.1.4.4, ACT, RGAAv4, RGAA-10.4.2 | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | +| [nested-interactive](https://dequeuniversity.com/rules/axe/4.12/nested-interactive?application=RuleDescription) | Ensure interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | +| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.12/no-autoplay-audio?application=RuleDescription) | Ensure <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, TTv5, TT2.a, EN-301-549, EN-9.1.4.2, ACT, RGAAv4, RGAA-4.10.1 | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | +| [object-alt](https://dequeuniversity.com/rules/axe/4.12/object-alt?application=RuleDescription) | Ensure <object> elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-1.1.6 | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | +| [role-img-alt](https://dequeuniversity.com/rules/axe/4.12/role-img-alt?application=RuleDescription) | Ensure [role="img"] elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.12/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard in Safari | Serious | cat.keyboard, wcag2a, wcag211, wcag213, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, EN-9.2.1.3, RGAAv4, RGAA-7.3.2 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | +| [select-name](https://dequeuniversity.com/rules/axe/4.12/select-name?application=RuleDescription) | Ensure select element has an accessible name | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.12/server-side-image-map?application=RuleDescription) | Ensure that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-1.1.4 | needs review | | +| [summary-name](https://dequeuniversity.com/rules/axe/4.12/summary-name?application=RuleDescription) | Ensure summary elements have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | | +| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.12/svg-img-alt?application=RuleDescription) | Ensure <svg> elements with an img, graphics-document or graphics-symbol role have accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.5 | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | +| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.12/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | +| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.12/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.1 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | +| [valid-lang](https://dequeuniversity.com/rules/axe/4.12/valid-lang?application=RuleDescription) | Ensure lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT, RGAAv4, RGAA-8.8.1 | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | +| [video-caption](https://dequeuniversity.com/rules/axe/4.12/video-caption?application=RuleDescription) | Ensure <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2, RGAAv4, RGAA-4.3.1 | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | ## WCAG 2.1 Level A & AA Rules | Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | | :------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------- | :------ | :------------------------------------------------------------------------------ | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.11/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, EN-301-549, EN-9.1.3.5, ACT, RGAAv4, RGAA-11.13.1 | failure, needs review | [73f2c2](https://act-rules.github.io/rules/73f2c2) | -| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.11/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, EN-301-549, EN-9.1.4.12, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | +| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.12/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, EN-301-549, EN-9.1.3.5, ACT, RGAAv4, RGAA-11.13.1 | failure, needs review | [73f2c2](https://act-rules.github.io/rules/73f2c2) | +| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.12/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, EN-301-549, EN-9.1.4.12, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | ## WCAG 2.2 Level A & AA Rules @@ -89,42 +90,41 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r | Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | | :------------------------------------------------------------------------------------------------ | :-------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | -| [target-size](https://dequeuniversity.com/rules/axe/4.11/target-size?application=RuleDescription) | Ensure touch targets have sufficient size and space | Serious | cat.sensory-and-visual-cues, wcag22aa, wcag258 | failure, needs review | | +| [target-size](https://dequeuniversity.com/rules/axe/4.12/target-size?application=RuleDescription) | Ensure touch targets have sufficient size and space | Serious | cat.sensory-and-visual-cues, wcag22aa, wcag258 | failure, needs review | | ## Best Practices Rules 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 | | -| [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](https://dequeuniversity.com/rules/axe/4.11/heading-order?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.semantics, best-practice | 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.12/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.12/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.12/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.12/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.12/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.12/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.12/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.12/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](https://dequeuniversity.com/rules/axe/4.12/heading-order?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/landmark-unique?application=RuleDescription) | Ensure landmarks are unique | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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 @@ -132,9 +132,9 @@ Rules that check for conformance to WCAG AAA success criteria that can be fully | Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | | :---------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | -| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.11/color-contrast-enhanced?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.11/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | -| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.11/meta-refresh-no-exceptions?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | +| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.12/color-contrast-enhanced?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.12/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | +| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.12/meta-refresh-no-exceptions?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | ## Experimental Rules @@ -142,21 +142,22 @@ Rules we are still testing and developing. They are disabled by default in axe-c | Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | | :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------- | :------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | -| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.11/css-orientation-lock?application=RuleDescription) | Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, EN-301-549, EN-9.1.3.4, RGAAv4, RGAA-13.9.1, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | -| [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, experimental | failure | | -| [hidden-content](https://dequeuniversity.com/rules/axe/4.11/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, experimental, review-item | failure, needs review | | -| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.11/label-content-name-mismatch?application=RuleDescription) | Ensure that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, EN-301-549, EN-9.2.5.3, RGAAv4, RGAA-6.1.5, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | -| [p-as-heading](https://dequeuniversity.com/rules/axe/4.11/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.1.3, experimental | failure, needs review | | -| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.11/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.4.1 | failure | | -| [td-has-header](https://dequeuniversity.com/rules/axe/4.11/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure | | +| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.12/css-orientation-lock?application=RuleDescription) | Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, EN-301-549, EN-9.1.3.4, RGAAv4, RGAA-13.9.1, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | +| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.12/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, experimental | failure | | +| [hidden-content](https://dequeuniversity.com/rules/axe/4.12/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, experimental, review-item | failure, needs review | | +| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.12/label-content-name-mismatch?application=RuleDescription) | Ensure that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, EN-301-549, EN-9.2.5.3, RGAAv4, RGAA-6.1.5, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | +| [p-as-heading](https://dequeuniversity.com/rules/axe/4.12/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.1.3, experimental | failure, needs review | | +| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.12/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.4.1 | failure | | +| [td-has-header](https://dequeuniversity.com/rules/axe/4.12/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure | | ## Deprecated 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.12/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.12/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.12/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.12/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.12/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/lib/checks/aria/aria-conditional-attr-evaluate.js b/lib/checks/aria/aria-conditional-attr-evaluate.js index 92fca67525..c772c9a25f 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 c55829016c..5f395ba63f 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 0000000000..4f6897c8fa --- /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 403cd4979b..0330d2a86b 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/label/label-content-name-mismatch-evaluate.js b/lib/checks/label/label-content-name-mismatch-evaluate.js index acd53c6e4b..208dc1afd1 100644 --- a/lib/checks/label/label-content-name-mismatch-evaluate.js +++ b/lib/checks/label/label-content-name-mismatch-evaluate.js @@ -1,9 +1,9 @@ import { accessibleText, isHumanInterpretable, - subtreeText, + removeUnicode, sanitize, - removeUnicode + visibleVirtual } from '../../commons/text'; /** @@ -42,14 +42,11 @@ function labelContentNameMismatchEvaluate(node, options, virtualNode) { const occurrenceThreshold = options?.occurrenceThreshold ?? options?.occuranceThreshold; const accText = accessibleText(node).toLowerCase(); - const visibleText = sanitize( - subtreeText(virtualNode, { - subtreeDescendant: true, - ignoreIconLigature: true, - pixelThreshold, - occurrenceThreshold - }) - ).toLowerCase(); + const visibleText = visibleVirtual(virtualNode, false, false, { + ignoreIconLigature: true, + pixelThreshold, + occurrenceThreshold + }).toLowerCase(); if (!visibleText) { return true; diff --git a/lib/checks/lists/invalid-children-evaluate.js b/lib/checks/lists/invalid-children-evaluate.js index 684e2eb657..633a64bb93 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 dde5dcb4f4..630056c498 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 509d772172..72bf1e3fd7 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 6e5453aae6..fbfa5ccd1f 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 7957e2dcde..8c7b00282c 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 3353545af1..e0d51b4aab 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 4a19c8b021..e0c7e5da43 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 19dedc20f1..ab34bbfe37 100644 --- a/lib/core/core.js +++ b/lib/core/core.js @@ -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'; @@ -49,6 +51,7 @@ axe.imports = imports; axe.cleanup = cleanup; axe.configure = configure; +axe.externalAPIs = externalAPIs; axe.frameMessenger = frameMessenger; axe.getRules = getRules; axe._load = load; @@ -58,6 +61,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/public/configure.js b/lib/core/public/configure.js index afe73582b1..587f617fb2 100644 --- a/lib/core/public/configure.js +++ b/lib/core/public/configure.js @@ -52,7 +52,6 @@ function configure(spec) { spec.checks.forEach(check => { if (!check.id) { throw new TypeError( - // eslint-disable-next-line max-len `Configured check ${JSON.stringify( check )} is invalid. Checks must be an object with at least an id property` @@ -72,7 +71,6 @@ function configure(spec) { spec.rules.forEach(rule => { if (!rule.id) { throw new TypeError( - // eslint-disable-next-line max-len `Configured rule ${JSON.stringify( rule )} is invalid. Rules must be an object with at least an id property` @@ -92,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) { @@ -107,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 0000000000..ea4668c139 --- /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 31e6417c2a..83f3ca8f10 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 fa23a3b5b2..776810e82c 100644 --- a/lib/core/public/get-rules.js +++ b/lib/core/public/get-rules.js @@ -22,7 +22,8 @@ function getRules(tags) { help: rd.help, helpUrl: rd.helpUrl, tags: matchingRule.tags, - actIds: matchingRule.actIds + actIds: matchingRule.actIds, + enabled: matchingRule.enabled }; }); } diff --git a/lib/core/public/reset-locale.js b/lib/core/public/reset-locale.js new file mode 100644 index 0000000000..c82b4ba684 --- /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 50ab6465da..7c2e0d3e57 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 0000000000..fcfbd414ef --- /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 44230841db..9a574fef55 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'; @@ -99,3 +101,4 @@ export { default as uniqueArray } from './unique-array'; export { default as uuid } from './uuid'; export { default as validInputTypes } from './valid-input-type'; export { default as isValidLang, validLangs } from './valid-langs'; +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 a2f3f112f5..6785257294 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 ab8aedd003..0bd6a9fd32 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 0000000000..63e15ea5e4 --- /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 0000000000..1c65d518c0 --- /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 3ec44fa526..825ba31a9a 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 0000000000..cc9063449a --- /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 0000000000..c1cf0fe125 --- /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 0000000000..97a959a798 --- /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 638e392f1d..60f7e319e5 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 7e3304e215..f34a1cf617 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 19caad9995..e07bf095cc 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 4dbf8dbe9d..e5948942a6 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 404d5ab293..69e1d5c9fc 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 47ee235c2a..c013211ccf 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 ef5e6876dc..961e050108 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 37a9d3262b..f62a515973 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/package-lock.json b/package-lock.json index 92d28ff1e8..82eb59b0cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "axe-core", - "version": "4.11.4", + "version": "4.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "axe-core", - "version": "4.11.4", + "version": "4.12.0", "license": "MPL-2.0", "devDependencies": { "@axe-core/webdriverjs": "^4.10.2", @@ -15,7 +15,7 @@ "@babel/preset-env": "^7.20.2", "@babel/runtime-corejs3": "^7.20.7", "@deque/dot": "^1.1.5", - "@types/node": "^4.9.5", + "@types/node": "^25.6.0", "aria-practices": "github:w3c/aria-practices#ce0336bd82d7d3651abcbde86af644197ddbc629", "aria-query": "^5.1.3", "chai": "^4.3.7", @@ -49,7 +49,7 @@ "inquirer": "^8.2.5", "jquery": "^4.0.0", "jsdoc": "^4.0.2", - "jsdom": "^28.1.0", + "jsdom": "^29.0.2", "karma": "^6.4.1", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^3.1.1", @@ -64,17 +64,19 @@ "node-notifier": "^10.0.1", "npm-run-all": "^4.1.5", "outdent": "^0.8.0", + "parse5": "^8.0.0", "patch-package": "^8.0.0", "prettier": "^3.0.3", + "recast": "^0.23.11", "revalidator": "^0.3.1", "selenium-webdriver": "^4.7.1", "serve-handler": "^6.1.5", "sinon": "^21.0.0", "sri-toolbox": "^0.2.0", "standard-version": "^9.5.0", - "start-server-and-test": "^2.0.1", + "start-server-and-test": "^3.0.2", "typedarray": "^0.0.7", - "typescript": "^5.2.2", + "typescript": "^6.0.3", "uglify-js": "^3.17.4", "wcag-act-rules": "github:w3c/wcag-act-rules#5adb55d19eb2cd7fb23213b2d1acedf9004dc063", "weakmap-polyfill": "^2.0.4" @@ -92,57 +94,45 @@ "node": ">=0.10.0" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true - }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "dependencies": { - "@csstools/css-calc": "^3.1.1", - "@csstools/css-color-parser": "^4.0.2", + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -152,12 +142,12 @@ "dev": true }, "node_modules/@axe-core/webdriverjs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.11.1.tgz", - "integrity": "sha512-UVq7uBsqaQlHj7PQPg+WhE0nV6ccYtE0cnGglStS5RE3RAB+gQcb30R1k3usI+Q/xyROSJHhusOus4BzqrlZ1w==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.11.3.tgz", + "integrity": "sha512-JGaYFUQq2Jebo7JOve/HZFnoxlNKKCfLSUuZKaZro+TKy2Fh1uPFeNBdCt+zzxa8+7YMkIKmDieGPIQLLuWJRg==", "dev": true, "dependencies": { - "axe-core": "~4.11.1" + "axe-core": "~4.11.4" }, "peerDependencies": { "selenium-webdriver": ">3.0.0-beta || >=2.53.1 || >4.0.0-alpha" @@ -178,9 +168,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -593,6 +583,22 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", @@ -1123,9 +1129,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -1543,18 +1549,19 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.29.0", + "@babel/compat-data": "^7.29.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", @@ -1586,7 +1593,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", @@ -1744,9 +1751,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", "dev": true, "funding": [ { @@ -1767,9 +1774,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", - "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", "dev": true, "funding": [ { @@ -1783,7 +1790,7 @@ ], "dependencies": { "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.1.1" + "@csstools/css-calc": "^3.2.0" }, "engines": { "node": ">=20.19.0" @@ -1816,9 +1823,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", "dev": true, "funding": [ { @@ -1829,7 +1836,15 @@ "type": "opencollective", "url": "https://opencollective.com/csstools" } - ] + ], + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -1999,9 +2014,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.11.0.tgz", - "integrity": "sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -2426,18 +2441,18 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.2.1.tgz", - "integrity": "sha512-QdfpQFIwYrTK8lFsII4bJ1AO1ZLbw7B+oxfP+/qSsiTrVerFp7aY2O+d2GNGrTxP58ezEbjbf7mTTLMsd7M7XQ==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.1", @@ -2533,10 +2548,14 @@ "dev": true }, "node_modules/@types/node": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-4.9.5.tgz", - "integrity": "sha512-+8fpgbXsbATKRF2ayAlYhPl2E9MPdLjrnK/79ZEpyPJ+k7dZwJm9YM8FK+l4rqL//xHk7PgQhGwz6aA2ckxbCQ==", - "dev": true + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -2785,18 +2804,18 @@ "dev": true }, "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "dev": true, "dependencies": { "follow-redirects": "^1.15.11", @@ -4355,13 +4374,13 @@ "dev": true }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -4379,30 +4398,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssstyle": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", - "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", - "dev": true, - "dependencies": { - "@asamuzakjp/css-color": "^5.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -4955,15 +4950,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", - "dev": true, - "dependencies": { - "undici-types": "~6.20.0" - } - }, "node_modules/engine.io/node_modules/ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", @@ -5488,33 +5474,6 @@ "es5-ext": "~0.10.14" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/event-stream/node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/eventemitter2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", @@ -5945,12 +5904,6 @@ "node": ">= 6" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -6398,9 +6351,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "engines": { "node": ">=18" @@ -6480,9 +6433,9 @@ "dev": true }, "node_modules/grunt": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", - "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.2.tgz", + "integrity": "sha512-bUzh5nA/P5L66ihXTDP6J5BGnMB/8lXJXejYWSbH4Y4TvWM9t2S39sggQDYYQlx06cYcCsmu63HMYHGCIzUVfg==", "dev": true, "dependencies": { "dateformat": "~4.6.2", @@ -6490,14 +6443,14 @@ "exit": "~0.1.2", "findup-sync": "~5.0.0", "glob": "~7.1.6", - "grunt-cli": "~1.4.3", + "grunt-cli": "^1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" + "minimatch": "^3.1.5", + "nopt": "^5.0.0" }, "bin": { "grunt": "bin/grunt" @@ -6787,18 +6740,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/grunt/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/grunt/node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7334,17 +7275,6 @@ } } }, - "node_modules/inquirer/node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~7.10.0" - } - }, "node_modules/inquirer/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7386,14 +7316,6 @@ "node": ">=8" } }, - "node_modules/inquirer/node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/inquirer/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -8007,35 +7929,35 @@ } }, "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -8058,6 +7980,15 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8997,12 +8928,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -9082,9 +9007,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true }, "node_modules/mdurl": { @@ -9655,15 +9580,18 @@ "dev": true }, "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/normalize-package-data": { @@ -10267,24 +10195,24 @@ } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -10485,15 +10413,6 @@ "node": "*" } }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "dependencies": { - "through": "~2.3" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -10581,9 +10500,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -10647,21 +10566,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11018,6 +10922,46 @@ "node": ">=8.10.0" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -11340,9 +11284,9 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", - "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz", + "integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==", "dev": true, "funding": [ { @@ -11358,7 +11302,7 @@ "@bazel/runfiles": "^6.5.0", "jszip": "^3.10.1", "tmp": "^0.2.5", - "ws": "^8.19.0" + "ws": "^8.20.0" }, "engines": { "node": ">= 20.0.0" @@ -11547,16 +11491,15 @@ "dev": true }, "node_modules/sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "21.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.1.2.tgz", + "integrity": "sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.3.2", + "@sinonjs/samsam": "^10.0.2", + "diff": "^8.0.4" }, "funding": { "type": "opencollective", @@ -11943,19 +11886,19 @@ } }, "node_modules/start-server-and-test": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz", - "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-3.0.2.tgz", + "integrity": "sha512-g6v4zPr1RRL5XxXJ+Wnk1GFLb+DGZLjFqse+5lNZ0X7m4SRMC6eOA+AXYboQDfNCEjpnTu0AGrvJb/JTUOg8dQ==", "dev": true, "dependencies": { - "arg": "^5.0.2", + "arg": "5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", "debug": "4.4.3", "execa": "5.1.1", "lazy-ass": "1.6.0", - "ps-tree": "1.2.0", - "wait-on": "9.0.4" + "tree-kill": "1.2.2", + "wait-on": "9.0.5" }, "bin": { "server-test": "src/bin/start.js", @@ -11963,7 +11906,7 @@ "start-test": "src/bin/start.js" }, "engines": { - "node": ">=16" + "node": "^22 || >=24" } }, "node_modules/start-server-and-test/node_modules/debug": { @@ -11998,15 +11941,6 @@ "node": ">= 0.6" } }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1" - } - }, "node_modules/streamroller": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz", @@ -12376,6 +12310,13 @@ "next-tick": "1" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tiny-lr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", @@ -12409,21 +12350,21 @@ } }, "node_modules/tldts": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", - "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", + "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", "dev": true, "dependencies": { - "tldts-core": "^7.0.16" + "tldts-core": "^7.0.29" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", - "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", + "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", "dev": true }, "node_modules/tmp": { @@ -12457,9 +12398,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "dependencies": { "tldts": "^7.0.5" @@ -12480,6 +12421,15 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -12557,9 +12507,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -12650,19 +12600,20 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -12872,14 +12823,14 @@ } }, "node_modules/wait-on": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", - "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.5.tgz", + "integrity": "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA==", "dev": true, "dependencies": { - "axios": "^1.13.5", - "joi": "^18.0.2", - "lodash": "^4.17.23", + "axios": "^1.15.0", + "joi": "^18.1.2", + "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, @@ -12890,6 +12841,12 @@ "node": ">=20.0.0" } }, + "node_modules/wait-on/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, "node_modules/wcag-act-rules": { "resolved": "git+ssh://git@github.com/w3c/wcag-act-rules.git#5adb55d19eb2cd7fb23213b2d1acedf9004dc063", "integrity": "sha512-LbyKmuAqKyKj6lni8qFmt7GnihTUOus8y0U0CEavtTYXeg7rXaaefBP4FZZFHLyIK6Ja7CvCXs7hUOJGl1QYnA==", @@ -13136,9 +13093,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "engines": { "node": ">=10.0.0" @@ -13350,54 +13307,38 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true - }, "@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "requires": { - "@csstools/css-calc": "^3.1.1", - "@csstools/css-color-parser": "^4.0.2", + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" - }, - "dependencies": { - "lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true - } + "@csstools/css-tokenizer": "^4.0.0" } }, "@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - }, - "dependencies": { - "lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true - } + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" } }, + "@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true + }, "@asamuzakjp/nwsapi": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", @@ -13405,12 +13346,12 @@ "dev": true }, "@axe-core/webdriverjs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.11.1.tgz", - "integrity": "sha512-UVq7uBsqaQlHj7PQPg+WhE0nV6ccYtE0cnGglStS5RE3RAB+gQcb30R1k3usI+Q/xyROSJHhusOus4BzqrlZ1w==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.11.3.tgz", + "integrity": "sha512-JGaYFUQq2Jebo7JOve/HZFnoxlNKKCfLSUuZKaZro+TKy2Fh1uPFeNBdCt+zzxa8+7YMkIKmDieGPIQLLuWJRg==", "dev": true, "requires": { - "axe-core": "~4.11.1" + "axe-core": "~4.11.4" } }, "@babel/code-frame": { @@ -13425,9 +13366,9 @@ } }, "@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true }, "@babel/core": { @@ -13721,6 +13662,16 @@ "@babel/helper-plugin-utils": "^7.27.1" } }, + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + } + }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", @@ -14046,9 +13997,9 @@ } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "requires": { "@babel/helper-module-transforms": "^7.28.6", @@ -14299,18 +14250,19 @@ } }, "@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", "dev": true, "requires": { - "@babel/compat-data": "^7.29.0", + "@babel/compat-data": "^7.29.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", @@ -14342,7 +14294,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", @@ -14460,20 +14412,20 @@ "dev": true }, "@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", "dev": true, "requires": {} }, "@csstools/css-color-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", - "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", "dev": true, "requires": { "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.1.1" + "@csstools/css-calc": "^3.2.0" } }, "@csstools/css-parser-algorithms": { @@ -14484,10 +14436,11 @@ "requires": {} }, "@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", - "dev": true + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "requires": {} }, "@csstools/css-tokenizer": { "version": "4.0.0", @@ -14593,9 +14546,9 @@ } }, "@exodus/bytes": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.11.0.tgz", - "integrity": "sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "requires": {} }, @@ -14933,18 +14886,18 @@ } }, "@sinonjs/fake-timers": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.2.1.tgz", - "integrity": "sha512-QdfpQFIwYrTK8lFsII4bJ1AO1ZLbw7B+oxfP+/qSsiTrVerFp7aY2O+d2GNGrTxP58ezEbjbf7mTTLMsd7M7XQ==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1" } }, "@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1", @@ -15039,10 +14992,13 @@ "dev": true }, "@types/node": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-4.9.5.tgz", - "integrity": "sha512-+8fpgbXsbATKRF2ayAlYhPl2E9MPdLjrnK/79ZEpyPJ+k7dZwJm9YM8FK+l4rqL//xHk7PgQhGwz6aA2ckxbCQ==", - "dev": true + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "requires": { + "undici-types": "~7.19.0" + } }, "@types/normalize-package-data": { "version": "2.4.1", @@ -15234,15 +15190,15 @@ "dev": true }, "axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", "dev": true }, "axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "dev": true, "requires": { "follow-redirects": "^1.15.11", @@ -16421,13 +16377,13 @@ "dev": true }, "css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "requires": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" } }, "css-what": { @@ -16436,26 +16392,6 @@ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true }, - "cssstyle": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", - "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", - "dev": true, - "requires": { - "@asamuzakjp/css-color": "^5.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "dependencies": { - "lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true - } - } - }, "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -16866,15 +16802,6 @@ "ws": "~8.11.0" }, "dependencies": { - "@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", - "dev": true, - "requires": { - "undici-types": "~6.20.0" - } - }, "ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", @@ -17257,32 +17184,6 @@ "es5-ext": "~0.10.14" } }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - }, - "dependencies": { - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "requires": { - "through": "2" - } - } - } - }, "eventemitter2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", @@ -17612,12 +17513,6 @@ "mime-types": "^2.1.12" } }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -17949,9 +17844,9 @@ } }, "globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true }, "globule": { @@ -18009,9 +17904,9 @@ "dev": true }, "grunt": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", - "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.2.tgz", + "integrity": "sha512-bUzh5nA/P5L66ihXTDP6J5BGnMB/8lXJXejYWSbH4Y4TvWM9t2S39sggQDYYQlx06cYcCsmu63HMYHGCIzUVfg==", "dev": true, "requires": { "dateformat": "~4.6.2", @@ -18019,14 +17914,14 @@ "exit": "~0.1.2", "findup-sync": "~5.0.0", "glob": "~7.1.6", - "grunt-cli": "~1.4.3", + "grunt-cli": "^1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" + "minimatch": "^3.1.5", + "nopt": "^5.0.0" }, "dependencies": { "argparse": { @@ -18077,15 +17972,6 @@ "esprima": "^4.0.0" } }, - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -18624,17 +18510,6 @@ "iconv-lite": "^0.6.3" } }, - "@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "undici-types": "~7.10.0" - } - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -18667,14 +18542,6 @@ "strip-ansi": "^6.0.1" } }, - "undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "optional": true, - "peer": true - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -19110,31 +18977,31 @@ } }, "jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", + "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "requires": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", + "@asamuzakjp/css-color": "^5.1.5", + "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "dependencies": { @@ -19146,6 +19013,12 @@ "requires": { "@exodus/bytes": "^1.6.0" } + }, + "lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true } } }, @@ -19858,12 +19731,6 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, "markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -19918,9 +19785,9 @@ } }, "mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true }, "mdurl": { @@ -20350,9 +20217,9 @@ "dev": true }, "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, "requires": { "abbrev": "1" @@ -20807,18 +20674,18 @@ "dev": true }, "parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "requires": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "dependencies": { "entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true } } @@ -20971,15 +20838,6 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "requires": { - "through": "~2.3" - } - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -21051,9 +20909,9 @@ "dev": true }, "prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true }, "pretty-bytes": { @@ -21098,15 +20956,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "requires": { - "event-stream": "=3.3.4" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -21385,6 +21234,36 @@ "picomatch": "^2.2.1" } }, + "recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "requires": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "dependencies": { + "ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -21632,15 +21511,15 @@ "dev": true }, "selenium-webdriver": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", - "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz", + "integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==", "dev": true, "requires": { "@bazel/runfiles": "^6.5.0", "jszip": "^3.10.1", "tmp": "^0.2.5", - "ws": "^8.19.0" + "ws": "^8.20.0" } }, "semver": { @@ -21790,16 +21669,15 @@ "dev": true }, "sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "21.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.1.2.tgz", + "integrity": "sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.3.2", + "@sinonjs/samsam": "^10.0.2", + "diff": "^8.0.4" }, "dependencies": { "diff": { @@ -22100,19 +21978,19 @@ } }, "start-server-and-test": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz", - "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-3.0.2.tgz", + "integrity": "sha512-g6v4zPr1RRL5XxXJ+Wnk1GFLb+DGZLjFqse+5lNZ0X7m4SRMC6eOA+AXYboQDfNCEjpnTu0AGrvJb/JTUOg8dQ==", "dev": true, "requires": { - "arg": "^5.0.2", + "arg": "5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", "debug": "4.4.3", "execa": "5.1.1", "lazy-ass": "1.6.0", - "ps-tree": "1.2.0", - "wait-on": "9.0.4" + "tree-kill": "1.2.2", + "wait-on": "9.0.5" }, "dependencies": { "debug": { @@ -22138,15 +22016,6 @@ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, "streamroller": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz", @@ -22428,6 +22297,12 @@ "next-tick": "1" } }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, "tiny-lr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", @@ -22460,18 +22335,18 @@ "dev": true }, "tldts": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", - "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", + "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", "dev": true, "requires": { - "tldts-core": "^7.0.16" + "tldts-core": "^7.0.29" } }, "tldts-core": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", - "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", + "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", "dev": true }, "tmp": { @@ -22496,9 +22371,9 @@ "dev": true }, "tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "requires": { "tldts": "^7.0.5" @@ -22513,6 +22388,12 @@ "punycode": "^2.3.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -22569,9 +22450,9 @@ "dev": true }, "typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true }, "ua-parser-js": { @@ -22627,15 +22508,15 @@ } }, "undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true }, "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true }, "unicode-canonical-property-names-ecmascript": { @@ -22783,16 +22664,24 @@ } }, "wait-on": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", - "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.5.tgz", + "integrity": "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA==", "dev": true, "requires": { - "axios": "^1.13.5", - "joi": "^18.0.2", - "lodash": "^4.17.23", + "axios": "^1.15.0", + "joi": "^18.1.2", + "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" + }, + "dependencies": { + "lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + } } }, "wcag-act-rules": { @@ -22989,9 +22878,9 @@ "dev": true }, "ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 9bf378ba59..6c3591657c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "axe-core", "description": "Accessibility engine for automated Web UI testing", - "version": "4.11.4", + "version": "4.12.0", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -61,7 +61,8 @@ "axe.d.ts", "sri-history.json", "locales/", - "LICENSE-3RD-PARTY.txt" + "LICENSE-3RD-PARTY.txt", + "gather-internals.js" ], "standard-version": { "scripts": { @@ -90,6 +91,7 @@ "test:unit:api": "npm run test:unit -- testDirs=api", "test:unit:integration": "npm run test:unit -- testDirs=integration", "test:unit:virtual-rules": "npm run test:unit -- testDirs=virtual-rules", + "test:unit:gather-internals": "npm run test:unit -- testDirs=gather-internals", "integration": "node test/integration/full/test-webdriver.js", "integration:apg": "mocha --fail-zero test/aria-practices/*.spec.js", "integration:chrome": "npm run integration -- browser=Chrome", @@ -110,6 +112,7 @@ "rule-gen": "node build/rule-generator", "sri-update": "grunt build && node build/sri-update && git add sri-history.json", "sri-validate": "node build/sri-update --validate", + "codemod:modernize-tests": "node build/codemods/modernize-test-files.mjs", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "prepare": "husky && npm run patch", @@ -124,7 +127,7 @@ "@babel/preset-env": "^7.20.2", "@babel/runtime-corejs3": "^7.20.7", "@deque/dot": "^1.1.5", - "@types/node": "^4.9.5", + "@types/node": "^25.6.0", "aria-practices": "github:w3c/aria-practices#ce0336bd82d7d3651abcbde86af644197ddbc629", "aria-query": "^5.1.3", "chai": "^4.3.7", @@ -158,7 +161,7 @@ "inquirer": "^8.2.5", "jquery": "^4.0.0", "jsdoc": "^4.0.2", - "jsdom": "^28.1.0", + "jsdom": "^29.0.2", "karma": "^6.4.1", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^3.1.1", @@ -173,17 +176,19 @@ "node-notifier": "^10.0.1", "npm-run-all": "^4.1.5", "outdent": "^0.8.0", + "parse5": "^8.0.0", "patch-package": "^8.0.0", "prettier": "^3.0.3", + "recast": "^0.23.11", "revalidator": "^0.3.1", "selenium-webdriver": "^4.7.1", "serve-handler": "^6.1.5", "sinon": "^21.0.0", "sri-toolbox": "^0.2.0", "standard-version": "^9.5.0", - "start-server-and-test": "^2.0.1", + "start-server-and-test": "^3.0.2", "typedarray": "^0.0.7", - "typescript": "^5.2.2", + "typescript": "^6.0.3", "uglify-js": "^3.17.4", "wcag-act-rules": "github:w3c/wcag-act-rules#5adb55d19eb2cd7fb23213b2d1acedf9004dc063", "weakmap-polyfill": "^2.0.4" diff --git a/sri-history.json b/sri-history.json index c9cab782fd..d5dfbc9984 100644 --- a/sri-history.json +++ b/sri-history.json @@ -414,5 +414,9 @@ "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=" } } diff --git a/test/checks/aria/aria-allowed-attr-elm.js b/test/checks/aria/aria-allowed-attr-elm.js index 2d90568e2f..e51ad40982 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 b25b9df4a1..224240766d 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 7c062b89ac..43090a5460 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 166faec839..aad9566c74 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 2a3f6dc4e6..68be521c26 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 a84f86e57f..fc8b9a138a 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 6ac88a4ea3..53db0b3167 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 75fbaf9a8e..3895ac6948 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 b98439bc28..d63b24baaf 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 3dda24c32d..2645c5c958 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 cef7ea31f3..ebfcefb4db 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 b40a29e362..3a6498de0e 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 199ebbe89e..019f0b27d3 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 7402707163..0f2cc62d9d 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 62d860deb4..b725dbb0db 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 8cfab3a17b..ec0b109bdd 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( - `