diff --git a/.circleci/config.yml b/.circleci/config.yml index f2f20d6f9..fce6e4a44 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ defaults: &defaults unix_box: &unix_box docker: - - image: cimg/node:16.18-browsers + - image: cimg/node:22.16-browsers unix_nightly_box: &unix_nightly_box docker: @@ -13,7 +13,7 @@ unix_nightly_box: &unix_nightly_box orbs: puppeteer: threetreeslight/puppeteer@0.1.2 - browser-tools: circleci/browser-tools@1.4.4 + browser-tools: circleci/browser-tools@1.5.1 set_npm_auth: &set_npm_auth run: npm config set "//registry.npmjs.org/:_authToken" $NPM_AUTH @@ -33,9 +33,7 @@ restore_build: &restore_build commands: browser-tools-job: steps: - - browser-tools/install-browser-tools: - # TODO: remove when chromedriver downloads are fixed - chrome-version: 116.0.5845.96 + - browser-tools/install-browser-tools jobs: # Fetch and cache dependencies. @@ -57,19 +55,21 @@ jobs: - browser-tools-job - <<: *set_npm_auth - run: npm ci + - run: npm rebuild esbuild --ignore-scripts=false --foreground-scripts - run: npx browser-driver-manager install chromedriver --verbose - save_cache: key: v9-cache-unix-{{ checksum "package-lock.json" }} paths: - node_modules - # Build and cache axe.js + # Build and cache built files build_unix: <<: *defaults <<: *unix_box steps: - checkout - <<: *restore_dependency_cache_unix + - run: npm run prepare - run: npm run build - save_cache: key: v9-cache-build-<< pipeline.git.revision >> @@ -185,8 +185,9 @@ jobs: - run: name: Install Firefox Nightly command: | - wget -O firefox-nightly.tar.bz2 "https://download.mozilla.org/?product=firefox-nightly-latest-ssl&os=linux64&lang=en-US" - tar xf firefox-nightly.tar.bz2 + # Assumes Firefox >= 135; in earlier versions, this resolves to a .tar.bz2 instead + wget -O firefox-nightly.tar.xz "https://download.mozilla.org/?product=firefox-nightly-latest-ssl&os=linux64&lang=en-US" + tar xf firefox-nightly.tar.xz - run: name: Set Environment Variable command: echo "export FIREFOX_NIGHTLY_BIN=$(pwd)/firefox/firefox-bin" >> $BASH_ENV @@ -243,15 +244,15 @@ jobs: - <<: *restore_dependency_cache_unix - run: npm run test:rule-help-version - # Test node API - test_node: + # Test jsdom API + test_jsdom: <<: *defaults <<: *unix_box steps: - checkout - <<: *restore_dependency_cache_unix - <<: *restore_build - - run: npm run test:node + - run: npm run test:jsdom # Release a "next" version next_release: @@ -363,7 +364,7 @@ workflows: - test_rule_help_version: requires: - build_unix - - test_node: + - test_jsdom: requires: - build_unix # Verify the sri history is correct @@ -388,7 +389,7 @@ workflows: - test_virtual_rules - build_api_docs - test_rule_help_version - - test_node + - test_jsdom - verify_sri filters: branches: @@ -406,7 +407,7 @@ workflows: - test_virtual_rules - build_api_docs - test_rule_help_version - - test_node + - test_jsdom filters: branches: only: develop diff --git a/.eslintignore b/.eslintignore index ca1d987e9..df541335e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,5 +7,6 @@ doc/api/* doc/examples/jest_react/*.js lib/core/imports/*.js +lib/core/utils/uuid.js axe.js -axe.min.js \ No newline at end of file +axe.min.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index d5dd272f2..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,157 +0,0 @@ -module.exports = { - root: true, - extends: ['prettier'], - parserOptions: { - ecmaVersion: 2021 - }, - env: { - node: true, - es6: true - }, - globals: { - axe: true, - Promise: true - }, - rules: { - 'no-bitwise': 2, - camelcase: 2, - curly: 2, - eqeqeq: 2, - 'guard-for-in': 2, - 'wrap-iife': [2, 'any'], - 'no-use-before-define': [ - 2, - { - functions: false - } - ], - 'new-cap': 2, - 'no-caller': 2, - 'no-empty': 2, - 'no-new': 2, - 'no-plusplus': 0, - 'no-undef': 2, - 'no-unused-vars': 2, - strict: 0, - 'max-params': [2, 6], - 'max-depth': [2, 5], - 'max-len': 0, - semi: 0, - 'no-cond-assign': 0, - 'no-debugger': 2, - 'no-eq-null': 0, - 'no-eval': 2, - 'no-unused-expressions': 0, - 'block-scoped-var': 0, - 'no-iterator': 0, - 'linebreak-style': 0, - 'no-loop-func': 0, - 'no-multi-str': 0, - 'no-proto': 0, - 'no-script-url': 0, - 'dot-notation': 2, - 'no-new-func': 0, - 'no-new-wrappers': 0, - 'no-shadow': 2, - 'no-restricted-syntax': [ - 'error', - { - selector: 'MemberExpression[property.name=tagName]', - message: "Don't use node.tagName, use node.nodeName instead." - }, - { - // node.attributes can be clobbered so is unsafe to use - // @see https://github.com/dequelabs/axe-core/pull/1432 - selector: - 'MemberExpression[object.name=node][property.name=attributes]', - message: - "Don't use node.attributes, use node.hasAttributes() or axe.utils.getNodeAttributes(node) instead." - } - ] - }, - overrides: [ - { - files: ['lib/**/*.js'], - excludedFiles: ['lib/core/reporters/**/*.js', 'lib/**/*-after.js'], - parserOptions: { - sourceType: 'module' - }, - env: { - // do not access global window properties without going through window - browser: false, - es6: true - }, - globals: { - window: true, - document: true - }, - rules: { - 'func-names': [2, 'as-needed'], - 'prefer-const': 2, - 'no-use-before-define': 'off' - } - }, - { - // after functions and reporters will not be run inside the same context as axe.run so should not access browser globals that require context specific information (window.location, window.getComputedStyles, etc.) - files: ['lib/**/*-after.js', 'lib/core/reporters/**/*.js'], - parserOptions: { - sourceType: 'module' - }, - env: { - browser: false - }, - globals: {}, - rules: { - 'func-names': [2, 'as-needed'], - 'prefer-const': 2, - 'no-use-before-define': 'off' - } - }, - { - // polyfills are mostly copy-pasted from sources so we don't control their styling - files: [ - 'lib/core/imports/polyfills.js', - 'lib/core/utils/pollyfill-elements-from-point.js' - ], - env: { - browser: false - }, - rules: { - 'func-names': 0, - 'no-bitwise': 0, - curly: 0, - eqeqeq: 0 - } - }, - { - files: ['test/act-rules/**/*.js', 'test/aria-practices/**/*.js'], - env: { - mocha: true - } - }, - { - files: ['test/**/*.js'], - excludedFiles: ['test/act-rules/**/*.js', 'test/aria-practices/**/*.js'], - parserOptions: { - ecmaVersion: 2021 - }, - env: { - browser: true, - es6: false, - mocha: true - }, - globals: { - assert: true, - helpers: true, - checks: true, - sinon: true - }, - plugins: ['mocha-no-only'], - rules: { - 'new-cap': 0, - 'no-use-before-define': 0, - 'mocha-no-only/mocha-no-only': ['error'] - } - } - ] -}; diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..b234e24e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ + + +Product: <> + +Expectation: << Describe what you expect the product to do >> + +Actual: << Describe what the product actually does >> + +Motivation: << Describe why you want the behavior to be changed >> + +

+axe-core version: X.Y.Z
+axe-webdriver, extension or other integration version: X.Y.Z
+
+Browser and Assistive Technology versions
+
+For Tooling issues:
+- Node version: XX  
+- Platform:  
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..4ed3712ff --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,60 @@ +version: 2 + +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + open-pull-requests-limit: 10 + commit-message: + prefix: 'chore' + groups: + # Any updates not caught by the group config will get individual PRs + gha-low-risk: + update-types: + - 'minor' + - 'patch' + cooldown: + default-days: 7 + + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + open-pull-requests-limit: 10 + commit-message: + prefix: 'chore' + ignore: + # Prevent updates to ESM-only versions + - dependency-name: 'chalk' + versions: ['>=5.0.0'] + - dependency-name: 'execa' + versions: ['>=6.0.0'] + - dependency-name: 'inquirer' + versions: ['>=9.0.0'] + - dependency-name: 'chai' + versions: ['>=5.0.0'] + - dependency-name: 'conventional-commits-parser' + versions: ['>=6.0.0'] + # Prevent Webpack error caused by v0.11+ of esbuild + # @see https://github.com/dequelabs/axe-core/issues/3771 + - dependency-name: 'esbuild' + versions: ['>=0.11.0'] + # Prevent colorjs.io issue caused by >v0.4.3 + # @see https://github.com/dequelabs/axe-core/issues/4428 + - dependency-name: 'colorjs.io' + versions: ['>0.4.3'] + # Still need to support node 18 + - dependency-name: 'glob' + versions: ['>=11.0.0'] + # Use node 4 types for backward compatibility + - dependency-name: '@types/node' + versions: ['>=5.0.0'] + groups: + # Any updates not caught by the group config will get individual PRs + npm-low-risk: + update-types: + - 'minor' + - 'patch' + cooldown: + default-days: 7 diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml new file mode 100644 index 000000000..4c0570b79 --- /dev/null +++ b/.github/workflows/Semgrep.yml @@ -0,0 +1,44 @@ +# Name of this GitHub Actions workflow. +name: Semgrep + +permissions: + contents: read + security-events: write + +on: + # Scan changed files in PRs (diff-aware scanning): + pull_request: {} + # Scan on-demand through GitHub Actions interface: + workflow_dispatch: {} + # Scan mainline branches and report all findings: + push: + branches: ['master', 'main'] + # Schedule the CI job (this method uses cron syntax): + schedule: + - cron: '20 17 * * *' # Sets Semgrep to scan every day at 17:20 UTC. + # It is recommended to change the schedule to a random time. + +jobs: + semgrep: + # User definable name of this GitHub Actions job. + name: semgrep/ci + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-latest + + container: + # A Docker image with Semgrep installed. Do not change this. + image: semgrep/semgrep + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. Use either v3 or v4. + - uses: actions/checkout@v4 + # Run the "semgrep ci" command on the command line of the docker image. + - run: semgrep ci + env: + # Connect to Semgrep Cloud Platform through your SEMGREP_APP_TOKEN. + # Generate a token from Semgrep Cloud Platform > Settings + # and add it to your GitHub secrets. + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} diff --git a/.github/workflows/auto-assign-issues.yml b/.github/workflows/auto-assign-issues.yml new file mode 100644 index 000000000..832601f08 --- /dev/null +++ b/.github/workflows/auto-assign-issues.yml @@ -0,0 +1,18 @@ +name: Auto assign axe-core integration issues to Axe API team project board + +on: + issues: + types: [opened] +env: + MY_GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} + +jobs: + assign_one_project: + runs-on: ubuntu-latest + name: Auto assign axe-core integration issues to Axe API team project board + steps: + - name: Auto assign axe-core integration issues to Axe API team project board + uses: srggrs/assign-one-project-github-action@1.2.1 + with: + project: https://github.com/orgs/dequelabs/projects/53 + column_name: 'Ungroomed' diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 49e317eef..ac7402759 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,31 +1,34 @@ name: Formatter -on: [pull_request] +on: + pull_request: + branches: + - develop jobs: prettier: + # This conditional prevents running the job on PRs from forks; won't + # have permissions to commit changes, so the job would fail if it ran. + # PRs from forks will instead rely on failing the fmt_check job in test.yml + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + timeout-minutes: 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.ref }} - name: Install dependencies run: npm ci - - name: Cache node_modules - uses: actions/cache@v3 + - uses: actions/setup-node@v5 with: - path: | - ./node_modules - key: npm-workspace-cache-${{ runner.os }}-${{ hashFiles('./package-lock.json') }} - - uses: actions/setup-node@v1 - with: - node-version: 16 + node-version-file: .nvmrc + cache: 'npm' # Workflows are not allowed to edit workflows. As result, we need to prevent Prettier from formatting them. - name: Prevent workflows from being formatted - run: echo ".github" >> .prettierignore + run: echo ".github" >> .prettierignore && cat .prettierignore - run: npm run fmt # Prevent the prettierignore change from being committed. - run: git checkout .prettierignore - - uses: stefanzweifel/git-auto-commit-action@v4 + - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # tag=v5 with: commit_message: ':robot: Automated formatting fixes' diff --git a/.github/workflows/label-extension-linter-issues.yml b/.github/workflows/label-extension-linter-issues.yml index c0505e81f..fee190e94 100644 --- a/.github/workflows/label-extension-linter-issues.yml +++ b/.github/workflows/label-extension-linter-issues.yml @@ -11,13 +11,13 @@ jobs: issues: write steps: - name: Label Extension Issues - uses: andymckay/labeler@1.0.4 + uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # tag=1 if: contains(toJson(github.event.issue.body), '### Product\n\naxe Extension\n\n') with: add-labels: 'extension' repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Label Linting Issues - uses: andymckay/labeler@1.0.4 + uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # tag=1 if: contains(toJson(github.event.issue.body), '### Product\n\naxe Linter\n\n') with: add-labels: 'linting' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1926d1331..a2b765b7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,16 +7,13 @@ jobs: name: Create release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/cache@v3 + - uses: actions/setup-node@v5 with: - path: ./node_modules - key: npm-cache-${{ runner.os }}-${{ hashFiles('./package-lock.json') }} - - uses: actions/setup-node@v3 - with: - node-version: 16 + node-version-file: .nvmrc + cache: 'npm' - name: Run release script and open PR run: | git config user.name "API Team CI User" @@ -42,11 +39,9 @@ jobs: --output-indicator-new=! CHANGELOG.md | egrep '^!' | awk -F'^[!]' '{print $2}' | sed -e 's/\n/$0A/g' ) - echo "chore(release): v$Version" > /tmp/pr.txt - echo "" >> /tmp/pr.txt echo "$ReleaseNotes" >> /tmp/pr.txt echo "" >> /tmp/pr.txt echo "This PR was opened by a robot :robot: :tada:" >> /tmp/pr.txt - hub pull-request --file /tmp/pr.txt --base master + gh pr create --title "chore(release): v$Version" --body-file "/tmp/pr.txt" --base master env: GITHUB_TOKEN: ${{ secrets.PAT }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..bf8d43fcd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + - develop + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: 'npm' + - run: npm ci + - run: npm rebuild esbuild --ignore-scripts=false --foreground-scripts + - run: npm run prepare + - run: npm run build + - uses: actions/upload-artifact@v4 + with: + name: axe-core + path: axe.js + retention-days: 1 + + fmt_check: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: 'npm' + - run: npm ci + - run: npm run fmt:check + + test_node: + strategy: + matrix: + node: [6, 18, 20, 22] + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: build + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node}} + - uses: actions/download-artifact@v5 + with: + name: axe-core + - run: npm run test:node diff --git a/.github/workflows/update-generated-files.yaml b/.github/workflows/update-generated-files.yaml index dc2889cdd..f10368fa9 100644 --- a/.github/workflows/update-generated-files.yaml +++ b/.github/workflows/update-generated-files.yaml @@ -9,13 +9,17 @@ env: BRANCH_NAME: sync-generated-files jobs: - update_genereated_files: + update_generated_files: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: 'npm' - name: Build run: | @@ -28,7 +32,7 @@ jobs: changes=$(git status --porcelain) # see https://unix.stackexchange.com/a/509498 echo $changes | grep . && echo "Changes detected" || echo "No changes" - echo ::set-output name=changes::"$changes" + echo "changes=$changes" >> "$GITHUB_OUTPUT" - name: Check branch exists id: branchExists @@ -36,7 +40,7 @@ jobs: run: | exists=$(git ls-remote --heads origin $BRANCH_NAME) echo $exists | grep . && echo "Branch '$BRANCH_NAME' already exists on remote" || echo "Branch does not exists in remote" - echo ::set-output name=exists::"$exists" + echo "exists=$exists" >> "$GITHUB_OUTPUT" - name: Create pull request if: ${{ steps.changes.outputs.changes && !steps.branchExists.outputs.exists }} diff --git a/.gitignore b/.gitignore index fe79e6d26..9aacdeb71 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ npm-shrinkwrap.json doc/examples/*/package-lock.json .DS_Store .idea +.vscode # tmp - `dev` time generated assets tmp @@ -28,4 +29,3 @@ typings/axe-core/axe-core-tests.js # doc doc/rule-descriptions.*.md - diff --git a/.husky/pre-commit b/.husky/pre-commit index d57ebf71f..bce63161f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - npx grunt configure npx lint-staged \ No newline at end of file diff --git a/.jsdoc.json b/.jsdoc.json index 4d7c51274..df05309b3 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -12,7 +12,7 @@ "destination": "./doc/api", "encoding": "utf8", "recurse": true, - "template": "./node_modules/minami" + "template": "./node_modules/clean-jsdoc-theme" }, "plugins": ["plugins/markdown"] } diff --git a/.npmrc b/.npmrc index 0453efcd4..829ddb75b 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,11 @@ -registry=https://registry.npmjs.org \ No newline at end of file +registry=https://registry.npmjs.org + +# Supply-chain hardening directives (AXE-3444). Public repo: access is left +# unset (access=restricted applies to private repos only). +# Do NOT add tokens or secrets here; this repo installs from the public registry. +ignore-scripts=true +strict-ssl=true +save-exact=true +audit-level=high +engine-strict=true +legacy-peer-deps=false diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..2bd5a0a98 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore index 5ae2c1f2b..2bd1eb115 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ node_modules/ -doc/api \ No newline at end of file +doc/api diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 405d9dc51..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["deque-systems.vscode-axe-linter"] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa5753ef..e1efdf286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,143 @@ 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.11.0](https://github.com/dequelabs/axe-core/compare/v4.10.3...v4.11.0) (2025-10-07) + +### Features + +- add RGAA tags to rules ([#4862](https://github.com/dequelabs/axe-core/issues/4862)) ([53a925a](https://github.com/dequelabs/axe-core/commit/53a925a31bb2bf4a1584252fa7a58c0662225320)) +- **aria-prohibited-attr:** add support for fallback roles ([#4325](https://github.com/dequelabs/axe-core/issues/4325)) ([62a19a9](https://github.com/dequelabs/axe-core/commit/62a19a9f753f8c49885dafbab7a2a9468eb6571d)) +- **axe.d.ts:** add nodeSerializer typings ([#4551](https://github.com/dequelabs/axe-core/issues/4551)) ([a2f3a48](https://github.com/dequelabs/axe-core/commit/a2f3a485d5e02993c0f35762cd9d80a6ce4ced5f)), closes [#4093](https://github.com/dequelabs/axe-core/issues/4093) +- **DqElement:** deprecate fromFrame function ([#4881](https://github.com/dequelabs/axe-core/issues/4881)) ([374c376](https://github.com/dequelabs/axe-core/commit/374c376d0b4a043e8beaa7cc9a47521314eee02c)), closes [#4093](https://github.com/dequelabs/axe-core/issues/4093) +- **DqElement:** Truncate large `html` strings when the element has a large outerHTML string ([#4796](https://github.com/dequelabs/axe-core/issues/4796)) ([404a4fb](https://github.com/dequelabs/axe-core/commit/404a4fb24a156dc433ac9c70dbefe415d41ca980)), closes [#4544](https://github.com/dequelabs/axe-core/issues/4544) +- **get-xpath:** return proper relative selector for id ([#4846](https://github.com/dequelabs/axe-core/issues/4846)) ([1035f9e](https://github.com/dequelabs/axe-core/commit/1035f9ef134cbfc02c91c37f881eb5759f602bf3)), closes [#4845](https://github.com/dequelabs/axe-core/issues/4845) +- **i18n:** Add Portugal Portuguese translation ([#4725](https://github.com/dequelabs/axe-core/issues/4725)) ([5b6a65a](https://github.com/dequelabs/axe-core/commit/5b6a65a103188251568862f46020488cf7fd8a94)) +- incomplete with node on which an error occurred ([#4863](https://github.com/dequelabs/axe-core/issues/4863)) ([32ed8da](https://github.com/dequelabs/axe-core/commit/32ed8daad1111772559f6e1cf6c8171e83c0f299)) +- **locale:** Added ru locale ([#4565](https://github.com/dequelabs/axe-core/issues/4565)) ([067b01d](https://github.com/dequelabs/axe-core/commit/067b01d66db1d2c276f26743a8d13d5d60d33446)) +- **tap:** some best practice rules map to RGAA ([#4895](https://github.com/dequelabs/axe-core/issues/4895)) ([bc33f4c](https://github.com/dequelabs/axe-core/commit/bc33f4cf5d4d384118c08d8be1afc0c4fc9272ec)) +- **td-headers-attr:** report headers attribute referencing other elements as unsupported ([#4589](https://github.com/dequelabs/axe-core/issues/4589)) ([ec7c6c8](https://github.com/dequelabs/axe-core/commit/ec7c6c8875970388c4fe2c73147a3dd43497161e)), closes [#3987](https://github.com/dequelabs/axe-core/issues/3987) + +### Bug Fixes + +- **aria-allowed-role:** add form to allowed roles of form element ([#4588](https://github.com/dequelabs/axe-core/issues/4588)) ([8aa47ac](https://github.com/dequelabs/axe-core/commit/8aa47ac01f9959b9d47ac24dcd2fd8c88c9279f7)), closes [/github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js#L264](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js/issues/L264) +- **aria-allowed-role:** Add math to allowed roles for img element ([#4658](https://github.com/dequelabs/axe-core/issues/4658)) ([95b6c18](https://github.com/dequelabs/axe-core/commit/95b6c184872cf70c7f54a96aa813a9e8bc2c066d)), closes [#4657](https://github.com/dequelabs/axe-core/issues/4657) +- **autocomplete-valid :** Ignore readonly autocomplete field ([#4721](https://github.com/dequelabs/axe-core/issues/4721)) ([491f4ec](https://github.com/dequelabs/axe-core/commit/491f4ecdbbb79d019daa63debc03ac0efb47adf8)), closes [#4708](https://github.com/dequelabs/axe-core/issues/4708) +- **autocomplete-valid:** treat values "xon" and "xoff" as non-WCAG-violations ([#4878](https://github.com/dequelabs/axe-core/issues/4878)) ([52bc611](https://github.com/dequelabs/axe-core/commit/52bc61162aa170a30a38246ade099ba3fc10cc2a)), closes [#4877](https://github.com/dequelabs/axe-core/issues/4877) +- **axe.d.ts:** add typings for preload options object ([#4543](https://github.com/dequelabs/axe-core/issues/4543)) ([cfd2974](https://github.com/dequelabs/axe-core/commit/cfd297498c0e34edd5ff7e62935060bb9dda4db7)) +- **button-name,input-button-name,input-img-alt:** allow label to give accessible name ([#4607](https://github.com/dequelabs/axe-core/issues/4607)) ([a9710d7](https://github.com/dequelabs/axe-core/commit/a9710d757c6ca6ee0ce5d26be3427bab54b87a7a)), closes [#4472](https://github.com/dequelabs/axe-core/issues/4472) [#3696](https://github.com/dequelabs/axe-core/issues/3696) [#3696](https://github.com/dequelabs/axe-core/issues/3696) +- **captions:** fix grammar in captions check incomplete message ([#4661](https://github.com/dequelabs/axe-core/issues/4661)) ([11de515](https://github.com/dequelabs/axe-core/commit/11de515858a7c10a3d7400163fc2b834715152fc)) +- **color-contrast:** do not run on elements with font-size: 0 ([#4822](https://github.com/dequelabs/axe-core/issues/4822)) ([d77c885](https://github.com/dequelabs/axe-core/commit/d77c8854c847573597eccf54c00091a4a2134cfd)), closes [#4820](https://github.com/dequelabs/axe-core/issues/4820) +- consistently parse tabindex, following HTML 5 spec ([#4637](https://github.com/dequelabs/axe-core/issues/4637)) ([645a850](https://github.com/dequelabs/axe-core/commit/645a850f601f4f3f18cc4aaca399aad18a9fa5d2)), closes [#4632](https://github.com/dequelabs/axe-core/issues/4632) +- **core:** measure perf for async checks ([#4609](https://github.com/dequelabs/axe-core/issues/4609)) ([7e9bacf](https://github.com/dequelabs/axe-core/commit/7e9bacf1ecb8c53404fac3eeed087e370e2a9cfa)) +- fix grammar when using "alternative text" in a sentence ([#4811](https://github.com/dequelabs/axe-core/issues/4811)) ([237a586](https://github.com/dequelabs/axe-core/commit/237a5861b0fb044c885b154436696279deca7a13)), closes [#4394](https://github.com/dequelabs/axe-core/issues/4394) +- **get-ancestry:** add nth-child selector for multiple siblings of shadow root ([#4606](https://github.com/dequelabs/axe-core/issues/4606)) ([1cdd6c3](https://github.com/dequelabs/axe-core/commit/1cdd6c3e698a6a4c28604448284993c4c20ca272)), closes [#4563](https://github.com/dequelabs/axe-core/issues/4563) +- **get-ancestry:** don't error when there is no parent ([#4617](https://github.com/dequelabs/axe-core/issues/4617)) ([a005703](https://github.com/dequelabs/axe-core/commit/a0057039072f68bd605e8bacdca64692d57f612e)) +- **locale:** fix typos in japanese (ja) locale ([#4856](https://github.com/dequelabs/axe-core/issues/4856)) ([3462cc5](https://github.com/dequelabs/axe-core/commit/3462cc57e8480334c125c38b7ecb42344b405dd4)) +- **locale:** fixed typos in german (DE) locale ([#4631](https://github.com/dequelabs/axe-core/issues/4631)) ([b7736de](https://github.com/dequelabs/axe-core/commit/b7736deae9ec14a4e81182adb53be73f3cce9894)) +- **locale:** proofread and updated de.json ([#4643](https://github.com/dequelabs/axe-core/issues/4643)) ([8060ada](https://github.com/dequelabs/axe-core/commit/8060ada737a23cdf68bb5b4c95b8c0e2cca45dad)) +- **meta-viewport:** lower impact to moderate ([#4887](https://github.com/dequelabs/axe-core/issues/4887)) ([2f32aa5](https://github.com/dequelabs/axe-core/commit/2f32aa5bada78ffcfc965ed2b64be533263c6bd5)), closes [#4714](https://github.com/dequelabs/axe-core/issues/4714) +- **no-autoplay-audio:** don't timeout for preload=none media elements ([#4684](https://github.com/dequelabs/axe-core/issues/4684)) ([cdc871e](https://github.com/dequelabs/axe-core/commit/cdc871e68f3dbc6acbfed12d3ec63ea4da1a4065)) +- **performanceTimer:** throwing in axe catch clause ([#4852](https://github.com/dequelabs/axe-core/issues/4852)) ([a4ade04](https://github.com/dequelabs/axe-core/commit/a4ade04bc2ba93dcad8a24094fb0dc5edb6da8b2)), closes [/github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js#L297-L300](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js/issues/L297-L300) +- **performanceTimer:** work in frames ([#4834](https://github.com/dequelabs/axe-core/issues/4834)) ([d7dfebc](https://github.com/dequelabs/axe-core/commit/d7dfebc0271d2970c0937024ce693a771885002c)) +- **rules:** Change "alternate text" to "alternative text" ([#4582](https://github.com/dequelabs/axe-core/issues/4582)) ([b03ada3](https://github.com/dequelabs/axe-core/commit/b03ada3dd3b6490bb88e366bf807b10e1a4038a4)) +- **target-size:** do not treat focusable tabpanels as targets ([#4702](https://github.com/dequelabs/axe-core/issues/4702)) ([60d11f2](https://github.com/dequelabs/axe-core/commit/60d11f2d01b9e859e54a15ae0232b8b5d1c48d35)), closes [#4421](https://github.com/dequelabs/axe-core/issues/4421) [#4701](https://github.com/dequelabs/axe-core/issues/4701) +- **type:** correct RuleError type ([#4893](https://github.com/dequelabs/axe-core/issues/4893)) ([d1aa8e2](https://github.com/dequelabs/axe-core/commit/d1aa8e2094031159b041a3e9cf2c796a26b3daba)) +- **types:** correct raw types ([#4903](https://github.com/dequelabs/axe-core/issues/4903)) ([3eade11](https://github.com/dequelabs/axe-core/commit/3eade110a7ac173e3537e8eb18dd0db026f13c75)) + +### [4.10.3](https://github.com/dequelabs/axe-core/compare/v4.10.2...v4.10.3) (2025-03-04) + +### Bug Fixes + +- **aria-allowed-role:** Add math to allowed roles for img element ([#4658](https://github.com/dequelabs/axe-core/issues/4658)) ([f6dddd9](https://github.com/dequelabs/axe-core/commit/f6dddd905bb86d2073d760d4c1ff39996e59a4f4)), closes [#4657](https://github.com/dequelabs/axe-core/issues/4657) +- **captions:** fix grammar in captions check incomplete message ([#4661](https://github.com/dequelabs/axe-core/issues/4661)) ([3ef7058](https://github.com/dequelabs/axe-core/commit/3ef7058d3a16a6898eaed718e39b34b45f9ed74f)) +- consistently parse tabindex, following HTML5 spec ([#4637](https://github.com/dequelabs/axe-core/issues/4637)) ([3b0a361](https://github.com/dequelabs/axe-core/commit/3b0a3619d5e6147d8885725cce196868dac89f7f)), closes [#4632](https://github.com/dequelabs/axe-core/issues/4632) +- **core:** measure perf for async checks ([#4609](https://github.com/dequelabs/axe-core/issues/4609)) ([e7dc26e](https://github.com/dequelabs/axe-core/commit/e7dc26e6cbaf9089611853805e05216b6529e1f9)) +- **locale:** fixed typos in german (DE) locale ([#4631](https://github.com/dequelabs/axe-core/issues/4631)) ([0740980](https://github.com/dequelabs/axe-core/commit/07409802115c199a68f58dcaf7467e35b4867140)) +- **locale:** proofread and updated de.json ([#4643](https://github.com/dequelabs/axe-core/issues/4643)) ([910cdb2](https://github.com/dequelabs/axe-core/commit/910cdb20702ce116c781d58f021adc05095ffcbb)) +- **no-autoplay-audio:** don't timeout for preload=none media elements ([#4684](https://github.com/dequelabs/axe-core/issues/4684)) ([b7f1ad1](https://github.com/dequelabs/axe-core/commit/b7f1ad1ccf719b7149a5ef1805b405707a474e31)) +- **target-size:** do not treat focusable tabpanels as targets ([#4702](https://github.com/dequelabs/axe-core/issues/4702)) ([67d4e4f](https://github.com/dequelabs/axe-core/commit/67d4e4f7d0d0b803b66f216ff86f401649ed024f)), closes [#4421](https://github.com/dequelabs/axe-core/issues/4421) [#4701](https://github.com/dequelabs/axe-core/issues/4701) + +### [4.10.2](https://github.com/dequelabs/axe-core/compare/v4.10.1...v4.10.2) (2024-10-21) + +### Bug Fixes + +- **get-ancestry:** don't error when there is no parent ([#4617](https://github.com/dequelabs/axe-core/issues/4617)) ([6c07102](https://github.com/dequelabs/axe-core/commit/6c07102b1d29145b8dc5f1d96229f3d0b8b38068)) + +### [4.10.1](https://github.com/dequelabs/axe-core/compare/v4.10.0...v4.10.1) (2024-10-16) + +### Bug Fixes + +- **aria-allowed-role:** add form to allowed roles of form element ([#4588](https://github.com/dequelabs/axe-core/issues/4588)) ([d462d67](https://github.com/dequelabs/axe-core/commit/d462d674bb7de0848ce2695f80b95d677c5016e0)), closes [/github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js#L264](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js/issues/L264) +- **axe.d.ts:** add typings for preload options object ([#4543](https://github.com/dequelabs/axe-core/issues/4543)) ([72e269f](https://github.com/dequelabs/axe-core/commit/72e269f1e6d6039e70e614005f04ebfd3fe5aca5)) +- **button-name,input-button-name,input-img-alt:** allow label to give accessible name ([#4607](https://github.com/dequelabs/axe-core/issues/4607)) ([364eb72](https://github.com/dequelabs/axe-core/commit/364eb72bb8f20b0ffc31be24cc96cbd349c301cb)), closes [#4472](https://github.com/dequelabs/axe-core/issues/4472) [#3696](https://github.com/dequelabs/axe-core/issues/3696) [#3696](https://github.com/dequelabs/axe-core/issues/3696) +- **get-ancestry:** add nth-child selector for multiple siblings of shadow root ([#4606](https://github.com/dequelabs/axe-core/issues/4606)) ([bdd94a2](https://github.com/dequelabs/axe-core/commit/bdd94a227a95cd5b9f8e2a1e0fd259ddd702810c)), closes [#4563](https://github.com/dequelabs/axe-core/issues/4563) +- **rules:** Change "alternate text" to "alternative text" ([#4582](https://github.com/dequelabs/axe-core/issues/4582)) ([31e0f61](https://github.com/dequelabs/axe-core/commit/31e0f61ca871b3df86468577c449a02c8ece12f0)) + +## [4.10.0](https://github.com/dequelabs/axe-core/compare/v4.9.1...v4.10.0) (2024-07-29) + +### Features + +- **new-rule:** summary elements must have an accessible name ([#4511](https://github.com/dequelabs/axe-core/issues/4511)) ([0d8a99e](https://github.com/dequelabs/axe-core/commit/0d8a99eadd8d49e5d3ea0f11ad77be732148431e)), closes [#4510](https://github.com/dequelabs/axe-core/issues/4510) + +### Bug Fixes + +- **aria-allowed-attr:** allow aria-multiline=false for element with contenteditable ([#4537](https://github.com/dequelabs/axe-core/issues/4537)) ([f019068](https://github.com/dequelabs/axe-core/commit/f0190685722495d00be644cabb1c9741d74acdea)) +- **aria-allowed-attr:** allow aria-required=false when normally not allowed ([#4532](https://github.com/dequelabs/axe-core/issues/4532)) ([2e242e1](https://github.com/dequelabs/axe-core/commit/2e242e146929902c97e181e41fa45e656cf3eb51)) +- **aria-prohibited-attr:** allow aria-label/ledby on descendants of widget ([#4541](https://github.com/dequelabs/axe-core/issues/4541)) ([07c5d91](https://github.com/dequelabs/axe-core/commit/07c5d91c658bda6bcd2743950bf70f25abd1f9ae)) +- **aria-roledescription:** keep disabled with { runOnly: 'wcag2a' } ([#4526](https://github.com/dequelabs/axe-core/issues/4526)) ([5b4cb9d](https://github.com/dequelabs/axe-core/commit/5b4cb9d7992a4c07745e64708040777de64874bd)), closes [#4523](https://github.com/dequelabs/axe-core/issues/4523) +- **autocomplete-valid:** incomplete for invalid but safe values ([#4500](https://github.com/dequelabs/axe-core/issues/4500)) ([e31a974](https://github.com/dequelabs/axe-core/commit/e31a974de395845c08af345f9458a8091e2b1c4b)), closes [#4492](https://github.com/dequelabs/axe-core/issues/4492) +- **build:** limit locales to valid files when using the --all-lang option ([#4486](https://github.com/dequelabs/axe-core/issues/4486)) ([d3db593](https://github.com/dequelabs/axe-core/commit/d3db593991261ad44eef1c142d8a4646edde93fa)), closes [#4485](https://github.com/dequelabs/axe-core/issues/4485) +- Prevent errors when loading axe in Angular + Jest ([#4456](https://github.com/dequelabs/axe-core/issues/4456)) ([3ef9353](https://github.com/dequelabs/axe-core/commit/3ef93531a574c2be76a92d59599d978714cca9d0)), closes [#4400](https://github.com/dequelabs/axe-core/issues/4400) +- Minor grammatical fixes for some rules and checks ([#4499](https://github.com/dequelabs/axe-core/issues/4499)) ([11fad59](https://github.com/dequelabs/axe-core/commit/11fad598c25eadd29f35ef6be382d907057d4537)) +- **landmark-unique:** follow spec, aside -> landmark ([#4469](https://github.com/dequelabs/axe-core/issues/4469)) ([e32f803](https://github.com/dequelabs/axe-core/commit/e32f8034246a92e4132dc04f6310e2b414d6d43f)), closes [#4460](https://github.com/dequelabs/axe-core/issues/4460) +- **required-attr:** allow aria-valuetext on slider instead of valuenow ([#4518](https://github.com/dequelabs/axe-core/issues/4518)) ([135898b](https://github.com/dequelabs/axe-core/commit/135898b38d5eb46c42170527a0ac9add425c5c3d)), closes [#4515](https://github.com/dequelabs/axe-core/issues/4515) + +### [4.9.1](https://github.com/dequelabs/axe-core/compare/v4.9.0...v4.9.1) (2024-05-06) + +### Bug Fixes + +- Prevent errors when loading axe in a page with prototype.js +- **aria-allowed-attr:** allow meter role allowed aria-\* attributes on meter element ([#4435](https://github.com/dequelabs/axe-core/issues/4435)) ([7ac6392](https://github.com/dequelabs/axe-core/commit/7ac63921e7fab21f3359dcfc8affa7585bc9c25b)) +- **aria-allowed-role:** add gridcell, separator, slider and treeitem to allowed roles of button element ([#4398](https://github.com/dequelabs/axe-core/issues/4398)) ([4788bf8](https://github.com/dequelabs/axe-core/commit/4788bf8d6fd963d7b017dad950b122ffcea8d151)) +- **aria-roles:** correct abstract roles (types) for aria-roles([#4421](https://github.com/dequelabs/axe-core/pull/4421)) +- **aria-valid-attr-value:** aria-controls & aria-haspopup incomplete ([#4418](https://github.com/dequelabs/axe-core/pull/4418)) +- fix building axe-core translation files with region locales ([#4396](https://github.com/dequelabs/axe-core/issues/4396)) ([5c318f3](https://github.com/dequelabs/axe-core/commit/5c318f3537056be5779cb53374bc6f4785947c91)), closes [#4388](https://github.com/dequelabs/axe-core/issues/4388) +- **invalidrole:** allow upper and mixed case role names ([#4358](https://github.com/dequelabs/axe-core/issues/4358)) ([105016c](https://github.com/dequelabs/axe-core/commit/105016cfe9d82876cfed2ff5c656a7842c5b3761)), closes [#2695](https://github.com/dequelabs/axe-core/issues/2695) +- **isVisibleOnScreen:** account for position: absolute elements inside overflow container ([#4405](https://github.com/dequelabs/axe-core/issues/4405)) ([2940f6e](https://github.com/dequelabs/axe-core/commit/2940f6ee36ba52d8cf089be2a3c8e7c516c81dd6)), closes [#4016](https://github.com/dequelabs/axe-core/issues/4016) +- **label-content-name-mismatch:** better dismiss and wysiwyg symbolic text characters ([#4402](https://github.com/dequelabs/axe-core/issues/4402)) +- **region:** Decorative images ignored by region rule ([#4412](https://github.com/dequelabs/axe-core/pull/4412)) +- **target-size:** ignore descendant elements in shadow dom ([#4410](https://github.com/dequelabs/axe-core/issues/4410)) ([6091367](https://github.com/dequelabs/axe-core/commit/6091367a20f70e536fc7e8d77eae4fa7232bc7c0)) +- **target-size:** pass for element that has nearby elements that are obscured ([#4422](https://github.com/dequelabs/axe-core/issues/4422)) ([3a90bb7](https://github.com/dequelabs/axe-core/commit/3a90bb70c8db087b2f03cc30a4aee756995c311c)), closes [#4387](https://github.com/dequelabs/axe-core/issues/4387) + +## [4.9.0](https://github.com/dequelabs/axe-core/compare/v4.8.4...v4.9.0) (2024-03-25) + +### Features + +- adding the wcag131 tag to the aria-hidden-body rule ([#4349](https://github.com/dequelabs/axe-core/issues/4349)) ([dd4c3c3](https://github.com/dequelabs/axe-core/commit/dd4c3c34a42d2b96f5495890f5c5d5e8f6ca8d32)), closes [#4315](https://github.com/dequelabs/axe-core/issues/4315) +- **checks:** deprecate aria-busy check ([#4356](https://github.com/dequelabs/axe-core/issues/4356)) ([be0b555](https://github.com/dequelabs/axe-core/commit/be0b5558acfbeb6bbb176ac7fd7d8fdfb973b30b)), closes [#4347](https://github.com/dequelabs/axe-core/issues/4347) [#4340](https://github.com/dequelabs/axe-core/issues/4340) +- **color:** add color channel values and luminosity, saturation, clip functions ([#4366](https://github.com/dequelabs/axe-core/issues/4366)) ([9e70199](https://github.com/dequelabs/axe-core/commit/9e7019990bbbf5182ab50c5c968143b81d216dcb)), closes [/github.com/dequelabs/axe-core/pull/4365/files#r1517706612](https://github.com/dequelabs//github.com/dequelabs/axe-core/pull/4365/files/issues/r1517706612) +- **i18n:** add Greek Translations ([#3836](https://github.com/dequelabs/axe-core/issues/3836)) ([3ea9a48](https://github.com/dequelabs/axe-core/commit/3ea9a48cf88d02271db8b19651bff0415237b856)) +- **i18n:** Add Italian translation ([#4344](https://github.com/dequelabs/axe-core/issues/4344)) ([de1baa9](https://github.com/dequelabs/axe-core/commit/de1baa9a9f6495f695d25d61d14ed55983dded76)) +- **i18n:** Add Simplified Chinese translation ([#4379](https://github.com/dequelabs/axe-core/issues/4379)) ([bda7c8d](https://github.com/dequelabs/axe-core/commit/bda7c8d8bf5936a56c66240e1ea0373a3b769809)) +- **i18n:** Add Taiwanese Mandarin translation ([#4299](https://github.com/dequelabs/axe-core/issues/4299)) ([c5e11de](https://github.com/dequelabs/axe-core/commit/c5e11de06973392b113906c05e3a3004af4c38ae)) + +### Bug Fixes + +- Add LICENSE-3RD-PARTY.txt file ([#4304](https://github.com/dequelabs/axe-core/issues/4304)) ([daa0fe6](https://github.com/dequelabs/axe-core/commit/daa0fe677d4837c9c79bad8ee6c77aff11212339)) +- add Object.values polyfill for node <=6 ([#4274](https://github.com/dequelabs/axe-core/issues/4274)) ([5eb867b](https://github.com/dequelabs/axe-core/commit/5eb867b04e174140122c62eb5c705a842a3489e1)) +- **aria-required-children:** avoid confusing aria-busy message in failures ([#4347](https://github.com/dequelabs/axe-core/issues/4347)) ([591607d](https://github.com/dequelabs/axe-core/commit/591607dd829c11e2cca5beee12c75628d1a8235e)), closes [#fail13](https://github.com/dequelabs/axe-core/issues/fail13) [#4340](https://github.com/dequelabs/axe-core/issues/4340) +- avoid reading element-specific node properties of non-element node types ([#4317](https://github.com/dequelabs/axe-core/issues/4317)) ([b853b18](https://github.com/dequelabs/axe-core/commit/b853b18a24dd2d1c9408705b821cc11146ae1186)), closes [#4316](https://github.com/dequelabs/axe-core/issues/4316) [#4316](https://github.com/dequelabs/axe-core/issues/4316) +- **color-contrast:** handle text that is outside `overflow: hidden` ancestor ([#4357](https://github.com/dequelabs/axe-core/issues/4357)) ([bdb7300](https://github.com/dequelabs/axe-core/commit/bdb7300c67d451d3b0169707924a0c6bc4defe40)), closes [#4253](https://github.com/dequelabs/axe-core/issues/4253) +- **color-contrast:** support color blend modes hue, saturation, color, luminosity ([#4365](https://github.com/dequelabs/axe-core/issues/4365)) ([7ae4761](https://github.com/dequelabs/axe-core/commit/7ae476124d60eafd28d85abf48188cd85c99543a)) +- **d.ts:** RawNodesResult issues ([#4229](https://github.com/dequelabs/axe-core/issues/4229)) ([d660518](https://github.com/dequelabs/axe-core/commit/d6605181ec942bcca46e3bfe889064b3781919ca)) +- **d.ts:** RunOptions.reporter can be any string ([#4218](https://github.com/dequelabs/axe-core/issues/4218)) ([e53f5c5](https://github.com/dequelabs/axe-core/commit/e53f5c5184a0e5f75db65e7929a9da84d7ee6df6)) +- **i18n:** update Italian translations ([#4377](https://github.com/dequelabs/axe-core/issues/4377)) ([4d65d4b](https://github.com/dequelabs/axe-core/commit/4d65d4bf40f4ee2697e079451dd84a0155e8fb51)) +- **listitem:** clarify roleNotValid message ([#4374](https://github.com/dequelabs/axe-core/issues/4374)) ([0f8a9af](https://github.com/dequelabs/axe-core/commit/0f8a9af2a82d49e7d8ff3024da0e4c485ca46d38)) +- **scrollable-region-focusable:** missing wcag213 tag ([#4201](https://github.com/dequelabs/axe-core/issues/4201)) ([0080a72](https://github.com/dequelabs/axe-core/commit/0080a7255eb7f246bb7b6f53974a95b65983b83a)) +- **target-size:** always pass 10x targets (avoid perf bottleneck) ([#4376](https://github.com/dequelabs/axe-core/issues/4376)) ([be327c4](https://github.com/dequelabs/axe-core/commit/be327c422f67ac657218f711b3b799567ba3aa37)) +- **target-size:** do not crash for nodes with many overlapping widgets ([#4373](https://github.com/dequelabs/axe-core/issues/4373)) ([1dbea83](https://github.com/dequelabs/axe-core/commit/1dbea83d4749f9f71f263883869b076b0d42021f)), closes [#4359](https://github.com/dequelabs/axe-core/issues/4359) [#4359](https://github.com/dequelabs/axe-core/issues/4359) [#4360](https://github.com/dequelabs/axe-core/issues/4360) +- **utils/get-selector:** ignore 'xmlns' attribute when generating a selector ([#4303](https://github.com/dequelabs/axe-core/issues/4303)) ([938b411](https://github.com/dequelabs/axe-core/commit/938b411bb0609b54e5c46a8e5b50c9ea4de4bdee)) + ### [4.8.4](https://github.com/dequelabs/axe-core/compare/v4.8.3...v4.8.4) (2024-02-07) ### Bug Fixes diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..8b01d9218 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,7 @@ +CODEOWNERS############################################################## +# +# List of approvers/reviewers for a11y-engine-axe-core +# +############################################################## + +* @Jimesh-browserstack @sunny-se diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d692677a2..a65031166 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,46 @@ The files in this project are formatted by [Prettier](https://prettier.io/) and npm run eslint ``` +### When to use HTMLElement vs Virtual Node + +Axe-core uses an internal copy of the HTML page for most things internal to axe-core (called the Virtual Tree). Each HTML element on the page has an equivalent [Virtual Node](./lib/core/base/virtual-node) element, which allows us to cache or normalize information about the HTML element without mutating the actual DOM node. + +Typically we use the Virtual Node when possible, but understand that's not always possible (such as accessing DOM only APIs like `getRootNode` or `getBoundingClientRect`). Furthermore, any function within the [utils directory](./lib/core/utils/) should not use Virtual Nodes. The reason for this is that using Virtual Nodes requires that axe first be [setup](./lib/core/public/setup.js) and create the Virtual Tree. For the most part, util functions are functions that can be run when no Virtual Tree exists. + +### Directory Structure + +Axe-core + +- `standards` - Data objects of the HTML, WCAG, and ARIA standards and specs. +- `core/base` - Defines many of the internal object structures for axe-core (rules, checks, virtual node, etc.). +- `core/imports` - Polyfills or imports from node modules. +- `core/public` - Functions for how axe is run or configured. +- `core/reporters` - The different reporters that configure what data is reported from a run. +- `core/utils` - Utility functions that can be run without needing to set up the Virtual Tree. +- `commons/aria` - Functions that are used to validate ARIA spec or implement ARIA calculations (role, value, etc.). +- `commons/color` - Functions that are used to calculate the foreground or background color of an element. +- `commons/dom` - Functions that access information about the page, DOM, nodes, or its visual state. +- `commons/forms` - Functions for helping with forms and their associated inputs. +- `commons/matches` - Functions used to match a virtual node against a special matcher object. +- `commons/math` - Math functions mainly used for target size and position calculations. +- `commons/standards` - Functions for querying information from `standards`. +- `commons/tables` - Functions for helping with data tables. +- `commons/text` - Functions for calculating the accessible name of an element and handling strings or text related attributes. +- `rules` - JSON metadata files for each axe-core rule as well as their associated matches functions. +- `checks` - JSON metadata files for each axe-core check as well as their associated evaluate functions. + +### Import Requirements + +Which functions can be imported and how they can be imported depends on where you are trying to import them from. The following is a list of directories and their import restrictions: + +- `standards` - Shouldn't use imports as they are just hard coded data objects. +- `core/utils` - Can import other `core/utils`, `core`, `core/base`, or `standards` functions by direct file path (no import from index files). +- `core/public` - Can import other `core/public` by direct file path, or any import allowed by `core/utils` by direct file path. +- `core/imports` - The only files allowed to import from node modules, but shouldn't import from any other directory. +- `core/reporters` Can import from `core/utils` by import from index files. +- `commons` - Can import other `commons` by direct file path, or any import allowed by `core/utils` by import from index files. +- `checks` and `rules` - Can import from any directory by import from index files. + ### Shadow DOM For any proposed changes to rules, checks, commons, or other APIs to be accepted in axe-core, your code must support open Shadow DOM. See [API.md](./doc/API.md) and the [developer guide](./doc/developer-guide.md) for documentation on the available methods and test utilities. You can also look at existing tests for examples using our APIs. @@ -162,7 +202,7 @@ If you need to debug the unit tests in a browser, you can run: npm run test:debug ``` -This will start the Karma server and open up the Chrome browser. Click the `Debug` button to start debugging the tests. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. +This will start the Karma server and open up the Chrome browser. Click the `Debug` button to start debugging the tests. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](./.vscode/launch.json) is provided. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. Because the amount of tests is so large, it's recommended to debug only a specific set of unit tests rather than the whole test suite. You can use the `testDirs` argument when using the debug command and pass a specific test directory. The test directory names are the same as those used for `test:unit:*`: diff --git a/Gruntfile.js b/Gruntfile.js index 0a888fe1c..3b9872799 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -15,6 +15,7 @@ module.exports = function (grunt) { grunt.loadTasks('build/tasks'); var langs; + var wrapper = !!grunt.option('wrapper'); if (grunt.option('lang')) { langs = (grunt.option('lang') || '').split(/[,;]/g).map(function (lang) { lang = lang.trim(); @@ -22,9 +23,13 @@ module.exports = function (grunt) { }); } else if (grunt.option('all-lang')) { var localeFiles = require('fs').readdirSync('./locales'); - langs = localeFiles.map(function (file) { - return '.' + file.replace('.json', ''); - }); + langs = localeFiles + .filter(function (file) { + return !file.startsWith('_') && file.endsWith('.json'); + }) + .map(function (file) { + return '.' + file.replace('.json', ''); + }); langs.unshift(''); // Add default } else { langs = ['']; @@ -85,11 +90,11 @@ module.exports = function (grunt) { files: langs.map(function (lang, i) { return { src: [ - 'lib/intro.stub', + wrapper ? 'lib/custom/intro.stub' : 'lib/intro.stub', '<%= concat.engine.coreFiles %>', // include rules / checks / commons '<%= configure.rules.files[' + i + '].dest.auto %>', - 'lib/outro.stub' + wrapper ? 'lib/custom/outro.stub' : 'lib/outro.stub' ], dest: 'axe' + lang + '.js' }; @@ -127,7 +132,8 @@ module.exports = function (grunt) { entry: 'lib/commons/aria/index.js', destFile: 'doc/aria-supported.md', options: { - langs: langs + langs: langs, + wrapper: wrapper }, listType: 'unsupported' // Possible values for listType: 'supported', 'unsupported', 'all' } diff --git a/README.md b/README.md index 5697270b1..505643baa 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,26 @@ -# axe-core +# a11y-engine-axe-core -[![License](https://img.shields.io/npm/l/axe-core.svg)](LICENSE) +[![License](https://img.shields.io/npm/l/axe-core.svg?color=c41)](LICENSE) [![Version](https://img.shields.io/npm/v/axe-core.svg)](https://www.npmjs.com/package/axe-core) -[![Total npm downloads](https://img.shields.io/npm/dt/axe-core.svg)](https://www.npmjs.com/package/axe-core) +[![NPM downloads](https://img.shields.io/npm/dw/axe-core.svg?color=080)![](https://img.shields.io/npm/dy/axe-core.svg?color=080&label=)](https://npm-stat.com/charts.html?package=axe-core&from=2017-01-01) [![Commits](https://img.shields.io/github/commit-activity/y/dequelabs/axe-core.svg)](https://github.com/dequelabs/axe-core/commits/develop) -[![GitHub contributors](https://img.shields.io/github/contributors/dequelabs/axe-core.svg)](https://github.com/dequelabs/axe-core/graphs/contributors) +[![GitHub contributors](https://img.shields.io/github/contributors/dequelabs/axe-core.svg?color=080)](https://github.com/dequelabs/axe-core/graphs/contributors) [![Join our Slack chat](https://img.shields.io/badge/slack-chat-purple.svg?logo=slack)](https://accessibility.deque.com/axe-community) [![Package Quality](https://npm.packagequality.com/shield/axe-core.svg)](https://packagequality.com/#?package=axe-core) -Axe is an accessibility testing engine for websites and other HTML-based user interfaces. It's fast, secure, lightweight, and was built to seamlessly integrate with any existing test environment so you can automate accessibility testing alongside your regular functional testing. +## Pull latest changes from public axe-core repo -[Sign up for axe news](https://hubs.ly/H0fsN0b0) to get the latest on axe features, future releases, and events. - -## The Accessibility Rules - -Axe-core has different types of rules, for WCAG 2.0, 2.1, 2.2 on level A, AA and AAA as well as a number of best practices that help you identify common accessibility practices like ensuring every page has an `h1` heading, and to help you avoid "gotchas" in ARIA like where an ARIA attribute you used will get ignored. The complete list of rules, grouped WCAG level and best practice, can found in [doc/rule-descriptions.md](./doc/rule-descriptions.md). - -With axe-core, you can find **on average 57% of WCAG issues automatically**. Additionally, axe-core will return elements as "incomplete" where axe-core could not be certain, and manual review is needed. - -To catch bugs earlier in the development cycle we recommend using the [axe-linter vscode extension](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter). To improve test coverage even further we recommend the [intelligent guided tests](https://www.youtube.com/watch?v=AtsX0dPCG_4&feature=youtu.be&ab_channel=DequeSystems) in the [axe Extension](https://www.deque.com/axe/browser-extensions/). +``` +# cd into the repo +cd a11y-engine-axe-core -## Getting started +# Add public axe-core repo as remote public +git remote add public git@github.com:dequelabs/axe-core.git -First download the package: +Axe-core has different types of rules, for WCAG 2.0, 2.1, 2.2 on level A, AA and AAA as well as a number of best practices that help you identify common accessibility practices like ensuring every page has an `h1` heading, and to help you avoid "gotchas" in ARIA like where an ARIA attribute you used will get ignored. The complete list of rules, grouped WCAG level and best practice, can be found in [doc/rule-descriptions.md](./doc/rule-descriptions.md). -```console -npm install axe-core --save-dev +# push the changes to internal private repo +git push origin master ``` Now include the javascript file in each of your iframes in your fixtures or test systems: @@ -65,7 +60,7 @@ Axe was built to reflect how web development actually works. It works with all m - It's actively supported by [Deque Systems](https://www.deque.com), a major accessibility vendor. - It integrates with your existing functional/acceptance automated tests. - It automatically determines which rules to run based on the evaluation context. -- Axe supports in-memory fixtures, static fixtures, integration tests and iframes of infinite depth. +- Axe supports in-memory fixtures, static fixtures, integration tests, and iframes of infinite depth. - Axe is highly configurable. ## Supported Browsers @@ -93,7 +88,7 @@ The [axe-core API](doc/API.md) package consists of: ## Localization -Axe can be built using your local language. To do so, a localization file must be added to the `./locales` directory. This file must have be named in the following manner: `.json`. To build axe using this locale, instead of the default, run axe with the `--lang` flag, like so: +Axe can be built using your local language. To do so, a localization file must be added to the `./locales` directory. This file must be named in the following manner: `.json`. To build axe using this locale, instead of the default, run axe with the `--lang` flag, like so: `grunt build --lang=nl` @@ -101,11 +96,11 @@ or equivalently: `npm run build -- --lang=nl` -This will create a new build for axe, called `axe..js` and `axe..min.js`. If you want to build localized versions, simply pass in `--all-lang` instead. If you want to build multiple localized versions (but not all of them), you can pass in a comma-separated list of languages to the `--lang` flag, like `--lang=nl,ja`. +This will create a new build for axe, called `axe..js` and `axe..min.js`. If you want to build all localized versions, simply pass in `--all-lang` instead. If you want to build multiple localized versions (but not all of them), you can pass in a comma-separated list of languages to the `--lang` flag, like `--lang=nl,ja`. -To create a new translation for axe, start by running `grunt translate --lang=`. This will create a json file fin the `./locales` directory, with the default English text in it for you to translate. Alternatively, you could copy `./locales/_template.json`. We welcome any localization for axe-core. For details on how to contribute, see the Contributing section below. For details on the message syntax, see [Check Message Template](/docs/check-message-template.md). +To create a new translation for axe, start by running `grunt translate --lang=`. This will create a json file in the `./locales` directory, with the default English text in it for you to translate. Alternatively, you could copy `./locales/_template.json`. We welcome any localization for axe-core. For details on how to contribute, see the Contributing section below. For details on the message syntax, see [Check Message Template](/doc/check-message-template.md). -To update existing translation file, re-run `grunt translate --lang=`. This will add new messages used in English and remove messages which were not used in English. +To update an existing translation file, re-run `grunt translate --lang=`. This will add new messages used in English and remove messages which were not used in English. Additionally, locale can be applied at runtime by passing a `locale` object to `axe.configure()`. The locale object must be of the same shape as existing locales in the `./locales` directory. For example: @@ -138,11 +133,15 @@ axe.configure({ Axe-core supports the following locales. Do note that since locales are contributed by our community, they are not guaranteed to include all translations needed in a release. - Basque +- Chinese (Simplified) +- Chinese (Traditional) - Danish - Dutch - French - German +- Greek - Hebrew +- Italian - Japanese - Korean - Norwegian (Bokmål) diff --git a/axe.d.ts b/axe.d.ts index 8ecd9de75..82722edd8 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -70,16 +70,19 @@ declare namespace axe { | LabelledShadowDomSelector | LabelledFramesSelector; type SelectorList = Array | NodeList; + type ContextProp = Selector | SelectorList; type ContextObject = | { - include: Selector | SelectorList; - exclude?: Selector | SelectorList; + include: ContextProp; + exclude?: ContextProp; } | { - exclude: Selector | SelectorList; - include?: Selector | SelectorList; + exclude: ContextProp; + include?: ContextProp; }; - type ElementContext = Selector | SelectorList | ContextObject; + type ContextSpec = ContextProp | ContextObject; + /** Synonym to ContextSpec */ + type ElementContext = ContextSpec; type SerialSelector = | BaseSelector @@ -140,15 +143,19 @@ declare namespace axe { iframes?: boolean; elementRef?: boolean; frameWaitTime?: number; - preload?: boolean; + preload?: boolean | PreloadOptions; performanceTimer?: boolean; pingWaitTime?: number; } + interface PreloadOptions { + assets: string[]; + timeout?: number; + } interface AxeResults extends EnvironmentData { toolOptions: RunOptions; passes: Result[]; violations: Result[]; - incomplete: Result[]; + incomplete: IncompleteResult[]; inapplicable: Result[]; } interface Result { @@ -160,6 +167,9 @@ declare namespace axe { tags: TagValue[]; nodes: NodeResult[]; } + interface IncompleteResult extends Result { + error?: Omit; + } interface NodeResult { html: string; impact?: ImpactValue; @@ -197,6 +207,21 @@ declare namespace axe { fail: string | { [key: string]: string }; incomplete?: string | { [key: string]: string }; } + interface RuleError { + name: string; + message: string; + stack: string; + ruleId?: string; + method?: string; + cause?: SerialError; + errorNode?: SerialDqElement; + } + interface SerialError { + message: string; + stack: string; + name: string; + cause?: SerialError; + } interface CheckLocale { [key: string]: CheckMessages; } @@ -335,6 +360,9 @@ declare namespace axe { interface DqElement extends SerialDqElement { element: Element; toJSON(): SerialDqElement; + } + interface DqElementConstructor { + new (elm: Element, options?: { absolutePaths?: boolean }): DqElement; mergeSpecs( childSpec: SerialDqElement, parentSpec: SerialDqElement @@ -358,23 +386,25 @@ declare namespace axe { frameContext: FrameContextObject; } - interface RawCheckResult extends Omit { + interface RawCheckResult + extends Omit { relatedNodes?: Array; + impact?: ImpactValue; } - interface RawNodeResult { + interface RawNodeResult { node: SerialDqElement | DqElement; any: RawCheckResult[]; all: RawCheckResult[]; none: RawCheckResult[]; - impact: ImpactValue | null; + impact: ImpactValue | undefined; result: T; } interface RawResult extends Omit { inapplicable: Array; passes: RawNodeResult<'passed'>[]; - incomplete: RawNodeResult<'incomplete'>[]; + incomplete: RawNodeResult<'cantTell'>[]; violations: RawNodeResult<'failed'>[]; pageLevel: boolean; result: 'failed' | 'passed' | 'incomplete' | 'inapplicable'; @@ -398,6 +428,42 @@ declare namespace axe { boundingClientRect: DOMRect; } + type GridCell = VirtualNode[]; + + interface Grid { + container: VirtualNode | null; + cells: unknown; // opaque implementation detail + boundaries?: DOMRect; + toGridIndex(num: number): number; + getCellFromPoint(point: { x: number; y: number }): GridCell; + loopGridPosition( + gridPosition: DOMRect, + callback: (gridCell: GridCell, pos: { row: number; col: number }) => void + ): void; + getGridPositionOfRect( + rect: { top: number; right: number; bottom: number; left: number }, + margin?: number + ): DOMRect; + } + + interface CustomNodeSerializer { + toSpec: (dqElm: DqElement) => T; + mergeSpecs: (nodeSpec: T, parentFrameSpec: T) => T; + } + + interface NodeSerializer { + update: (serializer: CustomNodeSerializer) => void; + toSpec: (node: Element | VirtualNode) => SerialDqElement; + dqElmToSpec: ( + dqElm: DqElement | SerialDqElement, + options?: RunOptions + ) => SerialDqElement; + mergeSpecs: ( + nodeSpec: SerialDqElement, + parentFrameSpec: SerialDqElement + ) => SerialDqElement; + } + interface Utils { getFrameContexts: ( context?: ElementContext, @@ -406,15 +472,29 @@ declare namespace axe { shadowSelect: (selector: CrossTreeSelector) => Element | null; shadowSelectAll: (selector: CrossTreeSelector) => Element[]; getStandards(): Required; - DqElement: new ( - elm: Element, - options?: { absolutePaths?: boolean } - ) => DqElement; + isContextSpec: (context: unknown) => context is ContextSpec; + isContextObject: (context: unknown) => context is ContextObject; + isContextProp: (context: unknown) => context is ContextProp; + isLabelledFramesSelector: ( + selector: unknown + ) => selector is LabelledFramesSelector; + isLabelledShadowDomSelector: ( + selector: unknown + ) => selector is LabelledShadowDomSelector; + RuleError: new (options: { + error: Error; + ruleId?: string; + method?: string; + errorNode?: SerialDqElement; + }) => RuleError; + serializeError: (error: Error) => SerialError; + DqElement: DqElementConstructor; uuid: ( options?: { random?: Uint8Array | Array }, buf?: Uint8Array | Array, offset?: number ) => string | Uint8Array | Array; + nodeSerializer: NodeSerializer; } interface Aria { @@ -424,6 +504,7 @@ declare namespace axe { interface Dom { isFocusable: (node: Element | VirtualNode) => boolean; isNativelyFocusable: (node: Element | VirtualNode) => boolean; + getNodeGrid: (node: Node | VirtualNode) => Grid; } type AccessibleTextOptions = { diff --git a/bower.json b/bower.json index 27a80bd8c..52beb0a4f 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.8.4", + "version": "4.11.0", "deprecated": true, "contributors": [ { diff --git a/build/check-node-version.js b/build/check-node-version.js index b56cf0abd..ab0672597 100644 --- a/build/check-node-version.js +++ b/build/check-node-version.js @@ -1,8 +1,13 @@ #! /usr/bin/env node -const currentVersion = process.version.replace('v', ''); +const fs = require('fs'); +const path = require('path'); + +const PATH_TO_NVMRC = path.join(__dirname, '..', '.nvmrc'); +const nvmrc = fs.readFileSync(PATH_TO_NVMRC, 'utf8'); +const minimumVersionMajor = parseInt(nvmrc.trim(), 10); -const minimumVersionMajor = 12; +const currentVersion = process.version.replace('v', ''); const currentVersionMajor = parseInt(currentVersion.split('.')[0]); const usesMinimumVersion = currentVersionMajor >= minimumVersionMajor; diff --git a/build/cherry-pick.js b/build/cherry-pick.js old mode 100644 new mode 100755 index 2b3dc2afe..f8040807b --- a/build/cherry-pick.js +++ b/build/cherry-pick.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const { execSync } = require('child_process'); const conventionalCommitsParser = require('conventional-commits-parser'); const chalk = require('chalk'); @@ -129,7 +131,7 @@ commitsToCherryPick.forEach(({ hash, type, scope, subject }) => { try { execSync(`git cherry-pick ${hash} -X theirs`); - } catch (e) { + } catch { console.error( chalk.red.bold('\nAborting cherry-pick and reseting to master') ); diff --git a/build/configure.js b/build/configure.js index d4998985e..a8a1da734 100644 --- a/build/configure.js +++ b/build/configure.js @@ -6,7 +6,7 @@ var clone = require('clone'); var doT = require('@deque/dot'); var templates = require('./templates'); var buildManual = require('./build-manual'); -var entities = new (require('html-entities').AllHtmlEntities)(); +var { encode } = require('html-entities'); var packageJSON = require('../package.json'); var doTRegex = /\{\{.+?\}\}/g; @@ -16,7 +16,7 @@ var axeVersion = packageJSON.version.substring( ); var descriptionTableHeader = - '| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules |\n| :------- | :------- | :------- | :------- | :------- | :------- |\n'; + '| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) |\n| :------- | :------- | :------- | :------- | :------- | :------- |\n'; // prevent striping newline characters from strings (e.g. failure // summaries). must be synced with lib/core/imports/index.js @@ -365,7 +365,7 @@ function buildRules(grunt, options, commons, callback) { result.push([ `[${rule.id}](https://dequeuniversity.com/rules/axe/${axeVersion}/${rule.id}?application=RuleDescription)`, - entities.encode(rule.metadata.description), + encode(rule.metadata.description), impact, rule.tags.join(', '), issueType.join(', '), @@ -401,6 +401,8 @@ ${ .join('\n\n'); var descriptions = ` + + # Rule Descriptions ## Table of Contents diff --git a/build/rule-generator/questions.js b/build/rule-generator/questions.js index 35d5fcfab..28f9a511f 100644 --- a/build/rule-generator/questions.js +++ b/build/rule-generator/questions.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const globby = require('globby'); +const { glob } = require('glob'); const directories = require('./directories'); /** @@ -33,8 +33,9 @@ const validateGetRuleName = async input => { throw new Error(`RULE name conflicts with an existing rule's filename.`); } // 3) ensure no rule id overlaps - const ruleSpecs = await globby(directories.rules, { - expandDirectories: { extensions: ['json'] } + const ruleSpecs = await glob(`${directories.rules}/**/*.json`, { + posix: true, + absolute: true }); const axeRulesIds = ruleSpecs.reduce((out, specPath) => { const spec = require(specPath); @@ -64,8 +65,9 @@ const validateGetCheckName = async input => { ); } // 2) ensure no check filename overlaps - const checkSpecs = await globby(directories.checks, { - expandDirectories: { extensions: ['json'] } + const checkSpecs = await glob(`${directories.checks}/**/*.json`, { + posix: true, + absolute: true }); // cannot use `fs.existsSync` here, as we do not know which category of checks to look under const axeChecksFileNames = checkSpecs.map( @@ -75,8 +77,9 @@ const validateGetCheckName = async input => { throw new Error('CHECK name conflicts with an existing filename.'); } // 3) ensure no check id overlaps - const ruleSpecs = await globby(directories.rules, { - expandDirectories: { extensions: ['json'] } + const ruleSpecs = await glob(`${directories.rules}/**/*.json`, { + posix: true, + absolute: true }); const axe = require(directories.axePath); const axeChecksIds = ruleSpecs.reduce((out, specPath) => { diff --git a/build/shared/create-file.js b/build/shared/create-file.js index d5faccff3..ea459da27 100644 --- a/build/shared/create-file.js +++ b/build/shared/create-file.js @@ -1,8 +1,5 @@ -const fs = require('fs'); -const { promisify } = require('util'); -const { dirname: getDirName } = require('path'); -const makeDir = require('make-dir'); -const writeFile = promisify(fs.writeFile); +const { promises: fs } = require('fs'); +const { dirname } = require('path'); /** * Create file with given contents at specified location @@ -12,6 +9,8 @@ const writeFile = promisify(fs.writeFile); * @returns {Promise} */ const createFile = (path, content) => - makeDir(getDirName(path)).then(() => writeFile(path, content)); + fs + .mkdir(dirname(path), { recursive: true }) + .then(() => fs.writeFile(path, content)); module.exports = createFile; diff --git a/build/tasks/aria-supported.js b/build/tasks/aria-supported.js index 5f76ca2b1..efe43437f 100644 --- a/build/tasks/aria-supported.js +++ b/build/tasks/aria-supported.js @@ -2,7 +2,6 @@ 'use strict'; const { roles, aria: props } = require('aria-query'); -const mdTable = require('markdown-table'); const format = require('../shared/format'); module.exports = function (grunt) { @@ -16,7 +15,9 @@ module.exports = function (grunt) { * as `axe` does not exist until grunt task `build:uglify` is complete, * hence cannot be required at the top of the file. */ - const { langs } = this.options(); + const done = this.async(); + const { langs, wrapper } = this.options(); + if (wrapper) return true; const fileNameSuffix = langs && langs.length > 0 ? `${langs[0]}` : ''; const axe = require(`../../axe${fileNameSuffix}`); const listType = this.data.listType.toLowerCase(); @@ -50,10 +51,16 @@ module.exports = function (grunt) { ariaAttrs, listType ); - const attributesTableMarkdown = mdTable([ + const formatMarkdownTableRow = columnValues => + `| ${columnValues.join(' | ')} |`; + const attributesTableWithHeader = [ headings.attributesMdTableHeader, + ['---', '---'], ...attributesTable - ]); + ]; + const attributesTableMarkdown = attributesTableWithHeader + .map(formatMarkdownTableRow) + .join('\n'); const footnotes = [...rolesFootnotes, ...attributesFootnotes].map( (footnote, index) => `[^${index + 1}]: ${footnote}` @@ -64,10 +71,16 @@ module.exports = function (grunt) { const destFile = this.data.destFile; // Format the content so Prettier doesn't create a diff after running. // See https://github.com/dequelabs/axe-core/issues/1310. - const formattedContent = format(content, destFile); - - // write `aria supported` file contents - grunt.file.write(destFile, formattedContent); + format(content, destFile) + .then(formattedContent => { + // write `aria supported` file contents + grunt.file.write(destFile, formattedContent); + done(); + }) + .catch(err => { + console.error(err.message); + done(false); + }); /** * Get list of aria attributes, from `aria-query` diff --git a/build/tasks/configure.js b/build/tasks/configure.js index 86599e995..19a8372ef 100644 --- a/build/tasks/configure.js +++ b/build/tasks/configure.js @@ -1,15 +1,15 @@ /*eslint-env node */ 'use strict'; -var buildRules = require('../configure'); -var format = require('../shared/format'); +const buildRules = require('../configure'); +const format = require('../shared/format'); module.exports = function (grunt) { grunt.registerMultiTask( 'configure', 'Task for configuring rules and checks', function () { - var done = this.async(); - var options = this.options({ + const done = this.async(); + const options = this.options({ rules: ['lib/rules/**/*.json'], checks: ['lib/checks/**/*.json'], misc: ['lib/misc/**/*.json'], @@ -18,9 +18,11 @@ module.exports = function (grunt) { }); this.files.forEach(function (file) { - var match = file.dest.auto.match(/\.([a-z]{2,3})\.js/); - if (match) { - options.locale = match[1]; + // locale will always be the 2nd to last part of the + // filename and in the format of "..js" + const parts = file.dest.auto.split('.'); + if (parts.length > 2) { + options.locale = parts[parts.length - 2]; } buildRules(grunt, options, null, function (result) { @@ -28,12 +30,15 @@ module.exports = function (grunt) { // Format the content so Prettier doesn't create a diff after running. // See https://github.com/dequelabs/axe-core/issues/1310. - const descriptionsContent = format( - result.descriptions, - file.dest.descriptions - ); - grunt.file.write(file.dest.descriptions, descriptionsContent); - done(); + format(result.descriptions, file.dest.descriptions) + .then(descriptionsContent => { + grunt.file.write(file.dest.descriptions, descriptionsContent); + done(); + }) + .catch(err => { + console.error(err.message); + done(false); + }); }); }); } diff --git a/build/tasks/esbuild.js b/build/tasks/esbuild.js index 30dbeb4ef..546c9317e 100644 --- a/build/tasks/esbuild.js +++ b/build/tasks/esbuild.js @@ -1,5 +1,36 @@ const { build } = require('esbuild'); const path = require('path'); +const fs = require('fs'); + +// [a11y-core]: resolve ip-protection imports to real file in monorepo, +// or fall back to a stub when building axe-core standalone (CI). +// Set A11Y_FINGERPRINT_PATH env var to override the fingerprint module location. +const fingerprintFallback = { + name: 'a11y-fingerprint-fallback', + setup(pluginBuild) { + pluginBuild.onResolve( + { filter: /ip-protection\/utils\/fingerprint/ }, + args => { + if (process.env.A11Y_FINGERPRINT_PATH) { + const envPath = path.resolve(process.env.A11Y_FINGERPRINT_PATH); + if (fs.existsSync(envPath)) { + return { path: envPath }; + } + } + const realPath = path.resolve(args.resolveDir, args.path + '.js'); + if (fs.existsSync(realPath)) { + return { path: realPath }; + } + return { + path: path.resolve( + __dirname, + '../../lib/core/utils/fingerprint-stub.js' + ) + }; + } + ); + } +}; module.exports = function (grunt) { grunt.registerMultiTask( @@ -23,7 +54,9 @@ module.exports = function (grunt) { entryPoints: [entry], outfile: path.join(dest, name), minify: false, - bundle: true + format: 'esm', + bundle: true, + plugins: [fingerprintFallback] }) .then(done) .catch(e => { diff --git a/build/tasks/metadata-function-map.js b/build/tasks/metadata-function-map.js index e2e9d6054..36b583670 100644 --- a/build/tasks/metadata-function-map.js +++ b/build/tasks/metadata-function-map.js @@ -22,7 +22,7 @@ module.exports = function (grunt) { '// This file is automatically generated using build/tasks/metadata-function-map.js\n'; src.forEach(globPath => { - glob.sync(globPath).forEach(filePath => { + glob.sync(globPath, { posix: true }).forEach(filePath => { const relativePath = path.relative( path.dirname(file.dest), filePath diff --git a/build/tasks/validate.js b/build/tasks/validate.js index 9af5edb28..79218aa74 100644 --- a/build/tasks/validate.js +++ b/build/tasks/validate.js @@ -14,7 +14,7 @@ function fileExists(v, o) { var exists; try { exists = fs.existsSync(file); - } catch (e) { + } catch { return false; } return exists; @@ -227,6 +227,15 @@ function createSchemas() { description: { required: true, type: 'string' + }, + violationConfidence: { + type: 'number' + }, + needsReviewConfidence: { + type: 'number' + }, + defaultCategory: { + type: 'string' } } } @@ -307,7 +316,19 @@ function validateRule({ tags, metadata }) { return issues; } -const miscTags = ['ACT', 'experimental', 'review-item', 'deprecated']; +const miscTags = [ + 'ACT', + 'experimental', + 'review-item', + 'deprecated', + 'a11y-engine', + 'a11y-engine-experimental', + 'advanced', + 'ai', + 'new', + 'medium-accuracy', + 'high-accuracy' +]; const categories = [ 'aria', @@ -321,8 +342,10 @@ const categories = [ 'sensory-and-visual-cues', 'structure', 'tables', + 'labels', 'text-alternatives', - 'time-and-media' + 'time-and-media', + 'images' ]; const standardsTags = [ @@ -349,6 +372,12 @@ const standardsTags = [ standardRegex: /^EN-301-549$/, criterionRegex: /^EN-9\.[1-4]\.[1-9]\.\d{1,2}$/, wcagLevelRegex: /^wcag21?aa?$/ + }, + { + name: 'RGAA', + standardRegex: /^RGAAv4$/, + criterionRegex: /^RGAA-\d{1,2}\.\d{1,2}\.\d{1,2}$/, + wcagLevelRegex: /^wcag21?aa?$/ } ]; @@ -399,7 +428,7 @@ function findTagIssues(tags) { standardTag: standardTags[0] ?? null, criterionTags }; - if (bestPracticeTags.length !== 0) { + if (name !== 'RGAA' && bestPracticeTags.length !== 0) { issues.push(`${name} tags cannot be used along side best-practice tag`); } if (standardTags.length === 0) { @@ -411,7 +440,7 @@ function findTagIssues(tags) { issues.push(`Expected at least one ${name} criterion tag, got 0`); } - if (wcagLevelRegex) { + if (wcagLevelRegex && standards.WCAG) { const wcagLevel = standards.WCAG.standardTag; if (!wcagLevel.match(wcagLevelRegex)) { issues.push(`${name} rules not allowed on ${wcagLevel}`); diff --git a/doc/API.md b/doc/API.md index 5f916713b..18f4dbb9c 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1,4 +1,4 @@ -# Axe Javascript Accessibility API +# Axe JavaScript Accessibility API ## Table of Contents @@ -23,7 +23,7 @@ 1. [API Name: axe.setup](#api-name-axesetup) 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.runPartial / axe.finishRun](#api-name-axerunpartial--axefinishrun) 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) @@ -49,8 +49,8 @@ This section gives a quick description of how to use the axe APIs to analyze web The axe API can be used as part of a broader process that is performed on many, if not all, pages of a website. The API is used to analyze web page content and return a JSON object that lists any accessibility violations found. Here is how to get started: 1. Load page in testing system -2. Optionally, set configuration options for the javascript API (`axe.configure`) -3. Call analyze javascript API (`axe.run`) +2. Optionally, set configuration options for the JavaScript API (`axe.configure`) +3. Call analyze JavaScript API (`axe.run`) 4. Either assert against results or save them for later processing 5. Repeat for any inactive or non-rendered content after making it visible @@ -58,7 +58,7 @@ The axe API can be used as part of a broader process that is performed on many, ### Overview -The axe APIs are provided in the javascript file axe.js. It must be included in the web page under test, as well as each `iframe` under test. Parameters are sent as javascript function parameters. Results are returned in JSON format. +The axe APIs are provided in the JavaScript file axe.js. It must be included in the web page under test, as well as each `iframe` under test. Parameters are sent as JavaScript function parameters. Results are returned in JSON format. ### Full API Reference for Developers @@ -72,7 +72,7 @@ For a full listing of API offered by axe, clone the repository and run `npm run ### Axe-core Tags -Each rule in axe-core has a number of tags. These provide metadata about the rule. Each rule has one tag that indicates which WCAG version / level it belongs to, or if it doesn't it have the `best-practice` tag. If the rule is required by WCAG, there is a tag that references the success criterion number. For example, the `wcag111` tag means a rule is required for WCAG 2 success criterion 1.1.1. +Each rule in axe-core has a number of tags. These provide metadata about the rule. Each rule has one tag that indicates which WCAG version / level it belongs to, or if it doesn't, it has the `best-practice` tag. If the rule is required by WCAG, there is a tag that references the success criterion number. For example, the `wcag111` tag means a rule is required for WCAG 2 success criterion 1.1.1. The `experimental`, `ACT`, `TT`, and `section508` tags are only added to some rules. Each rule with a `section508` tag also has a tag to indicate what requirement in old Section 508 the rule is required by. For example `section508.22.a`. @@ -94,6 +94,8 @@ The `experimental`, `ACT`, `TT`, and `section508` tags are only added to some ru | `TT*.*` | Test ID in Trusted Tester | | `EN-301-549` | Rule required under [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf) | | `EN-9.*` | Section in EN 301 549 listing the requirement | +| `RGAAv4` | Rule required under [RGAA](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/) | +| `RGAA-*.*.*` | Section in RGAA listing the requirement | | `experimental` | Cutting-edge rules, disabled by default | | `cat.*` | Category mappings used by Deque (see below) | @@ -146,7 +148,7 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr ```js [ { - description: "Ensures elements of image maps have alternate text", + description: "Ensure elements of image maps have alternate text", help: "Active elements must have alternate text", helpUrl: "https://dequeuniversity.com/rules/axe/3.5/area-alt?application=axeAPI", ruleId: "area-alt", @@ -162,7 +164,7 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr actIds: ['c487ae'] }, { - description: "Ensures ARIA attributes are allowed for an element's role", + description: "Ensure ARIA attributes are allowed for an element's role", help: "Elements must only use allowed ARIA attributes", helpUrl: "https://dequeuniversity.com/rules/axe/3.5/aria-allowed-attr?application=axeAPI", ruleId: "aria-allowed-attr", @@ -182,7 +184,7 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr To configure the format of the data used by axe. This can be used to add new rules, which must be registered with the library to execute. -**important**: `axe.configure()` does not communicate configuration calls into iframes. Instead `axe.configure()` must be called with the same argument in each `frame` / `iframe` individually. +**Important**: `axe.configure()` does not communicate configuration calls into iframes. Instead `axe.configure()` must be called with the same argument in each `frame` / `iframe` individually. #### Description @@ -213,7 +215,7 @@ axe.configure({ - `reporter` - Used to set the output format that the axe.run function will pass to the callback function. Can pass a reporter name or a custom reporter function. Valid names are: - `v1` to use the previous version's format: `axe.configure({ reporter: "v1" });` - `v2` to use the current version's format: `axe.configure({ reporter: "v2" });` - - `raw` to return the raw result data without formating: `axe.configure({ reporter: "raw" });` + - `raw` to return the raw result data without formatting: `axe.configure({ reporter: "raw" });` - `raw-env` to return the raw result data with environment data: `axe.configure({ reporter: "raw-env" });` - `no-passes` to return only violation results: `axe.configure({ reporter: "no-passes" });` - `checks` - Used to add checks to the list of checks used by rules, or to override the properties of existing checks @@ -567,7 +569,7 @@ The `resultTypes` option can be used to limit the number of nodes for a rule to After axe has processed all rules normally, it generates a unique selector for all nodes in all rules. This process can be time consuming, especially for pages with lots of nodes. By limiting the nodes to a maximum of one for result types you are not interested in, you can greatly speed up the tail end performance of axe. -Types listed in this option will cause rules that fall under those types to show all nodes. Types _not_ listed will causes rules that fall under one of the missing types to show a maximum of one node. This allows you to still see those results and inform the user of them if appropriate. +Types listed in this option will cause rules that fall under those types to show all nodes. Types _not_ listed will cause rules that fall under one of the missing types to show a maximum of one node. This allows you to still see those results and inform the user of them if appropriate. ```js axe.run( @@ -670,7 +672,7 @@ The results of axe are grouped according to their outcome into the following arr - `passes`: These results indicate what elements passed the rules - `violations`: These results indicate what elements failed the rules - `inapplicable`: These results indicate which rules did not run because no matching content was found on the page. For example, with no video, those rules won't run. -- `incomplete`: These results were aborted and require further testing. This can happen either because of technical restrictions to what the rule can test, or because a javascript error occurred. +- `incomplete`: Also known as "needs review," these results were aborted and require further testing. This can happen either because of technical restrictions to what the rule can test, or because a JavaScript error occurred. Each object returned in these arrays have the following properties: @@ -710,7 +712,6 @@ axe.run(document, function (err, results) { - `passes[0]` ... - - `help` - `"Elements must have sufficient color contrast"` - `helpUrl` - `"https://dequeuniversity.com/courses/html-css/visual-layout/color-contrast"` - `id` - `"color-contrast"` @@ -723,7 +724,6 @@ axe.run(document, function (err, results) { ###### `violations` - `violations[0]` - - `help` - `"' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns true when native link has an accessible name', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns true for ARIA link has an accessible name', function () { - var vNode = queryFixture('Book Tour'); - var actual = rule.matches(vNode.actualNode, vNode); + const vNode = queryFixture( + 'Book Tour' + ); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns true when `area` has parent `map` that is referred by `img`', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' + '' + '' + 'MDN infographic' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); }); diff --git a/test/rule-matches/inserted-into-focus-order-matches.js b/test/rule-matches/inserted-into-focus-order-matches.js index f9a56f3ed..ff7ec134f 100644 --- a/test/rule-matches/inserted-into-focus-order-matches.js +++ b/test/rule-matches/inserted-into-focus-order-matches.js @@ -1,9 +1,9 @@ describe('inserted-into-focus-order-matches', function () { 'use strict'; - var fixture = document.getElementById('fixture'); - var flatTreeSetup = axe.testUtils.flatTreeSetup; - var rule; + const fixture = document.getElementById('fixture'); + const flatTreeSetup = axe.testUtils.flatTreeSetup; + let rule; beforeEach(function () { rule = axe.utils.getRule('focus-order-semantics'); @@ -16,35 +16,35 @@ describe('inserted-into-focus-order-matches', function () { it('should return true for a non-focusable element with tabindex > -1', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isTrue(rule.matches(node)); }); it('should return false for a non-focusable element with tabindex == -1', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); it('should return false for a native focusable element with tabindex > 0', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); it('should return false for a native focusable element with no tabindex', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); it('should return false for non-numeric tabindex value', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); }); diff --git a/test/rule-matches/is-initiator-matches.js b/test/rule-matches/is-initiator-matches.js index 8007f1f56..91199ed24 100644 --- a/test/rule-matches/is-initiator-matches.js +++ b/test/rule-matches/is-initiator-matches.js @@ -1,14 +1,14 @@ describe('is-initiator-matches', function () { 'use strict'; - var rule; + let rule; beforeEach(function () { rule = axe.utils.getRule('html-has-lang'); }); afterEach(function () { - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); fixture.innerHTML = ''; }); diff --git a/test/rule-matches/is-visible-matches.js b/test/rule-matches/is-visible-matches.js index 847b796c2..b86098684 100644 --- a/test/rule-matches/is-visible-matches.js +++ b/test/rule-matches/is-visible-matches.js @@ -1,12 +1,12 @@ describe('is-visible-matches', function () { 'use strict'; - var isVisibleMatches = + let isVisibleMatches = axe._thisWillBeDeletedDoNotUse.base.metadataFunctionMap[ 'is-visible-matches' ]; - var fixture = document.getElementById('fixture'); - var fixtureSetup = axe.testUtils.fixtureSetup; + let fixture = document.getElementById('fixture'); + let fixtureSetup = axe.testUtils.fixtureSetup; it('returns true for visible elements', function () { fixtureSetup('

Hello world

'); diff --git a/test/rule-matches/label-content-name-mismatch-matches.js b/test/rule-matches/label-content-name-mismatch-matches.js index b47d2847d..7252a98e8 100644 --- a/test/rule-matches/label-content-name-mismatch-matches.js +++ b/test/rule-matches/label-content-name-mismatch-matches.js @@ -1,9 +1,9 @@ describe('label-content-name-mismatch-matches tests', function () { 'use strict'; - var fixture = document.getElementById('fixture'); - var queryFixture = axe.testUtils.queryFixture; - var rule = axe.utils.getRule('label-content-name-mismatch'); + const fixture = document.getElementById('fixture'); + const queryFixture = axe.testUtils.queryFixture; + const rule = axe.utils.getRule('label-content-name-mismatch'); afterEach(function () { fixture.innerHTML = ''; @@ -11,145 +11,145 @@ describe('label-content-name-mismatch-matches tests', function () { }); it('returns false if given element has no role', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false if element role is not supported with name from contents', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
20 %
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false if implicit element role is overridden to a role that does not support name from contents', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
Status message
' + '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false if element does not have accessible name attributes (`aria-label` or `aria-labelledby`)', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false if element has empty accessible name via `aria-label`', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns true if element has accessible name via `aria-label`', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns true if element has accessible name via `aria-labelledby`', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
some content
' + '
Foo text
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns false if element has empty accessible name (`aria-labelledby`)', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
some content
' + '
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false if element has empty accessible name (`aria-labelledby`) because idref does not exist', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
some content
' + '
Right Label
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns true if element has accessible name (`aria-labelledby`) - multiple refs', function () { - var vNode = queryFixture( + const vNode = queryFixture( '
some content
' + '
Foo
' + '
Bar
' + '
Baz
' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns false for non-widget role', function () { - var vNode = queryFixture( + const vNode = queryFixture( 'Content Information' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false for non-widget role that does support name from content', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false for empty text content', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false for non text content', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns false for hidden (non visible) text content', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isFalse(actual); }); it('returns true when visible text is combination of alphanumeric and emoji characters', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); it('returns true when visible text is combination of alphanumeric and punctuation characters', function () { - var vNode = queryFixture( + const vNode = queryFixture( '' ); - var actual = rule.matches(vNode.actualNode, vNode); + const actual = rule.matches(vNode.actualNode, vNode); assert.isTrue(actual); }); }); diff --git a/test/rule-matches/label-matches.js b/test/rule-matches/label-matches.js index 8dbdc304d..4e4058920 100644 --- a/test/rule-matches/label-matches.js +++ b/test/rule-matches/label-matches.js @@ -1,9 +1,9 @@ describe('label-matches', function () { 'use strict'; - var fixture = document.getElementById('fixture'); - var queryFixture = axe.testUtils.queryFixture; - var rule; + const fixture = document.getElementById('fixture'); + const queryFixture = axe.testUtils.queryFixture; + let rule; beforeEach(function () { fixture.innerHTML = ''; @@ -11,25 +11,25 @@ describe('label-matches', function () { }); it('returns true for non-input elements', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isTrue(rule.matches(null, vNode)); }); it('returns true for input elements without type', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isTrue(rule.matches(null, vNode)); }); it('returns false for input buttons', function () { ['button', 'submit', 'image', 'reset'].forEach(function (type) { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isFalse(rule.matches(null, vNode)); }); }); it('returns false for input elements type=hidden', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isFalse(rule.matches(null, vNode)); }); @@ -37,7 +37,7 @@ describe('label-matches', function () { it('returns true for other input types', function () { ['text', 'password', 'url', 'range', 'date', 'checkbox', 'radio'].forEach( function (type) { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isTrue(rule.matches(null, vNode)); } ); diff --git a/test/rule-matches/landmark-has-body-context.js b/test/rule-matches/landmark-has-body-context.js index 124ba3dc9..3afa72e0b 100644 --- a/test/rule-matches/landmark-has-body-context.js +++ b/test/rule-matches/landmark-has-body-context.js @@ -1,8 +1,8 @@ describe('landmark-has-body-context', function () { 'use strict'; - var fixtureSetup = axe.testUtils.fixtureSetup; - var rule; - var shadowSupport = axe.testUtils.shadowSupport.v1; + const fixtureSetup = axe.testUtils.fixtureSetup; + let rule; + const shadowSupport = axe.testUtils.shadowSupport.v1; beforeEach(function () { rule = axe.utils.getRule('landmark-banner-is-top-level'); @@ -11,21 +11,21 @@ describe('landmark-has-body-context', function () { it('returns true for elements with a role', function () { fixtureSetup('
'); - var vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; assert.isTrue(rule.matches(vNode.actualNode, vNode)); }); it('returns true for elements not contained in a landmark', function () { fixtureSetup('
'); - var vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; assert.isTrue(rule.matches(vNode.actualNode, vNode)); }); it('returns false for elements contained in a landmark', function () { fixtureSetup('
'); - var vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; assert.isFalse(rule.matches(vNode.actualNode, vNode)); }); @@ -35,12 +35,12 @@ describe('landmark-has-body-context', function () { // Safari has a bug in 12.0 that throws an error when calling // attachShadow on
// @see https://bugs.webkit.org/show_bug.cgi?id=197726 - var article = document.createElement('article'); - var shadow = article.attachShadow({ mode: 'open' }); + const article = document.createElement('article'); + const shadow = article.attachShadow({ mode: 'open' }); shadow.innerHTML = '
'; fixtureSetup(article); - var vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; assert.isFalse(rule.matches(vNode.actualNode, vNode)); } ); diff --git a/test/rule-matches/landmark-unique-matches.js b/test/rule-matches/landmark-unique-matches.js index 539d83769..cb3cef7bd 100644 --- a/test/rule-matches/landmark-unique-matches.js +++ b/test/rule-matches/landmark-unique-matches.js @@ -1,17 +1,13 @@ describe('landmark-unique-matches', function () { 'use strict'; - var rule; - var fixture; - var axeFixtureSetup; - var shadowSupport = axe.testUtils.shadowSupport.v1; - var excludedDescendantsForHeadersFooters = [ - 'article', - 'aside', - 'main', - 'nav', - 'section' - ]; - var headerFooterElements = ['header', 'footer']; + let rule; + let fixture; + let axeFixtureSetup; + const shadowSupport = axe.testUtils.shadowSupport.v1; + const sectioningContentElements = ['article', 'aside', 'nav', 'section']; + const excludedDescendantsForHeadersFooters = + sectioningContentElements.concat('main'); + const headerFooterElements = ['header', 'footer']; beforeEach(function () { fixture = document.getElementById('fixture'); @@ -21,30 +17,30 @@ describe('landmark-unique-matches', function () { it('should not match because not a landmark', function () { axeFixtureSetup('

some heading

'); - var node = fixture.querySelector('h1'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const node = fixture.querySelector('h1'); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isFalse(rule.matches(node, virtualNode)); }); it('should pass because is a landmark', function () { axeFixtureSetup('
some banner
'); - var node = fixture.querySelector('div'); + const node = fixture.querySelector('div'); fixture.appendChild(node); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isTrue(rule.matches(node, virtualNode)); }); it('should not match because landmark is hidden', function () { axeFixtureSetup('
some banner
'); - var node = fixture.querySelector('div'); + const node = fixture.querySelector('div'); node.style.display = 'none'; fixture.appendChild(node); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isFalse(rule.matches(node, virtualNode)); }); describe('form and section elements must have accessible names to be matched', function () { - var sectionFormElements = ['section', 'form']; + const sectionFormElements = ['section', 'form']; sectionFormElements.forEach(function (elementType) { it( @@ -59,8 +55,8 @@ describe('landmark-unique-matches', function () { elementType + '>' ); - var node = fixture.querySelector(elementType); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const node = fixture.querySelector(elementType); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isTrue(rule.matches(node, virtualNode)); } ); @@ -77,8 +73,8 @@ describe('landmark-unique-matches', function () { elementType + '>' ); - var node = fixture.querySelector(elementType); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const node = fixture.querySelector(elementType); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isFalse(rule.matches(node, virtualNode)); } ); @@ -105,8 +101,8 @@ describe('landmark-unique-matches', function () { exclusionType + '>' ); - var node = fixture.querySelector(elementType); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const node = fixture.querySelector(elementType); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isFalse(rule.matches(node, virtualNode)); } ); @@ -120,28 +116,73 @@ describe('landmark-unique-matches', function () { axeFixtureSetup( '<' + elementType + '>an element' ); - var node = fixture.querySelector(elementType); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const node = fixture.querySelector(elementType); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isTrue(rule.matches(node, virtualNode)); } ); }); }); + describe('aside should not match when scoped to a sectioning content element unless it has an accessible name', function () { + sectioningContentElements.forEach(function (exclusionType) { + it( + 'should not match because aside is scoped to ' + + exclusionType + + ' and has no label', + function () { + axeFixtureSetup( + '<' + + exclusionType + + '>' + ); + const node = fixture.querySelector('aside[data-test]'); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + assert.isFalse(rule.matches(node, virtualNode)); + } + ); + + it( + 'should match because aside within ' + exclusionType + ' has a label', + function () { + axeFixtureSetup( + '<' + + exclusionType + + '>' + ); + const node = fixture.querySelector('aside[data-test]'); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + assert.isTrue(rule.matches(node, virtualNode)); + } + ); + }); + + it('should match because aside is not scoped to a sectioning content element', function () { + axeFixtureSetup(''); + const node = fixture.querySelector('aside'); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + assert.isTrue(rule.matches(node, virtualNode)); + }); + }); + if (shadowSupport) { it('return true for landmarks contained within shadow dom', function () { - var container = document.createElement('div'); - var shadow = container.attachShadow({ mode: 'open' }); + const container = document.createElement('div'); + const shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
'; axeFixtureSetup(container); - var vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree[0], 'footer')[0]; assert.isTrue(rule.matches(vNode.actualNode, vNode)); }); describe('header/footers should only match when not inside the excluded descendants within shadow dom', function () { - var container; - var shadow; + let container; + let shadow; beforeEach(function () { container = document.createElement('div'); @@ -169,7 +210,7 @@ describe('landmark-unique-matches', function () { '>'; axeFixtureSetup(container); - var virtualNode = axe.utils.querySelectorAll( + const virtualNode = axe.utils.querySelectorAll( axe._tree[0], elementType )[0]; @@ -186,7 +227,7 @@ describe('landmark-unique-matches', function () { shadow.innerHTML = '<' + elementType + '>an element'; axeFixtureSetup(container); - var virtualNode = axe.utils.querySelectorAll( + const virtualNode = axe.utils.querySelectorAll( axe._tree[0], elementType )[0]; @@ -195,5 +236,66 @@ describe('landmark-unique-matches', function () { ); }); }); + + describe('aside should match inside shadow dom unless it is both within sectioning content and has no accessible name', function () { + let container; + let shadow; + + beforeEach(function () { + container = document.createElement('div'); + shadow = container.attachShadow({ mode: 'open' }); + }); + + sectioningContentElements.forEach(function (exclusionType) { + it( + 'should not match because aside is scoped to ' + + exclusionType + + ' and has no label', + function () { + shadow.innerHTML = + '<' + + exclusionType + + ' aria-label="sample label">'; + + axeFixtureSetup(container); + const virtualNode = axe.utils.querySelectorAll( + axe._tree[0], + 'aside[data-test]' + )[0]; + assert.isFalse(rule.matches(virtualNode.actualNode, virtualNode)); + } + ); + + it( + 'should match because aside within ' + exclusionType + ' has a label', + function () { + shadow.innerHTML = + '<' + + exclusionType + + '>'; + axeFixtureSetup(container); + const virtualNode = axe.utils.querySelectorAll( + axe._tree[0], + 'aside[data-test]' + )[0]; + assert.isTrue(rule.matches(virtualNode.actualNode, virtualNode)); + } + ); + }); + + it('should match because aside is not scoped to a sectioning content element', function () { + shadow.innerHTML = ''; + axeFixtureSetup(container); + const virtualNode = axe.utils.querySelectorAll( + axe._tree[0], + 'aside' + )[0]; + assert.isTrue(rule.matches(virtualNode.actualNode, virtualNode)); + }); + }); } }); diff --git a/test/rule-matches/layout-table-matches.js b/test/rule-matches/layout-table-matches.js index cae2b1ff7..c86cb5ac3 100644 --- a/test/rule-matches/layout-table-matches.js +++ b/test/rule-matches/layout-table-matches.js @@ -1,9 +1,9 @@ describe('layout-table-matches', function () { 'use strict'; - var fixture = document.getElementById('fixture'); - var flatTreeSetup = axe.testUtils.flatTreeSetup; - var rule; + const fixture = document.getElementById('fixture'); + const flatTreeSetup = axe.testUtils.flatTreeSetup; + let rule; beforeEach(function () { axe.configure({ @@ -26,7 +26,7 @@ describe('layout-table-matches', function () { it('should return false for data table', function () { fixture.innerHTML = '
Hello>
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); @@ -34,7 +34,7 @@ describe('layout-table-matches', function () { it('should return false if the table is focusable', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isFalse(rule.matches(node)); }); @@ -42,7 +42,7 @@ describe('layout-table-matches', function () { it('should return true if table has role=presentation', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isTrue(rule.matches(node)); }); @@ -50,7 +50,7 @@ describe('layout-table-matches', function () { it('should return true if table has role=none', function () { fixture.innerHTML = '
'; flatTreeSetup(fixture); - var node = fixture.firstChild; + const node = fixture.firstChild; assert.isTrue(rule.matches(node)); }); diff --git a/test/rule-matches/link-in-text-block-matches.js b/test/rule-matches/link-in-text-block-matches.js index e1740114d..871f2d9e7 100644 --- a/test/rule-matches/link-in-text-block-matches.js +++ b/test/rule-matches/link-in-text-block-matches.js @@ -1,6 +1,4 @@ describe('link-in-text-block-matches', () => { - 'use strict'; - const { fixtureSetup } = axe.testUtils; const rule = axe.utils.getRule('link-in-text-block'); diff --git a/test/rule-matches/nested-interactive-matches.js b/test/rule-matches/nested-interactive-matches.js index c4bebe514..9c76d7082 100644 --- a/test/rule-matches/nested-interactive-matches.js +++ b/test/rule-matches/nested-interactive-matches.js @@ -1,7 +1,7 @@ describe('nested-interactive-matches', function () { - var fixture = document.querySelector('#fixture'); - var queryFixture = axe.testUtils.queryFixture; - var rule; + const fixture = document.querySelector('#fixture'); + const queryFixture = axe.testUtils.queryFixture; + let rule; beforeEach(function () { rule = axe.utils.getRule('nested-interactive'); @@ -12,22 +12,22 @@ describe('nested-interactive-matches', function () { }); it('should match if element has children presentational', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isTrue(rule.matches(null, vNode)); }); it('should match if aria element has children presentational', function () { - var vNode = queryFixture('
'); + const vNode = queryFixture('
'); assert.isTrue(rule.matches(null, vNode)); }); it('should not match if element does not have children presentational', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isFalse(rule.matches(null, vNode)); }); it('should not match if element has no role', function () { - var vNode = queryFixture(''); + const vNode = queryFixture(''); assert.isFalse(rule.matches(null, vNode)); }); }); diff --git a/test/rule-matches/no-autoplay-audio-matches.js b/test/rule-matches/no-autoplay-audio-matches.js index 1769e3e89..037582cbf 100644 --- a/test/rule-matches/no-autoplay-audio-matches.js +++ b/test/rule-matches/no-autoplay-audio-matches.js @@ -1,10 +1,10 @@ describe('no-autoplay-audio-matches', function () { 'use strict'; - var fixture = document.getElementById('fixture'); - var queryFixture = axe.testUtils.queryFixture; - var preloadOptions = { preload: { assets: ['media'] } }; - var rule; + const fixture = document.getElementById('fixture'); + const queryFixture = axe.testUtils.queryFixture; + const preloadOptions = { preload: { assets: ['media'] } }; + let rule; beforeEach(function () { rule = axe.utils.getRule('no-autoplay-audio'); @@ -15,37 +15,37 @@ describe('no-autoplay-audio-matches', function () { }); it('returns false for