diff --git a/.circleci/verify-release.sh b/.circleci/verify-release.sh deleted file mode 100755 index 93a350f06..000000000 --- a/.circleci/verify-release.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -set -e - -# Verifying the release can fail due to race condition of -# npm not publishing the package before we try to install -# it -function wait_for_publish() { - echo "Installing $1@$2" - - set +e - for i in {1..10}; do - npm install "$1@$2" 2> /dev/null - if [ $? -eq 0 ]; then - echo "Successfully installed" - set -e - return - else - echo "Retrying..." - sleep 10 - fi - done - - echo "Unable to install. Exiting..." - exit 1 -} - -if [ -n "$1" ] && [ "$1" == "post" ] -then - # verify the released npm package in another dir as we can't - # install a package with the same name - version=$(node -pe "require('./package.json').version") - name=$(node -pe "require('./package.json').name") - - mkdir "verify-release-$version" - cd "verify-release-$version" - npm init -y - - wait_for_publish "$name" "$version" - - node -pe "window={}; document={}; require('$name')" - - cd "node_modules/${name}" -else - # verify main file exists - main=$(node -pe "require('./package.json').main") - node -pe "window={}; document={}; require('./$main')" -fi - -# Test if typescript file exists (if declared) -# -# Note: because we are using node to read the package.json, the -# variable gets set to the string `undefined` if the property -# does not exists, rather than an empty variable. -types=$(node -pe "require('./package.json').types") -if [ "$types" == "undefined" ] -then - types=$(node -pe "require('./package.json').typings") -fi - -if [ "$types" != "undefined" ] && [ ! -f "$types" ] -then - echo "types file missing" - exit 1; -fi \ No newline at end of file diff --git a/.github/actions/install-deps/action.yml b/.github/actions/install-deps/action.yml new file mode 100644 index 000000000..40c9aa43d --- /dev/null +++ b/.github/actions/install-deps/action.yml @@ -0,0 +1,79 @@ +name: 'Install Dependencies' +description: 'Install OS and Project dependencies' + +inputs: + node-version: + description: 'Node.js version to install' + required: false + start-xvfb: + description: 'If provided, this is the display number to run xvfb on. Should be in `:N` format, e.g., `:99`.' + required: false + nightly: + description: 'If true, installs the nightly versions of browsers.' + required: false +outputs: + chrome-path: + description: 'Path to the installed Chrome binary' + value: ${{ steps.setup-chrome.outputs.chrome-path }} + firefox-path: + description: 'Path to the installed Firefox binary' + value: ${{ steps.setup-firefox.outputs.firefox-path }} + chromedriver-path: + description: 'Path to the installed ChromeDriver binary' + value: ${{ steps.setup-chrome.outputs.chromedriver-path }} + chrome-version: + description: 'Version of the installed Chrome binary' + value: ${{ steps.setup-chrome.outputs.chrome-version }} + chromedriver-version: + description: 'Version of the installed ChromeDriver binary' + value: ${{ steps.setup-chrome.outputs.chromedriver-version }} + firefox-version: + description: 'Version of the installed Firefox binary' + value: ${{ steps.setup-firefox.outputs.firefox-version }} + +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + registry-url: 'https://registry.npmjs.org' + node-version: ${{ inputs.node-version }} + node-version-file: ${{ inputs.node-version == '' && '.nvmrc' || '' }} + cache: npm + - name: Fix Chrome Sandbox Permissions + shell: bash + run: | + sudo chown root:root /opt/google/chrome/chrome-sandbox + sudo chmod 4755 /opt/google/chrome/chrome-sandbox + - name: Install Xvfb + shell: bash + if: ${{ inputs.start-xvfb }} + run: | + sudo apt-get update + sudo apt-get install -y xvfb x11-xserver-utils + - name: Install Google Chrome for Testing + id: setup-chrome + uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 + with: + # @see https://github.com/dequelabs/axe-core/issues/5027 + chrome-version: ${{ inputs.nightly == 'true' && 'beta' || 145 }} + install-chromedriver: true + install-dependencies: true + - name: Install Firefox + id: setup-firefox + uses: browser-actions/setup-firefox@5914774dda97099441f02628f8d46411fcfbd208 # v1.7.0 + with: + firefox-version: ${{ inputs.nightly == 'true' && 'latest-nightly' || 'latest' }} + - name: Install Project Dependencies + shell: bash + run: npm ci + - name: Start Xvfb + if: ${{ inputs.start-xvfb }} + env: + DISPLAY: ${{ inputs.start-xvfb }} + shell: bash + # This is the same resolution as what CircleCI used. + # Maintaining it for consistency between the environments + # since something may be resolution dependent. + run: Xvfb "$DISPLAY" -screen 0 1280x1024x24 & diff --git a/.github/bin/determine-version.sh b/.github/bin/determine-version.sh new file mode 100755 index 000000000..076e7a77b --- /dev/null +++ b/.github/bin/determine-version.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -eo pipefail + +echo "::group::Determining new prerelease version" + +NAME=$(npm pkg get name | tr -d '"') +LATEST_VERSION=$(npm pkg get version | tr -d '"') + +SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) +NEW_VERSION="$LATEST_VERSION-canary.${SHORT_SHA}" + +echo "Latest version in package.json: $LATEST_VERSION" +echo "New prerelease version: $NEW_VERSION" + +echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" +echo "name=$NAME" >> "$GITHUB_OUTPUT" + +echo "::endgroup::" diff --git a/.github/bin/extract-release-notes.sh b/.github/bin/extract-release-notes.sh new file mode 100755 index 000000000..409c89024 --- /dev/null +++ b/.github/bin/extract-release-notes.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +# Extracts the body of the most recent release section from CHANGELOG.md — +# everything between the latest version header and the previous one. The +# version header itself is not included. The result is written as a +# multiline `notes` step output to $GITHUB_OUTPUT when set (GitHub +# Actions), otherwise to stdout. +# +# The awk pattern /# \[?[0-9]/ matches version-header lines (`#`, space, +# optional `[`, digit), e.g. `### [4.11.4](...) (2026-04-23)`. The pattern +# is not anchored, so it matches the `# [4` substring inside `###`. On the +# first match it sets `found=1` and skips the line (so the header itself +# is not printed); on the next match (the previous release's header) it +# exits. `found{print}` prints every line in between. +# +# Walking through a typical CHANGELOG.md: +# +# Line v-header? found Action +# -------------------------------- --------- ------ ------- +# # Changelog no 0 nothing +# All notable changes... no 0 nothing +# ### [4.11.4](...) (2026-04-23) yes 0 -> 1 skip +# (blank) no 1 print +# ### Bug Fixes no 1 print +# - **commons/text:** ... no 1 print +# - **utils/getAncestry:** ... no 1 print +# ### [4.11.3](...) (2026-04-13) yes 1 exit + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +{ + echo 'notes<> "${GITHUB_OUTPUT:-/dev/stdout}" diff --git a/.github/bin/validate-npm-deploy.sh b/.github/bin/validate-npm-deploy.sh new file mode 100755 index 000000000..0e2029b5f --- /dev/null +++ b/.github/bin/validate-npm-deploy.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [ -z "$PACKAGE_NAME" ] || [ -z "$VERSION" ]; then + echo "::error::PACKAGE_NAME and VERSION environment variables must be set." + exit 1 +fi + +NPM_ROOT_PATH=$(npm root -g) + +npm install -g "${PACKAGE_NAME}@${VERSION}" || { + echo "::error::✗ Failed to install package: ${PACKAGE_NAME}@${VERSION}" + exit 1 +} + +cd "$NPM_ROOT_PATH" || { + echo "::error::✗ Failed to change directory to global npm root: $NPM_ROOT_PATH" + exit 1 +} + +node -pe "window={}; document={}; require('${PACKAGE_NAME}');" || { + echo "::error::✗ Failed to import CommonJS module for package: ${PACKAGE_NAME}" + exit 1 +} + +cd "${NPM_ROOT_PATH}/${PACKAGE_NAME}" || { + echo "::error::✗ Failed to change directory to package path: ${NPM_ROOT_PATH}/${PACKAGE_NAME}" + exit 1 +} + +types=$(node -pe "require('./package.json').types") +if [ "$types" == "undefined" ] +then + types=$(node -pe "require('./package.json').typings") +fi +if [ "$types" != "undefined" ] && [ ! -f "$types" ] +then + echo "::error::The types file is missing" + exit 1; +fi +echo "Types file '$types' is present in the package" diff --git a/.github/bin/validate-package.mjs b/.github/bin/validate-package.mjs new file mode 100755 index 000000000..c1126b131 --- /dev/null +++ b/.github/bin/validate-package.mjs @@ -0,0 +1,468 @@ +#!/usr/bin/env node + +/** + * @fileoverview Validates the package before publishing. + * This script performs several checks to ensure the package + * is correctly set up, including: + * - Verifying the existence of files listed in `package.json`'s `files` array. + * - Ensuring the package can be imported using both ESM `import` and CommonJS `require()`. + * - Validating Subresource Integrity (SRI) hashes for the built files. + * + * The script generates a summary report compatible with + * GitHub Actions, providing detailed feedback on each + * validation step. + * + * Running this script locally has a few implications to be + * aware of: + * 1. It links and unlinks the package globally. So this + * could impact other workspaces where current links are used. + * 2. To test the step summary, set the `GITHUB_STEP_SUMMARY` + * environment variable to a file path. If this file does not + * exist, it will be created. + */ + +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { access, appendFile, readFile } from 'node:fs/promises'; +import { execSync } from 'node:child_process'; +import pkg from '../../package.json' with { type: 'json' }; + +const isDebug = process.env.DEBUG === 'true'; +const repoRoot = resolve(import.meta.dirname, '..', '..'); +/** + * Start the exit code at 0 for a successful run. If any checks + * fail, we increment by 1 for each failure. When every check is done, + * we exit with the final exit code. + * + * This means our exit code informs us of how many failures happened. + * + * For anyone unfamiliar with exit codes in shell programs, + * an exit code of `0` means success, and any non-zero exit code + * means failure. + * + * Special note as well, in theory this _could_ go above `255`, + * causing the actual exit code to wrap around back to `0` and + * keep counting. But, if we have that many checks in here down + * the road then all the validation will need a major refactor. + */ +let exitCode = 0; +const missing = []; +const summaryFile = process.env.GITHUB_STEP_SUMMARY; +let summary = `# Package Validation + +
+
Package Name
+
${pkg.name}
+
Package Version
+
${pkg.version}
+
License
+
${pkg.license}
+
+ +`; + +console.group('Package Information'); +console.log('Name:', pkg.name); +console.log('Version:', pkg.version); +console.log('License:', pkg.license); +console.groupEnd(); + +/** + * Checks if a file or folder exists on the filesystem. + * + * @param {string} path - The path to check (relative to repo root) + * @returns {Promise} True if the path exists, false otherwise + */ +const exists = async path => { + const absolutePath = resolve(repoRoot, path); + try { + await access(absolutePath); + return true; + } catch { + return false; + } +}; + +/** + * Appends text to the GitHub Actions step summary file if it + * exists. This is mostly useful for local testing where the + * summary file may not be set. However if we want to set it + * for testing, we can. + * + * Since we build the summary in chunks, this function + * appends the current summary and then clears it for the + * next section. + * + * @param {string} text - The text to append to the summary file + * @returns {Promise} + */ +const appendToSummaryFile = async text => { + if (summaryFile) { + await appendFile(summaryFile, text); + summary = ''; + } +}; + +/** + * Verifies that all files and folders listed in the `files` + * array of `package.json` exist in the repository. + */ +const fileExistenceCheck = async () => { + summary += ` +\n## File Existence Check + +The following results table shows the status of files and folders +listed in the \`files\` array of \`package.json\`. + +> [!NOTE] +> This check only validates the existence of files and folders +> defined. It does not validate the contents. Thus a folder +> could exist but be empty and still pass this check. Or +> a file could exist but have incorrect syntax. + +| File | Status |\n|------|--------| +`; + + console.log('Checking for existence of package files:'); + + for (const file of pkg.files) { + if (await exists(file)) { + console.info(`✓ ${file}`); + summary += `| \`${file}\` | ✓ Found |\n`; + } else { + console.error(`✗ ${file}`); + summary += `| \`${file}\` | ✗ Missing |\n`; + missing.push(file); + } + } + + await appendToSummaryFile(summary); + + if (missing.length > 0) { + await appendToSummaryFile( + `\n**ERROR: Missing files: ${missing.join(', ')}**\n` + ); + console.error(`::error::Missing files: ${missing.join(', ')}`); + exitCode++; + } +}; + +/** + * Validates that the main package file can be loaded via + * CommonJS require. This ensures backward compatibility + * for projects using CommonJS. + */ +const validateCommonJS = async () => { + summary += `\n## CommonJS Compatibility Check + +This check validates that the main package file can be loaded +using CommonJS \`require()\`, ensuring backward compatibility. + +| File | Status | Version |\n|------|--------|--------| +`; + + const require = createRequire(import.meta.url); + + console.log('Validating CommonJS compatibility:'); + + try { + const axe = require(`${pkg.name}`); + + if (!axe || typeof axe !== 'object') { + throw new Error('Module did not export an object'); + } + + if (!axe.version) { + throw new Error('Missing version property'); + } + + console.info(`✓ ${pkg.name} (CommonJS)`); + summary += `| \`${pkg.name}\` | ✓ CommonJS Compatible | ${axe.version} |\n`; + } catch (error) { + console.error(`✗ ${pkg.name} (CommonJS):`, error.message); + summary += `| \`${pkg.name}\` | ✗ CommonJS Failed | Not Found |\n`; + summary += `\n\`\`\`\n${error.message}\n\`\`\`\n`; + exitCode++; + } + + await appendToSummaryFile(summary); +}; + +/** + * Validates that the package and all files listed in the + * `files` array of `package.json` can be imported using + * ESM `import` statements. + */ +const validateImportable = async () => { + summary += `\n## Importable Check + +This check attempts to import the package. As well as all +defined files in the \`files\` array of \`package.json\`. + +> [!NOTE] +> This check fails anything that resolves to \`node_modules\`, +> this is because \`axe-core\` should be linked before +> this is called. When \`exports\` can be added to the +> package definition, then we can self reference imports and +> the link will no longer be required. + +| File | Status | Version |\n|------|--------|--------| +`; + + // these files are not part of the importable api + const nonImportableFiles = ['gather-internals.js']; + const importTargets = [ + ...pkg.files + .filter(file => !nonImportableFiles.includes(file)) + .map(file => `${pkg.name}/${file}`) + ]; + let anyCaught = false; + + console.log('Validating package files are importable:'); + + try { + const axe = await import(pkg.name); + console.info(`✓ ${pkg.name}`); + + if (!axe.default?.version) { + throw new Error('Missing version property'); + } + + summary += `| \`${pkg.name}\` | ✓ Importable | ${axe.default.version} |\n`; + } catch { + console.error(`✗ ${pkg.name}`); + summary += `| \`${pkg.name}\` | ✗ Not Importable | Not Found |\n`; + anyCaught = true; + } + + for (const target of importTargets) { + // Skip things that can't be imported directly + // One day we can hopefully import anything as bytes to validate. + // Ref: https://github.com/tc39/proposal-import-bytes + if ( + target.endsWith('.txt') || + target.endsWith('/') || + target.endsWith('.d.ts') + ) { + continue; + } + + // `import.meta.resolve` is used here to determine + // where the import would be resolved from, following + // any symlinks. Since this package is linked, it + // should never have `node_modules` in the resolved + // path. It *could* happen if a dev is writing code + // within a parent folder named as such, but that is + // unsafe anyways. + // ------------------------------------------------- + // If this is ever setup to run in the post-deploy + // test, then this will cause issues as that runs + // from this folder specifically. + if (import.meta.resolve(target).includes('node_modules')) { + exitCode++; + summary += `| \`${target}\` | ✗ Resolves to node_modules |\n`; + console.error(`✗ ${target} resolves to node_modules`); + continue; + } + + try { + let version = ''; + if (target.endsWith('.json')) { + const data = await import(target, { with: { type: 'json' } }); + version = Object.keys(data.default).at(-1); + } else { + const axe = await import(target); + + if (!axe.default?.version) { + throw new Error('Missing version property'); + } + + version = axe.default.version; + } + console.info(`✓ ${target}`); + summary += `| \`${target}\` | ✓ Importable | ${version} |\n`; + } catch (error) { + console.error(`✗ ${target}`); + summary += `| \`${target}\` | ✗ Not Importable | Not Found |\n`; + summary += `\n\`\`\`\n${error.message}\n\`\`\`\n`; + anyCaught = true; + } + } + + // check that the non-importable files are parsable + for (const file of nonImportableFiles) { + const target = `${pkg.name}/${file}`; + try { + const resolved = import.meta.resolve(target); + if (resolved.includes('node_modules')) { + console.error(`✗ ${target} resolves to node_modules`); + summary += `| \`${target}\` | ✗ Resolves to node_modules |\n`; + exitCode++; + continue; + } + execSync(`node --check "${fileURLToPath(resolved)}"`, { stdio: 'pipe' }); + console.info(`✓ ${target} (parse-only, browser-only)`); + summary += `| \`${target}\` | ✓ Parse OK (browser-only) | n/a |\n`; + } catch (error) { + console.error(`✗ ${target}: ${error.message}`); + summary += `| \`${target}\` | ✗ Parse failed (browser-only) | n/a |\n`; + exitCode++; + } + } + + if (anyCaught) { + exitCode++; + } + + await appendToSummaryFile(summary); +}; + +/** + * When a PR targets `master` or a `release-*` branch, + * or these branches are pushed to, we run SRI validation. + * Otherwise, it is skipped since the SRI hashes are only + * updated when releasing. + * + * The history file is deprecated. However, until it is removed + * we should be prudent and continue to validate it. + */ +const validateSriHashes = async () => { + const currentBranch = + process.env.GITHUB_REF_NAME || process.env.GITHUB_HEAD_REF || ''; + + if (!/^release-.+/.test(currentBranch) && currentBranch !== 'master') { + console.log(`Skipping SRI validation (current branch: ${currentBranch})`); + return; + } + + summary += `\n## Subresource Integrity Check + +This check validates the current build against the SRI hash +for the version defined in \`sri-history.json\`. + +| File | Status | +|------|--------| +`; + + const sriHistory = await import(`${pkg.name}/sri-history.json`, { + with: { type: 'json' } + }); + const expectedSri = sriHistory.default[pkg.version]; + // calculate the SRI hash for `axe.js` and `axe.min.js` + // Using `sri-toolbox` as that is what is used in the build process + const { generate } = await import('sri-toolbox'); + + const filesToCheck = [ + { + name: 'axe.js', + path: fileURLToPath(import.meta.resolve(`${pkg.name}/axe.js`)) + }, + { + name: 'axe.min.js', + path: fileURLToPath(import.meta.resolve(`${pkg.name}/axe.min.js`)) + } + ]; + const mismatches = []; + + for (const file of filesToCheck) { + const calculatedSri = generate( + { algorithms: ['sha256'] }, + await readFile(file.path) + ); + + console.log(`Expected SRI for ${file.name}:`, expectedSri[file.name]); + console.log(`Calculated SRI for ${file.name}:`, calculatedSri); + if (calculatedSri !== expectedSri[file.name]) { + console.error(`✗ ${file.name}`); + summary += `| \`${file.name}\` | ✗ Invalid SRI |\n`; + mismatches.push({ + name: file.name, + expected: expectedSri[file.name], + calculated: calculatedSri + }); + continue; + } + + console.info(`✓ ${file.name}`); + summary += `| \`${file.name}\` | ✓ Valid SRI |\n`; + } + + if (mismatches.length > 0) { + summary += `\n### SRI Mismatches\n\n`; + + for (const mismatch of mismatches) { + summary += `**${mismatch.name}:**\n`; + summary += `- Expected: \`${mismatch.expected}\`\n`; + summary += `- Calculated: \`${mismatch.calculated}\`\n\n`; + } + + exitCode++; + } + + await appendToSummaryFile(summary); +}; + +// Start running checks that don't require linking first. +await fileExistenceCheck(); + +/** + * @type {import('child_process').ExecSyncOptionsWithBufferEncoding} + */ +const execOptions = { + cwd: repoRoot, + stdio: isDebug ? 'inherit' : 'pipe', + timeout: 200000 +}; + +console.log('Creating npm link for package validation...'); + +try { + // Link the package globally, then update the package + // internally to use the linked version. + // This is needed because we don't have `exports` defined + // yet, so self referencing imports won't work. + // We also have a circular dependency on the package. + // That means if we try to resolve the import without + // linking, it will resolve the version in `node_modules` + // from npm. + execSync('npm link', execOptions); + execSync(`npm link ${pkg.name}`, execOptions); + + // Run any checks that require the package to reference itself. + await validateCommonJS(); + await validateImportable(); + await validateSriHashes(); +} catch (error) { + console.error('Failed to create npm link:', error.message); + await appendToSummaryFile(` + ## Failed to create npm link + +
Click to expand error details + + \n\`\`\`\n${error.message}\n\`\`\`\n + +
+ + This failure prevented running critical validation checks. + Therefore the entire validation has failed. + `); + console.error(`Failed to create npm link: ${error.message}`); + exitCode++; +} + +console.log('Removing npm link...'); +try { + execSync(`npm unlink ${pkg.name}`, execOptions); + execSync('npm unlink -g', execOptions); +} catch (error) { + // Not a hard failure if unlinking fails since all these + // checks are last. As long as they completed fine, + // validation is acceptable. + // This is more for when running locally to test if + // something goes wrong. As the developer's machine state + // is impacted and they need to know about it. + console.error('Failed to remove npm link:', error.message); +} + +process.exit(exitCode); diff --git a/.github/bin/wait-for-npm-ready.sh b/.github/bin/wait-for-npm-ready.sh new file mode 100755 index 000000000..245f8abf7 --- /dev/null +++ b/.github/bin/wait-for-npm-ready.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [ -z "$VERSION" ] || [ -z "$PACKAGE_NAME" ]; then + echo "✗ ERROR: VERSION and PACKAGE_NAME environment variables must be set." + exit 1 +fi + +SLEEP_SECONDS=${SLEEP_SECONDS:-10} +MAX_ATTEMPTS=${MAX_ATTEMPTS:-30} + +echo "::group::Waiting for ${PACKAGE_NAME}@${VERSION} to be available on npm registry..." + +for i in $(seq 1 "$MAX_ATTEMPTS"); do + echo "Attempt $i of $MAX_ATTEMPTS..." + + if npm view "${PACKAGE_NAME}@${VERSION}" version > /dev/null 2>&1; then + PUBLISHED_VERSION=$(npm view "${PACKAGE_NAME}@${VERSION}" version) + echo "✓ Package ${PACKAGE_NAME}@${PUBLISHED_VERSION} is now available on npm!" + echo "::endgroup::" + exit 0 + fi + + if [ "$i" -lt "$MAX_ATTEMPTS" ]; then + echo "Package not yet available, waiting ${SLEEP_SECONDS} seconds..." + sleep "$SLEEP_SECONDS" + fi +done + +echo "✗ Timeout: Package ${PACKAGE_NAME}@${VERSION} not available after $((MAX_ATTEMPTS * SLEEP_SECONDS)) seconds" +echo "::endgroup::" + +exit 1 diff --git a/.github/bin/wait-for-workflow-success.sh b/.github/bin/wait-for-workflow-success.sh new file mode 100755 index 000000000..7ba2246e5 --- /dev/null +++ b/.github/bin/wait-for-workflow-success.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash + +# This script waits for a specified GitHub Actions workflow to complete successfully. +# Debug mode can be enabled by setting the DEBUG environment variable to "true". +# Exit codes are as follows: +# 0 - Workflow completed successfully +# 1 - Workflow completed with failure +# 2 - Missing required tools on the host system +# 3 - Missing required environment variables for configuration +# 20 - Timeout waiting for workflow to complete + +set -eo pipefail + +if ! command -v jq &> /dev/null; then + echo "::error::jq is not installed. Please install jq to use this script." + exit 2 +fi + +if ! command -v gh &> /dev/null; then + echo "::error::GitHub CLI (gh) is not installed. Please install gh to use this script." + exit 2 +fi + +if [ -z "$REPOSITORY" ]; then + echo "::error::REPOSITORY environment variable must be set." + exit 3 +fi + +if [ -z "$SHA" ]; then + echo "::error::SHA environment variable must be set." + exit 3 +fi + +if [ -z "$WORKFLOW_NAME" ]; then + echo "::error::WORKFLOW_NAME environment variable must be set." + exit 3 +fi + +if [ -z "$BRANCH" ]; then + echo "::error::BRANCH environment variable must be set." + exit 3 +fi + +# When running locally for testing, this might be forgotten to get set. +# Create a temp file just so there is something to write to that will get thrown away. +if [ -z "$GITHUB_STEP_SUMMARY" ]; then + GITHUB_STEP_SUMMARY=$(mktemp) +fi + +echo "Waiting for '$WORKFLOW_NAME' workflow to complete for commit $SHA" + +# If not provided, default to 5 minutes for the job runner to time out. +TIMEOUT_MINUTES=${TIMEOUT_MINUTES:-5} +# Round down if given a fractional number by just removing the decimal portion. +TIMEOUT_MINUTES=${TIMEOUT_MINUTES%.*} +sleep_seconds=30 +max_attempts=$(( (TIMEOUT_MINUTES * 60) / sleep_seconds )) +attempt=0 + +# We *could* do `status=success` as a query parameter. But then we lose visibility +# into "in-progress" for debugging purposes to at least know if it found a run +# while waiting. +# Ref: https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository +api_url="repos/$REPOSITORY/actions/runs?head_sha=$SHA&branch=$BRANCH&exclude_pull_requests=true&event=push" + +# This jq filter can seem complicated. So here is the breakdown: +# 1. `.workflow_runs` - Get the array of workflow runs from the API response +# 2. `sort_by(.created_at) | reverse` - Sort the runs by creation date in descending order. Since the API has no guaranteed order. +# 3. `[.[] | select(.name == "'"$WORKFLOW_NAME"'")][0]` - Filter the runs to only include those with the specified workflow name. Then take the first one (most recent) +# 4. `{status: .status, conclusion: .conclusion}` - Extract only the status and conclusion fields. Since we know this is the most recent run, we only care about these fields later. +# 5. `select(. != null)` - Ensure that we only get a result if there is a matching workflow run +jq_filter='.workflow_runs | sort_by(.created_at) | reverse | [.[] | select(.name == "'"$WORKFLOW_NAME"'")][0] | {status: .status, conclusion: .conclusion} | select(. != null)' + +cat >> "$GITHUB_STEP_SUMMARY" <> "$GITHUB_STEP_SUMMARY" +} + +while [ "$attempt" -lt "$max_attempts" ]; do + # Redirect errors to /dev/null to avoid unusable data in the variable in case of failure. + # If we seem to be having issues in CI later, it would be valuable to setup debugging to log to $GITHUB_STEP_SUMMARY. + workflow_data=$(gh api "$api_url" --jq "$jq_filter" 2>"$log_output" || echo "") + + if [ -z "$workflow_data" ]; then + echo "Attempt $((attempt + 1))/$max_attempts - Workflow run not found yet" + else + status=$(echo "$workflow_data" | jq -r '.status') + conclusion=$(echo "$workflow_data" | jq -r '.conclusion') + + echo "Attempt $((attempt + 1))/$max_attempts - Status: $status, Conclusion: $conclusion" + + if [ "$status" = "completed" ]; then + # Write the result to the summary file + function writeResultToSummary() { + cat >> "$GITHUB_STEP_SUMMARY" <> "$GITHUB_STEP_SUMMARY" < [!TIP] +> Re-running this workflow with debug mode enabled will capture API error logs to help diagnose issues. + +> [!WARNING] +> This can typically indicate that GitHub Action runners are experiencing delays. +> Please check the [GitHub Status Page](https://www.githubstatus.com/) for any ongoing incidents. +> If the status is normal, or if it already is, wait a little bit before re-running the workflow. + +> [!CAUTION] +> If another commit is already deployed, then do *not* re-run this deployment workflow. +> Re-running this would cause an older commit to be the next tag. +> If multiple deployments are failed in a row, then re-run them sequentially as the incident is resolved. + +EOF +writeLogToSummary + +echo "::error::Timeout waiting for '$WORKFLOW_NAME' workflow to complete" +exit 20 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4ed3712ff..c74cbed1f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,12 +44,16 @@ updates: # @see https://github.com/dequelabs/axe-core/issues/4428 - dependency-name: 'colorjs.io' versions: ['>0.4.3'] - # Still need to support node 18 + # Still need to support node 18 in our tests - dependency-name: 'glob' versions: ['>=11.0.0'] # Use node 4 types for backward compatibility - dependency-name: '@types/node' versions: ['>=5.0.0'] + # Breaking change that we need to handle in its own pr first + # @see https://github.com/dequelabs/axe-core/pull/4264 + - dependency-name: 'css-selector-parser' + versions: ['>=2.0.0'] groups: # Any updates not caught by the group config will get individual PRs npm-low-risk: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..f482037a1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,202 @@ +# Do not rename this file. The name "deploy.yml" is known to +# npm for trusted OIDC publishing. +name: Deploy + +on: + # Run on push and not `workflow_run` after tests finish. + # Specifically because `workflow_run` only runs from the context + # of the default branch, regardless of which branch triggered the tests. + # That means no non-default branches could deploy. + push: + branches: + - master + - develop + +concurrency: + group: deploy/${{ github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + # Since we can't run against `workflow_run`, we have to + # wait for for the Tests to succeed first before any + # processing can happen. + wait-for-tests: + name: Wait for Tests to Pass + if: github.repository_owner == 'dequelabs' + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: read + statuses: read + timeout-minutes: 15 + steps: + - &checkout + name: Checkout repository + timeout-minutes: 2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + - name: Wait for Tests workflow to complete + timeout-minutes: 13 + env: + SHA: ${{ github.sha }} + REPOSITORY: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + WORKFLOW_NAME: Tests + DEBUG: ${{ runner.debug == '1' }} + # One minute less than the job timeout to allow for the script to do cleanup work. + TIMEOUT_MINUTES: 12 + GH_TOKEN: ${{ github.token }} + run: ./.github/bin/wait-for-workflow-success.sh + deploy-next: + name: Deploy "next" to npm + needs: wait-for-tests + if: ${{ github.ref_name == 'develop' }} + environment: + name: registry.npmjs.org + permissions: + contents: read + id-token: write # Required for OIDC + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.determine-version.outputs.version }} + packageName: ${{ steps.determine-version.outputs.name }} + steps: + - *checkout + - &setup-node + name: Setup NodeJS + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + registry-url: 'https://registry.npmjs.org' + node-version-file: .nvmrc + cache: npm + - &install-project-deps + name: Install Project Dependencies + shell: bash + run: npm ci + - &build + name: Build + run: | + npm run prepare + npm run build + - name: Determine prerelease version + id: determine-version + run: ./.github/bin/determine-version.sh + - name: Bump version + env: + NEW_VERSION: ${{ steps.determine-version.outputs.version }} + run: npm version "$NEW_VERSION" --no-git-tag-version --ignore-scripts + - &validate-package + name: Validate package is consumable + env: + # Ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#runner-context + # Linting shows this context might be invalid, but it shouldn't be per docs. + # Probably something missing in the schema. + DEBUG: ${{ runner.debug == '1' }} + run: node .github/bin/validate-package.mjs + - name: Publish "next" version to npm + run: npm publish --tag=next + validate-next-deploy: + name: Validate Next Deployment + needs: deploy-next + runs-on: ubuntu-24.04 + steps: + - *checkout + - *setup-node + # In theory since this is a new job now, by the time + # this would kick off the package should be available. + # But, to be safe in case of delays in propagation, + # we'll implement a retry mechanism. + - name: Wait for package to be available on npm + env: + VERSION: ${{ needs.deploy-next.outputs.version }} + PACKAGE_NAME: ${{ needs.deploy-next.outputs.packageName }} + run: ./.github/bin/wait-for-npm-ready.sh + - name: Validate installation of "next" version + env: + PACKAGE_NAME: ${{ needs.deploy-next.outputs.packageName }} + VERSION: ${{ needs.deploy-next.outputs.version }} + run: ./.github/bin/validate-npm-deploy.sh + prod-hold: + name: Await approval to deploy to production + needs: wait-for-tests + if: ${{ github.ref_name == 'master' }} + environment: + name: production-hold + runs-on: ubuntu-24.04 + steps: + - name: Awaiting approval to deploy to production + run: echo "Approval granted to proceed to production deployment." + prod-deploy: + name: Deploy stable to npm + needs: prod-hold + if: ${{ needs.prod-hold.result == 'success' }} + environment: + name: registry.npmjs.org + permissions: + contents: read + id-token: write # Required for OIDC + outputs: + version: ${{ steps.get-data.outputs.version }} + packageName: ${{ steps.get-data.outputs.name }} + runs-on: ubuntu-24.04 + steps: + - *checkout + - *setup-node + - *install-project-deps + - *build + - *validate-package + - name: Publish stable version to npm + run: npm publish + - name: Get published package data + id: get-data + run: | + VERSION=$(npm pkg get version | tr -d '"') + NAME=$(npm pkg get name | tr -d '"') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "name=$NAME" >> $GITHUB_OUTPUT + create-github-release: + name: Create GitHub Release + needs: prod-deploy + runs-on: ubuntu-24.04 + permissions: + contents: write # Required to create releases + steps: + - *checkout + - name: Extract release notes + id: release-notes + run: ./.github/bin/extract-release-notes.sh + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.prod-deploy.outputs.version }} + NOTES: ${{ steps.release-notes.outputs.notes }} + run: | + gh release create "v${VERSION}" \ + --title "Release ${VERSION}" \ + --target ${{ github.sha }} \ + --notes "$NOTES" + validate-deploy: + name: Validate Deployment + needs: prod-deploy + runs-on: ubuntu-24.04 + steps: + - *checkout + - *setup-node + # In theory since this is a new job now, by the time + # this would kick off the package should be available. + # But, to be safe in case of delays in propagation, + # we'll implement a retry mechanism. + - name: Wait for package to be available on npm + env: + VERSION: ${{ needs.prod-deploy.outputs.version }} + PACKAGE_NAME: ${{ needs.prod-deploy.outputs.packageName }} + run: ./.github/bin/wait-for-npm-ready.sh + - name: Validate installation of stable version + env: + PACKAGE_NAME: ${{ needs.prod-deploy.outputs.packageName }} + VERSION: ${{ needs.prod-deploy.outputs.version }} + run: ./.github/bin/validate-npm-deploy.sh diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ac7402759..545beb920 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.ref }} - name: Install dependencies run: npm ci - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: 'npm' @@ -29,6 +29,6 @@ jobs: - run: npm run fmt # Prevent the prettierignore change from being committed. - run: git checkout .prettierignore - - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # tag=v5 + - uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # tag=v5 with: commit_message: ':robot: Automated formatting fixes' diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 000000000..8ea4beaad --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,83 @@ +name: Nightly Tests + +on: + schedule: + # Runs every day at 2:17 AM UTC + # Schedules should try to be offset from common times + # to avoid high contention times on GitHub runners. + - cron: '17 2 * * *' + workflow_dispatch: + +env: + CHROME_DEVEL_SANDBOX: /opt/google/chrome/chrome-sandbox + +permissions: + contents: read + +jobs: + browsers: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + DISPLAY: ':99' + steps: + - &checkout + name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + - name: Install Dependencies + id: install-deps + uses: ./.github/actions/install-deps + with: + nightly: 'true' + start-xvfb: ${{ env.DISPLAY }} + - &build + name: Build + id: build + run: | + npm run prepare + npm run build + - name: Run Firefox Nightly Browser Tests + env: + WTR_BROWSER: firefox-nightly + FIREFOX_NIGHTLY_BIN: ${{ steps.install-deps.outputs.firefox-path }} + run: npm run test + - name: Run Chrome Beta Browser Tests + if: ${{ !cancelled() && steps.build.conclusion == 'success' }} + env: + WTR_BROWSER: chrome + CHROME_BIN: ${{ steps.install-deps.outputs.chrome-path }} + CHROMEDRIVER_BIN: ${{ steps.install-deps.outputs.chromedriver-path }} + run: npm run test + act: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - *checkout + - &install-deps + name: Install Deps + id: install-deps + uses: ./.github/actions/install-deps + - *build + - name: Install Latest WCAG ACT Rules + run: npm install w3c/wcag-act-rules#main + - name: Run ACT Tests + env: + CHROME_BIN: ${{ steps.install-deps.outputs.chrome-path }} + CHROMEDRIVER_BIN: ${{ steps.install-deps.outputs.chromedriver-path }} + run: npm run test:act + aria-practices: + runs-on: ubuntu-24.04 + timeout-minutes: 7 + steps: + - *checkout + - *install-deps + - *build + - name: Install Latest W3C Aria Practices + run: npm install w3c/aria-practices#main + - name: Run ARIA Practices Tests + env: + CHROME_BIN: ${{ steps.install-deps.outputs.chrome-path }} + CHROMEDRIVER_BIN: ${{ steps.install-deps.outputs.chromedriver-path }} + run: npm run test:apg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2b765b7f..ad707b7f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,10 @@ jobs: name: Create release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: 'npm' diff --git a/.github/workflows/update-generated-files.yaml b/.github/workflows/update-generated-files.yaml index f10368fa9..6ac667002 100644 --- a/.github/workflows/update-generated-files.yaml +++ b/.github/workflows/update-generated-files.yaml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: 'npm' @@ -29,7 +29,7 @@ jobs: - name: Check for changes id: changes run: | - changes=$(git status --porcelain) + changes=$(git status --porcelain | tr -d '\n') # see https://unix.stackexchange.com/a/509498 echo $changes | grep . && echo "Changes detected" || echo "No changes" echo "changes=$changes" >> "$GITHUB_OUTPUT" diff --git a/.gitignore b/.gitignore index 9aacdeb71..4f16ba245 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ test/integration/*/index.html # dist axe.js axe.*.js +/gather-internals.js # generated src file lib/core/base/metadata-function-map.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..25c4925c7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "attach", + "name": "Attach to WTR test:debug", + "address": "localhost", + "port": 9765, // keep in sync with debugPort in wtr.config.mjs chrome-debug group + "webRoot": "${workspaceFolder}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "*": "${webRoot}/*" + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index e1efdf286..9d577f100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,88 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [4.12.1](https://github.com/dequelabs/axe-core/compare/v4.12.0...v4.12.1) (2026-06-09) + +### Bug Fixes + +- **axe.d.ts:** make enabled property of RuleMetadata optional ([#5129](https://github.com/dequelabs/axe-core/issues/5129)) ([7eb3d2d](https://github.com/dequelabs/axe-core/commit/7eb3d2d7ce283926a786c8a0fbf2a52c81d5e694)) + +## [4.12.0](https://github.com/dequelabs/axe-core/compare/v4.11.4...v4.12.0) (2026-06-01) + +### Features + +- add gather-internals.js external script ([#5099](https://github.com/dequelabs/axe-core/issues/5099)) ([c61d58b](https://github.com/dequelabs/axe-core/commit/c61d58b40d87f81152526edcea67292aa7e3ae1d)), closes [#5080](https://github.com/dequelabs/axe-core/issues/5080) +- **aria-allowed/prohibited-attr, aria-required-parent/children:** partially support element internals role ([#5080](https://github.com/dequelabs/axe-core/issues/5080)) ([417b48a](https://github.com/dequelabs/axe-core/commit/417b48a0d60f0c01ce81e69cc50c2c59e45aa4de)), closes [#5039](https://github.com/dequelabs/axe-core/issues/5039) [#4259](https://github.com/dequelabs/axe-core/issues/4259) +- **axe.externalAPIs:** add public api for setting elementInternal data ([#5105](https://github.com/dequelabs/axe-core/issues/5105)) ([63bab8f](https://github.com/dequelabs/axe-core/commit/63bab8fec82817849a8e69b7cd00f1c1bf3ddf6e)) +- **core:** expose normalizeRunOptions ([#4998](https://github.com/dequelabs/axe-core/issues/4998)) ([b8e6a59](https://github.com/dequelabs/axe-core/commit/b8e6a5943f3d7613e770f36dd15fdb27621ca18c)) +- expose axe.resetLocale() to restore the default locale ([#5108](https://github.com/dequelabs/axe-core/issues/5108)) ([c2b5292](https://github.com/dequelabs/axe-core/commit/c2b5292397727e1f9d63ae1675db447a5cf58a23)), closes [#5107](https://github.com/dequelabs/axe-core/issues/5107) +- **getRules:** include rule enabled state in returned objects ([#5118](https://github.com/dequelabs/axe-core/issues/5118)) ([75bf772](https://github.com/dequelabs/axe-core/commit/75bf772d47ec1cc6027de55b47aaa63ffef171da)), closes [#5116](https://github.com/dequelabs/axe-core/issues/5116) +- **list,listitem:** support element internals role ([#5119](https://github.com/dequelabs/axe-core/issues/5119)) ([7d9d696](https://github.com/dequelabs/axe-core/commit/7d9d69678df257ee72b962d45371ae27e3aa82ca)) +- **new-rule:** check that aria-tab have an accessible name ([#5001](https://github.com/dequelabs/axe-core/issues/5001)) ([0d4e4e7](https://github.com/dequelabs/axe-core/commit/0d4e4e70aa9f46519eb6000744e043c058fd994e)), closes [#4842](https://github.com/dequelabs/axe-core/issues/4842) +- **rules:** deprecate landmark-complementary-is-top-level rules ([#4992](https://github.com/dequelabs/axe-core/issues/4992)) ([9e09139](https://github.com/dequelabs/axe-core/commit/9e091391189dba452ea485275609120e1e6ae8ba)), closes [#4950](https://github.com/dequelabs/axe-core/issues/4950) +- **utils:** add `getElementInternals` function ([#5077](https://github.com/dequelabs/axe-core/issues/5077)) ([1c15f82](https://github.com/dequelabs/axe-core/commit/1c15f8224a184d3c0da95942e99d9d73ad5645c0)) + +### Bug Fixes + +- **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](https://github.com/dequelabs/axe-core/issues/4974)) ([c6245e7](https://github.com/dequelabs/axe-core/commit/c6245e7aee824434fcdae3c77c24365493dbe4be)) +- **aria-conditional-attr:** add support for radio ([#5100](https://github.com/dequelabs/axe-core/issues/5100)) ([8223c98](https://github.com/dequelabs/axe-core/commit/8223c989ff4fd2b8002f4961a8ee005a371f39cc)) +- **aria-valid-attr-value:** handle multiple aria-errormessage IDs ([#4973](https://github.com/dequelabs/axe-core/issues/4973)) ([0489e30](https://github.com/dequelabs/axe-core/commit/0489e30aad3d80790d8fb9cf5b1807d7c3a2179f)) +- **aria:** prevent getOwnedVirtual from returning duplicate nodes ([#4987](https://github.com/dequelabs/axe-core/issues/4987)) ([48ca955](https://github.com/dequelabs/axe-core/commit/48ca9554e2f0400caeec55c09aa100cbb415422d)), closes [#4840](https://github.com/dequelabs/axe-core/issues/4840) +- **commons/text:** exclude natively hidden elements from aria-labelledby accessible name ([#5076](https://github.com/dequelabs/axe-core/issues/5076)) ([ea7202c](https://github.com/dequelabs/axe-core/commit/ea7202c6bf1a6166c878dbf19bb5454372b61fae)), closes [#4704](https://github.com/dequelabs/axe-core/issues/4704) +- **DqElement:** avoid calling constructors with cloneNode ([#5013](https://github.com/dequelabs/axe-core/issues/5013)) ([0281fa1](https://github.com/dequelabs/axe-core/commit/0281fa16f7110b793ac8b3b5b46f93e81be75ee4)) +- **existing-rule:** aria-busy now shows an error message for a use with unallowed children ([#5017](https://github.com/dequelabs/axe-core/issues/5017)) ([2067b87](https://github.com/dequelabs/axe-core/commit/2067b87195552daa3065be7aca1aa2a02c135f28)) +- **helpUrl:** ensure axe.configure always updates the help URLs ([#5114](https://github.com/dequelabs/axe-core/issues/5114)) ([c4f60ff](https://github.com/dequelabs/axe-core/commit/c4f60ffcd47eb64514e8cbafbc68ad357ce60e77)) +- **label-content-name-mismatch:** match visible text with aria-label and exclude invisible text ([#5096](https://github.com/dequelabs/axe-core/issues/5096)) ([3a012a1](https://github.com/dequelabs/axe-core/commit/3a012a141f56b76d6a58fcfb01598ba45e91a442)) +- **locale:** ensure all subtags are correctly set ([#5112](https://github.com/dequelabs/axe-core/issues/5112)) ([13005ed](https://github.com/dequelabs/axe-core/commit/13005eda098db154f3d78df3923ed85389344353)) +- **scrollable-region-focusable:** clarify the issue is in safari ([#4995](https://github.com/dequelabs/axe-core/issues/4995)) ([4ec5211](https://github.com/dequelabs/axe-core/commit/4ec52112b67b1b44f82b3eade1825789ee8cb659)), closes [WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870) [WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290) +- **scrollable-region-focusable:** do not fail scroll areas when all content is visible without scrolling ([#4993](https://github.com/dequelabs/axe-core/issues/4993)) ([838707a](https://github.com/dequelabs/axe-core/commit/838707a8f224907042221bbf6fb28d6ad59d7cb0)) +- **target-size:** determine offset using clientRects if target is display:inline ([#5012](https://github.com/dequelabs/axe-core/issues/5012)) ([a4b8091](https://github.com/dequelabs/axe-core/commit/a4b809183f43c4296a3ec57cd80d8a8f34743361)) +- **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](https://github.com/dequelabs/axe-core/issues/5066)) ([1229a6e](https://github.com/dequelabs/axe-core/commit/1229a6e7162768a283f5e2307024dee0d0566452)), closes [#5065](https://github.com/dequelabs/axe-core/issues/5065) +- **target-size:** ignore widgets that are inline with other inline elements ([#5000](https://github.com/dequelabs/axe-core/issues/5000)) ([a8dd81b](https://github.com/dequelabs/axe-core/commit/a8dd81be759c670203784acf7b1894257df5457c)) +- **utils/getAncestry:** escape node name ([#5079](https://github.com/dequelabs/axe-core/issues/5079)) ([d1fabaa](https://github.com/dequelabs/axe-core/commit/d1fabaad99b1b055b2436a0c3efc22cb66df3934)), closes [#5078](https://github.com/dequelabs/axe-core/issues/5078) +- **utils:** Add null check to parseCrossOriginStylesheet, closes [#5074](https://github.com/dequelabs/axe-core/issues/5074) ([#5075](https://github.com/dequelabs/axe-core/issues/5075)) ([f12ef32](https://github.com/dequelabs/axe-core/commit/f12ef32554deb116238ac29d854ad8e46baa9adb)) +- **utils:** update isShadowRoot to use spec-compliant custom element regex ([#5059](https://github.com/dequelabs/axe-core/issues/5059)) ([edc6ce2](https://github.com/dequelabs/axe-core/commit/edc6ce2815b79a976bdb654bd8062f28132a3cdd)), closes [#5030](https://github.com/dequelabs/axe-core/issues/5030) + +### [4.11.4](https://github.com/dequelabs/axe-core/compare/v4.11.3...v4.11.4) (2026-04-23) + +### Bug Fixes + +- **commons/text:** exclude natively hidden elements from aria-labelledby accessible name ([#5076](https://github.com/dequelabs/axe-core/issues/5076)) ([df34adf](https://github.com/dequelabs/axe-core/commit/df34adfc1967919d667d40a76ab5c85b6e47ddfe)), closes [#4704](https://github.com/dequelabs/axe-core/issues/4704) +- **utils/getAncestry:** escape node name ([#5079](https://github.com/dequelabs/axe-core/issues/5079)) ([6e68d0a](https://github.com/dequelabs/axe-core/commit/6e68d0a5d26999b996152df82238bc3f3a041cb3)), closes [#5078](https://github.com/dequelabs/axe-core/issues/5078) + +### [4.11.3](https://github.com/dequelabs/axe-core/compare/v4.11.2...v4.11.3) (2026-04-13) + +### Bug Fixes + +- **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](https://github.com/dequelabs/axe-core/issues/4974)) ([1d80163](https://github.com/dequelabs/axe-core/commit/1d801636f058f2abd885c488baff954872b13846)) +- **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](https://github.com/dequelabs/axe-core/issues/5066)) ([5906273](https://github.com/dequelabs/axe-core/commit/5906273841cbd7ac9e08af730dffc244cf42b39b)), closes [#5065](https://github.com/dequelabs/axe-core/issues/5065) + +### [4.11.2](https://github.com/dequelabs/axe-core/compare/v4.11.1...v4.11.2) (2026-03-30) + +### Bug Fixes + +- **aria-valid-attr-value:** handle multiple aria-errormessage IDs ([#4973](https://github.com/dequelabs/axe-core/issues/4973)) ([9322148](https://github.com/dequelabs/axe-core/commit/9322148b69924d6ae2d2dd46e6109bc2fc53abd3)) +- **aria:** prevent getOwnedVirtual from returning duplicate nodes ([#4987](https://github.com/dequelabs/axe-core/issues/4987)) ([99d1e77](https://github.com/dequelabs/axe-core/commit/99d1e77f351db586c79020372efc59608e604a1c)), closes [#4840](https://github.com/dequelabs/axe-core/issues/4840) +- **DqElement:** avoid calling constructors with cloneNode ([#5013](https://github.com/dequelabs/axe-core/issues/5013)) ([88bc57f](https://github.com/dequelabs/axe-core/commit/88bc57fd3de12cd69d365b3f385ce9a2e30b7bd5)) +- **existing-rule:** aria-busy now shows an error message for a use with unallowed children ([#5017](https://github.com/dequelabs/axe-core/issues/5017)) ([dded75a](https://github.com/dequelabs/axe-core/commit/dded75a9acc9a2350926f46f04e0f1de522f43d6)) +- **scrollable-region-focusable:** clarify the issue is in safari ([#4995](https://github.com/dequelabs/axe-core/issues/4995)) ([2567afd](https://github.com/dequelabs/axe-core/commit/2567afd5c32398c6a488240b066bb0d335f6dc6a)), closes [WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870) [WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290) +- **scrollable-region-focusable:** do not fail scroll areas when all content is visible without scrolling ([#4993](https://github.com/dequelabs/axe-core/issues/4993)) ([240f8b5](https://github.com/dequelabs/axe-core/commit/240f8b53ad168521a63b54d0053b96ce430c7184)) +- **target-size:** determine offset using clientRects if target is display:inline ([#5012](https://github.com/dequelabs/axe-core/issues/5012)) ([69d81c1](https://github.com/dequelabs/axe-core/commit/69d81c1cbb6a61a272884516c7983dcd17d28a42)) +- **target-size:** ignore widgets that are inline with other inline elements ([#5000](https://github.com/dequelabs/axe-core/issues/5000)) ([cf8a3c0](https://github.com/dequelabs/axe-core/commit/cf8a3c039b121c0c64a1eb8805c586659b69f3c1)) + +### [4.11.1](https://github.com/dequelabs/axe-core/compare/v4.11.0...v4.11.1) (2026-01-06) + +### Bug Fixes + +- allow shadow roots in axe.run contexts ([#4952](https://github.com/dequelabs/axe-core/issues/4952)) ([d4aee16](https://github.com/dequelabs/axe-core/commit/d4aee16494f3225e9f5065f23a9e1deccb46fc9a)), closes [#4941](https://github.com/dequelabs/axe-core/issues/4941) +- color contrast fails for oklch and oklab with none ([#4959](https://github.com/dequelabs/axe-core/issues/4959)) ([8f249fd](https://github.com/dequelabs/axe-core/commit/8f249fdcffe379466fcff8ec8ac46e37b65fdbce)) +- **color-contrast:** do not incomplete on textarea ([#4968](https://github.com/dequelabs/axe-core/issues/4968)) ([d271788](https://github.com/dequelabs/axe-core/commit/d27178866d4962e1157b1be435143d028873f545)), closes [#4947](https://github.com/dequelabs/axe-core/issues/4947) +- **commons/color:** Match browser behavior for out-of-gamut oklch colors ([#4908](https://github.com/dequelabs/axe-core/issues/4908)) ([5036be8](https://github.com/dequelabs/axe-core/commit/5036be811e0ede4bf061ab1f970f78b7e9c7ec0c)) +- don't runs rules that select `html` on nested `html` elements ([#4969](https://github.com/dequelabs/axe-core/issues/4969)) ([1e9a5c3](https://github.com/dequelabs/axe-core/commit/1e9a5c36812ff69a75f23fed3d290497f9fba37d)) +- replaced luminance threshold constant 0.03928 with 0.04045 ([#4934](https://github.com/dequelabs/axe-core/issues/4934)) ([316967d](https://github.com/dequelabs/axe-core/commit/316967d50c554e71bcdf59ac945d1d5bb2f0684b)), closes [#4933](https://github.com/dequelabs/axe-core/issues/4933) +- **rgaa:** adjust mapping of aria-hidden-\* and valid-lang ([#4935](https://github.com/dequelabs/axe-core/issues/4935)) ([77571f2](https://github.com/dequelabs/axe-core/commit/77571f2103a90a5703233729c78be008395f1572)) +- **valid-lang:** update valid-langs for newer language codes ([#4966](https://github.com/dequelabs/axe-core/issues/4966)) ([c3f5446](https://github.com/dequelabs/axe-core/commit/c3f54464fd0995edc6619203b46b65d2984b218d)), closes [#4963](https://github.com/dequelabs/axe-core/issues/4963) + ## [4.11.0](https://github.com/dequelabs/axe-core/compare/v4.10.3...v4.11.0) (2025-10-07) ### Features diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..8a81ac332 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# axe-core — Claude Code Context + +**Last updated:** 2026-03-19 + +## 0. Fundamental Standards + +- **Formatting:** Run `npm run fmt` (Prettier) and `npm run eslint` before every commit. No exceptions. +- **Zero-Exception Testing:** 100% coverage goal. Run `npm test` before completion. `feat`/`fix` and other behavior-changing PRs require unit + integration tests where applicable; see `doc/code-submission-guidelines.md` for exceptions (e.g., some `chore`/docs changes). +- **Import Rule:** Directory-level import restrictions are strictly enforced. See `eslint.config.js`. +- **Commits:** Angular commit convention is mandatory. PRs with non-conforming commits will be rejected. See `doc/code-submission-guidelines.md`. +- **Issues:** All unresolved issues tracked in [GitHub Issues](https://github.com/dequelabs/axe-core/issues). + +## 1. Technical Guidelines + +### Code Structure + +- **Return Early:** Keep the happy path left-aligned. Handle errors/edge cases first with early returns — never nest when you can exit. +- **Exports:** Default export at the top of the file, immediately after imports. +- **JSDoc:** Add JSDoc/DocBlock comments where appropriate, especially for exported APIs and complex logic. Document parameters and return types. See `doc/code-submission-guidelines.md` (source of truth) and `doc/developer-guide.md` for additional guidance. +- **Naming:** Files and rule/check IDs in kebab-case. Functions in camelCase. Booleans prefixed `is`/`has`/`should`. Constants in `UPPER_SNAKE_CASE`. +- **Variables:** Declare at point of use, not at the top of the function. + +### Virtual Nodes vs. HTMLElement + +- **Prefer Virtual Nodes** for attribute access and property reads (`virtualNode.attr()`, `virtualNode.props`). +- **Use real DOM** only when you need DOM APIs (e.g., `getBoundingClientRect`, `getRootNode`). +- **`core/utils/` functions** should default to real DOM inputs/outputs, except utilities that are explicitly documented as operating on `VirtualNode` (for example, tree or selector helpers). Avoid introducing new `VirtualNode`-dependent utilities unless there is a clear performance or API benefit. +- **Conversion:** Use `getNodeFromTree()` from `core/utils` when you receive an ambiguous input (such as a `VirtualNode`, selector, or mixed type) and need to resolve it to a real DOM `Node`. + +### Import Restrictions (Hard Rules) + +- `standards/` → nothing (pure data, no imports). +- `core/utils/` → other `core/utils`, `core/base`, `standards` via **direct file paths only** (no index). +- `core/imports/` → node modules **only** (the only place npm imports are allowed). +- `commons/` → other `commons` (direct paths), `core/utils` (index OK). +- `checks/` and `rules/` → any directory (index OK). +- **Never** import `commons` from `core/utils`. This is the most commonly rejected violation. + +### Checks & Rules + +- **Check evaluate functions:** Return `true` (pass), `false` (fail), or `undefined` (incomplete). Use `this.data()` to pass values to message templates and to provide incomplete-result detail. +- **Rule JSON:** Use `all`, `any`, `none` check arrays. `selector` + optional `matches` to scope candidates. Valid `impact` values: `"minor"`, `"moderate"`, `"serious"`, `"critical"`. +- **Standards data:** Never hardcode ARIA/HTML lists in checks. Query from `standards/` via `commons/standards` functions. +- **Messages:** All user-facing strings live in check/rule JSON `metadata.messages`. Use `${data.property}` templates. Support singular/plural variants. Update `locales/_template.json` whenever messages change (auto-synced on `npm run build`). + +### High-Risk Areas (Extra Scrutiny Required) + +- **Color contrast:** Handles all CSS color formats, opacity, blend modes, stacking contexts, text-shadow. Return `undefined` when background cannot be determined. See `doc/developer-guide.md`. +- **ARIA validation:** Must stay current with spec. Query roles/attrs from `standards/aria-roles.js` and `standards/aria-attrs.js`. Handle implicit vs. explicit roles. See `doc/rule-development.md`. +- **Hidden elements:** Use `isVisibleToScreenReaders()`, not CSS visibility alone. Account for `aria-hidden="true"` and Shadow DOM boundaries. +- **i18n:** Update `locales/_template.json` on every message change and commit the generated file alongside source. + +## 2. Testing + +- **Structure:** Mirror `lib/` exactly under `test/`. File `lib/commons/text/sanitize.js` → `test/commons/text/sanitize.js`. +- **Checks:** Use `axe.testUtils.MockCheckContext()`. Only reset `checkContext` in `afterEach` — `fixture` and `axe._tree` are auto-cleared. +- **Integration tests:** All rule changes require an HTML + JSON pair. Use `test/integration/rules//` for mocha-hosted tests or `test/integration/full//` when the rule requires a full HTML page. JSON selectors must use axe array format (`["#id"]`; iframes: `["iframe", "#id"]`). Also update or create virtual-rules tests where appropriate. +- **Shadow DOM:** Every relevant check/rule must include an open Shadow DOM test case using `queryShadowFixture`. +- **Logging:** Do not commit `console.log` statements. + +## 3. Build & Commits + +- **Bundles (`axe.js`, `axe.min.js`)** are auto-generated for releases/publishing and are not committed to the repo (they are gitignored). +- **Locales template (`locales/_template.json`)** is auto-generated. When message strings change, regenerate this file and commit it in the same commit as the source changes — never in a separate commit. +- **One change per PR.** Do not mix refactoring with feature work. +- **Commit format:** `(): ` — imperative, lowercase, no period, ≤100 chars total. Body explains motivation. Footer: `Closes issue #123` or full URL. See `doc/code-submission-guidelines.md` for the full type list. + +**Example:** + +``` +fix(aria-valid-attr-value): handle multiple aria-errormessage IDs + +When aria-errormessage contains multiple space-separated IDs, verify +all IDs exist in aria-describedby instead of matching the full string. + +Closes issue #4957 +``` + +## 4. Documentation & API Changes + +- **New rules:** Update `CHANGELOG.md`. `doc/rule-descriptions.md` is auto-generated by `npm run build`. +- **API changes:** Update `doc/API.md` and `axe.d.ts` TypeScript definitions. +- **Breaking changes:** Avoid breaking changes — prefer supporting both old and new formats simultaneously. If unavoidable, add `BREAKING CHANGE: description` to commit footer, include migration guide in `CHANGELOG.md`, and tag deprecated code with `@deprecated` JSDoc. + +## 5. Examples (Copy-Paste Reference) + +- **Code patterns:** `doc/examples/code-patterns.md` — return early, default export, imports, JSDoc, Virtual Node usage +- **Test patterns:** `doc/examples/test-patterns.md` — unit tests, check tests, Shadow DOM tests, integration test HTML+JSON +- **Rule & check templates:** `doc/examples/rule-check-templates.md` — JSON templates for rules and checks, evaluate function pattern +- **PR review patterns:** `doc/examples/pr-review-patterns.md` — common reviewer feedback, anti-patterns, what reviewers look for + +## 6. Reference Docs & Help + +- **Contributing guide:** `CONTRIBUTING.md` +- **Import rules detail:** `eslint.config.js` +- **Code submission standards:** `doc/code-submission-guidelines.md` +- **Developer guide:** `doc/developer-guide.md` +- **Rule development:** `doc/rule-development.md` +- **API reference:** `doc/API.md` +- **Pull Request Checklist:** `doc/pull-request-checklist.md` +- **Slack:** [axe-community](https://accessibility.deque.com/axe-community) +- **Issues:** [GitHub Issues](https://github.com/dequelabs/axe-core/issues) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65031166..1719460ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ We expect all code to be 100% covered by tests. We don't have or want code cover Tests should be added to the `test` directory using the same file path and name of the source file the test is for. For example, the source file `lib/commons/text/sanitize.js` should have a test file at `test/commons/text/sanitize.js`. -Axe uses Karma / Mocha / Chai as its testing framework. +Axe uses Web Test Runner / Mocha / Chai / Sinon as its testing framework. ### Documentation and Comments @@ -202,13 +202,12 @@ If you need to debug the unit tests in a browser, you can run: npm run test:debug ``` -This will start the Karma server and open up the Chrome browser. Click the `Debug` button to start debugging the tests. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](./.vscode/launch.json) is provided. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. +This will start the Web Test Runner server. Press `D` to open the Chrome browser. Click the link to a test to start debugging. You can either use that browser's debugger or attach an external debugger on port 9765; [a VS Code launch profile](./.vscode/launch.json) is provided. You can also navigate to the listed URL in your browser of choice to debug tests using that browser. -Because the amount of tests is so large, it's recommended to debug only a specific set of unit tests rather than the whole test suite. You can use the `testDirs` argument when using the debug command and pass a specific test directory. The test directory names are the same as those used for `test:unit:*`: +Because the amount of tests is so large, it's recommended to debug only a specific set of unit tests rather than the whole test suite. You can use the `files` argument when using the debug command and pass a test files glob pattern. ```console -# accepts a single directory or a comma-separated list of directories -npm run test:debug -- testDirs=core,commons +npm run test:debug -- --files 'test/core' ``` ## Using axe with TypeScript diff --git a/Gruntfile.js b/Gruntfile.js index 3b9872799..365674cce 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,7 +37,7 @@ module.exports = function (grunt) { // run tests only for affected files instead of all tests grunt.event.on('watch', function (action, filepath) { - grunt.config.set('watch.file', filepath); + grunt.option('changed-file', filepath); }); process.env.NODE_NO_HTTP2 = 1; // to hide node warning - (node:18740) ExperimentalWarning: The http2 module is an experimental API. @@ -46,7 +46,7 @@ module.exports = function (grunt) { pkg: grunt.file.readJSON('package.json'), clean: { core: ['dist', 'tmp/core', 'tmp/rules.js', 'axe.js', 'axe.*.js'], - tests: ['tmp/integration-tests.js'] + tests: ['tmp/integration-tests'] }, babel: { options: { @@ -109,6 +109,39 @@ module.exports = function (grunt) { cwd: 'lib/core', src: ['core.js'], dest: 'tmp/core' + }, + // build so we can test it by itself + { + expand: true, + cwd: 'lib/gather-internals', + src: ['walk-tree.js'], + dest: 'tmp/', + options: { + globalName: '_gatherInternals' + } + }, + { + expand: true, + src: ['lib/gather-internals/main.js'], + dest: './', + options: { + outfile: 'gather-internals.js', + // esbuild doesn't support returning from an iife so we'll have to do a bit of a hack to make it work + // @see https://github.com/evanw/esbuild/issues/2277 + banner: { + js: '(() => {' + }, + footer: { + js: `return elementInternalsMap; +})();` + }, + globalName: 'elementInternalsMap', + metafile: true + }, + validateImports: { + max: 10, + maxSize: 4000 + } } ] } @@ -231,9 +264,7 @@ module.exports = function (grunt) { } }, test: { - data: { - testFile: '<%= watch.file %>' - } + data: {} }, watch: { axe: { @@ -243,7 +274,7 @@ module.exports = function (grunt) { }, tests: { options: { spawn: false }, - files: ['test/**/*'], + files: ['test/**/*', '!test/integration/full/**/*'], tasks: ['test'] } }, diff --git a/axe.d.ts b/axe.d.ts index 82722edd8..57d8ab446 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -147,6 +147,9 @@ declare namespace axe { performanceTimer?: boolean; pingWaitTime?: number; } + interface NormalizedRunOptions extends RunOptions { + runOnly?: RunOnly; + } interface PreloadOptions { assets: string[]; timeout?: number; @@ -331,7 +334,7 @@ declare namespace axe { matches?: string | ((node: Element, virtualNode: VirtualNode) => boolean); reviewOnFail?: boolean; actIds?: string[]; - metadata?: Omit; + metadata?: Omit; } interface AxePlugin { id: string; @@ -349,6 +352,7 @@ declare namespace axe { helpUrl: string; tags: string[]; actIds?: string[]; + enabled?: boolean; } interface SerialDqElement { source: string; @@ -465,6 +469,10 @@ declare namespace axe { } interface Utils { + getElementSource: ( + element: Node | null | undefined, + options?: { maxLength?: number; attrLimit?: number } + ) => string; getFrameContexts: ( context?: ElementContext, options?: RunOptions @@ -495,6 +503,7 @@ declare namespace axe { offset?: number ) => string | Uint8Array | Array; nodeSerializer: NodeSerializer; + normalizeRunOptions: (options?: RunOptions) => NormalizedRunOptions; } interface Aria { @@ -612,13 +621,23 @@ declare namespace axe { * @param {Array} tags Optional array of tags * @return {Array} Array of rules */ - function getRules(tags?: string[]): RuleMetadata[]; + function getRules( + tags?: string[] + ): (Omit & { enabled: boolean })[]; /** * Restores the default axe configuration */ function reset(): void; + /** + * Restores the default locale that was active before any + * `axe.configure({ locale })` call. No-op if no non-default + * locale has ever been applied. Does not affect any other + * configuration. + */ + function resetLocale(): void; + /** * Function to register a plugin configuration in document and its subframes * @param {Object} plugin A plugin configuration object @@ -664,6 +683,31 @@ declare namespace axe { isDefault?: boolean ): void; + /** + * Run axe in the current window only + * @param {ElementContext} context Optional The `Context` specification object @see Context + * @param {RunOptions} options Optional Options passed into rules or checks, temporarily modifying them. + * @returns {Promise} Partial result, for use in axe.finishRun. + */ + function externalAPIs(params?: { + elementInternalsTimeout?: number; + getElementInternals?: () => Promise; + }): void; + + type ElementInternalsMap = Array<{ + ancestry: CrossTreeSelector; + internals: Record; + }>; + type InternalsData = string | InternalsDataIdref | InternalsDataIdrefs; + type InternalsDataIdref = { + type: 'HTMLElement'; + value: CrossTreeSelector; + }; + type InternalsDataIdrefs = { + type: 'NodeList'; + value: CrossTreeSelector[]; + }; + // axe.frameMessenger type FrameMessenger = { open: (topicHandler: TopicHandler) => Close | void; diff --git a/bower.json b/bower.json index 52beb0a4f..ee0f36409 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.11.0", + "version": "4.12.1", "deprecated": true, "contributors": [ { diff --git a/build/cherry-pick.js b/build/cherry-pick.js index f8040807b..c5575f364 100755 --- a/build/cherry-pick.js +++ b/build/cherry-pick.js @@ -5,6 +5,9 @@ const conventionalCommitsParser = require('conventional-commits-parser'); const chalk = require('chalk'); const { version } = require('../package.json'); +// Node's default execSync buffer (~1 MiB) is too small for `git log` on long histories. +const GIT_LOG_EXEC_OPTS = { maxBuffer: 50 * 1024 * 1024 }; + const releaseType = process.argv[2]; let ignoreCommits = process.argv[3]; @@ -50,7 +53,8 @@ function getCommits(branch) { // all commits are too large for execSync buffer size so we'll just get since the last 3 years const date = new Date(new Date().setFullYear(new Date().getFullYear() - 3)); const stdout = execSync( - `git log ${branch || ''} --abbrev-commit --since=${date.getFullYear()}` + `git log ${branch || ''} --no-merges --abbrev-commit --since=${date.getFullYear()}`, + GIT_LOG_EXEC_OPTS ).toString(); const allCommits = stdout .split(/commit (?=[\w\d]{8}[\n\r])/) diff --git a/build/generate-integration-tests.js b/build/generate-integration-tests.js new file mode 100644 index 000000000..0143e28c7 --- /dev/null +++ b/build/generate-integration-tests.js @@ -0,0 +1,60 @@ +'use strict'; + +/** + * Generates test files for integration rule tests in tmp/integration-tests/. + * This replaces the Karma preprocessor (test/integration/rules/preprocessor.js) + * that combined *.json + *.html pairs into executable JS at serve-time. + * + * Output files are picked up by web-test-runner via the test:unit:integration script. + */ + +const path = require('path'); +const fs = require('fs'); +const { globSync } = require('glob'); + +const rootDir = path.join(__dirname, '..'); +const rulesDir = path.join(rootDir, 'test', 'integration', 'rules'); +const outDir = path.join(rootDir, 'tmp', 'integration-tests'); +const runnerTemplate = fs.readFileSync( + path.join(rulesDir, 'runner.js'), + 'utf-8' +); + +// Clean and recreate output directory +fs.rmSync(outDir, { recursive: true, force: true }); +fs.mkdirSync(outDir, { recursive: true }); + +const jsonFiles = globSync('**/*.json', { cwd: rulesDir }); + +let count = 0; +for (const relPath of jsonFiles) { + const jsonPath = path.join(rulesDir, relPath); + const htmlPath = jsonPath.replace(/\.json$/, '.html'); + + if (!fs.existsSync(htmlPath)) { + // Some JSON files may not have a sibling HTML (e.g. nested frame fixtures) + continue; + } + + let test; + try { + test = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); + } catch (e) { + throw new Error(`Unable to parse ${jsonPath}: ${e.message}`); + } + + const html = fs.readFileSync(htmlPath, 'utf-8'); + test.content = html; + + const outPath = path.join(outDir, relPath.replace(/\.json$/, '.test.js')); + const outDirForFile = path.dirname(outPath); + fs.mkdirSync(outDirForFile, { recursive: true }); + + const output = runnerTemplate.replace('{}; /*tests*/', JSON.stringify(test)); + fs.writeFileSync(outPath, output, 'utf-8'); + count++; +} + +console.log( + `Generated ${count} integration test files in tmp/integration-tests/` +); diff --git a/build/next-version.js b/build/next-version.js deleted file mode 100755 index 189311c0a..000000000 --- a/build/next-version.js +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); - -const pkgFile = path.resolve(__dirname, '..', 'package.json'); -const pkg = JSON.parse(fs.readFileSync(pkgFile)); - -const { CIRCLE_SHA1, CIRCLE_BRANCH } = process.env; -assert(CIRCLE_BRANCH, 'CIRCLE_BRANCH environment variable not set'); -assert(CIRCLE_SHA1, 'CIRCLE_SHA1 environment variable not set'); -assert( - CIRCLE_BRANCH === 'develop', - 'This script should only be run from "develop"' -); - -// Shorten the SHA -const GIT_SHA = CIRCLE_SHA1.substr(0, 7); - -// Strip the "dist tag" from the version (if it exists) -const version = pkg.version.replace(/-\w+\.\w+$/, ''); -const nextVersion = `${version}-canary.${GIT_SHA}`; -console.log(nextVersion); diff --git a/build/tasks/test.js b/build/tasks/test.js index c1f51a7b4..13e6a3d2b 100644 --- a/build/tasks/test.js +++ b/build/tasks/test.js @@ -1,5 +1,6 @@ const execSync = require('child_process').execSync; const chalk = require('chalk'); +const path = require('node:path'); /*eslint-env node */ ('use strict'); @@ -9,12 +10,37 @@ module.exports = function (grunt) { 'test', 'This task runs unit tests based on which file was changed', function () { - const testFile = this.data.testFile; + const testFile = grunt.option('changed-file'); console.log(`${chalk.green('>>')} File "${testFile}"`); + const files = []; - execSync(`npm run test:unit -- testFiles=${testFile}`, { - stdio: 'inherit' - }); + // build the integration tests before testing + if ( + (testFile.startsWith(path.join('test', 'integration', 'rules')) && + testFile.endsWith('.html')) || + testFile.endsWith('.json') + ) { + execSync('npm run build:integration-tests', { stdio: 'inherit' }); + const rule = testFile.split(path.sep)[3]; + files.push( + path.join('tmp', 'integration-tests', rule, rule + '.test.js') + ); + } + + if ( + testFile && + testFile.startsWith(`test${path.sep}`) && + testFile.endsWith('.js') + ) { + files.push(testFile); + } + + let cmd = 'npm run test:unit'; + if (files.length) { + cmd += ` -- --files "${files.join(',')}"`; + } + + execSync(cmd, { stdio: 'inherit' }); } ); }; diff --git a/doc/API.md b/doc/API.md index 18f4dbb9c..f85a53e50 100644 --- a/doc/API.md +++ b/doc/API.md @@ -10,6 +10,7 @@ 1. [API Name: axe.getRules](#api-name-axegetrules) 1. [API Name: axe.configure](#api-name-axeconfigure) 1. [API Name: axe.reset](#api-name-axereset) + 1. [API Name: axe.resetLocale](#api-name-axeresetlocale) 1. [API Name: axe.run](#api-name-axerun) 1. [Parameters axe.run](#parameters-axerun) 1. [Context Parameter](#context-parameter) @@ -24,6 +25,7 @@ 1. [API Name: axe.teardown](#api-name-axeteardown) 1. [API Name: axe.frameMessenger](#api-name-axeframemessenger) 1. [API name: axe.runPartial / axe.finishRun](#api-name-axerunpartial--axefinishrun) + 1. [API name: axe.externalAPIs](#api-name-axeexternal-apis) 1. [Virtual DOM Utilities](#virtual-dom-utilities) 1. [API Name: axe.utils.querySelectorAll](#api-name-axeutilsqueryselectorall) 1. [API Name: axe.utils.getRule](#api-name-axeutilsgetrule) @@ -135,7 +137,7 @@ Returns a list of all rules with their ID and description - `tags` - **optional** Array of tags used to filter returned rules. If omitted, it will return all rules. See [axe-core tags](#axe-core-tags). -**Returns:** Array of rules that match the input filter with each entry having a format of `{ruleId: , description: , helpUrl: , help: , tags: }` +**Returns:** Array of rules that match the input filter with each entry having a format of `{ruleId: , description: , helpUrl: , help: , tags: , enabled: }`. `enabled` is `true` for rules that run by default when `axe.run()` is called with no options, and `false` for rules that are disabled by default (i.e. experimental and deprecated rules). #### Example 1 @@ -161,7 +163,8 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr "section508", "section508.22.a" ], - actIds: ['c487ae'] + actIds: ['c487ae'], + enabled: true }, { description: "Ensure ARIA attributes are allowed for an element's role", @@ -172,7 +175,8 @@ In this example, we pass in the WCAG 2 A and AA tags into `axe.getRules` to retr "cat.aria", "wcag2a", "wcag412" - ] + ], + enabled: true } … ] @@ -296,6 +300,24 @@ axe.reset(); None +### API Name: axe.resetLocale + +#### Description + +Restore the default locale that was active before any `axe.configure({ locale })` call, without touching the rest of the configuration. + +`axe.configure({ locale })` has no inverse, and `axe.reset()` also clears branding, rule enable/disable overrides, `frameMessenger`, and other configuration. `axe.resetLocale()` reverts only the locale (rule descriptions, check messages, failure summaries, `lang`) back to the default that was in effect before the first `applyLocale` call. It is a no-op if no non-default locale has ever been applied, and safe to call repeatedly. + +#### Synopsis + +```js +axe.resetLocale(); +``` + +#### Parameters + +None + ### API Name: axe.run #### Purpose @@ -840,6 +862,10 @@ Set up an alternative communication channel between parent and child frames. By Run axe without frame communication. This is the recommended way to run axe in browser drivers such as Selenium and Puppeteer. See [run-partial.md](run-partial.md) for details. +### API name: axe.externalAPIs + +Set external API data for axe-core to use during the run. See [external-apis.md](external-apis.md) for details. + ### Virtual DOM Utilities Note: If you’re writing rules or checks, you’ll have both the `node` and `virtualNode` passed in. diff --git a/doc/check-options.md b/doc/check-options.md index d35e9575a..532937e0b 100644 --- a/doc/check-options.md +++ b/doc/check-options.md @@ -517,7 +517,7 @@ h6:not([role]),
"passLength": 1
- Relative length, if the the candidate heading is X times or greater the length of the candidate paragraph, it will pass. + Relative length, if the candidate heading is X times or greater the length of the candidate paragraph, it will pass. @@ -526,7 +526,7 @@ h6:not([role]),
"failLength": 0.5
- Relative length, if the the candidate heading is X times or less the length of the candidate paragraph, it can fail. + Relative length, if the candidate heading is X times or less the length of the candidate paragraph, it can fail. diff --git a/doc/element-internals.md b/doc/element-internals.md new file mode 100644 index 000000000..19c101f4d --- /dev/null +++ b/doc/element-internals.md @@ -0,0 +1,47 @@ +# ElementInternals + +Axe-core supports CustomElements with attached ElementInternals or ARIA Properties. However, JavaScript does not provide an API for accessing ElementInternals information on a node so axe-core must rely on developers implementing a [community protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/element-internals-declaration.md) on their CustomElements in order to find this information. + +> [!Note] +> At this time, axe-core only supports the ElementInternals `role` property and no others (e.g. `ariaLabel`), though support for other properties is planned. Additionally axe-core will not validate the value of the ElementInternals `role`, and many rules will not run against the element (e.g. `aria-required-attr`). Rules that do run may only be partially supported (e.g. `aria-required-children`). +> +> Lastly, support for ElementInternals is behind a feature flag `axe._enableElementInternals`, which must manually be set to `true` before axe runs. + +```js +CustomElements.define( + 'my-custom-button', + class MyCustomButton extends HTMLElement { + constructor() { + super(); + + const internals = this.attachInternals(); + internals.role = 'button'; + + globalThis._elementInternals ??= new WeakMap(); + globalThis._elementInternals.set(this, internals); + } + } +); +``` + +In addition to the global WeakMap `globalThis._elementInternals`, axe-core also supports the following public properties or Symbols on the CustomElement: + +- `_internals` +- `internals` +- `internals_` +- `Symbol('internals')` +- `Symbol('privateInternals')` + +```js +CustomElements.define( + 'my-custom-button', + class MyCustomButton extends HTMLElement { + constructor() { + super(); + + this._internals = this.attachInternals(); + this._internals.role = 'button'; + } + } +); +``` diff --git a/doc/examples/code-patterns.md b/doc/examples/code-patterns.md new file mode 100644 index 000000000..e342b8f1e --- /dev/null +++ b/doc/examples/code-patterns.md @@ -0,0 +1,152 @@ +# Code Pattern Examples + +Quick-reference examples for axe-core coding conventions. + +## Default Export at Top + +```javascript +// GOOD: Default export right after imports +import { getRole } from '../../commons/aria'; +import { isVisible } from '../../commons/dom'; + +export default function myFunction(node, options) { + // function body +} + +// BAD: Export buried at bottom of file +import { getRole } from '../../commons/aria'; + +function myFunction(node, options) { + // body +} + +// ... more code ... + +export default myFunction; // Too far from top +``` + +## Return Early Pattern + +```javascript +// GOOD: Main path left-aligned, edge cases exit early +export default function processValue(value) { + if (!value) { + return null; + } + + if (value.length < 3) { + throw new Error('Value too short'); + } + + const normalized = normalize(value); + const result = transform(normalized); + return result; +} + +// BAD: Nested conditionals +export default function processValue(value) { + let result; + if (value) { + if (value.length >= 3) { + const normalized = normalize(value); + result = transform(normalized); + } else { + throw new Error('Value too short'); + } + } else { + result = null; + } + return result; +} +``` + +## Import Restrictions + +```javascript +// GOOD: commons importing from core/utils via index +import { getNodeFromTree } from '../../core/utils'; + +// GOOD: commons importing other commons directly +import getExplicitRole from '../aria/get-explicit-role'; + +// BAD: core/utils importing from commons — NEVER DO THIS +import { isDisabled } from '../../commons/forms'; + +// BAD: importing from index in core/utils — use direct path +import { someUtil } from './index'; // Use: import someUtil from './some-util'; +``` + +## JSDoc Comments + +### Standard function + +```javascript +/** + * Determines if an element is a native select element + * @method isNativeSelect + * @memberof axe.commons.forms + * @param {VirtualNode|Element} node Node to determine if select + * @returns {Boolean} + */ +import nodeLookup from '../../core/utils/node-lookup'; + +function isNativeSelect(node) { + const { vNode } = nodeLookup(node); + const nodeName = vNode.props.nodeName; + return nodeName === 'select'; +} +``` + +### Check evaluate function + +```javascript +/** + * Check if an element's `role` attribute uses any abstract role values. + * + * Abstract roles are taken from the `ariaRoles` standards object from the roles `type` property. + * + * ##### Data: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
TypeDescription
String[]List of all abstract roles
+ * + * @memberof checks + * @return {Boolean} True if the element uses an `abstract` role. False otherwise. + */ +function abstractroleEvaluate(node, options, virtualNode) { + // implementation +} +``` + +## Virtual Node vs HTMLElement + +```javascript +// Use Virtual Node for attribute access and property reads +function myCheck(node, options, virtualNode) { + const role = virtualNode.attr('role'); // Cached attribute access + const nodeName = virtualNode.props.nodeName; + + // Only access real node when you need DOM APIs + const rect = node.getBoundingClientRect(); + const rootNode = node.getRootNode(); +} + +// Convert ambiguous input using nodeLookup +import nodeLookup from '../../core/utils/node-lookup'; + +function myFunction(nodeOrVirtual) { + const { vNode, domNode } = nodeLookup(nodeOrVirtual); + // vNode = VirtualNode, domNode = real DOM node +} +``` diff --git a/doc/examples/pr-review-patterns.md b/doc/examples/pr-review-patterns.md new file mode 100644 index 000000000..a56324054 --- /dev/null +++ b/doc/examples/pr-review-patterns.md @@ -0,0 +1,69 @@ +# PR Review Patterns + +Common feedback and anti-patterns observed in axe-core code reviews. + +## What Gets Called Out + +### 1. Missing Tests + +- Every behavior-changing code change needs unit tests +- Rule changes need integration tests (HTML + JSON pair) +- Shadow DOM test coverage required for relevant checks/rules + +### 2. Commit Message Format + +- Wrong type/scope +- Not imperative present tense +- Subject too long or capitalized +- Missing issue reference in footer + +### 3. Import Violations + +- `core/utils` importing from `commons` (forbidden) +- Using index imports where direct file paths are required +- Importing node modules outside `core/imports` + +### 4. Code Style Issues + +- Not using return early pattern +- Default export not at top of file +- Nested conditionals when early return would work +- Missing JSDoc comments + +### 5. Performance Concerns + +- Unnecessary DOM queries in loops +- Not caching Virtual Node properties +- Computing same value repeatedly instead of storing it + +### 6. Incomplete Results + +- Not returning `undefined` when a check can't determine the result +- Missing `incomplete` message variants in check JSON +- Not setting appropriate `this.data()` for incomplete cases + +### 7. Accessibility Edge Cases + +- Not considering all ARIA states +- Missing edge cases for hidden elements +- Not handling Shadow DOM properly + +## What Reviewers Love + +1. Comprehensive test coverage including edge cases +2. Clear, detailed commit messages with motivation +3. JSDoc comments that explain "why" not just "what" +4. Performance-conscious code that caches appropriately +5. Following existing patterns in similar files +6. Integration tests that cover both pass and fail cases + +## Common PR Mistakes + +1. Not running `npm test` locally before pushing +2. Not committing auto-generated `locales/_template.json` in the same commit as message source changes +3. Changing multiple unrelated things in one PR — split refactoring from feature work +4. Not updating integration tests when changing rule behavior +5. `console.log` statements should not be committed +6. Not handling `null` or `undefined` gracefully +7. Hardcoding strings that should come from `standards/` data +8. Changing public APIs without `BREAKING CHANGE` in commit footer diff --git a/doc/examples/puppeteer/axe-puppeteer.js b/doc/examples/puppeteer/axe-puppeteer.js index 64f657b33..08b4a6e4a 100644 --- a/doc/examples/puppeteer/axe-puppeteer.js +++ b/doc/examples/puppeteer/axe-puppeteer.js @@ -1,12 +1,15 @@ const puppeteer = require('puppeteer'); const axeCore = require('axe-core'); -const { parse: parseURL } = require('url'); const assert = require('assert'); // Cheap URL validation const isValidURL = input => { - const u = parseURL(input); - return u.protocol && u.host; + try { + const u = new URL(input); + return u.protocol && u.host; + } catch { + return false; + } }; (async () => { @@ -18,7 +21,15 @@ const isValidURL = input => { let results; try { // Setup Puppeteer - browser = await puppeteer.launch(); + browser = await puppeteer.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu' + ] + }); // Get new page const page = await browser.newPage(); diff --git a/doc/examples/puppeteer/set-content.js b/doc/examples/puppeteer/set-content.js index 60e85f8ef..97d746f06 100644 --- a/doc/examples/puppeteer/set-content.js +++ b/doc/examples/puppeteer/set-content.js @@ -3,7 +3,15 @@ const puppeteer = require('puppeteer'); const axe = require('axe-core'); (async () => { - const browser = await puppeteer.launch(); + const browser = await puppeteer.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu' + ] + }); const page = await browser.newPage(); await page.setContent(` diff --git a/doc/examples/rule-check-templates.md b/doc/examples/rule-check-templates.md new file mode 100644 index 000000000..5bfd25109 --- /dev/null +++ b/doc/examples/rule-check-templates.md @@ -0,0 +1,112 @@ +# Rule & Check JSON Templates + +Quick-reference templates for creating axe-core rules and checks. + +## Rule JSON + +```json +{ + "id": "aria-input-field-name", + "impact": "serious", + "selector": "[role=\"textbox\"], [role=\"combobox\"]", + "matches": "no-naming-method-matches", + "tags": ["cat.aria", "wcag2a", "wcag412"], + "metadata": { + "description": "Ensure every ARIA input field has an accessible name", + "help": "ARIA input fields must have an accessible name" + }, + "all": [], + "any": ["aria-label", "aria-labelledby", "non-empty-title"], + "none": ["no-implicit-explicit-label"] +} +``` + +**Key properties:** + +| Property | Description | +| ----------- | ------------------------------------------------------------- | +| `selector` | CSS selector for candidate elements | +| `matches` | Optional function to further filter elements | +| `all` | All checks must pass | +| `any` | At least one check must pass | +| `none` | All checks must fail (for the rule to pass) | +| `impact` | `"minor"`, `"moderate"`, `"serious"`, `"critical"` | +| `enabled` | Defaults to `true`; set `false` for disabled/deprecated rules | +| `tags` | Grouping: `wcag2a`, `wcag2aa`, `best-practice`, `cat.*`, etc. | +| `pageLevel` | `true` if rule should only run on the top-level window | + +## Check JSON + +```json +{ + "id": "aria-allowed-attr", + "evaluate": "aria-allowed-attr-evaluate", + "options": { + "validTreeRowAttrs": ["aria-posinset", "aria-setsize"] + }, + "metadata": { + "impact": "critical", + "messages": { + "pass": "ARIA attributes are used correctly for the defined role", + "fail": { + "singular": "ARIA attribute is not allowed: ${data.values}", + "plural": "ARIA attributes are not allowed: ${data.values}" + }, + "incomplete": "Check that there is no problem if the ARIA attribute is ignored: ${data.values}" + } + } +} +``` + +**Message templates:** + +- Use `${data.property}` for dynamic values set via `this.data()` +- Support singular/plural variants via object with `singular`/`plural` keys +- `locales/_template.json` is auto-generated by `npm run build` — do not edit it manually, but commit the regenerated file alongside source changes + +## Check Evaluate Function + +```javascript +export default function myCheckEvaluate(node, options, virtualNode) { + // node: HTMLElement + // options: from check JSON or runtime config + // virtualNode: Virtual Node representation + + // Early returns for edge cases + if (someEdgeCase) { + return true; // Pass + } + + // Compute what you need + const relevantData = computeStuff(virtualNode); + + // Set data for messages (optional) + // this.data() sets an arbitrary object passed to message templates + if (relevantData.hasIssues) { + this.data({ + messageKey: 'someIssue', // Selects which message variant to use (e.g., singular/plural) + values: relevantData.issueList // ${data.values} in templates resolves arrays + }); + } + + // Return boolean or undefined + if (cannotDetermine) { + this.data({ reason: 'could not determine background color' }); + return undefined; // Incomplete + } + + return isValid; // true = pass, false = fail +} +``` + +**Return values:** + +- `true` — check passed +- `false` — check failed +- `undefined` — incomplete (cannot determine result) + +**`this.data()` usage:** + +- Sets an arbitrary data object that gets passed to message templates via `${data.propertyName}` +- `messageKey` — selects which message variant to use (e.g., `singular`/`plural`) +- Any other keys are available in templates as `${data.keyName}` — arrays are automatically joined diff --git a/doc/examples/test-patterns.md b/doc/examples/test-patterns.md new file mode 100644 index 000000000..e373ee0ce --- /dev/null +++ b/doc/examples/test-patterns.md @@ -0,0 +1,94 @@ +# Test Pattern Examples + +Quick-reference examples for axe-core test conventions. + +## Unit Test — Commons Function + +```javascript +describe('text.sanitize', function () { + it('should collapse whitespace and trim', function () { + assert.equal(axe.commons.text.sanitize('\thi\t'), 'hi'); + assert.equal(axe.commons.text.sanitize('\t\nhi \t'), 'hi'); + assert.equal(axe.commons.text.sanitize('hello\u00A0there'), 'hello there'); + }); + + it('should accept null', function () { + assert.equal(axe.commons.text.sanitize(null), ''); + }); +}); +``` + +## Unit Test — Check Evaluate + +```javascript +describe('aria-allowed-attr', function () { + const fixture = document.getElementById('fixture'); + const checkContext = axe.testUtils.MockCheckContext(); + + afterEach(function () { + checkContext.reset(); + }); + + it('should return true if all ARIA attributes are allowed', function () { + const vNode = queryFixture( + '
' + ); + + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, vNode.actualNode, {}, vNode) + ); + }); +}); +``` + +## Shadow DOM Test + +```javascript +it('should work with Shadow DOM', function () { + const vNode = queryShadowFixture( + '
', + '
Test
' + ); + // Test your function against the shadow DOM content +}); +``` + +## Integration Test — Rule + +Each rule change requires an HTML + JSON pair in `test/integration/rules//` (mocha-hosted) or `test/integration/full//` (full HTML page). Virtual-rules tests in `test/virtual-rules/` should also be updated or created for appropriate rules. See sections below for details. + +### HTML file (`aria-allowed-attr.html`) + +```html +
Valid
+
Invalid
+``` + +### JSON file (`aria-allowed-attr.json`) + +```json +{ + "description": "aria-allowed-attr test", + "rule": "aria-allowed-attr", + "violations": [["#fail1"]], + "passes": [["#pass1"]] +} +``` + +IDs use axe selector array format. For elements inside iframes: + +```json +{ + "violations": [["iframe", "#fail-inside-iframe"]] +} +``` + +### Full-page integration tests + +For rules that need a complete HTML page (e.g., landmark rules, page-level rules), use `test/integration/full/` instead. These tests run against a full HTML document rather than injected fragments. + +### Virtual rules tests + +Rules that can run without a DOM (using only virtual nodes) should have tests in `test/virtual-rules/`. These tests verify that rules work correctly with `axe.run()` on serialized node data. diff --git a/doc/external-apis.md b/doc/external-apis.md new file mode 100644 index 000000000..4fd792f2a --- /dev/null +++ b/doc/external-apis.md @@ -0,0 +1,68 @@ +# External APIs + +Axe externalAPIs can be used to pass information to axe-core that it would be otherwise unable to obtain on its own (such as when it is running in an isolated JavaScript context of an extension). By default axe-core will try to gather the information if the external API for it is not set. If the external API is set axe-core will only rely on the passed in data and will not gather it itself. + +```js +axe.externalAPIs({ + // Pass ElementInternal data to axe-core + elementInternals() { + return Promise.resolve(internalData); + } +}); +``` + +## axe.externalAPIs({ elementInternals }) + +`elementInternals` is a function that can be used to pass ElementInternal data for the elements on the page. It should return a Promise with the ElementInternal data array. The data inside the ElementInternal array must have the following properties: + +```js +axe.externalAPIs({ + elementInternals() { + return Promise.resolve([ + { + // The CSS ancestry selector, which is either a string or an array of strings if the element is inside a ShadowDom tree. Generated using `axe.utils.getAncestry(node)` + ancestry: 'html > body > main > my-custom-button', + + // Object of each ElementInternals property name and value. The properties must be serialized so some serialization work is needed for element reference properties (idref(s)) + internals: { + // A simple String value + role: 'button', + + // An idref value. The type will always be "HTMLElement" and the value is a CSS ancestry selector + ariaActiveDescendantElement: { + type: 'HTMLElement', + value: 'html > body > main > my-custom-button > div:nth-child(1)' + }, + + // An idrefs value. The type will always be "NodeList" and the value is an array of CSS ancestry selectors for each node + ariaLabelledbyElements: { + type: 'NodeList', + value: ['html > body > div:nth-child(4)'] + } + } + } + ]); + } +}); +``` + +Axe-core provides a package script `gather-internals.js` that can be used to inject into the main context when working in an extension. The returned object from the script can be used directly as the returned value of the Promise for `elementInternals`. + +```js +const internals = await chrome.scripting.executeScript({ + target: { + tabId: tab.id + }, + files: ['/node_modules/axe-core/gather-internals.js'], + world: 'MAIN' +}); +``` + +See [element-internals.md](element-internals.md) for more information on what ElementInternal properties are supported. + +> [!Note] +> Support for ElementInternals is behind a feature flag `axe._enableElementInternals`, which must manually be set to `true` before axe runs, even when passing in `elementInternals` data. + +## axe.externalAPIs({ elementInternalsTimeout }) + +Since gathering ElementInternals data is an async operation, you can configure how long axe-core will wait for `elementInternals` promise to resolve. By default the timeout is set to 1 second. If the timeout occurs axe-core will not run and will throw an error. diff --git a/doc/projects.md b/doc/projects.md index 8d6da5328..8b97957a9 100644 --- a/doc/projects.md +++ b/doc/projects.md @@ -61,5 +61,6 @@ Add your project/integration to this file and submit a pull request. 1. [Webatool](https://github.com/balajihambeere/webatool) 1. [A11y Audit Elixir](https://github.com/angelikatyborska/a11y-audit-elixir) 1. [Accented](https://accented.dev) +1. [Happo](https://happo.io) Axe® and axe-core® are registered trademarks of Deque Systems Inc. diff --git a/doc/pull-request-checklist.md b/doc/pull-request-checklist.md new file mode 100644 index 000000000..c36a6e05d --- /dev/null +++ b/doc/pull-request-checklist.md @@ -0,0 +1,42 @@ +# axe-core — PR Checklist + +Complete all applicable items before opening a PR. For items that do not apply to this PR (for example, tests for a `chore`-only change), mark them as "N/A" and follow `doc/code-submission-guidelines.md`. Reviewers will not merge until all required boxes are checked. + +## Code + +- [ ] Default export is at the top of the file, immediately after imports +- [ ] Return early pattern used — no nested conditionals where an early return works +- [ ] DocBlock comments where appropriate (especially for public/exported APIs), per `doc/code-submission-guidelines.md` +- [ ] No `console.log` statements committed +- [ ] No hardcoded ARIA/HTML lists — queried from `standards/` via `commons/standards` +- [ ] Imports follow directory restrictions — especially no `commons` from `core/utils` + +## Tests + +- [ ] Unit tests cover all code paths (`null`/`undefined` input handling only needed for public API functions) +- [ ] Integration test HTML + JSON pair added/updated for any rule changes — use `integration/rules` for mocha-hosted tests or `integration/full` when the rule requires a full HTML page +- [ ] Virtual-rules tests updated or created for appropriate rules +- [ ] Shadow DOM test case added where relevant +- [ ] Code built with `npm run build` before testing +- [ ] All applicable tests pass locally: `npm test` (unit), `test:integration`, `test:virtual-rules`, `test:act`, `test:apg`, `test:examples`, `test:node`, `test:jsdom` + +## Formatting & Build + +- [ ] `npm run fmt` passes (Prettier) +- [ ] `npm run eslint` passes +- [ ] `npm run build` run — generated artifacts that are tracked in git (for example `locales/_template.json`) are committed in the same commit as source changes + +## Commits + +- [ ] All commits follow Angular format: `(): ` +- [ ] Subject is imperative, lowercase, no trailing period, ≤100 chars total +- [ ] Commit body explains motivation (not just what changed) +- [ ] Footer includes issue reference: `Closes issue: #123` or full URL +- [ ] Breaking changes avoided — prefer supporting both old and new formats simultaneously. If unavoidable, document in footer: `BREAKING CHANGE: description` + +## Docs + +- [ ] `doc/rule-descriptions.md` is auto-generated by `npm run build` — verify it regenerates correctly for new rules +- [ ] `doc/API.md` and `axe.d.ts` updated for API changes +- [ ] `locales/_template.json` updated if messages changed +- [ ] `CHANGELOG.md` updated with migration guide for breaking changes diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 0b865f072..b08878b28 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -14,79 +14,80 @@ ## WCAG 2.0 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :-------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [area-alt](https://dequeuniversity.com/rules/axe/4.11/area-alt?application=RuleDescription) | Ensure <area> elements of image maps have alternative text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.2 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-attr?application=RuleDescription) | Ensure an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.11/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | | -| [aria-command-name](https://dequeuniversity.com/rules/axe/4.11/aria-command-name?application=RuleDescription) | Ensure every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.11/aria-conditional-attr?application=RuleDescription) | Ensure ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.11/aria-deprecated-role?application=RuleDescription) | Ensure elements do not use deprecated roles | Minor | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [674b10](https://act-rules.github.io/rules/674b10) | -| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-body?application=RuleDescription) | Ensure aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.1.3.1, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | | -| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-focus?application=RuleDescription) | Ensure aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | -| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-input-field-name?application=RuleDescription) | Ensure every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.11/aria-meter-name?application=RuleDescription) | Ensure every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | -| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.11/aria-progressbar-name?application=RuleDescription) | Ensure every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | -| [aria-prohibited-attr](https://dequeuniversity.com/rules/axe/4.11/aria-prohibited-attr?application=RuleDescription) | Ensure ARIA attributes are not prohibited for an element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.11/aria-required-attr?application=RuleDescription) | Ensure elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | -| [aria-required-children](https://dequeuniversity.com/rules/axe/4.11/aria-required-children?application=RuleDescription) | Ensure elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.11/aria-required-parent?application=RuleDescription) | Ensure elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-roles](https://dequeuniversity.com/rules/axe/4.11/aria-roles?application=RuleDescription) | Ensure all elements with a role attribute use a valid value | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [674b10](https://act-rules.github.io/rules/674b10) | -| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-toggle-field-name?application=RuleDescription) | Ensure every ARIA toggle field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-7.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.11/aria-tooltip-name?application=RuleDescription) | Ensure every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | | -| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr-value?application=RuleDescription) | Ensure all ARIA attributes have valid values | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | -| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr?application=RuleDescription) | Ensure attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | -| [blink](https://dequeuniversity.com/rules/axe/4.11/blink?application=RuleDescription) | Ensure <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | -| [button-name](https://dequeuniversity.com/rules/axe/4.11/button-name?application=RuleDescription) | Ensure buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1), [m6b1q3](https://act-rules.github.io/rules/m6b1q3) | -| [bypass](https://dequeuniversity.com/rules/axe/4.11/bypass?application=RuleDescription) | Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a, EN-301-549, EN-9.2.4.1, RGAAv4, RGAA-12.7.1 | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | -| [color-contrast](https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, TTv5, TT13.c, EN-301-549, EN-9.1.4.3, ACT, RGAAv4, RGAA-3.2.1 | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [definition-list](https://dequeuniversity.com/rules/axe/4.11/definition-list?application=RuleDescription) | Ensure <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | -| [dlitem](https://dequeuniversity.com/rules/axe/4.11/dlitem?application=RuleDescription) | Ensure <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | -| [document-title](https://dequeuniversity.com/rules/axe/4.11/document-title?application=RuleDescription) | Ensure each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, TTv5, TT12.a, EN-301-549, EN-9.2.4.2, ACT, RGAAv4, RGAA-8.5.1 | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | -| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-aria?application=RuleDescription) | Ensure every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-8.2.1 | needs review | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.11/form-field-multiple-labels?application=RuleDescription) | Ensure form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c, EN-301-549, EN-9.3.3.2, RGAAv4, RGAA-11.2.1 | needs review | | -| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.11/frame-focusable-content?application=RuleDescription) | Ensure <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-7.3.2 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.11/frame-title-unique?application=RuleDescription) | Ensure <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag2a, wcag412, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.2.1 | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | -| [frame-title](https://dequeuniversity.com/rules/axe/4.11/frame-title?application=RuleDescription) | Ensure <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.1.1 | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | -| [html-has-lang](https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=RuleDescription) | Ensure every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | -| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.11/html-lang-valid?application=RuleDescription) | Ensure the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.4.1 | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | -| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.11/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | -| [image-alt](https://dequeuniversity.com/rules/axe/4.11/image-alt?application=RuleDescription) | Ensure <img> elements have alternative text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, TT7.b, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [input-button-name](https://dequeuniversity.com/rules/axe/4.11/input-button-name?application=RuleDescription) | Ensure input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [input-image-alt](https://dequeuniversity.com/rules/axe/4.11/input-image-alt?application=RuleDescription) | Ensure <input type="image"> elements have alternative text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.3 | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | -| [label](https://dequeuniversity.com/rules/axe/4.11/label?application=RuleDescription) | Ensure every form element has a label | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.11/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a, EN-301-549, EN-9.1.4.1, RGAAv4, RGAA-10.6.1 | failure, needs review | | -| [link-name](https://dequeuniversity.com/rules/axe/4.11/link-name?application=RuleDescription) | Ensure links have discernible text | Serious | cat.name-role-value, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-6.2.1 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [list](https://dequeuniversity.com/rules/axe/4.11/list?application=RuleDescription) | Ensure that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | -| [listitem](https://dequeuniversity.com/rules/axe/4.11/listitem?application=RuleDescription) | Ensure <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | -| [marquee](https://dequeuniversity.com/rules/axe/4.11/marquee?application=RuleDescription) | Ensure <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | -| [meta-refresh](https://dequeuniversity.com/rules/axe/4.11/meta-refresh?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a, EN-301-549, EN-9.2.2.1, RGAAv4, RGAA-13.1.2 | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | -| [meta-viewport](https://dequeuniversity.com/rules/axe/4.11/meta-viewport?application=RuleDescription) | Ensure <meta name="viewport"> does not disable text scaling and zooming | Moderate | cat.sensory-and-visual-cues, wcag2aa, wcag144, EN-301-549, EN-9.1.4.4, ACT, RGAAv4, RGAA-10.4.2 | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | -| [nested-interactive](https://dequeuniversity.com/rules/axe/4.11/nested-interactive?application=RuleDescription) | Ensure interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | -| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.11/no-autoplay-audio?application=RuleDescription) | Ensure <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, TTv5, TT2.a, EN-301-549, EN-9.1.4.2, ACT, RGAAv4, RGAA-4.10.1 | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | -| [object-alt](https://dequeuniversity.com/rules/axe/4.11/object-alt?application=RuleDescription) | Ensure <object> elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-1.1.6 | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | -| [p-as-heading](https://dequeuniversity.com/rules/axe/4.11/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-9.1.3, medium-accuracy | failure, needs review | | -| [role-img-alt](https://dequeuniversity.com/rules/axe/4.11/role-img-alt?application=RuleDescription) | Ensure [role="img"] elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.11/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard | Serious | cat.keyboard, wcag2a, wcag211, wcag213, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, EN-9.2.1.3, RGAAv4, RGAA-7.3.2 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | -| [select-name](https://dequeuniversity.com/rules/axe/4.11/select-name?application=RuleDescription) | Ensure select element has an accessible name | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.11/server-side-image-map?application=RuleDescription) | Ensure that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-1.1.4 | needs review | | -| [summary-name](https://dequeuniversity.com/rules/axe/4.11/summary-name?application=RuleDescription) | Ensure summary elements have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | | -| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.11/svg-img-alt?application=RuleDescription) | Ensure <svg> elements with an img, graphics-document or graphics-symbol role have accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.5 | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | -| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.11/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.4.1, a11y-engine, a11y-engine-experimental, medium-accuracy | failure | | -| [td-has-header](https://dequeuniversity.com/rules/axe/4.11/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4, a11y-engine, a11y-engine-experimental, medium-accuracy | failure | | -| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.11/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | -| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.1 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | -| [valid-lang](https://dequeuniversity.com/rules/axe/4.11/valid-lang?application=RuleDescription) | Ensure lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT, RGAAv4, RGAA-8.7.1 | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | -| [video-caption](https://dequeuniversity.com/rules/axe/4.11/video-caption?application=RuleDescription) | Ensure <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2, RGAAv4, RGAA-4.3.1 | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :-------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [area-alt](https://dequeuniversity.com/rules/axe/4.11/area-alt?application=RuleDescription) | Ensure <area> elements of image maps have alternative text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.2 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-attr?application=RuleDescription) | Ensure an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.11/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | | +| [aria-command-name](https://dequeuniversity.com/rules/axe/4.11/aria-command-name?application=RuleDescription) | Ensure every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.11/aria-conditional-attr?application=RuleDescription) | Ensure ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.11/aria-deprecated-role?application=RuleDescription) | Ensure elements do not use deprecated roles | Minor | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-body?application=RuleDescription) | Ensure aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.1.3.1, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure | | +| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.11/aria-hidden-focus?application=RuleDescription) | Ensure aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-10.8.1 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | +| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-input-field-name?application=RuleDescription) | Ensure every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.11/aria-meter-name?application=RuleDescription) | Ensure every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | +| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.11/aria-progressbar-name?application=RuleDescription) | Ensure every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-11.1.1 | failure, needs review | | +| [aria-prohibited-attr](https://dequeuniversity.com/rules/axe/4.11/aria-prohibited-attr?application=RuleDescription) | Ensure ARIA attributes are not prohibited for an element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.11/aria-required-attr?application=RuleDescription) | Ensure elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | +| [aria-required-children](https://dequeuniversity.com/rules/axe/4.11/aria-required-children?application=RuleDescription) | Ensure elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.11/aria-required-parent?application=RuleDescription) | Ensure elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-roles](https://dequeuniversity.com/rules/axe/4.11/aria-roles?application=RuleDescription) | Ensure all elements with a role attribute use a valid value | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-tab-name](https://dequeuniversity.com/rules/axe/4.11/aria-tab-name?application=RuleDescription) | Ensure every ARIA tab node has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-7.1.1 | failure, needs review | | +| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.11/aria-toggle-field-name?application=RuleDescription) | Ensure every ARIA toggle field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-7.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.11/aria-tooltip-name?application=RuleDescription) | Ensure every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | | +| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr-value?application=RuleDescription) | Ensure all ARIA attributes have valid values | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | +| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr?application=RuleDescription) | Ensure attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | +| [blink](https://dequeuniversity.com/rules/axe/4.11/blink?application=RuleDescription) | Ensure <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | +| [button-name](https://dequeuniversity.com/rules/axe/4.11/button-name?application=RuleDescription) | Ensure buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1), [m6b1q3](https://act-rules.github.io/rules/m6b1q3) | +| [bypass](https://dequeuniversity.com/rules/axe/4.11/bypass?application=RuleDescription) | Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a, EN-301-549, EN-9.2.4.1, RGAAv4, RGAA-12.7.1 | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | +| [color-contrast](https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=RuleDescription) | Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, TTv5, TT13.c, EN-301-549, EN-9.1.4.3, ACT, RGAAv4, RGAA-3.2.1 | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [definition-list](https://dequeuniversity.com/rules/axe/4.11/definition-list?application=RuleDescription) | Ensure <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | +| [dlitem](https://dequeuniversity.com/rules/axe/4.11/dlitem?application=RuleDescription) | Ensure <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.3 | failure | | +| [document-title](https://dequeuniversity.com/rules/axe/4.11/document-title?application=RuleDescription) | Ensure each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, TTv5, TT12.a, EN-301-549, EN-9.2.4.2, ACT, RGAAv4, RGAA-8.5.1 | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | +| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-aria?application=RuleDescription) | Ensure every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-8.2.1 | needs review | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.11/form-field-multiple-labels?application=RuleDescription) | Ensure form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c, EN-301-549, EN-9.3.3.2, RGAAv4, RGAA-11.2.1 | needs review | | +| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.11/frame-focusable-content?application=RuleDescription) | Ensure <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-7.3.2 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.11/frame-title-unique?application=RuleDescription) | Ensure <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag2a, wcag412, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.2.1 | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | +| [frame-title](https://dequeuniversity.com/rules/axe/4.11/frame-title?application=RuleDescription) | Ensure <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-2.1.1 | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | +| [html-has-lang](https://dequeuniversity.com/rules/axe/4.11/html-has-lang?application=RuleDescription) | Ensure every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | +| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.11/html-lang-valid?application=RuleDescription) | Ensure the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.4.1 | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | +| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.11/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, EN-301-549, EN-9.3.1.1, ACT, RGAAv4, RGAA-8.3.1 | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | +| [image-alt](https://dequeuniversity.com/rules/axe/4.11/image-alt?application=RuleDescription) | Ensure <img> elements have alternative text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, TT7.b, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [input-button-name](https://dequeuniversity.com/rules/axe/4.11/input-button-name?application=RuleDescription) | Ensure input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.9.1 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [input-image-alt](https://dequeuniversity.com/rules/axe/4.11/input-image-alt?application=RuleDescription) | Ensure <input type="image"> elements have alternative text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, EN-9.4.1.2, ACT, RGAAv4, RGAA-1.1.3 | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | +| [label](https://dequeuniversity.com/rules/axe/4.11/label?application=RuleDescription) | Ensure every form element has a label | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.11/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a, EN-301-549, EN-9.1.4.1, RGAAv4, RGAA-10.6.1 | failure, needs review | | +| [link-name](https://dequeuniversity.com/rules/axe/4.11/link-name?application=RuleDescription) | Ensure links have discernible text | Serious | cat.name-role-value, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT, RGAAv4, RGAA-6.2.1 | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [list](https://dequeuniversity.com/rules/axe/4.11/list?application=RuleDescription) | Ensure that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | +| [listitem](https://dequeuniversity.com/rules/axe/4.11/listitem?application=RuleDescription) | Ensure <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-9.3.1 | failure | | +| [marquee](https://dequeuniversity.com/rules/axe/4.11/marquee?application=RuleDescription) | Ensure <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b, EN-301-549, EN-9.2.2.2, RGAAv4, RGAA-13.8.1 | failure | | +| [meta-refresh](https://dequeuniversity.com/rules/axe/4.11/meta-refresh?application=RuleDescription) | Ensure <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a, EN-301-549, EN-9.2.2.1, RGAAv4, RGAA-13.1.2 | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | +| [meta-viewport](https://dequeuniversity.com/rules/axe/4.11/meta-viewport?application=RuleDescription) | Ensure <meta name="viewport"> does not disable text scaling and zooming | Moderate | cat.sensory-and-visual-cues, wcag2aa, wcag144, EN-301-549, EN-9.1.4.4, ACT, RGAAv4, RGAA-10.4.2 | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | +| [nested-interactive](https://dequeuniversity.com/rules/axe/4.11/nested-interactive?application=RuleDescription) | Ensure interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, RGAAv4, RGAA-7.1.1 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | +| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.11/no-autoplay-audio?application=RuleDescription) | Ensure <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, TTv5, TT2.a, EN-301-549, EN-9.1.4.2, ACT, RGAAv4, RGAA-4.10.1 | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | +| [object-alt](https://dequeuniversity.com/rules/axe/4.11/object-alt?application=RuleDescription) | Ensure <object> elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, EN-301-549, EN-9.1.1.1, RGAAv4, RGAA-1.1.6 | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | +| [p-as-heading](https://dequeuniversity.com/rules/axe/4.11/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-9.1.3, high-accuracy | failure, needs review | | +| [role-img-alt](https://dequeuniversity.com/rules/axe/4.11/role-img-alt?application=RuleDescription) | Ensure [role="img"] elements have alternative text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.1 | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.11/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard in Safari | Serious | cat.keyboard, wcag2a, wcag211, wcag213, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, EN-9.2.1.3, RGAAv4, RGAA-7.3.2 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | +| [select-name](https://dequeuniversity.com/rules/axe/4.11/select-name?application=RuleDescription) | Ensure select element has an accessible name | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT, RGAAv4, RGAA-11.1.1 | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.11/server-side-image-map?application=RuleDescription) | Ensure that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a, EN-301-549, EN-9.2.1.1, RGAAv4, RGAA-1.1.4 | needs review | | +| [summary-name](https://dequeuniversity.com/rules/axe/4.11/summary-name?application=RuleDescription) | Ensure summary elements have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | | +| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.11/svg-img-alt?application=RuleDescription) | Ensure <svg> elements with an img, graphics-document or graphics-symbol role have accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT, RGAAv4, RGAA-1.1.5 | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | +| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.11/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.4.1, a11y-engine, a11y-engine-experimental, high-accuracy | failure | | +| [td-has-header](https://dequeuniversity.com/rules/axe/4.11/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4, a11y-engine, a11y-engine-experimental, high-accuracy | failure | | +| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.11/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.4 | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | +| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1, RGAAv4, RGAA-5.7.1 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | +| [valid-lang](https://dequeuniversity.com/rules/axe/4.11/valid-lang?application=RuleDescription) | Ensure lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT, RGAAv4, RGAA-8.8.1 | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | +| [video-caption](https://dequeuniversity.com/rules/axe/4.11/video-caption?application=RuleDescription) | Ensure <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2, RGAAv4, RGAA-4.3.1 | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | ## WCAG 2.1 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------- | :------------------------------------------------------------------------------------------------------------------------------------ | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.11/autocomplete-valid?application=RuleDescription) | Ensure that the necessary form fields use the autocomplete attribute with a valid input. | Moderate | cat.forms, wcag21aa, wcag135, EN-301-549, EN-9.1.3.5, ACT, a11y-engine, RGAAv4, RGAA-11.13.1 | failure, needs review | [73f2c2](https://act-rules.github.io/rules/73f2c2) | -| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.11/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, EN-301-549, EN-9.1.4.12, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | -| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.11/css-orientation-lock?application=RuleDescription) | Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, EN-301-549, EN-9.1.3.4, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-13.9.1, medium-accuracy | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | -| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.11/label-content-name-mismatch?application=RuleDescription) | Ensure that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, EN-301-549, EN-9.2.5.3, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-6.1.5, medium-accuracy | failure, needs review | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------- | :---------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.11/autocomplete-valid?application=RuleDescription) | Ensure that the necessary form fields use the autocomplete attribute with a valid input. | Moderate | cat.forms, wcag21aa, wcag135, EN-301-549, EN-9.1.3.5, ACT, a11y-engine, RGAAv4, RGAA-11.13.1 | failure, needs review | [73f2c2](https://act-rules.github.io/rules/73f2c2) | +| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.11/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, EN-301-549, EN-9.1.4.12, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | +| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.11/css-orientation-lock?application=RuleDescription) | Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, EN-301-549, EN-9.1.3.4, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-13.9.1, high-accuracy | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | +| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.11/label-content-name-mismatch?application=RuleDescription) | Ensure that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, EN-301-549, EN-9.2.5.3, a11y-engine, a11y-engine-experimental, RGAAv4, RGAA-6.1.5, high-accuracy | failure, needs review | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | ## WCAG 2.2 Level A & AA Rules @@ -100,38 +101,37 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r Rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience. -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.11/accesskeys?application=RuleDescription) | Ensure every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-role?application=RuleDescription) | Ensure role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | -| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.11/aria-dialog-name?application=RuleDescription) | Ensure every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-text](https://dequeuniversity.com/rules/axe/4.11/aria-text?application=RuleDescription) | Ensure role="text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.11/aria-treeitem-name?application=RuleDescription) | Ensure every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.11/empty-heading?application=RuleDescription) | Ensure headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | -| [empty-table-header](https://dequeuniversity.com/rules/axe/4.11/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | -| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.11/focus-order-semantics?application=RuleDescription) | Ensure elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, RGAAv4, RGAA-12.8.1, a11y-engine, a11y-engine-experimental, medium-accuracy | failure | | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.11/frame-tested?application=RuleDescription) | Ensure <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | -| [heading-order-bp](https://dequeuniversity.com/rules/axe/4.11/heading-order-bp?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.structure, best-practice, a11y-engine | failure, needs review | | -| [hidden-content](https://dequeuniversity.com/rules/axe/4.11/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, review-item, a11y-engine, a11y-engine-experimental, medium-accuracy | failure, needs review | | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.11/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | -| [label-title-only](https://dequeuniversity.com/rules/axe/4.11/label-title-only?application=RuleDescription) | Ensure that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | -| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-banner-is-top-level?application=RuleDescription) | Ensure the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-complementary-is-top-level?application=RuleDescription) | Ensure the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensure the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-main-is-top-level?application=RuleDescription) | Ensure the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-banner?application=RuleDescription) | Ensure the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensure the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-main?application=RuleDescription) | Ensure the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.11/landmark-one-main?application=RuleDescription) | Ensure the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.11/landmark-unique?application=RuleDescription) | Ensure landmarks are unique | Moderate | cat.semantics, best-practice | failure | | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.11/meta-viewport-large?application=RuleDescription) | Ensure <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | -| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.11/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | -| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.11/presentation-role-conflict?application=RuleDescription) | Ensure elements marked as presentational do not have global ARIA or tabindex so that all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | -| [region](https://dequeuniversity.com/rules/axe/4.11/region?application=RuleDescription) | Ensure all page content is contained by landmarks | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-9.2.1 | failure | | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.11/scope-attr-valid?application=RuleDescription) | Ensure the scope attribute is used correctly on tables | Moderate | cat.tables, best-practice | failure | | -| [skip-link](https://dequeuniversity.com/rules/axe/4.11/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-12.7.1 | failure, needs review | | -| [tabindex](https://dequeuniversity.com/rules/axe/4.11/tabindex?application=RuleDescription) | Ensure tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.11/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice, RGAAv4, RGAA-5.2.1 | failure, needs review | | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------------------------ | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.11/accesskeys?application=RuleDescription) | Ensure every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.11/aria-allowed-role?application=RuleDescription) | Ensure role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | +| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.11/aria-dialog-name?application=RuleDescription) | Ensure every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-text](https://dequeuniversity.com/rules/axe/4.11/aria-text?application=RuleDescription) | Ensure role="text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.11/aria-treeitem-name?application=RuleDescription) | Ensure every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.11/empty-heading?application=RuleDescription) | Ensure headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | +| [empty-table-header](https://dequeuniversity.com/rules/axe/4.11/empty-table-header?application=RuleDescription) | Ensure table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.11/focus-order-semantics?application=RuleDescription) | Ensure elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, RGAAv4, RGAA-12.8.1, a11y-engine, a11y-engine-experimental, high-accuracy | failure | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.11/frame-tested?application=RuleDescription) | Ensure <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | +| [heading-order-bp](https://dequeuniversity.com/rules/axe/4.11/heading-order-bp?application=RuleDescription) | Ensure the order of headings is semantically correct | Moderate | cat.structure, best-practice, a11y-engine | failure, needs review | | +| [hidden-content](https://dequeuniversity.com/rules/axe/4.11/hidden-content?application=RuleDescription) | Inform users about hidden content. | Minor | cat.structure, best-practice, review-item, a11y-engine, a11y-engine-experimental, high-accuracy | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.11/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | +| [label-title-only](https://dequeuniversity.com/rules/axe/4.11/label-title-only?application=RuleDescription) | Ensure that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | +| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-banner-is-top-level?application=RuleDescription) | Ensure the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensure the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-main-is-top-level?application=RuleDescription) | Ensure the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-banner?application=RuleDescription) | Ensure the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensure the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.11/landmark-no-duplicate-main?application=RuleDescription) | Ensure the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.11/landmark-one-main?application=RuleDescription) | Ensure the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.11/landmark-unique?application=RuleDescription) | Ensure landmarks are unique | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.11/meta-viewport-large?application=RuleDescription) | Ensure <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | +| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.11/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | +| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.11/presentation-role-conflict?application=RuleDescription) | Ensure elements marked as presentational do not have global ARIA or tabindex so that all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | +| [region](https://dequeuniversity.com/rules/axe/4.11/region?application=RuleDescription) | Ensure all page content is contained by landmarks | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-9.2.1 | failure | | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.11/scope-attr-valid?application=RuleDescription) | Ensure the scope attribute is used correctly on tables | Moderate | cat.tables, best-practice | failure | | +| [skip-link](https://dequeuniversity.com/rules/axe/4.11/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice, RGAAv4, RGAA-12.7.1 | failure, needs review | | +| [tabindex](https://dequeuniversity.com/rules/axe/4.11/tabindex?application=RuleDescription) | Ensure tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.11/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice, RGAAv4, RGAA-5.2.1 | failure, needs review | | ## WCAG 2.x level AAA rules @@ -154,9 +154,10 @@ _There are no matching rules_ Deprecated rules are disabled by default and will be removed in the next major release. -| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | -| :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | -| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.11/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, deprecated | failure, needs review | | -| [audio-caption](https://dequeuniversity.com/rules/axe/4.11/audio-caption?application=RuleDescription) | Ensure <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, EN-301-549, EN-9.1.2.1, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | -| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-active?application=RuleDescription) | Ensure every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id](https://dequeuniversity.com/rules/axe/4.11/duplicate-id?application=RuleDescription) | Ensure every id attribute value is unique | Minor | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| Rule ID | Description | Impact | Tags | Issue Type | [ACT Rules](https://www.w3.org/WAI/standards-guidelines/act/rules/) | +| :------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | +| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.11/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, deprecated | failure, needs review | | +| [audio-caption](https://dequeuniversity.com/rules/axe/4.11/audio-caption?application=RuleDescription) | Ensure <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, EN-301-549, EN-9.1.2.1, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | +| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.11/duplicate-id-active?application=RuleDescription) | Ensure every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [duplicate-id](https://dequeuniversity.com/rules/axe/4.11/duplicate-id?application=RuleDescription) | Ensure every id attribute value is unique | Minor | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.11/landmark-complementary-is-top-level?application=RuleDescription) | Ensure the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice, deprecated | failure | | diff --git a/doc/standards-object.md b/doc/standards-object.md index 42766eca3..eb460c179 100644 --- a/doc/standards-object.md +++ b/doc/standards-object.md @@ -95,7 +95,7 @@ The [`htmlElms`](../lib/standards/html-elms.js) object defines valid HTML elemen ### Used by Rules -- `aria-allowed-attr` - Checks if the attribute can be used on the element from the `noAriaAttrs` property. +- `aria-allowed-attr` - Checks if the attribute can be used on the element from the `noAriaAttrs` and `allowedAriaAttrs` properties. - `aria-allowed-role` - Checks if the role can be used on the HTML element from the `allowedRoles` property. - `aria-required-attrs` - Checks if any required attrs are defined implicitly on the element from the `implicitAttrs` property. @@ -110,6 +110,7 @@ The [`htmlElms`](../lib/standards/html-elms.js) object defines valid HTML elemen - `interactive` - `allowedRoles` - boolean or array(required). If element is allowed to use ARIA roles, a value of `true` means any role while a list of roles means only those are allowed. A value of `false` means no roles are allowed. - `noAriaAttrs` - boolean(optional. Defaults `true`). If the element is allowed to use global ARIA attributes and any allowed for the elements role. +- `allowedAriaAttrs` - array(optional). If specified, restricts which ARIA attributes may be used with this element (when no explicit role is set). Used by the `aria-allowed-attr` rule. - `shadowRoot` - boolean(optional. Default `false`). If the element is allowed to have a shadow root. - `implicitAttrs` - object(optional. Default `{}`). Any implicit ARIA attributes for the element and their default value. - `namingMethods` - array(optional. Default `[]`). The [native text method](../lib/commons/text/native-text-methods.js) used to calculate the accessible name of the element. diff --git a/eslint.config.js b/eslint.config.js index 203edc277..3e25970f2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -124,7 +124,7 @@ module.exports = [ }, { // disallow imports from node modules - ignores: ['lib/core/imports/**/*.js'], + ignores: ['lib/core/imports/**/*.js', 'test/**'], rules: { 'no-restricted-imports': [ 'error', @@ -363,6 +363,21 @@ module.exports = [ 'no-use-before-define': 0 } }, + { + files: ['.github/bin/*.mjs'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node, + ...globals.es2024 + } + }, + rules: { + // Helper scripts for github can import from anywhere + 'no-restricted-imports': ['off'] + } + }, { ignores: [ '**/node_modules/*', diff --git a/lib/checks/aria/aria-allowed-attr-elm-evaluate.js b/lib/checks/aria/aria-allowed-attr-elm-evaluate.js new file mode 100644 index 000000000..01af3b4f2 --- /dev/null +++ b/lib/checks/aria/aria-allowed-attr-elm-evaluate.js @@ -0,0 +1,45 @@ +import { getExplicitRole } from '../../commons/aria'; +import { getElementSpec, getGlobalAriaAttrs } from '../../commons/standards'; + +export default function ariaAllowedAttrElmEvaluate(node, options, virtualNode) { + const elmSpec = getElementSpec(virtualNode); + + // If no allowedAriaAttrs restriction, this check doesn't apply + if (!elmSpec.allowedAriaAttrs) { + return true; + } + + // If element has an explicit role, defer to the role-based check + const explicitRole = getExplicitRole(virtualNode); + if (explicitRole) { + return true; + } + + const { allowedAriaAttrs } = elmSpec; + const globalAriaAttrs = getGlobalAriaAttrs(); + const invalid = []; + + for (const attrName of virtualNode.attrNames) { + if ( + globalAriaAttrs.includes(attrName) && + !allowedAriaAttrs.includes(attrName) + ) { + invalid.push(attrName); + } + } + + if (!invalid.length) { + return true; + } + + const messageKey = invalid.length > 1 ? 'plural' : 'singular'; + this.data({ + messageKey, + nodeName: virtualNode.props.nodeName, + values: invalid + .map(attrName => attrName + '="' + virtualNode.attr(attrName) + '"') + .join(', ') + }); + + return false; +} diff --git a/lib/checks/aria/aria-allowed-attr-elm.json b/lib/checks/aria/aria-allowed-attr-elm.json new file mode 100644 index 000000000..ebbdf67e9 --- /dev/null +++ b/lib/checks/aria/aria-allowed-attr-elm.json @@ -0,0 +1,13 @@ +{ + "id": "aria-allowed-attr-elm", + "evaluate": "aria-allowed-attr-elm-evaluate", + "metadata": { + "messages": { + "pass": "ARIA attributes are allowed for this element", + "fail": { + "singular": "ARIA attribute is not allowed on ${data.nodeName} elements: ${data.values}", + "plural": "ARIA attributes are not allowed on ${data.nodeName} elements: ${data.values}" + } + } + } +} diff --git a/lib/checks/aria/aria-conditional-attr-evaluate.js b/lib/checks/aria/aria-conditional-attr-evaluate.js index 92fca6752..c772c9a25 100644 --- a/lib/checks/aria/aria-conditional-attr-evaluate.js +++ b/lib/checks/aria/aria-conditional-attr-evaluate.js @@ -1,10 +1,12 @@ import getRole from '../../commons/aria/get-role'; import ariaConditionalCheckboxAttr from './aria-conditional-checkbox-attr-evaluate'; +import ariaConditionalRadioAttr from './aria-conditional-radio-attr-evaluate'; import ariaConditionalRowAttr from './aria-conditional-row-attr-evaluate'; const conditionalRoleMap = { row: ariaConditionalRowAttr, - checkbox: ariaConditionalCheckboxAttr + checkbox: ariaConditionalCheckboxAttr, + radio: ariaConditionalRadioAttr }; export default function ariaConditionalAttrEvaluate( diff --git a/lib/checks/aria/aria-conditional-attr.json b/lib/checks/aria/aria-conditional-attr.json index c55829016..5f395ba63 100644 --- a/lib/checks/aria/aria-conditional-attr.json +++ b/lib/checks/aria/aria-conditional-attr.json @@ -15,6 +15,7 @@ "pass": "ARIA attribute is allowed", "fail": { "checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state", + "radio": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real radio state", "rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}", "rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}" } diff --git a/lib/checks/aria/aria-conditional-radio-attr-evaluate.js b/lib/checks/aria/aria-conditional-radio-attr-evaluate.js new file mode 100644 index 000000000..4f6897c8f --- /dev/null +++ b/lib/checks/aria/aria-conditional-radio-attr-evaluate.js @@ -0,0 +1,32 @@ +export default function ariaConditionalRadioAttr(node, options, virtualNode) { + const { nodeName, type } = virtualNode.props; + const ariaChecked = normalizeAriaChecked(virtualNode.attr('aria-checked')); + if (nodeName !== 'input' || type !== 'radio' || !ariaChecked) { + return true; + } + + const checkState = getCheckState(virtualNode); + if (ariaChecked === checkState) { + return true; + } + this.data({ + messageKey: 'radio', + checkState + }); + return false; +} + +function getCheckState(vNode) { + return vNode.props.checked ? 'true' : 'false'; +} + +function normalizeAriaChecked(ariaCheckedVal) { + if (!ariaCheckedVal) { + return ''; + } + ariaCheckedVal = ariaCheckedVal.toLowerCase(); + if (ariaCheckedVal === 'true') { + return ariaCheckedVal; + } + return 'false'; +} diff --git a/lib/checks/aria/aria-errormessage-evaluate.js b/lib/checks/aria/aria-errormessage-evaluate.js index 2ef53f35e..977b2cce7 100644 --- a/lib/checks/aria/aria-errormessage-evaluate.js +++ b/lib/checks/aria/aria-errormessage-evaluate.js @@ -42,6 +42,12 @@ export default function ariaErrormessageEvaluate(node, options, virtualNode) { if (attr.trim() === '') { return standards.ariaAttrs['aria-errormessage'].allowEmpty; } + + const errormessageTokens = tokenList(attr); + if (errormessageTokens.length > 1) { + this.data({ messageKey: 'unsupported', values: errormessageTokens }); + return false; + } let idref; try { @@ -49,7 +55,7 @@ export default function ariaErrormessageEvaluate(node, options, virtualNode) { } catch { this.data({ messageKey: 'idrefs', - values: tokenList(attr) + values: errormessageTokens }); return undefined; } @@ -58,15 +64,16 @@ export default function ariaErrormessageEvaluate(node, options, virtualNode) { if (!isVisibleToScreenReaders(idref)) { this.data({ messageKey: 'hidden', - values: tokenList(attr) + values: errormessageTokens }); return false; } + const describedbyTokens = tokenList(virtualNode.attr('aria-describedby')); return ( getExplicitRole(idref) === 'alert' || idref.getAttribute('aria-live') === 'assertive' || idref.getAttribute('aria-live') === 'polite' || - tokenList(virtualNode.attr('aria-describedby')).indexOf(attr) > -1 + errormessageTokens.some(token => describedbyTokens.includes(token)) ); } diff --git a/lib/checks/aria/aria-errormessage.json b/lib/checks/aria/aria-errormessage.json index e238d7f65..bfc90e7f6 100644 --- a/lib/checks/aria/aria-errormessage.json +++ b/lib/checks/aria/aria-errormessage.json @@ -8,6 +8,7 @@ "fail": { "singular": "aria-errormessage value `${data.values}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)", "plural": "aria-errormessage values `${data.values}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)", + "unsupported": "Multiple IDs in aria-errormessage is not widely supported in assistive technologies", "hidden": "aria-errormessage value `${data.values}` cannot reference a hidden element" }, "incomplete": { diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index 1e7512d96..0330d2a86 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -41,8 +41,11 @@ export default function ariaRequiredChildrenEvaluate( if (unallowed.length) { this.relatedNodes(unallowed.map(({ vNode }) => vNode)); + const messageKey = + virtualNode.attr('aria-busy') === 'true' ? 'aria-busy-fail' : 'unallowed'; + this.data({ - messageKey: 'unallowed', + messageKey, values: unallowed .map(({ vNode, attr }) => getUnallowedSelector(vNode, attr)) .filter((selector, index, array) => array.indexOf(selector) === index) @@ -100,7 +103,8 @@ function getOwnedRoles(virtualNode, required) { ) { ownedVirtual.push(...vNode.children); } else if (role || hasGlobalAriaOrFocusable) { - const attr = globalAriaAttr || 'tabindex'; + const attr = + globalAriaAttr || (vNode.hasAttr('tabindex') ? 'tabindex' : undefined); ownedRoles.push({ role, attr, vNode }); } } diff --git a/lib/checks/aria/aria-required-children.json b/lib/checks/aria/aria-required-children.json index 16c67dbd5..016f56b5e 100644 --- a/lib/checks/aria/aria-required-children.json +++ b/lib/checks/aria/aria-required-children.json @@ -27,7 +27,8 @@ "fail": { "singular": "Required ARIA child role not present: ${data.values}", "plural": "Required ARIA children role not present: ${data.values}", - "unallowed": "Element has children which are not allowed: ${data.values}" + "unallowed": "Element has children which are not allowed: ${data.values}", + "aria-busy-fail": "Element has children which are not allowed: ${data.values}; Having aria-busy=\"true\" does not allow children with roles that are not allowed" }, "incomplete": { "singular": "Expecting ARIA child role to be added: ${data.values}", diff --git a/lib/checks/color/color-contrast-enhanced.json b/lib/checks/color/color-contrast-enhanced.json index e14d8eb16..268b061cd 100644 --- a/lib/checks/color/color-contrast-enhanced.json +++ b/lib/checks/color/color-contrast-enhanced.json @@ -45,7 +45,8 @@ "equalRatio": "Element has a 1:1 contrast ratio with the background", "shortTextContent": "Element content is too short to determine if it is actual text content", "nonBmp": "Element content contains only non-text characters", - "pseudoContent": "Element's background color could not be determined due to a pseudo element" + "pseudoContent": "Element's background color could not be determined due to a pseudo element", + "colorParse": "Could not parse color string ${data.colorParse}" } } } diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index cceb09b58..b11a1c611 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -1,3 +1,4 @@ +/*global a11yEngine*/ import { isVisibleOnScreen } from '../../commons/dom'; import { visibleVirtual, @@ -124,12 +125,24 @@ export default function colorContrastEvaluate(node, options, virtualNode) { // if fgColor or bgColor are missing, get more information. let missing; + // colorParse (upstream 4.12.1): surface an unparseable color as its own reason + let colorParse; if (bgColor === null) { - missing = incompleteData.get('bgColor'); + if (incompleteData.get('colorParse')) { + missing = 'colorParse'; + colorParse = incompleteData.get('colorParse'); + } else { + missing = incompleteData.get('bgColor'); + } } else if (!isValid) { missing = contrastContributor; } + if (fgColor === null && incompleteData.get('colorParse')) { + missing = 'colorParse'; + colorParse = incompleteData.get('colorParse'); + } + const equalRatio = truncatedResult === 1; const shortTextContent = visibleText.length === 1; if (equalRatio) { @@ -149,6 +162,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { messageKey: missing, expectedContrastRatio: expected + ':1', shadowColor: shadowColor ? shadowColor.toHexString() : undefined, + colorParse: colorParse, // a11y-rule-color-contrast: reviewPayload for bulk NR reviewPayload: { visualHelperData: { diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index dd5b1f5b1..7e1051172 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -47,7 +47,8 @@ "equalRatio": "Element has a 1:1 contrast ratio with the background", "shortTextContent": "Element content is too short to determine if it is actual text content", "nonBmp": "Element content contains only non-text characters", - "pseudoContent": "Element's background color could not be determined due to a pseudo element" + "pseudoContent": "Element's background color could not be determined due to a pseudo element", + "colorParse": "Could not parse color string ${data.colorParse}" } } } diff --git a/lib/checks/lists/invalid-children-evaluate.js b/lib/checks/lists/invalid-children-evaluate.js index 684e2eb65..633a64bb9 100644 --- a/lib/checks/lists/invalid-children-evaluate.js +++ b/lib/checks/lists/invalid-children-evaluate.js @@ -1,5 +1,5 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; -import { getExplicitRole } from '../../commons/aria'; +import { getExplicitRole, getRole } from '../../commons/aria'; export default function invalidChildrenEvaluate( node, @@ -58,11 +58,16 @@ function getInvalidSelector( return false; } - const role = getExplicitRole(vChild); - if (role) { - return validRoles.includes(role) ? false : selector + `[role=${role}]`; + const explicitRole = getExplicitRole(vChild); + const role = getRole(vChild); + if (explicitRole) { + return validRoles.includes(explicitRole) + ? false + : selector + `[role=${role}]`; } else { - return validNodeNames.includes(nodeName) ? false : selector + nodeName; + return validNodeNames.includes(nodeName) || validRoles.includes(role) + ? false + : selector + nodeName; } } diff --git a/lib/checks/lists/listitem-evaluate.js b/lib/checks/lists/listitem-evaluate.js index dde5dcb4f..630056c49 100644 --- a/lib/checks/lists/listitem-evaluate.js +++ b/lib/checks/lists/listitem-evaluate.js @@ -1,4 +1,4 @@ -import { isValidRole, getExplicitRole } from '../../commons/aria'; +import { isValidRole, getExplicitRole, getRole } from '../../commons/aria'; export default function listitemEvaluate(node, options, virtualNode) { const { parent } = virtualNode; @@ -7,18 +7,17 @@ export default function listitemEvaluate(node, options, virtualNode) { return undefined; } - const parentNodeName = parent.props.nodeName; - const parentRole = getExplicitRole(parent); + const parentExplicitRole = getExplicitRole(parent); + const parentRole = getRole(parent); if (['presentation', 'none', 'list'].includes(parentRole)) { return true; } - if (parentRole && isValidRole(parentRole)) { + if (parentExplicitRole && isValidRole(parentExplicitRole)) { this.data({ messageKey: 'roleNotValid' }); - return false; } - return ['ul', 'ol', 'menu'].includes(parentNodeName); + return false; } diff --git a/lib/checks/mobile/target-size-evaluate.js b/lib/checks/mobile/target-size-evaluate.js index 1fb76e775..a87935d54 100644 --- a/lib/checks/mobile/target-size-evaluate.js +++ b/lib/checks/mobile/target-size-evaluate.js @@ -66,7 +66,11 @@ export default function targetSizeEvaluate(node, options, vNode) { return true; } - const largestInnerRect = getLargestUnobscuredArea(vNode, obscuredWidgets); + const largestInnerRect = getLargestUnobscuredArea( + vNode, + obscuredWidgets, + minSize + ); if (!largestInnerRect) { this.data({ minSize, messageKey: 'tooManyRects' }); return undefined; @@ -127,11 +131,16 @@ function filterByElmsOverlap(vNode, nearbyElms) { } // Find areas of the target that are not obscured -function getLargestUnobscuredArea(vNode, obscuredNodes) { +function getLargestUnobscuredArea(vNode, obscuredNodes, minSize) { const nodeRect = vNode.boundingClientRect; - const obscuringRects = obscuredNodes.map( - ({ boundingClientRect: rect }) => rect - ); + const obscuringRects = obscuredNodes + .map(obscuredNode => { + const display = obscuredNode.getComputedStylePropertyValue('display'); + return display === 'inline' + ? obscuredNode.clientRects + : obscuredNode.boundingClientRect; + }) + .flat(Infinity); let unobscuredRects; try { unobscuredRects = splitRects(nodeRect, obscuringRects); @@ -140,7 +149,7 @@ function getLargestUnobscuredArea(vNode, obscuredNodes) { } // Of the unobscured inner rects, work out the largest - return getLargestRect(unobscuredRects); + return getLargestRect(unobscuredRects, minSize); } // Find the largest rectangle in the array, prioritize ones that meet a minimum size diff --git a/lib/commons/aria/get-owned-virtual.js b/lib/commons/aria/get-owned-virtual.js index 5b24b7bdb..c65bc9ae9 100644 --- a/lib/commons/aria/get-owned-virtual.js +++ b/lib/commons/aria/get-owned-virtual.js @@ -18,7 +18,15 @@ function getOwnedVirtual(virtualNode) { const owns = idrefs(actualNode, 'aria-owns') .filter(element => !!element) .map(element => axe.utils.getNodeFromTree(element)); - return [...children, ...owns]; + + // Deduplicates by first occurrence to match browser accessibility tree behavior + // See: https://github.com/dequelabs/axe-core/pull/4987 + const uniqueOwns = owns.filter((own, index) => owns.indexOf(own) === index); + const nativeChildren = children.filter( + child => !uniqueOwns.includes(child) + ); + + return [...nativeChildren, ...uniqueOwns]; } return [...children]; diff --git a/lib/commons/aria/implicit-role.js b/lib/commons/aria/implicit-role.js index 509d77217..72bf1e3fd 100644 --- a/lib/commons/aria/implicit-role.js +++ b/lib/commons/aria/implicit-role.js @@ -1,7 +1,6 @@ import implicitHtmlRoles from '../standards/implicit-html-roles'; -import { getNodeFromTree } from '../../core/utils'; +import { nodeLookup } from '../../core/utils'; import getElementSpec from '../standards/get-element-spec'; -import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node'; /** * Get the implicit role for a given node @@ -12,20 +11,22 @@ import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-no * @return {Mixed} Either the role or `null` if there is none */ function implicitRole(node, { chromium } = {}) { - const vNode = - node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); - node = vNode.actualNode; + const { vNode } = nodeLookup(node); // this error is only thrown if the virtual tree is not a - // complete tree, which only happens in linting and if a - // user used `getFlattenedTree` manually on a subset of the - // DOM tree + // complete tree, which only happens in certain scenarios, + // such as if a user used `getFlattenedTree` manually on a + // subset of the DOM tree if (!vNode) { throw new ReferenceError( 'Cannot get implicit role of a node outside the current scope.' ); } + if (vNode.elementInternals?.role) { + return vNode.elementInternals.role; + } + const nodeName = vNode.props.nodeName; const role = implicitHtmlRoles[nodeName]; diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index 7a5eaa3af..de8e6ea62 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -1,4 +1,5 @@ import { Colorjs, ArrayFrom } from '../../core/imports'; +import incompleteData from './incomplete-data'; const hexRegex = /^#[0-9a-f]{3,8}$/i; const hslRegex = /hsl\(\s*([-\d.]+)(rad|turn)/; @@ -154,7 +155,12 @@ export default class Color { } // srgb values are between 0 and 1 - const color = new Colorjs(colorString).to('srgb'); + const color = new Colorjs(colorString) + .toGamut({ + space: 'srgb', + method: 'clip' + }) + .to('srgb'); if (prototypeArrayFrom) { Array.from = prototypeArrayFrom; @@ -167,6 +173,7 @@ export default class Color { // color.alpha is a Number object so convert it to a number this.alpha = +color.alpha; } catch { + incompleteData.set('colorParse', colorString); throw new Error(`Unable to parse color "${colorString}"`); } @@ -225,11 +232,11 @@ export default class Color { const { r: rSRGB, g: gSRGB, b: bSRGB } = this; const r = - rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4); + rSRGB <= 0.04045 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4); const g = - gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4); + gSRGB <= 0.04045 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4); const b = - bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4); + bSRGB <= 0.04045 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index 69eb1352a..8b2f18853 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -75,9 +75,17 @@ function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) { } // Get the background color - const bgColor = getOwnBackgroundColor(bgElmStyle); - if (bgColor.alpha === 0) { - continue; + let bgColor; + try { + bgColor = getOwnBackgroundColor(bgElmStyle); + if (bgColor.alpha === 0) { + continue; + } + } catch (error) { + if (error && incompleteData.get('colorParse')) { + return null; + } + throw error; } // abort if a node is partially obscured and obscuring element has a background @@ -147,13 +155,17 @@ function fullyEncompasses(node, rects) { let { right, bottom } = nodeRect; const style = window.getComputedStyle(node); const overflow = style.getPropertyValue('overflow'); + const paddingLeft = parseInt(style.getPropertyValue('padding-left'), 10); + const paddingRight = parseInt(style.getPropertyValue('padding-right'), 10); + const paddingTop = parseInt(style.getPropertyValue('padding-top'), 10); + const paddingBottom = parseInt(style.getPropertyValue('padding-bottom'), 10); if ( ['scroll', 'auto'].includes(overflow) || node instanceof window.HTMLHtmlElement ) { - right = nodeRect.left + node.scrollWidth; - bottom = nodeRect.top + node.scrollHeight; + right = nodeRect.left + node.scrollWidth + paddingLeft + paddingRight; + bottom = nodeRect.top + node.scrollHeight + paddingTop + paddingBottom; } return rects.every(rect => { diff --git a/lib/commons/color/get-foreground-color.js b/lib/commons/color/get-foreground-color.js index 70bdceb31..070c0857d 100644 --- a/lib/commons/color/get-foreground-color.js +++ b/lib/commons/color/get-foreground-color.js @@ -32,21 +32,29 @@ export default function getForegroundColor(node, _, bgColor, options = {}) { ]; let fgColors = []; - for (const colorFn of colorStack) { - const color = colorFn(); - if (!color) { - continue; - } + // colorParse (upstream 4.12.1): a parse throw with colorParse set -> Can't Tell + try { + for (const colorFn of colorStack) { + const color = colorFn(); + if (!color) { + continue; + } - fgColors = fgColors.concat(color); - // a11y-critical : If any color in the array is fully opaque, break - if (Array.isArray(color)) { - if (color.some(c => c && c.alpha === 1)) { + fgColors = fgColors.concat(color); + // a11y-critical : If any color in the array is fully opaque, break + if (Array.isArray(color)) { + if (color.some(c => c && c.alpha === 1)) { + break; + } + } else if (color.alpha === 1) { break; } - } else if (color.alpha === 1) { - break; } + } catch (error) { + if (error && incompleteData.get('colorParse')) { + return null; + } + throw error; } // a11y-critical : If any color in the array is fully opaque, break @@ -63,6 +71,12 @@ export default function getForegroundColor(node, _, bgColor, options = {}) { // Lastly blend the background bgColor ??= getBackgroundColor(node, []); + // colorParse (upstream 4.12.1): a parse failure -> Can't Tell, before the fork's + // degraded-mode fgColor return + if (incompleteData.get('colorParse')) { + return null; + } + if (bgColor === null) { // a11y-critical : the foreground color as-is if background color is not found return fgColor; diff --git a/lib/commons/dom/find-nearby-elms.js b/lib/commons/dom/find-nearby-elms.js index a0e421137..b6e69e52b 100644 --- a/lib/commons/dom/find-nearby-elms.js +++ b/lib/commons/dom/find-nearby-elms.js @@ -1,5 +1,5 @@ import getNodeGrid from './get-node-grid'; -import { memoize } from '../../core/utils'; +import isFixedPosition from './is-fixed-position'; export default function findNearbyElms(vNode, margin = 0) { const grid = getNodeGrid(vNode); @@ -7,7 +7,7 @@ export default function findNearbyElms(vNode, margin = 0) { return []; // Elements not in the grid don't have ._grid } const rect = vNode.boundingClientRect; - const selfIsFixed = hasFixedPosition(vNode); + const selfIsFixed = isFixedPosition(vNode); const gridPosition = grid.getGridPositionOfRect(rect, margin); const neighbors = []; @@ -17,7 +17,7 @@ export default function findNearbyElms(vNode, margin = 0) { vNeighbor && vNeighbor !== vNode && !neighbors.includes(vNeighbor) && - selfIsFixed === hasFixedPosition(vNeighbor) + selfIsFixed === isFixedPosition(vNeighbor) ) { neighbors.push(vNeighbor); } @@ -26,13 +26,3 @@ export default function findNearbyElms(vNode, margin = 0) { return neighbors; } - -const hasFixedPosition = memoize(vNode => { - if (!vNode) { - return false; - } - if (vNode.getComputedStylePropertyValue('position') === 'fixed') { - return true; - } - return hasFixedPosition(vNode.parent); -}); diff --git a/lib/commons/dom/get-target-rects.js b/lib/commons/dom/get-target-rects.js index a2bc2eec1..ebebb2c7f 100644 --- a/lib/commons/dom/get-target-rects.js +++ b/lib/commons/dom/get-target-rects.js @@ -13,7 +13,9 @@ export default memoize(getTargetRects); * @return {DOMRect[]} */ function getTargetRects(vNode) { - const nodeRect = vNode.boundingClientRect; + const display = vNode.getComputedStylePropertyValue('display'); + const nodeRects = + display === 'inline' ? vNode.clientRects : [vNode.boundingClientRect]; const overlappingVNodes = findNearbyElms(vNode).filter(vNeighbor => { return ( hasVisualOverlap(vNode, vNeighbor) && @@ -23,13 +25,19 @@ function getTargetRects(vNode) { }); if (!overlappingVNodes.length) { - return [nodeRect]; + return nodeRects; } - const obscuringRects = overlappingVNodes.map( - ({ boundingClientRect: rect }) => rect - ); - return splitRects(nodeRect, obscuringRects); + const obscuringRects = overlappingVNodes + .map(overlappingVNode => { + const overlappingDisplay = + overlappingVNode.getComputedStylePropertyValue('display'); + return overlappingDisplay === 'inline' + ? overlappingVNode.clientRects + : overlappingVNode.boundingClientRect; + }) + .flat(Infinity); + return splitRects(nodeRects, obscuringRects); } function isDescendantNotInTabOrder(vAncestor, vNode) { diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 8e7c34414..f8ee6c510 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -29,6 +29,7 @@ export { default as hasLangText } from './has-lang-text'; export { default as idrefs } from './idrefs'; export { default as insertedIntoFocusOrder } from './inserted-into-focus-order'; export { default as isCurrentPageLink } from './is-current-page-link'; +export { default as isFixedPosition } from './is-fixed-position'; export { default as isFocusable } from './is-focusable'; export { default as isHiddenWithCSS } from './is-hidden-with-css'; export { default as isHiddenForEveryone } from './is-hidden-for-everyone'; diff --git a/lib/commons/dom/is-fixed-position.js b/lib/commons/dom/is-fixed-position.js new file mode 100644 index 000000000..d065694cf --- /dev/null +++ b/lib/commons/dom/is-fixed-position.js @@ -0,0 +1,45 @@ +import memoize from '../../core/utils/memoize'; +import { nodeLookup } from '../../core/utils'; + +/** + * Determines if an element is inside a position:fixed subtree, even if the element itself is positioned differently. + * @param {VirtualNode|Element} node + * @param {Boolean} [options.skipAncestors] If the ancestor tree should not be used + * @return {Boolean} The element's position state + */ +export default function isFixedPosition(node, { skipAncestors } = {}) { + const { vNode } = nodeLookup(node); + + // detached element + if (!vNode) { + return false; + } + + if (skipAncestors) { + return isFixedSelf(vNode); + } + + return isFixedAncestors(vNode); +} + +/** + * Check the element for position:fixed + */ +const isFixedSelf = memoize(function isFixedSelfMemoized(vNode) { + return vNode.getComputedStylePropertyValue('position') === 'fixed'; +}); + +/** + * Check the element and ancestors for position:fixed + */ +const isFixedAncestors = memoize(function isFixedAncestorsMemoized(vNode) { + if (isFixedSelf(vNode)) { + return true; + } + + if (!vNode.parent) { + return false; + } + + return isFixedAncestors(vNode.parent); +}); diff --git a/lib/commons/dom/is-in-text-block.js b/lib/commons/dom/is-in-text-block.js index e00732772..74500ce5f 100644 --- a/lib/commons/dom/is-in-text-block.js +++ b/lib/commons/dom/is-in-text-block.js @@ -1,43 +1,11 @@ import getComposedParent from './get-composed-parent'; import sanitize from '../text/sanitize'; -import { getNodeFromTree } from '../../core/utils'; +import { getNodeFromTree, nodeLookup } from '../../core/utils'; import getRoleType from '../aria/get-role-type'; -function walkDomNode(node, functor) { - if (functor(node.actualNode) !== false) { - // a11y-critical : avoid processing STYLE elements - if ( - functor(node.actualNode) !== false && - node.actualNode.nodeName !== 'STYLE' - ) { - node.children.forEach(child => walkDomNode(child, functor)); - } - } -} +const blockLike = ['block', 'list-item', 'table', 'flex', 'grid']; -const blockLike = [ - 'block', - 'list-item', - 'table', - 'flex', - 'grid', - 'inline-block' -]; - -function isBlock(elm) { - const display = window.getComputedStyle(elm).getPropertyValue('display'); - return blockLike.includes(display) || display.substr(0, 6) === 'table-'; -} - -function getBlockParent(node) { - // Find the closest parent - let parentBlock = getComposedParent(node); - while (parentBlock && !isBlock(parentBlock)) { - parentBlock = getComposedParent(parentBlock); - } - - return getNodeFromTree(parentBlock); -} +const inlineBlockLike = ['inline-block', 'inline-flex', 'inline-grid']; /** * Determines if an element is within a text block @@ -47,16 +15,21 @@ function getBlockParent(node) { * @param {Element} node [description] * @param {Object} options Optional * @property {Bool} noLengthCompare + * @property {Bool} includeInlineBlock * @return {Boolean} [description] */ -function isInTextBlock(node, options) { - if (isBlock(node)) { - // Ignore if the link is a block +function isInTextBlock( + node, + { noLengthCompare, includeInlineBlock = false } = {} +) { + const { vNode, domNode } = nodeLookup(node); + if (isBlock(domNode) || (!includeInlineBlock && isInlineBlockLike(vNode))) { + // Ignore if the element is a block return false; } // Find all the text part of the parent block not in a link, and all the text in a link - const virtualParent = getBlockParent(node); + const virtualParent = getBlockParent(domNode); let parentText = ''; let widgetText = ''; let inBrBlock = 0; @@ -64,6 +37,10 @@ function isInTextBlock(node, options) { // We want to ignore hidden text, and if br / hr is used, only use the section of the parent // that has the link we're looking at walkDomNode(virtualParent, currNode => { + if (currNode === virtualParent.actualNode) { + return true; // Skip the element itself + } + // We're already passed it, skip everything else if (inBrBlock === 2) { return false; @@ -79,11 +56,12 @@ function isInTextBlock(node, options) { } const nodeName = (currNode.nodeName || '').toUpperCase(); - if (currNode === node) { + if (currNode === domNode) { inBrBlock = 1; } + const nodeIsBlock = isBlock(currNode); // BR and HR elements break the line - if (['BR', 'HR'].includes(nodeName)) { + if (nodeIsBlock || ['BR', 'HR'].includes(nodeName)) { if (inBrBlock === 0) { parentText = ''; widgetText = ''; @@ -92,7 +70,9 @@ function isInTextBlock(node, options) { } // Don't walk nodes with content not displayed on screen. - } else if ( + } + if ( + nodeIsBlock || currNode.style.display === 'none' || currNode.style.overflow === 'hidden' || !['', null, 'none'].includes(currNode.style.float) || @@ -109,7 +89,7 @@ function isInTextBlock(node, options) { }); parentText = sanitize(parentText); - if (options?.noLengthCompare) { + if (noLengthCompare) { return parentText.length !== 0; } @@ -118,3 +98,35 @@ function isInTextBlock(node, options) { } export default isInTextBlock; + +function isBlock(node) { + const { vNode } = nodeLookup(node); + const display = vNode.getComputedStylePropertyValue('display'); + return blockLike.includes(display) || display.substr(0, 6) === 'table-'; +} + +function isInlineBlockLike(vNode) { + const display = vNode.getComputedStylePropertyValue('display'); + return inlineBlockLike.includes(display); +} + +function walkDomNode(node, functor) { + // a11y-critical : do not descend into ' + - '

Content

' - ); + describe('with pseudo elements', () => { + it('should return undefined if :before pseudo element has a background color', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { @@ -445,11 +613,24 @@ describe('color-contrast', function () { ); }); - it('should return undefined if :after pseudo element has a background color', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should return undefined if :after pseudo element has a background color', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { @@ -464,18 +645,23 @@ describe('color-contrast', function () { ); }); - it('should return undefined if pseudo element has a background image', function () { - var dataURI = - 'data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/' + - 'XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkA' + - 'ABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKU' + - 'E1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'; - - var params = checkSetup( - '' + - '

Content

' + it('should return undefined if pseudo element has a background image', () => { + const params = checkSetup( + html` +
+

Content

+
` ); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); @@ -491,169 +677,286 @@ describe('color-contrast', function () { ); }); - it('should not return undefined if pseudo element has no content', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element has no content', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element is not absolutely positioned no content', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element is not absolutely positioned no content', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element is has zero dimension', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element is has zero dimension', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it("should not return undefined if pseudo element doesn't have a background", function () { - var params = checkSetup( - '' + - '

Content

' - ); + it("should not return undefined if pseudo element doesn't have a background", () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element has visibility: hidden', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element has visibility: hidden', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element has display: none', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element has display: none', () => { + const params = checkSetup(html` + +
+

Content

+
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should return undefined if pseudo element is more than 25% of the element', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should return undefined if pseudo element is more than 25% of the element', () => { + const params = checkSetup(html` + +

Content

+ `); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element is 25% of the element', function () { - var params = checkSetup( - '' + - '

Content

' - ); + it('should not return undefined if pseudo element is 25% of the element', () => { + const params = checkSetup(html` + +

Content

+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); }); - describe('with special texts', function () { - it('should return undefined for a single character text with insufficient contrast', function () { - var params = checkSetup( - '
' + - '
X
' + - '
' - ); + describe('with special texts', () => { + it('should return undefined for a single character text with insufficient contrast', () => { + const params = checkSetup(html` +
+
X
+
+ `); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isUndefined(actual); assert.equal(checkContext._data.messageKey, 'shortTextContent'); }); - it('should return true for a single character text with insufficient contrast', function () { - var params = checkSetup( - '
' + - '
X
' + - '
' - ); + it('should return true for a single character text with insufficient contrast', () => { + const params = checkSetup(html` +
+
X
+
+ `); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isUndefined(actual); assert.equal(checkContext._data.messageKey, 'shortTextContent'); }); - it('should return undefined when the text only contains nonBmp unicode when the ignoreUnicode option is true', function () { - var params = checkSetup( - '
' + - '
₠ ₡ ₢ ₣
' + - '
', + it('should return undefined when the text only contains nonBmp unicode when the ignoreUnicode option is true', () => { + const params = checkSetup( + html` +
+
+ ₠ ₡ ₢ ₣ +
+
+ `, { ignoreUnicode: true } ); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isUndefined(actual); assert.equal(checkContext._data.messageKey, 'nonBmp'); }); - it('should return true when the text only contains nonBmp unicode when the ignoreUnicode option is false, and there is sufficient contrast', function () { - var params = checkSetup( - '
' + - '
' + - '
', + it('should return true when the text only contains nonBmp unicode when the ignoreUnicode option is false, and there is sufficient contrast', () => { + const params = checkSetup( + html` +
+
+
+ `, { ignoreUnicode: false } ); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('should return undefined when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is default, and there is insufficient contrast', function () { - var params = checkSetup( - '
' + - '
' + - '
', + it('should return undefined when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is default, and there is insufficient contrast', () => { + const params = checkSetup( + html` +
+
+
+ `, { ignoreUnicode: false } ); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isUndefined(actual); assert.equal(checkContext._data.messageKey, 'shortTextContent'); }); - it('should return false when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is true, and there is insufficient contrast', function () { - var params = checkSetup( - '
' + - '
' + - '
', + it('should return false when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is true, and there is insufficient contrast', () => { + const params = checkSetup( + html` +
+
+
+ `, { ignoreUnicode: false, ignoreLength: true } ); - var actual = contrastEvaluate.apply(checkContext, params); + const actual = contrastEvaluate.apply(checkContext, params); assert.isFalse(actual); }); }); - describe('options', function () { - it('should support options.boldValue', function () { - var params = checkSetup( - '
' + - 'My text
', + describe('options', () => { + it('should support options.boldValue', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { boldValue: 100 } @@ -663,10 +966,16 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should support options.boldTextPt', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.boldTextPt', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { boldTextPt: 6 } @@ -676,10 +985,16 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should support options.largeTextPt', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.largeTextPt', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { largeTextPt: 6 } @@ -689,10 +1004,16 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should support options.contrastRatio.normal.expected', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.normal.expected', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { normal: { @@ -706,10 +1027,16 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should support options.contrastRatio.normal.minThreshold', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.normal.minThreshold', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { normal: { @@ -723,10 +1050,12 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should not report incomplete when options.contrastRatio.normal.minThreshold is set', function () { - var params = checkSetup( - ` -

+ it('should not report incomplete when options.contrastRatio.normal.minThreshold is set', () => { + const params = checkSetup( + html`

Some text in English

`, { @@ -742,10 +1071,16 @@ describe('color-contrast', function () { assert.isUndefined(checkContext._data.messageKey); }); - it('should support options.contrastRatio.normal.maxThreshold', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.normal.maxThreshold', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { normal: { @@ -759,10 +1094,12 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should not report incomplete when options.contrastRatio.normal.maxThreshold is set', function () { - var params = checkSetup( - ` -

+ it('should not report incomplete when options.contrastRatio.normal.maxThreshold is set', () => { + const params = checkSetup( + html`

Some text in English

`, { @@ -778,10 +1115,16 @@ describe('color-contrast', function () { assert.isUndefined(checkContext._data.messageKey); }); - it('should support options.contrastRatio.large.expected', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.large.expected', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { large: { @@ -795,10 +1138,16 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should support options.contrastRatio.large.minThreshold', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.large.minThreshold', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { large: { @@ -812,10 +1161,12 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should not report incomplete when options.contrastRatio.large.minThreshold is set', function () { - var params = checkSetup( - ` -

+ it('should not report incomplete when options.contrastRatio.large.minThreshold is set', () => { + const params = checkSetup( + html`

Some text in English

`, { @@ -831,10 +1182,16 @@ describe('color-contrast', function () { assert.isUndefined(checkContext._data.messageKey); }); - it('should support options.contrastRatio.large.maxThreshold', function () { - var params = checkSetup( - '
' + - 'My text
', + it('should support options.contrastRatio.large.maxThreshold', () => { + const params = checkSetup( + html` +
+ My text +
+ `, { contrastRatio: { large: { @@ -848,10 +1205,12 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, []); }); - it('should not report incomplete when options.contrastRatio.large.maxThreshold is set', function () { - var params = checkSetup( - ` -

+ it('should not report incomplete when options.contrastRatio.large.maxThreshold is set', () => { + const params = checkSetup( + html`

Some text in English

`, { @@ -867,10 +1226,25 @@ describe('color-contrast', function () { assert.isUndefined(checkContext._data.messageKey); }); - it('should ignore pseudo element with options.ignorePseudo', function () { - var params = checkSetup( - '' + - '

Content

', + it('should ignore pseudo element with options.ignorePseudo', () => { + const params = checkSetup( + html` + +
+

Content

+
+ `, { ignorePseudo: true } @@ -879,11 +1253,25 @@ describe('color-contrast', function () { assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', function () { - var params = checkSetup( - '' + - '

Content

', + it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', () => { + const params = checkSetup( + html` + +

Content

+ `, { pseudoSizeThreshold: 0.2 } @@ -892,21 +1280,18 @@ describe('color-contrast', function () { }); }); - (shadowSupported ? it : xit)( - 'returns colors across Shadow DOM boundaries', - function () { - var params = shadowCheckSetup( - '
', - '

Text

' - ); - var container = fixture.querySelector('#container'); - var result = contrastEvaluate.apply(checkContext, params); - assert.isFalse(result); - assert.deepEqual(checkContext._relatedNodes, [container]); - } - ); - - (shadowSupported ? it : xit)('handles elements', () => { + it('returns colors across Shadow DOM boundaries', () => { + const params = shadowCheckSetup( + '
', + '

Text

' + ); + const container = fixture.querySelector('#container'); + const result = contrastEvaluate.apply(checkContext, params); + assert.isFalse(result); + assert.deepEqual(checkContext._relatedNodes, [container]); + }); + + it('handles elements', () => { fixture.innerHTML = '

Slotted text

'; const container = fixture.querySelector('#container'); @@ -929,26 +1314,32 @@ describe('color-contrast', function () { assert.deepEqual(checkContext._relatedNodes, [shadowContainer]); }); - describe('with text-shadow', function () { - it('passes if thin text shadows have sufficient contrast with the text', function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + describe('with text-shadow', () => { + it('passes if thin text shadows have sufficient contrast with the text', () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('does not count text shadows of offset 0, blur 0 as part of the background color', function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it('does not count text shadows of offset 0, blur 0 as part of the background color', () => { + const params = checkSetup(html` +
+ Hello world +
+ `); - var white = new axe.commons.color.Color(255, 255, 255, 1); + const white = new axe.commons.color.Color(255, 255, 255, 1); assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.bgColor, white.toHexString()); @@ -956,78 +1347,103 @@ describe('color-contrast', function () { assert.equal(checkContext._data.contrastRatio, '4.84'); }); - it('passes if thin text shadows have sufficient contrast with the background', function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it('passes if thin text shadows have sufficient contrast with the background', () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('fails if text shadows have sufficient contrast with the background if its width is thicker than `shadowOutlineEmMax`', function () { - var checkOptions = { shadowOutlineEmMax: 0.05 }; - var params = checkSetup( - '
' + - ' Hello world' + - '
', + it('fails if text shadows have sufficient contrast with the background if its width is thicker than `shadowOutlineEmMax`', () => { + const checkOptions = { shadowOutlineEmMax: 0.05 }; + const params = checkSetup( + html` +
+ Hello world +
+ `, checkOptions ); assert.isFalse(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.messageKey, null); }); - it('fails if text shadows do not have sufficient contrast with the foreground', function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it('fails if text shadows do not have sufficient contrast with the foreground', () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isFalse(contrastEvaluate.apply(checkContext, params)); assert.isNull(checkContext._data.messageKey); }); - it('fails if text shadows do not have sufficient contrast with the background', function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it('fails if text shadows do not have sufficient contrast with the background', () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isFalse(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.messageKey, 'shadowOnBgColor'); }); - it("fails if thick text shadows don't have sufficient contrast", function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it("fails if thick text shadows don't have sufficient contrast", () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it("passes if thin text shadows don't have sufficient contrast, but the text and background do", function () { - var params = checkSetup( - '
' + - ' Hello world' + - '
' - ); + it("passes if thin text shadows don't have sufficient contrast, but the text and background do", () => { + const params = checkSetup(html` +
+ Hello world +
+ `); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('incompletes if text-shadow is only on part of the text', function () { - var params = checkSetup(` -
Hello world
+ it('incompletes if text-shadow is only on part of the text', () => { + const params = checkSetup(html` +
+ Hello world +
`); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); diff --git a/test/checks/color/link-in-text-block-style.js b/test/checks/color/link-in-text-block-style.js index cc8e3d812..4113d3228 100644 --- a/test/checks/color/link-in-text-block-style.js +++ b/test/checks/color/link-in-text-block-style.js @@ -1,6 +1,6 @@ describe('link-in-text-block-style', () => { + const html = axe.testUtils.html; const fixture = document.getElementById('fixture'); - const shadowSupport = axe.testUtils.shadowSupport; let styleElm; const checkContext = axe.testUtils.MockCheckContext(); @@ -58,35 +58,33 @@ describe('link-in-text-block-style', () => { if (!name) { return propPiece; } else { - return name + '-' + propPiece.toLowerCase(); + return `${name}-${propPiece.toLowerCase()}`; } }, null); // Return indented line of style code - return ' ' + cssPropName + ':' + styleObj[prop] + ';'; + return ` ${cssPropName}:${styleObj[prop]};`; }) .join('\n'); // Add to the style element - styleElm.innerHTML += selector + ' {\n' + cssLines + '\n}\n'; + styleElm.innerHTML += `${selector} { +${cssLines} +} +`; } function getLinkElm(linkStyle) { // Get a random id and build the style strings - const linkId = 'linkid-' + Math.floor(Math.random() * 100000); - const parId = 'parid-' + Math.floor(Math.random() * 100000); - - createStyleString('#' + linkId, linkStyle); - createStyleString('#' + parId, {}); - - fixture.innerHTML += - '

Text ' + - 'link' + - '

'; + const linkId = `linkid-${Math.floor(Math.random() * 100000)}`; + const parId = `parid-${Math.floor(Math.random() * 100000)}`; + + createStyleString(`#${linkId}`, linkStyle); + createStyleString(`#${parId}`, {}); + + fixture.innerHTML += `

Text + link +

`; axe.testUtils.flatTreeSetup(fixture); return document.getElementById(linkId); } @@ -99,12 +97,13 @@ describe('link-in-text-block-style', () => { }); it('passes the selected node and closest ancestral block element', () => { - fixture.innerHTML = - '
' + - '

' + - ' link text ' + - ' inside block

inside block' + - '
outside block
'; + fixture.innerHTML = html` +
+

+ link text + inside block

inside block +
outside block
+ `; axe.testUtils.flatTreeSetup(fixture); const linkElm = document.getElementById('link'); @@ -112,39 +111,32 @@ describe('link-in-text-block-style', () => { assert.isFalse(linkInBlockStyleCheck.call(checkContext, linkElm)); }); - (shadowSupport.v1 ? it : xit)( - 'works with the block outside the shadow tree', - () => { - const parentElm = document.createElement('div'); - const shadow = parentElm.attachShadow({ mode: 'open' }); - shadow.innerHTML = - 'Link'; - const linkElm = shadow.querySelector('a'); - fixture.appendChild(parentElm); + it('works with the block outside the shadow tree', () => { + const parentElm = document.createElement('div'); + const shadow = parentElm.attachShadow({ mode: 'open' }); + shadow.innerHTML = + 'Link'; + const linkElm = shadow.querySelector('a'); + fixture.appendChild(parentElm); - axe.testUtils.flatTreeSetup(fixture); + axe.testUtils.flatTreeSetup(fixture); - assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); - } - ); - - (shadowSupport.v1 ? it : xit)( - 'works with the link inside the shadow tree slot', - () => { - const div = document.createElement('div'); - div.setAttribute('style', 'text-decoration:none;'); - div.innerHTML = - 'Link'; - const shadow = div.attachShadow({ mode: 'open' }); - shadow.innerHTML = '

'; - fixture.appendChild(div); - - axe.testUtils.flatTreeSetup(fixture); - const linkElm = div.querySelector('a'); - - assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); - } - ); + assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); + }); + + it('works with the link inside the shadow tree slot', () => { + const div = document.createElement('div'); + div.setAttribute('style', 'text-decoration:none;'); + div.innerHTML = 'Link'; + const shadow = div.attachShadow({ mode: 'open' }); + shadow.innerHTML = '

'; + fixture.appendChild(div); + + axe.testUtils.flatTreeSetup(fixture); + const linkElm = div.querySelector('a'); + + assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); + }); }); describe('links distinguished through style', () => { @@ -165,10 +157,14 @@ describe('link-in-text-block-style', () => { }); it('returns undefined when the link has a :before pseudo element', () => { - const link = queryFixture(` + const link = queryFixture(html`

A link inside a block of text

`).actualNode; @@ -179,10 +175,14 @@ describe('link-in-text-block-style', () => { }); it('returns undefined when the link has a :after pseudo element', () => { - const link = queryFixture(` + const link = queryFixture(html`

A link inside a block of text

`).actualNode; @@ -193,10 +193,15 @@ describe('link-in-text-block-style', () => { }); it('does not return undefined when the pseudo element content is none', () => { - const link = queryFixture(` + const link = queryFixture(html`

A link inside a block of text

`).actualNode; diff --git a/test/checks/color/link-in-text-block.js b/test/checks/color/link-in-text-block.js index d8d46e0be..7a8569dfd 100644 --- a/test/checks/color/link-in-text-block.js +++ b/test/checks/color/link-in-text-block.js @@ -1,6 +1,6 @@ describe('link-in-text-block', () => { + const html = axe.testUtils.html; const fixture = document.getElementById('fixture'); - const shadowSupport = axe.testUtils.shadowSupport; let styleElm; const checkContext = axe.testUtils.MockCheckContext(); @@ -53,35 +53,33 @@ describe('link-in-text-block', () => { if (!name) { return propPiece; } else { - return name + '-' + propPiece.toLowerCase(); + return `${name}-${propPiece.toLowerCase()}`; } }, null); // Return indented line of style code - return ' ' + cssPropName + ':' + styleObj[prop] + ';'; + return ` ${cssPropName}:${styleObj[prop]};`; }) .join('\n'); // Add to the style element - styleElm.innerHTML += selector + ' {\n' + cssLines + '\n}\n'; + styleElm.innerHTML += `${selector} { +${cssLines} +} +`; } function getLinkElm(linkStyle, paragraphStyle) { // Get a random id and build the style strings - const linkId = 'linkid-' + Math.floor(Math.random() * 100000); - const parId = 'parid-' + Math.floor(Math.random() * 100000); - - createStyleString('#' + linkId, linkStyle); - createStyleString('#' + parId, paragraphStyle); - - fixture.innerHTML += - '

Text ' + - 'link' + - '

'; + const linkId = `linkid-${Math.floor(Math.random() * 100000)}`; + const parId = `parid-${Math.floor(Math.random() * 100000)}`; + + createStyleString(`#${linkId}`, linkStyle); + createStyleString(`#${parId}`, paragraphStyle); + + fixture.innerHTML += `

Text + link +

`; axe.testUtils.flatTreeSetup(fixture); return document.getElementById(linkId); } @@ -94,12 +92,17 @@ describe('link-in-text-block', () => { }); it('passes the selected node and closest ancestral block element', () => { - fixture.innerHTML = - '
' + - '

' + - ' link text ' + - ' inside block

inside block' + - '
outside block
'; + fixture.innerHTML = html` +
+ +

+ link text inside block +

+ inside block +
+ outside block +
+ `; axe.testUtils.flatTreeSetup(fixture); const linkElm = document.getElementById('link'); @@ -112,53 +115,44 @@ describe('link-in-text-block', () => { assert.equal(checkContext._data.messageKey, 'fgContrast'); }); - (shadowSupport.v1 ? it : xit)( - 'works with the block outside the shadow tree', - () => { - const parentElm = document.createElement('div'); - parentElm.setAttribute( - 'style', - 'color:#100; background-color:#FFFFFF;' - ); - const shadow = parentElm.attachShadow({ mode: 'open' }); - shadow.innerHTML = - 'Link'; - const linkElm = shadow.querySelector('a'); - fixture.appendChild(parentElm); + it('works with the block outside the shadow tree', () => { + const parentElm = document.createElement('div'); + parentElm.setAttribute('style', 'color:#100; background-color:#FFFFFF;'); + const shadow = parentElm.attachShadow({ mode: 'open' }); + shadow.innerHTML = + 'Link'; + const linkElm = shadow.querySelector('a'); + fixture.appendChild(parentElm); - axe.testUtils.flatTreeSetup(fixture); + axe.testUtils.flatTreeSetup(fixture); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('link-in-text-block') - .call(checkContext, linkElm) - ); - assert.equal(checkContext._data.messageKey, 'fgContrast'); - } - ); - - (shadowSupport.v1 ? it : xit)( - 'works with the link inside the shadow tree slot', - () => { - const div = document.createElement('div'); - div.setAttribute('style', 'color:#100; background-color:#FFFFFF;'); - div.innerHTML = - 'Link'; - const shadow = div.attachShadow({ mode: 'open' }); - shadow.innerHTML = '

'; - fixture.appendChild(div); - - axe.testUtils.flatTreeSetup(fixture); - const linkElm = div.querySelector('a'); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('link-in-text-block') + .call(checkContext, linkElm) + ); + assert.equal(checkContext._data.messageKey, 'fgContrast'); + }); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('link-in-text-block') - .call(checkContext, linkElm) - ); - assert.equal(checkContext._data.messageKey, 'fgContrast'); - } - ); + it('works with the link inside the shadow tree slot', () => { + const div = document.createElement('div'); + div.setAttribute('style', 'color:#100; background-color:#FFFFFF;'); + div.innerHTML = + 'Link'; + const shadow = div.attachShadow({ mode: 'open' }); + shadow.innerHTML = '

'; + fixture.appendChild(div); + + axe.testUtils.flatTreeSetup(fixture); + const linkElm = div.querySelector('a'); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('link-in-text-block') + .call(checkContext, linkElm) + ); + assert.equal(checkContext._data.messageKey, 'fgContrast'); + }); }); describe('links distinguished through color', () => { @@ -284,12 +278,18 @@ describe('link-in-text-block', () => { }); it('should return the proper values stored in data (fgContrast)', () => { - fixture.innerHTML = - '
' + - '

' + - ' link text ' + - ' inside block

inside block' + - '
outside block
'; + fixture.innerHTML = html` +
+ +

+ link text inside + block +

+ inside block +
+ outside block +
+ `; axe.testUtils.flatTreeSetup(fixture); const linkElm = document.getElementById('link'); diff --git a/test/checks/forms/autocomplete-appropriate.js b/test/checks/forms/autocomplete-appropriate.js index 96fd80255..5df0ab43c 100644 --- a/test/checks/forms/autocomplete-appropriate.js +++ b/test/checks/forms/autocomplete-appropriate.js @@ -1,98 +1,100 @@ -describe('autocomplete-appropriate', function () { - 'use strict'; +describe('autocomplete-appropriate', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const evaluate = axe.testUtils.getCheckEvaluate('autocomplete-appropriate'); - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var evaluate = axe.testUtils.getCheckEvaluate('autocomplete-appropriate'); - - beforeEach(function () { + beforeEach(() => { axe._tree = undefined; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; checkContext.reset(); }); function autocompleteCheckParams(term, type, options) { return checkSetup( - '', + ``, options ); } - it('returns true for non-select elements', function () { - ['div', 'button', 'select', 'textarea'].forEach(function (tagName) { - var elm = document.createElement(tagName); + it('returns true for non-select elements', () => { + ['div', 'button', 'select', 'textarea'].forEach(tagName => { + const elm = document.createElement(tagName); elm.setAttribute('autocomplete', 'foo'); elm.setAttribute('type', 'email'); - var params = checkSetup(elm); + const params = checkSetup(elm); assert.isTrue( evaluate.apply(checkContext, params), - 'failed for ' + tagName + `failed for ${tagName}` ); }); }); - it('returns true if the input type is in the map', function () { - var options = { foo: ['url'] }; - var params = autocompleteCheckParams('foo', 'url', options); + it('returns true if the input type is in the map', () => { + const options = { foo: ['url'] }; + const params = autocompleteCheckParams('foo', 'url', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns false if the input type is not in the map', function () { - var options = { foo: ['url'] }; - var params = autocompleteCheckParams('foo', 'email', options); + it('returns false if the input type is not in the map', () => { + const options = { foo: ['url'] }; + const params = autocompleteCheckParams('foo', 'email', options); assert.isFalse(evaluate.apply(checkContext, params)); }); - it('returns true if the input type is text and the term is undefined', function () { - var options = {}; - var params = autocompleteCheckParams('foo', 'text', options); + it('returns true if the input type is text and the term is undefined', () => { + const options = {}; + const params = autocompleteCheckParams('foo', 'text', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns true if the input type is tel and the term is off', function () { - var options = {}; - var params = autocompleteCheckParams('off', 'tel', options); + it('returns true if the input type is tel and the term is off', () => { + const options = {}; + const params = autocompleteCheckParams('off', 'tel', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns true if the input type is url and the term is on', function () { - var options = {}; - var params = autocompleteCheckParams('on', 'url', options); + it('returns true if the input type is url and the term is on', () => { + const options = {}; + const params = autocompleteCheckParams('on', 'url', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns true if the input type is foobar and the term is undefined', function () { - var options = {}; - var params = autocompleteCheckParams('foo', 'foobar', options); + it('returns true if the input type is foobar and the term is undefined', () => { + const options = {}; + const params = autocompleteCheckParams('foo', 'foobar', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns true if the input type is email and the term is username', function () { - var options = {}; - var params = autocompleteCheckParams('username', 'email', options); + it('returns true if the input type is email and the term is username', () => { + const options = {}; + const params = autocompleteCheckParams('username', 'email', options); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns false if the input type is text and the term maps to an empty array', function () { - var options = { foo: [] }; - var params = autocompleteCheckParams('foo', 'text', options); + it('returns false if the input type is text and the term maps to an empty array', () => { + const options = { foo: [] }; + const params = autocompleteCheckParams('foo', 'text', options); assert.isFalse(evaluate.apply(checkContext, params)); }); - it('returns false if the input type is month and term is bday-month', function () { - var options = {}; - var params = autocompleteCheckParams('bday-month', 'month', options); + it('returns false if the input type is month and term is bday-month', () => { + const options = {}; + const params = autocompleteCheckParams('bday-month', 'month', options); assert.isFalse(evaluate.apply(checkContext, params)); }); - it('returns false if the input type is MONTH (case-insensitive & sanitized) and term is bday-month', function () { - var options = {}; - var params = autocompleteCheckParams('bday-month', ' MONTH ', options); + it('returns false if the input type is MONTH (case-insensitive & sanitized) and term is bday-month', () => { + const options = {}; + const params = autocompleteCheckParams( + 'bday-month', + ' MONTH ', + options + ); assert.isFalse(evaluate.apply(checkContext, params)); }); }); diff --git a/test/checks/keyboard/accesskeys.js b/test/checks/keyboard/accesskeys.js index 5b99213f8..9a0e5a1ef 100644 --- a/test/checks/keyboard/accesskeys.js +++ b/test/checks/keyboard/accesskeys.js @@ -1,6 +1,4 @@ describe('accesskeys', () => { - 'use strict'; - const checkSetup = axe.testUtils.checkSetup; const checkContext = axe.testUtils.MockCheckContext(); const checkEvaluate = axe.testUtils.getCheckEvaluate('accesskeys'); diff --git a/test/checks/keyboard/focusable-content.js b/test/checks/keyboard/focusable-content.js index 3650cf831..8a13a02a4 100644 --- a/test/checks/keyboard/focusable-content.js +++ b/test/checks/keyboard/focusable-content.js @@ -1,125 +1,131 @@ -describe('focusable-content tests', function () { - 'use strict'; +describe('focusable-content tests', () => { + const html = axe.testUtils.html; - var check; - var fixture = document.getElementById('fixture'); - var fixtureSetup = axe.testUtils.fixtureSetup; - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; + let check; + const fixture = document.getElementById('fixture'); + const fixtureSetup = axe.testUtils.fixtureSetup; + const shadowSupported = axe.testUtils.shadowSupport.v1; + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - before(function () { + before(() => { check = checks['focusable-content']; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; checkContext.reset(); }); - it('returns false when there are no focusable content elements (content element `div` is not focusable)', function () { - var params = checkSetup( - '
' + '
Content
' + '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when there are no focusable content elements (content element `div` is not focusable)', () => { + const params = checkSetup(html` +
+
Content
+
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when content element is taken out of focusable order (tabindex = -1)', function () { - var params = checkSetup( - '
' + '' + '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when content element is taken out of focusable order (tabindex = -1)', () => { + const params = checkSetup(html` +
+ +
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when element is focusable (only checks if contents are focusable)', function () { - var params = checkSetup( - '
' + - '

' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when element is focusable (only checks if contents are focusable)', () => { + const params = checkSetup(html` +
+

+
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when all content elements are not focusable', function () { - var params = checkSetup( - '
' + - '' + - '' + - '' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when all content elements are not focusable', () => { + const params = checkSetup(html` +
+ + + +
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when one deeply nested content element is focusable', function () { - var params = checkSetup( - '
' + - '
' + - '
' + - '' + - '
' + - '
' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when one deeply nested content element is focusable', () => { + const params = checkSetup(html` +
+
+
+ +
+
+
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when content element can be focused', function () { - var params = checkSetup( - '
' + '' + '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when content element can be focused', () => { + const params = checkSetup(html` +
+ +
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when any one of the many content elements can be focused', function () { - var params = checkSetup( - '
' + - '' + - '' + - '' + - '

' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when any one of the many content elements can be focused', () => { + const params = checkSetup(html` +
+ + + +

+
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - describe('shadowDOM - focusable content', function () { + describe('shadowDOM - focusable content', () => { before(function () { if (!shadowSupported) { this.skip(); } }); - it('returns true when content element can be focused', function () { - fixtureSetup('
' + '
'); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); + it('returns true when content element can be focused', () => { + fixtureSetup(html`
`); + const node = fixture.querySelector('#target'); + const shadow = node.attachShadow({ mode: 'open' }); shadow.innerHTML = ''; axe._tree = axe.utils.getFlattenedTree(fixture); axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isTrue(actual); }); - it('returns false when no focusable content', function () { - fixtureSetup('
' + '
'); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); + it('returns false when no focusable content', () => { + fixtureSetup(html`
`); + const node = fixture.querySelector('#target'); + const shadow = node.attachShadow({ mode: 'open' }); shadow.innerHTML = '

just some text

'; axe._tree = axe.utils.getFlattenedTree(fixture); axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); }); }); diff --git a/test/checks/keyboard/focusable-disabled.js b/test/checks/keyboard/focusable-disabled.js index ffe1536f9..f00962863 100644 --- a/test/checks/keyboard/focusable-disabled.js +++ b/test/checks/keyboard/focusable-disabled.js @@ -1,105 +1,103 @@ -describe('focusable-disabled', function () { - 'use strict'; +describe('focusable-disabled', () => { + const html = axe.testUtils.html; - var check; - var fixture = document.getElementById('fixture'); - var fixtureSetup = axe.testUtils.fixtureSetup; - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; + let check; + const fixture = document.getElementById('fixture'); + const fixtureSetup = axe.testUtils.fixtureSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - before(function () { + before(() => { check = checks['focusable-disabled']; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; axe._selectorData = undefined; checkContext.reset(); }); - it('returns true when content not focusable by default (no tabbable elements)', function () { - var params = checkSetup(''); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when content not focusable by default (no tabbable elements)', () => { + const params = checkSetup( + '' + ); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when content hidden through CSS (no tabbable elements)', function () { - var params = checkSetup( + it('returns true when content hidden through CSS (no tabbable elements)', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when content made unfocusable through disabled (no tabbable elements)', function () { - var params = checkSetup( + it('returns true when content made unfocusable through disabled (no tabbable elements)', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when content made unfocusable through disabled fieldset', function () { - var params = checkSetup( + it('returns true when content made unfocusable through disabled fieldset', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - (shadowSupported ? it : xit)( - 'returns false when content is in a disabled fieldset but in another shadow tree', - function () { - var fieldset = document.createElement('fieldset'); - fieldset.setAttribute('disabled', 'true'); - fieldset.setAttribute('aria-hidden', 'true'); - fieldset.setAttribute('id', 'target'); - var disabledInput = document.createElement('input'); - fieldset.appendChild(disabledInput); - var shadowRoot = document.createElement('div'); - fieldset.appendChild(shadowRoot); - var shadow = shadowRoot.attachShadow({ mode: 'open' }); - shadow.innerHTML = ''; - var params = checkSetup(fieldset); - - var actual = check.evaluate.apply(checkContext, params); - - assert.isFalse(actual); - } - ); - - it('returns false when content is in the legend of a disabled fieldset', function () { - var params = checkSetup( + it('returns false when content is in a disabled fieldset but in another shadow tree', () => { + const fieldset = document.createElement('fieldset'); + fieldset.setAttribute('disabled', 'true'); + fieldset.setAttribute('aria-hidden', 'true'); + fieldset.setAttribute('id', 'target'); + const disabledInput = document.createElement('input'); + fieldset.appendChild(disabledInput); + const shadowRoot = document.createElement('div'); + fieldset.appendChild(shadowRoot); + const shadow = shadowRoot.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + const params = checkSetup(fieldset); + + const actual = check.evaluate.apply(checkContext, params); + + assert.isFalse(actual); + }); + + it('returns false when content is in the legend of a disabled fieldset', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when content is in an aria-hidden but not disabled fieldset', function () { - var params = checkSetup( + it('returns false when content is in an aria-hidden but not disabled fieldset', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when focusable off screen link (cannot be disabled)', function () { - var params = checkSetup( + it('returns true when focusable off screen link (cannot be disabled)', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); assert.lengthOf(checkContext._relatedNodes, 0); }); - it('returns false when focusable form field only disabled through ARIA', function () { - var params = checkSetup( + it('returns false when focusable form field only disabled through ARIA', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( @@ -108,18 +106,19 @@ describe('focusable-disabled', function () { ); }); - it('returns false when focusable SELECT element that can be disabled', function () { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when focusable SELECT element that can be disabled', () => { + const params = checkSetup(html` + + `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( @@ -128,92 +127,94 @@ describe('focusable-disabled', function () { ); }); - it('returns true when focusable AREA element (cannot be disabled)', function () { - var params = checkSetup( - '
' + - '' + - 'Mozilla' + - '' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when focusable AREA element (cannot be disabled)', () => { + const params = checkSetup(html` +
+ + Mozilla + +
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - (shadowSupported ? it : xit)( - 'returns false when focusable content inside shadowDOM, that can be disabled', - function () { - // Note: - // `testUtils.checkSetup` does not work for shadowDOM - // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction - fixtureSetup('
'); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = ''; - axe.testUtils.flatTreeSetup(fixture); - axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); - assert.isFalse(actual); - } - ); - - it('returns true when focusable target that cannot be disabled', function () { - var params = checkSetup( + it('returns false when focusable content inside shadowDOM, that can be disabled', () => { + // Note: + // `testUtils.checkSetup` does not work for shadowDOM + // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction + fixtureSetup('
'); + const node = fixture.querySelector('#target'); + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + axe.testUtils.flatTreeSetup(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + const virtualNode = axe.utils.getNodeFromTree(node); + const actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isFalse(actual); + }); + + it('returns true when focusable target that cannot be disabled', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns false when focusable target that can be disabled', function () { - var params = checkSetup( + it('returns false when focusable target that can be disabled', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true if there is a focusable element and modal is open', function () { - var params = checkSetup( - '' + - '
Modal
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true if there is a focusable element and modal is open', () => { + const params = checkSetup(html` + +
Modal
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns undefined when the control has onfocus', function () { - var params = checkSetup( + it('returns undefined when the control has onfocus', () => { + const params = checkSetup( '' ); assert.isUndefined(check.evaluate.apply(checkContext, params)); }); - it('returns undefined when all focusable controls have onfocus events', function () { - var params = checkSetup( - '' - ); + it('returns undefined when all focusable controls have onfocus events', () => { + const params = checkSetup(html` + + `); assert.isUndefined(check.evaluate.apply(checkContext, params)); }); - it('returns false when some, but not all focusable controls have onfocus events', function () { - var params = checkSetup( - '' - ); + it('returns false when some, but not all focusable controls have onfocus events', () => { + const params = checkSetup(html` + + `); assert.isFalse(check.evaluate.apply(checkContext, params)); }); it('returns undefined when control has 0 width and height and pointer events: none (focus trap bumper)', () => { - var params = checkSetup( + const params = checkSetup( '' ); assert.isUndefined(check.evaluate.apply(checkContext, params)); diff --git a/test/checks/keyboard/focusable-element.js b/test/checks/keyboard/focusable-element.js index d0d1cd5a0..d6b5c019f 100644 --- a/test/checks/keyboard/focusable-element.js +++ b/test/checks/keyboard/focusable-element.js @@ -1,94 +1,92 @@ -describe('focusable-element tests', function () { - 'use strict'; +describe('focusable-element tests', () => { + let check; + const fixture = document.getElementById('fixture'); + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - var check; - var fixture = document.getElementById('fixture'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - - before(function () { + before(() => { check = checks['focusable-element']; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; checkContext.reset(); }); - it('returns true when element is focusable', function () { - var params = checkSetup(''); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when element is focusable', () => { + const params = checkSetup(''); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns false when element made not focusable by tabindex', function () { - var params = checkSetup( + it('returns false when element made not focusable by tabindex', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when element is not focusable by default', function () { - var params = checkSetup('

I hold some text

'); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when element is not focusable by default', () => { + const params = checkSetup('

I hold some text

'); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when element made focusable by tabindex', function () { - var params = checkSetup( + it('returns true when element made focusable by tabindex', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when element made focusable by contenteditable', function () { - var params = checkSetup( + it('returns true when element made focusable by contenteditable', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when element made focusable by contenteditable="true"', function () { - var params = checkSetup( + it('returns true when element made focusable by contenteditable="true"', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns false when element made focusable by contenteditable="false"', function () { - var params = checkSetup( + it('returns false when element made focusable by contenteditable="false"', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when element made focusable by contenteditable="invalid" and parent is contenteditable', function () { - var params = checkSetup( + it('returns true when element made focusable by contenteditable="invalid" and parent is contenteditable', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns false when element made focusable by contenteditable="invalid" and parent is not contenteditable', function () { - var params = checkSetup( + it('returns false when element made focusable by contenteditable="invalid" and parent is not contenteditable', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when element made focusable by contenteditable="invalid" and parent is contenteditable="false"', function () { - var params = checkSetup( + it('returns false when element made focusable by contenteditable="invalid" and parent is contenteditable="false"', () => { + const params = checkSetup( '

I hold some text

' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); }); diff --git a/test/checks/keyboard/focusable-modal-open.js b/test/checks/keyboard/focusable-modal-open.js index bb7693573..fc5a10ad3 100644 --- a/test/checks/keyboard/focusable-modal-open.js +++ b/test/checks/keyboard/focusable-modal-open.js @@ -1,50 +1,50 @@ -describe('focusable-modal-open', function () { - 'use strict'; +describe('focusable-modal-open', () => { + const html = axe.testUtils.html; - var check; - var fixture = document.getElementById('fixture'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; + let check; + const fixture = document.getElementById('fixture'); + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - before(function () { + before(() => { check = checks['focusable-modal-open']; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; axe._selectorData = undefined; checkContext.reset(); }); - it('returns true when no modal is open', function () { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when no modal is open', () => { + const params = checkSetup(html` + + `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns undefined if a modal is open', function () { - var params = checkSetup( - '' + - '
Modal
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns undefined if a modal is open', () => { + const params = checkSetup(html` + +
Modal
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isUndefined(actual); }); - it('sets the tabbable elements as related nodes', function () { - var params = checkSetup( - '' + - '
Modal
' - ); + it('sets the tabbable elements as related nodes', () => { + const params = checkSetup(html` + +
Modal
+ `); check.evaluate.apply(checkContext, params); assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( diff --git a/test/checks/keyboard/focusable-no-name.js b/test/checks/keyboard/focusable-no-name.js index e170eb838..c9e749428 100644 --- a/test/checks/keyboard/focusable-no-name.js +++ b/test/checks/keyboard/focusable-no-name.js @@ -1,21 +1,18 @@ -describe('focusable-no-name', function () { - 'use strict'; +describe('focusable-no-name', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; + const shadowCheckSetup = axe.testUtils.shadowCheckSetup; - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var shadowCheckSetup = axe.testUtils.shadowCheckSetup; - var shadowSupport = axe.testUtils.shadowSupport; + const checkContext = axe.testUtils.MockCheckContext(); - var checkContext = axe.testUtils.MockCheckContext(); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; checkContext.reset(); }); - it('should pass if tabindex < 0', function () { - var params = checkSetup(''); + it('should pass if tabindex < 0', () => { + const params = checkSetup(''); assert.isFalse( axe.testUtils .getCheckEvaluate('focusable-no-name') @@ -23,8 +20,8 @@ describe('focusable-no-name', function () { ); }); - it('should pass element is not natively focusable', function () { - var params = checkSetup(''); + it('should pass element is not natively focusable', () => { + const params = checkSetup(''); assert.isFalse( axe.testUtils .getCheckEvaluate('focusable-no-name') @@ -32,8 +29,8 @@ describe('focusable-no-name', function () { ); }); - it('should fail if element is tabbable with no name - native', function () { - var params = checkSetup(''); + it('should fail if element is tabbable with no name - native', () => { + const params = checkSetup(''); assert.isTrue( axe.testUtils .getCheckEvaluate('focusable-no-name') @@ -41,8 +38,8 @@ describe('focusable-no-name', function () { ); }); - it('should fail if element is tabbable with no name - ARIA', function () { - var params = checkSetup( + it('should fail if element is tabbable with no name - ARIA', () => { + const params = checkSetup( '' ); assert.isTrue( @@ -52,8 +49,8 @@ describe('focusable-no-name', function () { ); }); - it('should pass if the element is tabbable but has an accessible name', function () { - var params = checkSetup(''); + it('should pass if the element is tabbable but has an accessible name', () => { + const params = checkSetup(''); assert.isFalse( axe.testUtils .getCheckEvaluate('focusable-no-name') @@ -61,25 +58,22 @@ describe('focusable-no-name', function () { ); }); - (shadowSupport.v1 ? it : xit)( - 'should pass if the content is passed in with shadow DOM', - function () { - var params = shadowCheckSetup( - '
Content!
', - '' - ); + it('should pass if the content is passed in with shadow DOM', () => { + const params = shadowCheckSetup( + '
Content!
', + '' + ); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('focusable-no-name') - .apply(checkContext, params) - ); - } - ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('focusable-no-name') + .apply(checkContext, params) + ); + }); - describe('Serial Virtual Node', function () { - it('should pass if tabindex < 0', function () { - var serialNode = new axe.SerialVirtualNode({ + describe('Serial Virtual Node', () => { + it('should pass if tabindex < 0', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'a', attributes: { tabindex: '-1', @@ -96,8 +90,8 @@ describe('focusable-no-name', function () { ); }); - it('should pass element is not natively focusable', function () { - var serialNode = new axe.SerialVirtualNode({ + it('should pass element is not natively focusable', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'span', attributes: { role: 'link', @@ -114,8 +108,8 @@ describe('focusable-no-name', function () { ); }); - it('should fail if element is tabbable with no name - native', function () { - var serialNode = new axe.SerialVirtualNode({ + it('should fail if element is tabbable with no name - native', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'a', attributes: { href: '#' @@ -132,8 +126,8 @@ describe('focusable-no-name', function () { ); }); - it('should return undefined if element is tabbable with no name nor children - native', function () { - var serialNode = new axe.SerialVirtualNode({ + it('should return undefined if element is tabbable with no name nor children - native', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'a', attributes: { href: '#' @@ -149,8 +143,8 @@ describe('focusable-no-name', function () { ); }); - it('should pass if the element is tabbable but has an accessible name', function () { - var serialNode = new axe.SerialVirtualNode({ + it('should pass if the element is tabbable but has an accessible name', () => { + const serialNode = new axe.SerialVirtualNode({ nodeName: 'a', attributes: { href: '#', diff --git a/test/checks/keyboard/focusable-not-tabbable.js b/test/checks/keyboard/focusable-not-tabbable.js index 60bd891d1..5b4c74d16 100644 --- a/test/checks/keyboard/focusable-not-tabbable.js +++ b/test/checks/keyboard/focusable-not-tabbable.js @@ -1,41 +1,40 @@ -describe('focusable-not-tabbable', function () { - 'use strict'; +describe('focusable-not-tabbable', () => { + const html = axe.testUtils.html; - var check; - var fixture = document.getElementById('fixture'); - var fixtureSetup = axe.testUtils.fixtureSetup; - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; + let check; + const fixture = document.getElementById('fixture'); + const fixtureSetup = axe.testUtils.fixtureSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - before(function () { + before(() => { check = checks['focusable-not-tabbable']; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; axe._selectorData = undefined; checkContext.reset(); }); - it('returns true when BUTTON removed from tab order through tabindex', function () { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when BUTTON removed from tab order through tabindex', () => { + const params = checkSetup(html` + + `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns false when LINK is in tab order', function () { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when LINK is in tab order', () => { + const params = checkSetup(html` + + `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( @@ -44,11 +43,11 @@ describe('focusable-not-tabbable', function () { ); }); - it('returns true when focusable SUMMARY element, that cannot be disabled', function () { - var params = checkSetup( + it('returns true when focusable SUMMARY element, that cannot be disabled', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( @@ -57,123 +56,126 @@ describe('focusable-not-tabbable', function () { ); }); - it('returns true when TEXTAREA is in tab order, but 0 related nodes as TEXTAREA can be disabled', function () { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true when TEXTAREA is in tab order, but 0 related nodes as TEXTAREA can be disabled', () => { + const params = checkSetup(html` + + `); + const actual = check.evaluate.apply(checkContext, params); assert.lengthOf(checkContext._relatedNodes, 0); assert.isTrue(actual); }); - it('returns false when focusable AREA element', function () { - var params = checkSetup( - '
' + - '' + - 'Mozilla' + - '' + - '
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns false when focusable AREA element', () => { + const params = checkSetup(html` +
+ + Mozilla + +
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when aria-hidden=false does not negate aria-hidden true', function () { + it('returns true when aria-hidden=false does not negate aria-hidden true', () => { // Note: aria-hidden can't be reset once you've set it to true on an ancestor - var params = checkSetup( + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - (shadowSupported ? it : xit)( - 'returns false when focusable text in shadowDOM', - function () { - // Note: - // `testUtils.checkSetup` does not work for shadowDOM - // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction - fixtureSetup('`'); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '

btn

'; - axe.testUtils.flatTreeSetup(fixture); - axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); - assert.isFalse(actual); - } - ); - - it('returns false when focusable content through tabindex', function () { - var params = checkSetup( + it('returns false when focusable text in shadowDOM', () => { + // Note: + // `testUtils.checkSetup` does not work for shadowDOM + // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction + fixtureSetup('`'); + const node = fixture.querySelector('#target'); + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '

btn

'; + axe.testUtils.flatTreeSetup(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + const virtualNode = axe.utils.getNodeFromTree(node); + const actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isFalse(actual); + }); + + it('returns false when focusable content through tabindex', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns false when focusable target that cannot be disabled', function () { - var params = checkSetup( + it('returns false when focusable target that cannot be disabled', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); - it('returns true when focusable target that can be disabled', function () { - var params = checkSetup( + it('returns true when focusable target that can be disabled', () => { + const params = checkSetup( '' ); - var actual = check.evaluate.apply(checkContext, params); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true if there is a focusable element and modal is open', function () { - var params = checkSetup( - '' + - '
Modal
' - ); - var actual = check.evaluate.apply(checkContext, params); + it('returns true if there is a focusable element and modal is open', () => { + const params = checkSetup(html` + +
Modal
+ `); + const actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns undefined when the control has onfocus', function () { - var params = checkSetup( + it('returns undefined when the control has onfocus', () => { + const params = checkSetup( '' ); assert.isUndefined(check.evaluate.apply(checkContext, params)); }); - it('returns undefined when all focusable controls have onfocus events', function () { - var params = checkSetup( - '' - ); + it('returns undefined when all focusable controls have onfocus events', () => { + const params = checkSetup(html` + + `); assert.isUndefined(check.evaluate.apply(checkContext, params)); }); - it('returns false when some, but not all focusable controls have onfocus events', function () { - var params = checkSetup( - '' - ); + it('returns false when some, but not all focusable controls have onfocus events', () => { + const params = checkSetup(html` + + `); assert.isFalse(check.evaluate.apply(checkContext, params)); }); it('returns undefined when control has 0 width and height and pointer events: none (focus trap bumper)', () => { - var params = checkSetup( + const params = checkSetup( '' ); assert.isUndefined(check.evaluate.apply(checkContext, params)); diff --git a/test/checks/keyboard/frame-focusable-content.js b/test/checks/keyboard/frame-focusable-content.js index 450645191..6526cef5f 100644 --- a/test/checks/keyboard/frame-focusable-content.js +++ b/test/checks/keyboard/frame-focusable-content.js @@ -1,52 +1,52 @@ -describe('frame-focusable-content tests', function () { - var fixture = document.querySelector('#fixture'); - var queryFixture = axe.testUtils.queryFixture; - var frameFocusableContent = axe.testUtils.getCheckEvaluate( +describe('frame-focusable-content tests', () => { + const fixture = document.querySelector('#fixture'); + const queryFixture = axe.testUtils.queryFixture; + const frameFocusableContent = axe.testUtils.getCheckEvaluate( 'frame-focusable-content' ); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should return true if element has no focusable content', function () { - var vNode = queryFixture('
Hello
'); + it('should return true if element has no focusable content', () => { + const vNode = queryFixture('
Hello
'); assert.isTrue(frameFocusableContent(null, null, vNode)); }); - it('should return true if element is empty', function () { - var vNode = queryFixture('
'); + it('should return true if element is empty', () => { + const vNode = queryFixture('
'); assert.isTrue(frameFocusableContent(null, null, vNode)); }); - it('should return true if element only has text content', function () { - var vNode = queryFixture('
Hello
'); + it('should return true if element only has text content', () => { + const vNode = queryFixture('
Hello
'); assert.isTrue(frameFocusableContent(null, null, vNode)); }); - it('should return false if element has focusable content', function () { - var vNode = queryFixture( + it('should return false if element has focusable content', () => { + const vNode = queryFixture( '
Hello
' ); assert.isFalse(frameFocusableContent(null, null, vNode)); }); - it('should return false if element has natively focusable content', function () { - var vNode = queryFixture( + it('should return false if element has natively focusable content', () => { + const vNode = queryFixture( '' ); assert.isFalse(frameFocusableContent(null, null, vNode)); }); - it('should return true if element is natively focusable but has tabindex=-1', function () { - var vNode = queryFixture( + it('should return true if element is natively focusable but has tabindex=-1', () => { + const vNode = queryFixture( '
' ); assert.isTrue(frameFocusableContent(null, null, vNode)); }); - it('should return false if element is natively focusable but has tabindex=0', function () { - var vNode = queryFixture( + it('should return false if element is natively focusable but has tabindex=0', () => { + const vNode = queryFixture( '
' ); assert.isFalse(frameFocusableContent(null, null, vNode)); diff --git a/test/checks/keyboard/landmark-is-top-level.js b/test/checks/keyboard/landmark-is-top-level.js index f990a0481..69fab6138 100644 --- a/test/checks/keyboard/landmark-is-top-level.js +++ b/test/checks/keyboard/landmark-is-top-level.js @@ -1,5 +1,4 @@ describe('landmark-is-top-level', () => { - const shadowSupported = axe.testUtils.shadowSupport.v1; const checkSetup = axe.testUtils.checkSetup; const shadowCheckSetup = axe.testUtils.shadowCheckSetup; const check = checks['landmark-is-top-level']; @@ -73,16 +72,13 @@ describe('landmark-is-top-level', () => { assert.deepEqual(checkContext._data, { role: 'banner' }); }); - (shadowSupported ? it : xit)( - 'should test if the landmark in shadow DOM is top level', - () => { - const params = shadowCheckSetup( - '
', - '
Main content
' - ); - axe.utils.getFlattenedTree(document.documentElement); - assert.isTrue(check.evaluate.apply(checkContext, params)); - assert.deepEqual(checkContext._data, { role: 'main' }); - } - ); + it('should test if the landmark in shadow DOM is top level', () => { + const params = shadowCheckSetup( + '
', + '
Main content
' + ); + axe.utils.getFlattenedTree(document.documentElement); + assert.isTrue(check.evaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { role: 'main' }); + }); }); diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index e30841b41..7bfd604cd 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -1,48 +1,51 @@ -describe('no-focusable-content tests', function () { - var queryFixture = axe.testUtils.queryFixture; - var noFocusableContent = axe.testUtils.getCheckEvaluate( +describe('no-focusable-content tests', () => { + const html = axe.testUtils.html; + const queryFixture = axe.testUtils.queryFixture; + const noFocusableContent = axe.testUtils.getCheckEvaluate( 'no-focusable-content' ); - var checkSetup = axe.testUtils.checkSetup; - var check = checks['no-focusable-content']; - var checkContext = new axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const check = checks['no-focusable-content']; + const checkContext = new axe.testUtils.MockCheckContext(); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true if element has no focusable content', function () { - var vNode = queryFixture(''); + it('should return true if element has no focusable content', () => { + const vNode = queryFixture( + '' + ); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return true if element is empty', function () { - var vNode = queryFixture(''); + it('should return true if element is empty', () => { + const vNode = queryFixture(''); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return true if element only has text content', function () { - var vNode = queryFixture(''); + it('should return true if element only has text content', () => { + const vNode = queryFixture(''); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return true if element has content which is focusable (tabindex=0) and does not have a widget role', function () { - var params = checkSetup( + it('should return true if element has content which is focusable (tabindex=0) and does not have a widget role', () => { + const params = checkSetup( '' ); assert.isTrue(noFocusableContent.apply(checkContext, params)); }); - it('should return true if element has content which has negative tabindex and non-widget role', function () { - var vNode = queryFixture( + it('should return true if element has content which has negative tabindex and non-widget role', () => { + const vNode = queryFixture( '' ); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return false if element has content which has negative tabindex and an explicit widget role', function () { - var params = checkSetup( + it('should return false if element has content which has negative tabindex and an explicit widget role', () => { + const params = checkSetup( '' ); @@ -51,8 +54,8 @@ describe('no-focusable-content tests', function () { assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should return false if element has content which is natively focusable and has a widget role', function () { - var params = checkSetup( + it('should return false if element has content which is natively focusable and has a widget role', () => { + const params = checkSetup( '' ); @@ -61,8 +64,8 @@ describe('no-focusable-content tests', function () { assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should add each focusable child as related nodes', function () { - var params = checkSetup( + it('should add each focusable child as related nodes', () => { + const params = checkSetup( '' ); @@ -74,8 +77,8 @@ describe('no-focusable-content tests', function () { ]); }); - it('should return false if element has natively focusable widget role content with negative tabindex', function () { - var params = checkSetup( + it('should return false if element has natively focusable widget role content with negative tabindex', () => { + const params = checkSetup( '' ); assert.isFalse(check.evaluate.apply(checkContext, params)); @@ -83,44 +86,49 @@ describe('no-focusable-content tests', function () { assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should return true if element has content which is natively focusable and has a widget role but is disabled', function () { - var vNode = queryFixture( + it('should return true if element has content which is natively focusable and has a widget role but is disabled', () => { + const vNode = queryFixture( '' ); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return false if "disabled" is specified on an element which doesn\'t allow it', function () { - var params = checkSetup( + it('should return false if "disabled" is specified on an element which doesn\'t allow it', () => { + const params = checkSetup( '' ); assert.isFalse(noFocusableContent.apply(checkContext, params)); }); - it('should return true on span with negative tabindex (focusable, does not have a widget role)', function () { - var vNode = queryFixture( - ' some text ' + - 'JavaScript is able to focus this ' + - '' - ); + it('should return true on span with negative tabindex (focusable, does not have a widget role)', () => { + const vNode = queryFixture(html` + + some text + JavaScript is able to focus this + + `); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return true on aria-hidden span with negative tabindex (focusable, does not have a widget role)', function () { - var vNode = queryFixture( - ' some text ' + - ' ' + - '' - ); + it('should return true on aria-hidden span with negative tabindex (focusable, does not have a widget role)', () => { + const vNode = queryFixture(html` + + some text + + + `); assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return true on nested span with tabindex=0 (focusable, does not have a widget role)', function () { - var vNode = queryFixture( - ' some text ' + - 'anyone is able to focus this ' + - '' - ); + it('should return true on nested span with tabindex=0 (focusable, does not have a widget role)', () => { + const vNode = queryFixture(html` + + some text + anyone is able to focus this + + `); assert.isTrue(noFocusableContent(null, null, vNode)); }); }); diff --git a/test/checks/keyboard/page-has-elm.js b/test/checks/keyboard/page-has-elm.js index 20e629516..c91a01fbd 100644 --- a/test/checks/keyboard/page-has-elm.js +++ b/test/checks/keyboard/page-has-elm.js @@ -1,90 +1,97 @@ -describe('page-has-*', function () { - 'use strict'; +describe('page-has-*', () => { + const html = axe.testUtils.html; - var fixture = document.getElementById('fixture'); - var checkContext = new axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var shadowSupported = axe.testUtils.shadowSupport.v1; - var shadowCheckSetup = axe.testUtils.shadowCheckSetup; + const fixture = document.getElementById('fixture'); + const checkContext = new axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const shadowCheckSetup = axe.testUtils.shadowCheckSetup; - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; checkContext.reset(); axe._tree = undefined; }); - describe('evaluate', function () { - var evaluate = axe.testUtils.getCheckEvaluate('page-has-main'); + describe('evaluate', () => { + const evaluate = axe.testUtils.getCheckEvaluate('page-has-main'); - it('throws if there is no selector', function () { - assert.throws(function () { - var params = checkSetup('
No role
', undefined); + it('throws if there is no selector', () => { + assert.throws(() => { + const params = checkSetup('
No role
', undefined); checks['page-has-main'].evaluate.apply(checkContext, params); }); - assert.throws(function () { - var params = checkSetup('
No role
', {}); + assert.throws(() => { + const params = checkSetup('
No role
', {}); checks['page-has-main'].evaluate.apply(checkContext, params); }); - assert.throws(function () { - var badOptions = { selector: [] }; - var params = checkSetup('
No role
', badOptions); + assert.throws(() => { + const badOptions = { selector: [] }; + const params = checkSetup('
No role
', badOptions); checks['page-has-main'].evaluate.apply(checkContext, params); }); }); - it('returns true if there are any matching elements', function () { - var options = { selector: 'b' }; - var params = checkSetup('
No role
', options); + it('returns true if there are any matching elements', () => { + const options = { selector: 'b' }; + const params = checkSetup( + '
No role
', + options + ); assert.isTrue(evaluate.apply(checkContext, params)); }); - it('returns false if there are no matching elements', function () { - var options = { selector: 'i' }; - var params = checkSetup('
No role
', options); + it('returns false if there are no matching elements', () => { + const options = { selector: 'i' }; + const params = checkSetup( + '
No role
', + options + ); assert.isFalse(evaluate.apply(checkContext, params)); }); - it('does not find hidden elements', function () { - var options = { selector: 'b' }; - var params = checkSetup( + it('does not find hidden elements', () => { + const options = { selector: 'b' }; + const params = checkSetup( '
No role
', options ); assert.isFalse(evaluate.apply(checkContext, params)); }); - it('does find screen-reader only elements', function () { - var options = { selector: 'b' }; - var params = checkSetup( - '' + - '
No role
', + it('does find screen-reader only elements', () => { + const options = { selector: 'b' }; + const params = checkSetup( + html` + +
No role
+ `, options ); assert.isTrue(evaluate.apply(checkContext, params)); }); }); - describe('after', function () { - var after = checks['page-has-main'].after; + describe('after', () => { + const after = checks['page-has-main'].after; - it('sets all results to true if any are true', function () { - var results = [ + it('sets all results to true if any are true', () => { + const results = [ { result: true }, { result: false }, { result: undefined } @@ -96,8 +103,8 @@ describe('page-has-*', function () { ]); }); - it('Leave the results as is if none of them were true', function () { - var results = [ + it('Leave the results as is if none of them were true', () => { + const results = [ { result: false }, { result: false }, { result: undefined } @@ -106,103 +113,103 @@ describe('page-has-*', function () { }); }); - describe('page-has-main', function () { - var check = checks['page-has-main']; + describe('page-has-main', () => { + const check = checks['page-has-main']; - it('should return false if no div has role property', function () { - var params = checkSetup('
No role
', check.options); - var mainIsFound = check.evaluate.apply(checkContext, params); + it('should return false if no div has role property', () => { + const params = checkSetup( + '
No role
', + check.options + ); + const mainIsFound = check.evaluate.apply(checkContext, params); assert.isFalse(mainIsFound); }); - it('should return false if div has role not equal to main', function () { - var params = checkSetup( + it('should return false if div has role not equal to main', () => { + const params = checkSetup( '
Wrong role
', check.options ); - var mainIsFound = check.evaluate.apply(checkContext, params); + const mainIsFound = check.evaluate.apply(checkContext, params); assert.isFalse(mainIsFound); }); - it('should return true if main landmark exists', function () { - var params = checkSetup( + it('should return true if main landmark exists', () => { + const params = checkSetup( '
main landmark
', check.options ); - var mainIsFound = check.evaluate.apply(checkContext, params); + const mainIsFound = check.evaluate.apply(checkContext, params); assert.isTrue(mainIsFound); }); - it('should return true if one div has role equal to main', function () { - var params = checkSetup( + it('should return true if one div has role equal to main', () => { + const params = checkSetup( '
Div with role main
', check.options ); - var mainIsFound = check.evaluate.apply(checkContext, params); + const mainIsFound = check.evaluate.apply(checkContext, params); assert.isTrue(mainIsFound); }); - (shadowSupported ? it : xit)( - 'should return true if main is inside of shadow dom', - function () { - var params = shadowCheckSetup( - '
', - '
main landmark
', - check.options - ); - var mainIsFound = check.evaluate.apply(checkContext, params); - assert.isTrue(mainIsFound); - } - ); + it('should return true if main is inside of shadow dom', () => { + const params = shadowCheckSetup( + '
', + '
main landmark
', + check.options + ); + const mainIsFound = check.evaluate.apply(checkContext, params); + assert.isTrue(mainIsFound); + }); }); - describe('page-has-heading-one', function () { - var check = checks['page-has-heading-one']; + describe('page-has-heading-one', () => { + const check = checks['page-has-heading-one']; - it('should return false if div has role not equal to heading', function () { - var params = checkSetup( + it('should return false if div has role not equal to heading', () => { + const params = checkSetup( '
Wrong role
', check.options ); - var h1IsFound = check.evaluate.apply(checkContext, params); + const h1IsFound = check.evaluate.apply(checkContext, params); assert.isFalse(h1IsFound); }); - it('should return false if div has role heading but not aria-level=1', function () { - var params = checkSetup( + it('should return false if div has role heading but not aria-level=1', () => { + const params = checkSetup( '
Wrong role
', check.options ); - var h1IsFound = check.evaluate.apply(checkContext, params); + const h1IsFound = check.evaluate.apply(checkContext, params); assert.isFalse(h1IsFound); }); - it('should return true if h1 exists', function () { - var params = checkSetup('

My heading

', check.options); - var h1IsFound = check.evaluate.apply(checkContext, params); + it('should return true if h1 exists', () => { + const params = checkSetup( + '

My heading

', + check.options + ); + const h1IsFound = check.evaluate.apply(checkContext, params); assert.isTrue(h1IsFound); }); - it('should return true if a div has role=heading and aria-level=1', function () { - var params = checkSetup( + it('should return true if a div has role=heading and aria-level=1', () => { + const params = checkSetup( '
Diversity heading
', check.options ); - var h1IsFound = check.evaluate.apply(checkContext, params); + const h1IsFound = check.evaluate.apply(checkContext, params); assert.isTrue(h1IsFound); }); - (shadowSupported ? it : xit)( - 'should return true if h1 is inside of shadow dom', - function () { - var params = shadowCheckSetup( - '
', - '

Shady Heading

', - check.options - ); - var h1IsFound = check.evaluate.apply(checkContext, params); - assert.isTrue(h1IsFound); - } - ); + it('should return true if h1 is inside of shadow dom', () => { + const params = shadowCheckSetup( + '
', + '

Shady Heading

', + check.options + ); + const h1IsFound = check.evaluate.apply(checkContext, params); + assert.isTrue(h1IsFound); + }); }); }); diff --git a/test/checks/keyboard/page-no-duplicate.js b/test/checks/keyboard/page-no-duplicate.js index a54677e7e..2df7a1433 100644 --- a/test/checks/keyboard/page-no-duplicate.js +++ b/test/checks/keyboard/page-no-duplicate.js @@ -1,8 +1,8 @@ describe('page-no-duplicate', () => { + const html = axe.testUtils.html; const fixture = document.getElementById('fixture'); const checkContext = new axe.testUtils.MockCheckContext(); const checkSetup = axe.testUtils.checkSetup; - const shadowSupported = axe.testUtils.shadowSupport.v1; const check = checks['page-no-duplicate-main']; @@ -65,52 +65,46 @@ describe('page-no-duplicate', () => { assert.isTrue(check.evaluate.apply(checkContext, params)); }); - (shadowSupported ? it : xit)( - 'should return false if there is a second matching element inside the shadow dom', - () => { - const options = { selector: 'main' }; - const div = document.createElement('div'); - div.innerHTML = '
'; - - const shadow = div - .querySelector('#shadow') - .attachShadow({ mode: 'open' }); - shadow.innerHTML = '
'; - axe.testUtils.fixtureSetup(div); - const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - - assert.isFalse( - check.evaluate.call(checkContext, vNode.actualNode, options, vNode) - ); - assert.deepEqual(checkContext._relatedNodes, [ - shadow.querySelector('main') - ]); - } - ); - - (shadowSupported ? it : xit)( - 'should return true if there is a second matching element inside the shadow dom but only one is visible to screenreaders', - () => { - const options = { selector: 'main' }; - const div = document.createElement('div'); - div.innerHTML = - '
'; - - const shadow = div - .querySelector('#shadow') - .attachShadow({ mode: 'open' }); - shadow.innerHTML = '
'; - axe.testUtils.fixtureSetup(div); - const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + it('should return false if there is a second matching element inside the shadow dom', () => { + const options = { selector: 'main' }; + const div = document.createElement('div'); + div.innerHTML = '
'; + + const shadow = div + .querySelector('#shadow') + .attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + axe.testUtils.fixtureSetup(div); + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + + assert.isFalse( + check.evaluate.call(checkContext, vNode.actualNode, options, vNode) + ); + assert.deepEqual(checkContext._relatedNodes, [ + shadow.querySelector('main') + ]); + }); - assert.isTrue( - check.evaluate.call(checkContext, vNode.actualNode, options, vNode) - ); - assert.deepEqual(checkContext._relatedNodes, [ - shadow.querySelector('main') - ]); - } - ); + it('should return true if there is a second matching element inside the shadow dom but only one is visible to screenreaders', () => { + const options = { selector: 'main' }; + const div = document.createElement('div'); + div.innerHTML = + '
'; + + const shadow = div + .querySelector('#shadow') + .attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + axe.testUtils.fixtureSetup(div); + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + + assert.isTrue( + check.evaluate.call(checkContext, vNode.actualNode, options, vNode) + ); + assert.deepEqual(checkContext._relatedNodes, [ + shadow.querySelector('main') + ]); + }); }); describe('option.nativeScopeFilter', () => { @@ -120,9 +114,12 @@ describe('page-no-duplicate', () => { nativeScopeFilter: 'main' }; const params = checkSetup( - '
' + - '
' + - '
', + html` +
+
+
+
+ `, options ); assert.isTrue(check.evaluate.apply(checkContext, params)); @@ -134,9 +131,12 @@ describe('page-no-duplicate', () => { nativeScopeFilter: 'main' }; const params = checkSetup( - '
' + - '
' + - '
', + html` +
+
+
+
+ `, options ); assert.isFalse(check.evaluate.apply(checkContext, params)); @@ -148,36 +148,35 @@ describe('page-no-duplicate', () => { nativeScopeFilter: 'article' }; const params = checkSetup( - '
' + - '
Article footer
' + - '
' + - '
Body footer
', + html` +
+
Article footer
+
+
Body footer
+ `, options ); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - (shadowSupported ? it : xit)( - 'elements if its ancestor is outside the shadow DOM tree', - () => { - const options = { - selector: 'footer', - nativeScopeFilter: 'header' - }; + it('elements if its ancestor is outside the shadow DOM tree', () => { + const options = { + selector: 'footer', + nativeScopeFilter: 'header' + }; - const div = document.createElement('div'); - div.innerHTML = - '
'; - div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = - '
'; - axe.testUtils.fixtureSetup(div); - const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const div = document.createElement('div'); + div.innerHTML = + '
'; + div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = + '
'; + axe.testUtils.fixtureSetup(div); + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - assert.isTrue( - check.evaluate.call(checkContext, vNode.actualNode, options, vNode) - ); - } - ); + assert.isTrue( + check.evaluate.call(checkContext, vNode.actualNode, options, vNode) + ); + }); }); describe('options.role', () => { @@ -187,7 +186,7 @@ describe('page-no-duplicate', () => { role: 'contentinfo' }; const params = checkSetup( - `
+ html`
@@ -204,7 +203,7 @@ describe('page-no-duplicate', () => { role: 'contentinfo' }; const params = checkSetup( - `
+ html`
@@ -224,35 +223,32 @@ describe('page-no-duplicate', () => { role: 'contentinfo' }; const params = checkSetup( - `
-
Article footer
-
-
Body footer
`, + html`
+
Article footer
+
+
Body footer
`, options ); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - (shadowSupported ? it : xit)( - "should pass if element's ancestor is outside the shadow DOM tree", - () => { - const options = { - selector: 'footer', - role: 'contentinfo' - }; + it("should pass if element's ancestor is outside the shadow DOM tree", () => { + const options = { + selector: 'footer', + role: 'contentinfo' + }; - const div = document.createElement('div'); - div.innerHTML = - '
'; - div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = - '
'; - axe.testUtils.fixtureSetup(div); - const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const div = document.createElement('div'); + div.innerHTML = + '
'; + div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = + '
'; + axe.testUtils.fixtureSetup(div); + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - assert.isTrue( - check.evaluate.call(checkContext, vNode.actualNode, options, vNode) - ); - } - ); + assert.isTrue( + check.evaluate.call(checkContext, vNode.actualNode, options, vNode) + ); + }); }); }); diff --git a/test/checks/keyboard/tabindex.js b/test/checks/keyboard/tabindex.js index 1a32f9c34..b578f5978 100644 --- a/test/checks/keyboard/tabindex.js +++ b/test/checks/keyboard/tabindex.js @@ -1,15 +1,13 @@ -describe('tabindex', function () { - 'use strict'; +describe('tabindex', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const queryFixture = axe.testUtils.queryFixture; - var checkContext = axe.testUtils.MockCheckContext(); - var queryFixture = axe.testUtils.queryFixture; - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should fail if the testutils.jstabindex is >= 0', function () { - var vNode = queryFixture('
'); + it('should fail if the testutils.jstabindex is >= 0', () => { + const vNode = queryFixture('
'); assert.isFalse( axe.testUtils .getCheckEvaluate('tabindex') @@ -17,8 +15,8 @@ describe('tabindex', function () { ); }); - it('should pass if the tabindex is <= 0', function () { - var vNode = queryFixture('
'); + it('should pass if the tabindex is <= 0', () => { + const vNode = queryFixture('
'); assert.isTrue( axe.testUtils .getCheckEvaluate('tabindex') @@ -26,11 +24,11 @@ describe('tabindex', function () { ); }); - it('should look at the attribute and not the property', function () { - var node = document.createElement('div'); + it('should look at the attribute and not the property', () => { + const node = document.createElement('div'); node.setAttribute('tabindex', '1'); node.tabindex = null; - var tree = axe.testUtils.flatTreeSetup(node); + const tree = axe.testUtils.flatTreeSetup(node); assert.isFalse( axe.testUtils .getCheckEvaluate('tabindex') @@ -38,8 +36,8 @@ describe('tabindex', function () { ); }); - it('should pass if tabindex is NaN', function () { - var vNode = queryFixture('
'); + it('should pass if tabindex is NaN', () => { + const vNode = queryFixture('
'); assert.isTrue( axe.testUtils .getCheckEvaluate('tabindex') diff --git a/test/checks/label/alt-space-value.js b/test/checks/label/alt-space-value.js index 603a811ad..3392329c6 100644 --- a/test/checks/label/alt-space-value.js +++ b/test/checks/label/alt-space-value.js @@ -1,31 +1,29 @@ -describe('alt-space-value', function () { - 'use strict'; +describe('alt-space-value', () => { + const checkSetup = axe.testUtils.checkSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const check = checks['alt-space-value']; - var checkSetup = axe.testUtils.checkSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var check = checks['alt-space-value']; - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true if alt contains a space character', function () { - var params = checkSetup(' '); + it('should return true if alt contains a space character', () => { + const params = checkSetup(' '); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should return true if alt contains a non-breaking space character', function () { - var params = checkSetup(' '); + it('should return true if alt contains a non-breaking space character', () => { + const params = checkSetup(' '); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should return false if alt attribute is empty', function () { - var params = checkSetup(''); + it('should return false if alt attribute is empty', () => { + const params = checkSetup(''); assert.isFalse(check.evaluate.apply(checkContext, params)); }); - it('should return false if alt attribute has a proper text value', function () { - var params = checkSetup('text content'); + it('should return false if alt attribute has a proper text value', () => { + const params = checkSetup('text content'); assert.isFalse(check.evaluate.apply(checkContext, params)); }); }); diff --git a/test/checks/label/duplicate-img-label.js b/test/checks/label/duplicate-img-label.js index 1a1de7d12..b840a3fae 100644 --- a/test/checks/label/duplicate-img-label.js +++ b/test/checks/label/duplicate-img-label.js @@ -1,20 +1,17 @@ -describe('duplicate-img-label', function () { - 'use strict'; +describe('duplicate-img-label', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var shadowSupport = axe.testUtils.shadowSupport; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; }); - it('should return false if no text is present', function () { + it('should return false if no text is present', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); - var result = axe.testUtils.getCheckEvaluate('duplicate-img-label')( + const result = axe.testUtils.getCheckEvaluate('duplicate-img-label')( node, undefined, axe.utils.getNodeFromTree(node) @@ -22,10 +19,10 @@ describe('duplicate-img-label', function () { assert.isFalse(result); }); - it('should return false if aria-label duplicates img alt', function () { + it('should return false if aria-label duplicates img alt', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -36,10 +33,10 @@ describe('duplicate-img-label', function () { ); }); - it('should return false if img and text have different text', function () { + it('should return false if img and text have different text', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -50,10 +47,10 @@ describe('duplicate-img-label', function () { ); }); - it('should return true if img and text have the same text', function () { + it('should return true if img and text have the same text', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isTrue( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -64,10 +61,10 @@ describe('duplicate-img-label', function () { ); }); - it('should return true if img has ARIA label with the same text', function () { + it('should return true if img has ARIA label with the same text', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isTrue( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -78,9 +75,9 @@ describe('duplicate-img-label', function () { ); }); - it('should return false if img and text are both blank', function () { + it('should return false if img and text are both blank', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -91,10 +88,10 @@ describe('duplicate-img-label', function () { ); }); - it('should return false if img and text have superset/subset text', function () { + it('should return false if img and text have superset/subset text', () => { fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -105,10 +102,10 @@ describe('duplicate-img-label', function () { ); }); - it('should return false if img does not have required parent', function () { + it('should return false if img does not have required parent', () => { fixture.innerHTML = '
Plain text and more

Plain text

'; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -119,10 +116,10 @@ describe('duplicate-img-label', function () { ); }); - it('should support options.parentSelector', function () { + it('should support options.parentSelector', () => { fixture.innerHTML = '
Plain text
'; - var node = fixture.querySelector('#target'); + const node = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); assert.isFalse( axe.testUtils.getCheckEvaluate('duplicate-img-label')( @@ -133,64 +130,55 @@ describe('duplicate-img-label', function () { ); }); - (shadowSupport.v1 ? it : xit)( - 'should return true if the img is part of a shadow tree', - function () { - var button = document.createElement('div'); - button.setAttribute('role', 'button'); - button.innerHTML = 'My button'; - var shadow = button.attachShadow({ mode: 'open' }); - shadow.innerHTML = 'My button'; - fixture.appendChild(button); - axe.testUtils.flatTreeSetup(fixture); - var node = shadow.querySelector('#target'); - assert.isTrue( - axe.testUtils.getCheckEvaluate('duplicate-img-label')( - node, - undefined, - axe.utils.getNodeFromTree(node) - ) - ); - } - ); + it('should return true if the img is part of a shadow tree', () => { + const button = document.createElement('div'); + button.setAttribute('role', 'button'); + button.innerHTML = 'My button'; + const shadow = button.attachShadow({ mode: 'open' }); + shadow.innerHTML = 'My button'; + fixture.appendChild(button); + axe.testUtils.flatTreeSetup(fixture); + const node = shadow.querySelector('#target'); + assert.isTrue( + axe.testUtils.getCheckEvaluate('duplicate-img-label')( + node, + undefined, + axe.utils.getNodeFromTree(node) + ) + ); + }); - (shadowSupport.v1 ? it : xit)( - 'should return true if the img is a slotted element', - function () { - var button = document.createElement('div'); - button.setAttribute('role', 'button'); - button.innerHTML = 'My button'; - var shadow = button.attachShadow({ mode: 'open' }); - shadow.innerHTML = 'My button '; + it('should return true if the img is a slotted element', () => { + const button = document.createElement('div'); + button.setAttribute('role', 'button'); + button.innerHTML = 'My button'; + const shadow = button.attachShadow({ mode: 'open' }); + shadow.innerHTML = 'My button '; - fixture.appendChild(button); - axe.testUtils.flatTreeSetup(fixture); - var node = button.querySelector('#target'); - assert.isTrue( - axe.testUtils.getCheckEvaluate('duplicate-img-label')( - node, - undefined, - axe.utils.getNodeFromTree(node) - ) - ); - } - ); + fixture.appendChild(button); + axe.testUtils.flatTreeSetup(fixture); + const node = button.querySelector('#target'); + assert.isTrue( + axe.testUtils.getCheckEvaluate('duplicate-img-label')( + node, + undefined, + axe.utils.getNodeFromTree(node) + ) + ); + }); - (shadowSupport.v1 ? it : xit)( - 'should return false if the shadow img has a different text', - function () { - var button = document.createElement('div'); - button.setAttribute('role', 'button'); - button.innerHTML = 'My button'; - var shadow = button.attachShadow({ mode: 'open' }); - shadow.innerHTML = 'My image'; - var checkArgs = checkSetup(button); + it('should return false if the shadow img has a different text', () => { + const button = document.createElement('div'); + button.setAttribute('role', 'button'); + button.innerHTML = 'My button'; + const shadow = button.attachShadow({ mode: 'open' }); + shadow.innerHTML = 'My image'; + const checkArgs = checkSetup(button); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('duplicate-img-label') - .apply(null, checkArgs) - ); - } - ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('duplicate-img-label') + .apply(null, checkArgs) + ); + }); }); diff --git a/test/checks/label/explicit.js b/test/checks/label/explicit.js index eafb00119..3bb6c0703 100644 --- a/test/checks/label/explicit.js +++ b/test/checks/label/explicit.js @@ -1,4 +1,5 @@ describe('explicit-label', () => { + const html = axe.testUtils.html; const fixtureSetup = axe.testUtils.fixtureSetup; const checkSetup = axe.testUtils.checkSetup; const checkEvaluate = axe.testUtils.getCheckEvaluate('explicit-label'); @@ -23,11 +24,11 @@ describe('explicit-label', () => { }); it('returns false if an empty label is present that uses aria-labelledby', () => { - const params = checkSetup( - '' + - '' + - 'aria label' - ); + const params = checkSetup(html` + + + aria label + `); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); @@ -65,22 +66,22 @@ describe('explicit-label', () => { }); it('includes the `explicitLabel` text of the first non-empty label', () => { - const params = checkSetup( - '' + - '' + - '' + - '' - ); + const params = checkSetup(html` + + + + + `); checkEvaluate.apply(checkContext, params); assert.deepEqual(checkContext._data, { explicitLabel: 'text' }); }); it('is empty { explicitLabel: "" } if the label is empty', () => { - const params = checkSetup( - '' + - '' + - '' - ); + const params = checkSetup(html` + + + + `); checkEvaluate.apply(checkContext, params); assert.deepEqual(checkContext._data, { explicitLabel: '' }); }); @@ -94,13 +95,13 @@ describe('explicit-label', () => { }); it('includes each associated label', () => { - const params = checkSetup( - '' + - '' + - '' - ); + const params = checkSetup(html` + + + + `); checkEvaluate.apply(checkContext, params); - const ids = checkContext._relatedNodes.map(node => '#' + node.id); + const ids = checkContext._relatedNodes.map(node => `#${node.id}`); assert.deepEqual(ids, ['#lbl1', '#lbl2']); }); }); diff --git a/test/checks/label/help-same-as-label.js b/test/checks/label/help-same-as-label.js index 8361c91bc..e3ba0f8fe 100644 --- a/test/checks/label/help-same-as-label.js +++ b/test/checks/label/help-same-as-label.js @@ -1,15 +1,13 @@ -describe('help-same-as-label', function () { - 'use strict'; +describe('help-same-as-label', () => { + const fixture = document.getElementById('fixture'); - var fixture = document.getElementById('fixture'); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; }); - it('should return true if an element has a label and a title with the same text', function () { - var node = document.createElement('input'); + it('should return true if an element has a label and a title with the same text', () => { + const node = document.createElement('input'); node.type = 'text'; node.title = 'Duplicate'; node.setAttribute('aria-label', 'Duplicate'); @@ -25,12 +23,12 @@ describe('help-same-as-label', function () { ); }); - it('should return true if an element has a label and aria-describedby with the same text', function () { - var node = document.createElement('input'); + it('should return true if an element has a label and aria-describedby with the same text', () => { + const node = document.createElement('input'); node.type = 'text'; node.setAttribute('aria-label', 'Duplicate'); node.setAttribute('aria-describedby', 'dby'); - var dby = document.createElement('div'); + const dby = document.createElement('div'); dby.id = 'dby'; dby.innerHTML = 'Duplicate'; @@ -47,8 +45,8 @@ describe('help-same-as-label', function () { ); }); - it('should return false if input only has a title', function () { - var node = document.createElement('input'); + it('should return false if input only has a title', () => { + const node = document.createElement('input'); node.type = 'text'; node.title = 'Duplicate'; @@ -64,11 +62,11 @@ describe('help-same-as-label', function () { ); }); - it('should return true if an input only has aria-describedby', function () { - var node = document.createElement('input'); + it('should return true if an input only has aria-describedby', () => { + const node = document.createElement('input'); node.type = 'text'; node.setAttribute('aria-describedby', 'dby'); - var dby = document.createElement('div'); + const dby = document.createElement('div'); dby.id = 'dby'; dby.innerHTML = 'Duplicate'; diff --git a/test/checks/label/hidden-explicit-label.js b/test/checks/label/hidden-explicit-label.js index dfb87f059..92cf81a15 100644 --- a/test/checks/label/hidden-explicit-label.js +++ b/test/checks/label/hidden-explicit-label.js @@ -1,18 +1,15 @@ -describe('hidden-explicit-label', function () { - 'use strict'; +describe('hidden-explicit-label', () => { + const shadowCheckSetup = axe.testUtils.shadowCheckSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const check = checks['hidden-explicit-label']; - var shadowSupport = axe.testUtils.shadowSupport; - var shadowCheckSetup = axe.testUtils.shadowCheckSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var check = checks['hidden-explicit-label']; - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true if a hidden non-empty label is present', function () { - var args = checkSetup( + it('should return true if a hidden non-empty label is present', () => { + const args = checkSetup( '', {}, '#target' @@ -20,60 +17,54 @@ describe('hidden-explicit-label', function () { assert.isTrue(check.evaluate.apply(check, args)); }); - it('should return false if a visible non-empty label is present', function () { - var args = checkSetup( + it('should return false if a visible non-empty label is present', () => { + const args = checkSetup( '' ); assert.isFalse(check.evaluate.apply(check, args)); }); - it('should return true if an invisible empty label is present', function () { - var args = checkSetup( + it('should return true if an invisible empty label is present', () => { + const args = checkSetup( '' ); assert.isTrue(check.evaluate.apply(check, args)); }); - (shadowSupport.v1 ? it : xit)( - 'should return true if content is inside of shadow DOM', - function () { - var params = shadowCheckSetup( - '
', - '' - ); + it('should return true if content is inside of shadow DOM', () => { + const params = shadowCheckSetup( + '
', + '' + ); - assert.isTrue(check.evaluate.apply(shadowCheckSetup, params)); - } - ); + assert.isTrue(check.evaluate.apply(shadowCheckSetup, params)); + }); - (shadowSupport.v1 ? it : xit)( - 'should return false if part of the pairing is inside of shadow DOM', - function () { - var params = shadowCheckSetup( - '
', - '' - ); + it('should return false if part of the pairing is inside of shadow DOM', () => { + const params = shadowCheckSetup( + '
', + '' + ); - assert.isFalse(check.evaluate.apply(shadowCheckSetup, params)); - } - ); + assert.isFalse(check.evaluate.apply(shadowCheckSetup, params)); + }); - it('should fail when the label has aria-hidden=true', function () { - var html = ''; + it('should fail when the label has aria-hidden=true', () => { + let html = ''; html += '
'; html += ' '; html += ' '; html += '
'; - var args = checkSetup(html, {}, '#target'); + const args = checkSetup(html, {}, '#target'); assert.isTrue(check.evaluate.apply(check, args)); }); - describe('if the label is hidden', function () { - describe('and the element has an accessible name', function () { - it('should not fail', function () { - var html = ''; + describe('if the label is hidden', () => { + describe('and the element has an accessible name', () => { + it('should not fail', () => { + let html = ''; html += '
'; html += '
'; - var args = checkSetup(html, {}, '#target'); + const args = checkSetup(html, {}, '#target'); assert.isFalse(check.evaluate.apply(check, args)); }); }); }); - describe('SerialVirtualNode', function () { - it('should return false if no id', function () { - var vNode = new axe.SerialVirtualNode({ + describe('SerialVirtualNode', () => { + it('should return false if no id', () => { + const vNode = new axe.SerialVirtualNode({ nodeName: 'input', attributes: { type: 'text' @@ -101,8 +92,8 @@ describe('hidden-explicit-label', function () { ); }); - it('should return undefined if it has id', function () { - var vNode = new axe.SerialVirtualNode({ + it('should return undefined if it has id', () => { + const vNode = new axe.SerialVirtualNode({ nodeName: 'input', attributes: { type: 'text', diff --git a/test/checks/label/implicit.js b/test/checks/label/implicit.js index 83eae364b..0df5f8b4d 100644 --- a/test/checks/label/implicit.js +++ b/test/checks/label/implicit.js @@ -78,7 +78,7 @@ describe('implicit-label', () => { '' ); checkEvaluate.apply(checkContext, params); - const ids = checkContext._relatedNodes.map(node => '#' + node.id); + const ids = checkContext._relatedNodes.map(node => `#${node.id}`); assert.deepEqual(ids, ['#lbl']); }); }); diff --git a/test/checks/label/label-content-name-mismatch-fork-pinning.js b/test/checks/label/label-content-name-mismatch-fork-pinning.js new file mode 100644 index 000000000..7347afe01 --- /dev/null +++ b/test/checks/label/label-content-name-mismatch-fork-pinning.js @@ -0,0 +1,59 @@ +describe('label-content-name-mismatch (BrowserStack fork pinning)', () => { + // AXE-3659 — pins the fork's reimplementation in + // lib/checks/label/label-content-name-mismatch-evaluate.js (all // a11y-rule-* tagged). + // The 4.12.1 merge DECLINES upstream's subtreeText->visibleVirtual switch (Tier-1 #1) + // because that switch would drop the fork's ignoreNativeTextAlternative flag and its + // NLP. These tests fail if upstream's simpler literal-includes logic were adopted: + // 1. wink-porter2 stemming (inflected visible text still matches the name) + // 2. whitespace/case-insensitive matching (curateString strips whitespace) + // 3. reviewPayload.visualHelperData.accessibleName on a genuine mismatch + // The subtreeText({ ignoreNativeTextAlternative: true }) call is the fork evaluate + // path these cases exercise end-to-end. + const { checkSetup, MockCheckContext, getCheckEvaluate } = axe.testUtils; + const fixture = document.getElementById('fixture'); + const checkContext = MockCheckContext(); + const evaluate = getCheckEvaluate('label-content-name-mismatch', { + verifyMessage: false + }); + + afterEach(() => { + fixture.innerHTML = ''; + checkContext.reset(); + axe._tree = undefined; + }); + + it('passes when visible text is an inflected (stemmed) form of the accessible name', () => { + // "saving change" stems to "save chang", matching "save changes"; upstream's + // literal includes-check would report a mismatch here. + const params = checkSetup( + '' + ); + assert.isTrue(evaluate.apply(checkContext, params)); + }); + + it('matches ignoring whitespace and case (fork curateString strips whitespace)', () => { + // "Log In" (name) vs "login" (content) match only because the fork strips + // whitespace before comparing; upstream's whitespace-sensitive compare would not. + const params = checkSetup( + '' + ); + assert.isTrue(evaluate.apply(checkContext, params)); + }); + + it('reports a genuine mismatch and emits reviewPayload.visualHelperData.accessibleName', () => { + const params = checkSetup( + '' + ); + const result = evaluate.apply(checkContext, params); + assert.isFalse(result, 'unrelated visible text vs name is a mismatch'); + const vhd = + checkContext._data && + checkContext._data.reviewPayload && + checkContext._data.reviewPayload.visualHelperData; + assert.isObject( + vhd, + 'reviewPayload.visualHelperData must be present on mismatch' + ); + assert.equal(vhd.accessibleName, 'cancel'); + }); +}); diff --git a/test/checks/label/label-content-name-mismatch.js b/test/checks/label/label-content-name-mismatch.js index 76b25bdb5..8cc8f8224 100644 --- a/test/checks/label/label-content-name-mismatch.js +++ b/test/checks/label/label-content-name-mismatch.js @@ -1,189 +1,227 @@ -describe('label-content-name-mismatch tests', function () { - 'use strict'; +describe('label-content-name-mismatch tests', () => { + const html = axe.testUtils.html; - var queryFixture = axe.testUtils.queryFixture; - var check = checks['label-content-name-mismatch']; - var options = undefined; + const queryFixture = axe.testUtils.queryFixture; + const check = checks['label-content-name-mismatch']; + const options = undefined; - var fontApiSupport = !!document.fonts; + const fontApiSupport = !!document.fonts; - before(function (done) { + before(done => { if (!fontApiSupport) { done(); } - var materialFont = new FontFace( + const materialFont = new FontFace( 'Material Icons', 'url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2)' ); - materialFont.load().then(function () { + materialFont.load().then(() => { document.fonts.add(materialFont); done(); }); }); - it('returns true when visible text and accessible name (`aria-label`) matches (text sanitized)', function () { - var vNode = queryFixture( + it('returns true when visible text and accessible name (`aria-label`) matches (text sanitized)', () => { + const vNode = queryFixture( '
next page
' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns true when visible text and accessible name (`aria-label`) matches (character insensitive)', function () { - var vNode = queryFixture( + it('returns true when visible text and accessible name (`aria-label`) matches (character insensitive)', () => { + const vNode = queryFixture( '
next pAge
' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns true when visible text and accessible name (`aria-labelledby`) matches (character insensitive & text sanitized)', function () { - var vNode = queryFixture( - '
UNTIL THE VeRy EnD
' + - '
uNtIl the very end  
' - ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + it('returns true when visible text and accessible name (`aria-labelledby`) matches (character insensitive & text sanitized)', () => { + const vNode = queryFixture(html` +
UNTIL THE VeRy EnD
+
uNtIl the very end  
+ `); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns true when visible text is contained in the accessible name', function () { - var vNode = queryFixture( + it('returns true when visible text is contained in the accessible name', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns false when visible text doesn’t match accessible name', function () { - var vNode = queryFixture( + it('returns false when visible text doesn’t match accessible name', () => { + const vNode = queryFixture( '
Next
' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isFalse(actual); }); - it('returns false when not all of visible text is included in accessible name', function () { - var vNode = queryFixture( + it('returns false when not all of visible text is included in accessible name', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isFalse(actual); }); - it('returns false when element has non-matching accessible name (`aria-labelledby`) and text content', function () { - var vNode = queryFixture( - '
some content
' + - '
123
' - ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + it('returns false when element has non-matching accessible name (`aria-labelledby`) and text content', () => { + const vNode = queryFixture(html` +
some content
+
123
+ `); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isFalse(actual); }); - it('returns true when visible text excluding emoji is part of accessible name', function () { - var vNode = queryFixture( + it('returns true when visible text excluding emoji is part of accessible name', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns true when visible text excluding punctuations/ symbols is part of accessible name', function () { - var vNode = queryFixture( + it('returns true when visible text excluding punctuations/ symbols is part of accessible name', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); (fontApiSupport ? it : it.skip)( 'returns true when visible text excluding ligature icon is part of accessible name', - function () { - var vNode = queryFixture( + () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); } ); - it('returns true when visible text excluding private use unicode is part of accessible name', function () { - var vNode = queryFixture( + it('returns true when visible text excluding private use unicode is part of accessible name', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns undefined (needs review) when visible text name is only an emoji', function () { - var vNode = queryFixture( + it('returns undefined (needs review) when visible text name is only an emoji', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) when accessible name is an emoji', function () { - var vNode = queryFixture( + it('returns undefined (needs review) when accessible name is an emoji', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) for visible text is single characters (punctuation) used as icon', function () { - var vNode = queryFixture( + it('returns undefined (needs review) for visible text is single characters (punctuation) used as icon', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) for unicode as accessible name and text content', function () { - var vNode = queryFixture( + it('returns undefined (needs review) for unicode as accessible name and text content', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) for unicode text content', function () { - var vNode = queryFixture( + it('returns undefined (needs review) for unicode text content', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns undefined (needs review) when punctuation is used as text content', function () { - var vNode = queryFixture( + it('returns undefined (needs review) when punctuation is used as text content', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isUndefined(actual); }); - it('returns true when normal text content which is punctuated', function () { - var vNode = queryFixture( + it('returns true when normal text content which is punctuated', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); - it('returns false when normal puntuated text content is not contained in accessible name is punctuated', function () { - var vNode = queryFixture( + it('returns false when normal puntuated text content is not contained in accessible name is punctuated', () => { + const vNode = queryFixture( '' ); - var actual = check.evaluate(vNode.actualNode, options, vNode); + const actual = check.evaluate(vNode.actualNode, options, vNode); assert.isFalse(actual); }); - it('returns true when text contains
', function () { - var vNode = queryFixture( + it('returns true when text contains
', () => { + const vNode = queryFixture( '' ); + const actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns true when aria-label and visible text match even though there is an image with alt text', function () { + var vNode = queryFixture( + '' + ); var actual = check.evaluate(vNode.actualNode, options, vNode); assert.isTrue(actual); }); + + it('returns false when aria-label and visible text do not match even though there is an image with alt text', function () { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + }); + + (fontApiSupport ? it : it.skip)( + 'returns true when aria-label and visible text match even though there is a ligature icon', + function () { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + } + ); + + (fontApiSupport ? it : it.skip)( + 'returns false when aria-label and visible text do not match even though there is a ligature icon', + function () { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + } + ); }); diff --git a/test/checks/label/multiple-label.js b/test/checks/label/multiple-label.js index 3e40cb97d..8f7f6169d 100644 --- a/test/checks/label/multiple-label.js +++ b/test/checks/label/multiple-label.js @@ -1,22 +1,21 @@ -describe('multiple-label', function () { - 'use strict'; +describe('multiple-label', () => { + const html = axe.testUtils.html; - var fixture = document.getElementById('fixture'); - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var fixtureSetup = axe.testUtils.fixtureSetup; + const fixture = document.getElementById('fixture'); + const checkContext = axe.testUtils.MockCheckContext(); + const fixtureSetup = axe.testUtils.fixtureSetup; - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return undefined if there are multiple implicit labels', function () { + it('should return undefined if there are multiple implicit labels', () => { fixtureSetup( '' ); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -25,10 +24,10 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return false if there is only one implicit label', function () { + it('should return false if there is only one implicit label', () => { fixtureSetup(''); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -37,17 +36,17 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return undefined if there are multiple explicit labels', function () { - fixtureSetup( - '' + - '' + - '' + - '' - ); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); - var l3 = fixture.querySelector('#l3'); + it('should return undefined if there are multiple explicit labels', () => { + fixtureSetup(html` + + + + + `); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); + const l3 = fixture.querySelector('#l3'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -56,12 +55,12 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2, l3]); }); - it('should return false if there is only one explicit label', function () { + it('should return false if there is only one explicit label', () => { fixtureSetup( '' ); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -70,14 +69,14 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return false if there are multiple explicit labels but one is hidden', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#test-input2'); - var l1 = fixture.querySelector('#l1'); + it('should return false if there are multiple explicit labels but one is hidden', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#test-input2'); + const l1 = fixture.querySelector('#l1'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -86,13 +85,13 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return undefined if there are multiple implicit labels and one is visually hidden', function () { + it('should return undefined if there are multiple implicit labels and one is visually hidden', () => { fixtureSetup( '' ); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -101,16 +100,16 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return undefined if there are multiple explicit labels but some are hidden', function () { - fixtureSetup( - '' + - '' + - '' + - '' - ); - var target = fixture.querySelector('#me'); - var l1 = fixture.querySelector('#l1'); - var l3 = fixture.querySelector('#l3'); + it('should return undefined if there are multiple explicit labels but some are hidden', () => { + fixtureSetup(html` + + + + + `); + const target = fixture.querySelector('#me'); + const l1 = fixture.querySelector('#l1'); + const l3 = fixture.querySelector('#l3'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -119,15 +118,15 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l3]); }); - it('should return undefined if there are multiple explicit labels and one is visually hidden', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#me'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); + it('should return undefined if there are multiple explicit labels and one is visually hidden', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#me'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -136,15 +135,15 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return undefined if there are multiple explicit labels and one is screen reader hidden', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#me'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); + it('should return undefined if there are multiple explicit labels and one is screen reader hidden', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#me'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -153,13 +152,13 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return undefined if there are implicit and explicit labels', function () { + it('should return undefined if there are implicit and explicit labels', () => { fixtureSetup( '' ); - var target = fixture.querySelector('#target'); - var l1 = fixture.querySelector('#l1'); - var l2 = fixture.querySelector('#l2'); + const target = fixture.querySelector('#target'); + const l1 = fixture.querySelector('#l1'); + const l2 = fixture.querySelector('#l2'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -168,11 +167,11 @@ describe('multiple-label', function () { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return false if there an implicit label uses for attribute', function () { + it('should return false if there an implicit label uses for attribute', () => { fixtureSetup( '' ); - var target = fixture.querySelector('#target'); + const target = fixture.querySelector('#target'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -180,17 +179,17 @@ describe('multiple-label', function () { ); }); - it('should return undefined given multiple labels and no aria-labelledby', function () { - fixtureSetup( - '' + - '' + - '' + - '' + - '' + - '' + - '' - ); - var target = fixture.querySelector('#A'); + it('should return undefined given multiple labels and no aria-labelledby', () => { + fixtureSetup(html` + + + + + + + + `); + const target = fixture.querySelector('#A'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -198,17 +197,17 @@ describe('multiple-label', function () { ); }); - it('should return undefined given multiple labels, one label AT visible, and no aria-labelledby', function () { - fixtureSetup( - '' + - '' + - '' + - '' + - '' + - '' + - '' - ); - var target = fixture.querySelector('#B'); + it('should return undefined given multiple labels, one label AT visible, and no aria-labelledby', () => { + fixtureSetup(html` + + + + + + + + `); + const target = fixture.querySelector('#B'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -216,13 +215,13 @@ describe('multiple-label', function () { ); }); - it('should return false given multiple labels, one label AT visible, and aria-labelledby for AT visible', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#D'); + it('should return false given multiple labels, one label AT visible, and aria-labelledby for AT visible', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#D'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -230,13 +229,13 @@ describe('multiple-label', function () { ); }); - it('should return false given multiple labels, one label AT visible, and aria-labelledby for all', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#F'); + it('should return false given multiple labels, one label AT visible, and aria-labelledby for all', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#F'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -244,13 +243,13 @@ describe('multiple-label', function () { ); }); - it('should return false given multiple labels, one label visible, and no aria-labelledby', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#I'); + it('should return false given multiple labels, one label visible, and no aria-labelledby', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#I'); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -258,17 +257,17 @@ describe('multiple-label', function () { ); }); - it('should return undefined given multiple labels, all visible, aria-labelledby for all', function () { - fixtureSetup( - '' + - '' + - '' + - '' + - '' + - '' + - '' - ); - var target = fixture.querySelector('#J'); + it('should return undefined given multiple labels, all visible, aria-labelledby for all', () => { + fixtureSetup(html` + + + + + + + + `); + const target = fixture.querySelector('#J'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -276,13 +275,13 @@ describe('multiple-label', function () { ); }); - it('should return undefined given multiple labels, one AT visible, no aria-labelledby', function () { - fixtureSetup( - '' + - '' + - '' - ); - var target = fixture.querySelector('#Q'); + it('should return undefined given multiple labels, one AT visible, no aria-labelledby', () => { + fixtureSetup(html` + + + + `); + const target = fixture.querySelector('#Q'); assert.isUndefined( axe.testUtils .getCheckEvaluate('multiple-label') @@ -290,61 +289,52 @@ describe('multiple-label', function () { ); }); - (shadowSupported ? it : xit)( - 'should consider labels in the same document/shadow tree', - function () { - fixture.innerHTML = '
'; - var target = document.querySelector('#target'); - var shadowRoot = target.attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = - ''; - var shadowTarget = target.shadowRoot; - fixtureSetup(); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('multiple-label') - .call(checkContext, shadowTarget.firstElementChild) - ); - } - ); + it('should consider labels in the same document/shadow tree', () => { + fixture.innerHTML = '
'; + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = + ''; + const shadowTarget = target.shadowRoot; + fixtureSetup(); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('multiple-label') + .call(checkContext, shadowTarget.firstElementChild) + ); + }); - (shadowSupported ? it : xit)( - 'should return false for valid multiple labels in the same document/shadow tree', - function () { - fixture.innerHTML = '
'; - var target = document.querySelector('#target'); - var shadowRoot = target.attachShadow({ mode: 'open' }); - var innerHTML = ''; - innerHTML += ''; - innerHTML += ''; - shadowRoot.innerHTML = innerHTML; - fixtureSetup(); - var shadowTarget = target.shadowRoot; - assert.isFalse( - axe.testUtils - .getCheckEvaluate('multiple-label') - .call(checkContext, shadowTarget.firstElementChild) - ); - } - ); + it('should return false for valid multiple labels in the same document/shadow tree', () => { + fixture.innerHTML = '
'; + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); + let innerHTML = ''; + innerHTML += ''; + innerHTML += ''; + shadowRoot.innerHTML = innerHTML; + fixtureSetup(); + const shadowTarget = target.shadowRoot; + assert.isFalse( + axe.testUtils + .getCheckEvaluate('multiple-label') + .call(checkContext, shadowTarget.firstElementChild) + ); + }); - (shadowSupported ? it : xit)( - 'should return undefined for invalid multiple labels in the same document/shadow tree', - function () { - fixture.innerHTML = '
'; - var target = document.querySelector('#target'); - var shadowRoot = target.attachShadow({ mode: 'open' }); - var innerHTML = ''; - innerHTML += ''; - innerHTML += ''; - shadowRoot.innerHTML = innerHTML; - fixtureSetup(); - var shadowTarget = target.shadowRoot; - assert.isUndefined( - axe.testUtils - .getCheckEvaluate('multiple-label') - .call(checkContext, shadowTarget.firstElementChild) - ); - } - ); + it('should return undefined for invalid multiple labels in the same document/shadow tree', () => { + fixture.innerHTML = '
'; + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); + let innerHTML = ''; + innerHTML += ''; + innerHTML += ''; + shadowRoot.innerHTML = innerHTML; + fixtureSetup(); + const shadowTarget = target.shadowRoot; + assert.isUndefined( + axe.testUtils + .getCheckEvaluate('multiple-label') + .call(checkContext, shadowTarget.firstElementChild) + ); + }); }); diff --git a/test/checks/label/title-only.js b/test/checks/label/title-only.js index e1439650c..46a607b5f 100644 --- a/test/checks/label/title-only.js +++ b/test/checks/label/title-only.js @@ -1,15 +1,13 @@ -describe('title-only', function () { - 'use strict'; +describe('title-only', () => { + const fixture = document.getElementById('fixture'); - var fixture = document.getElementById('fixture'); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; }); - it('should return true if an element only has a title', function () { - var node = document.createElement('input'); + it('should return true if an element only has a title', () => { + const node = document.createElement('input'); node.type = 'text'; node.title = 'Duplicate'; @@ -34,11 +32,11 @@ describe('title-only', function () { ); }); - it('should return true if an element only has aria-describedby', function () { - var node = document.createElement('input'); + it('should return true if an element only has aria-describedby', () => { + const node = document.createElement('input'); node.type = 'text'; node.setAttribute('aria-describedby', 'dby'); - var dby = document.createElement('div'); + const dby = document.createElement('div'); dby.id = 'dby'; dby.innerHTML = 'woop'; diff --git a/test/checks/landmarks/landmark-is-unique-after.js b/test/checks/landmarks/landmark-is-unique-after.js index 81eb404d3..ae9716808 100644 --- a/test/checks/landmarks/landmark-is-unique-after.js +++ b/test/checks/landmarks/landmark-is-unique-after.js @@ -1,7 +1,5 @@ -describe('landmark-is-unique-after', function () { - 'use strict'; - - var checkContext = axe.testUtils.MockCheckContext(); +describe('landmark-is-unique-after', () => { + const checkContext = axe.testUtils.MockCheckContext(); function createResult(result, data) { return { result: result, @@ -21,13 +19,13 @@ describe('landmark-is-unique-after', function () { }); } - afterEach(function () { + afterEach(() => { axe._tree = undefined; checkContext.reset(); }); - it('should update duplicate landmarks with failed result', function () { - var result = checks['landmark-is-unique'].after([ + it('should update duplicate landmarks with failed result', () => { + const result = checks['landmark-is-unique'].after([ createResultWithSameRelatedNodes(true, { role: 'some role', accessibleText: 'some accessibleText' @@ -46,7 +44,7 @@ describe('landmark-is-unique-after', function () { }) ]); - var expectedResult = [ + const expectedResult = [ createResultWithProvidedRelatedNodes( false, { diff --git a/test/checks/landmarks/landmark-is-unique.js b/test/checks/landmarks/landmark-is-unique.js index 9351b3d53..6e10c9c55 100644 --- a/test/checks/landmarks/landmark-is-unique.js +++ b/test/checks/landmarks/landmark-is-unique.js @@ -1,29 +1,27 @@ -describe('landmark-is-unique', function () { - 'use strict'; +describe('landmark-is-unique', () => { + const checkContext = new axe.testUtils.MockCheckContext(); + let fixture; + let axeFixtureSetup; - var checkContext = new axe.testUtils.MockCheckContext(); - var fixture; - var axeFixtureSetup; - - beforeEach(function () { + beforeEach(() => { fixture = document.getElementById('fixture'); axeFixtureSetup = axe.testUtils.fixtureSetup; }); - afterEach(function () { + afterEach(() => { axe._tree = undefined; checkContext.reset(); }); - it('should return true, with correct role and no accessible text', function () { + it('should return true, with correct role and no accessible text', () => { axeFixtureSetup('
test
'); - var node = fixture.querySelector('div'); - var expectedData = { + const node = fixture.querySelector('div'); + const expectedData = { accessibleText: null, role: 'main' }; axe._tree = axe.utils.getFlattenedTree(fixture); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isTrue( axe.testUtils .getCheckEvaluate('landmark-is-unique') @@ -33,15 +31,15 @@ describe('landmark-is-unique', function () { assert.deepEqual(checkContext._relatedNodes, [node]); }); - it('should return true, with correct role and the accessible text lowercased', function () { + it('should return true, with correct role and the accessible text lowercased', () => { axeFixtureSetup('
test
'); - var node = fixture.querySelector('div'); - var expectedData = { + const node = fixture.querySelector('div'); + const expectedData = { accessibleText: 'test text', role: 'main' }; axe._tree = axe.utils.getFlattenedTree(fixture); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + const virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); assert.isTrue( axe.testUtils .getCheckEvaluate('landmark-is-unique') diff --git a/test/checks/language/has-lang.js b/test/checks/language/has-lang.js index 7d8c18c62..5e4df0b0b 100644 --- a/test/checks/language/has-lang.js +++ b/test/checks/language/has-lang.js @@ -1,53 +1,51 @@ -describe('has-lang', function () { - 'use strict'; +describe('has-lang', () => { + const fixture = document.getElementById('fixture'); + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const hasLangEvaluate = axe.testUtils.getCheckEvaluate('has-lang'); - var fixture = document.getElementById('fixture'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var hasLangEvaluate = axe.testUtils.getCheckEvaluate('has-lang'); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; checkContext.reset(); }); - it('should return true if a lang attribute is present', function () { - var params = checkSetup('
'); + it('should return true if a lang attribute is present', () => { + const params = checkSetup('
'); assert.isTrue(hasLangEvaluate.apply(checkContext, params)); }); - it('should return false if only `xml:lang` attribute is present', function () { - var params = checkSetup('
'); + it('should return false if only `xml:lang` attribute is present', () => { + const params = checkSetup('
'); assert.isFalse(hasLangEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.messageKey, 'noXHTML'); }); - it('should return true if both `lang` and `xml:lang` attribute is present', function () { - var params = checkSetup( + it('should return true if both `lang` and `xml:lang` attribute is present', () => { + const params = checkSetup( '
' ); assert.isTrue(hasLangEvaluate.apply(checkContext, params)); }); - it('should return false if xml:lang and lang attributes are not present', function () { - var params = checkSetup('
'); + it('should return false if xml:lang and lang attributes are not present', () => { + const params = checkSetup('
'); assert.isFalse(hasLangEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.messageKey, 'noLang'); }); - it('should return false if lang is left empty', function () { - var params = checkSetup('
'); + it('should return false if lang is left empty', () => { + const params = checkSetup('
'); assert.isFalse(hasLangEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.messageKey, 'noLang'); }); - it('should support options.attributes', function () { - var params = checkSetup('
', { + it('should support options.attributes', () => { + const params = checkSetup('
', { attributes: ['foo'] }); diff --git a/test/checks/language/valid-lang.js b/test/checks/language/valid-lang.js index a42baa206..5a2a915d1 100644 --- a/test/checks/language/valid-lang.js +++ b/test/checks/language/valid-lang.js @@ -1,81 +1,82 @@ -describe('valid-lang', function () { - 'use strict'; +describe('valid-lang', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; + const checkContext = axe.testUtils.MockCheckContext(); + const validLangEvaluate = axe.testUtils.getCheckEvaluate('valid-lang'); - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var checkContext = axe.testUtils.MockCheckContext(); - var validLangEvaluate = axe.testUtils.getCheckEvaluate('valid-lang'); - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; checkContext.reset(); }); - it('should return false if a lang attribute is present in options', function () { - var params = checkSetup('
text
', { + it('should return false if a lang attribute is present in options', () => { + const params = checkSetup('
text
', { value: ['blah', 'blah', 'woohoo'] }); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); - it('should lowercase options and attribute first', function () { - var params = checkSetup('
text
', { + it('should lowercase options and attribute first', () => { + const params = checkSetup('
text
', { value: ['blah', 'blah', 'wOohoo'] }); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); - it('should return true if a lang attribute is not present in options', function () { - var params = checkSetup('
text
'); + it('should return true if a lang attribute is not present in options', () => { + const params = checkSetup('
text
'); assert.isTrue(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, ['lang="FOO"']); }); - it('should return false (and not throw) when given no present in options', function () { - var params = checkSetup('
text
'); + it('should return false (and not throw) when given no present in options', () => { + const params = checkSetup('
text
'); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); - it('should return true if the language is badly formatted', function () { - var params = checkSetup('
text
'); + it('should return true if the language is badly formatted', () => { + const params = checkSetup('
text
'); assert.isTrue(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, ['lang="en_US"']); }); - it('should return false if it matches a substring proceeded by -', function () { - var params = checkSetup('
text
'); + it('should return false if it matches a substring proceeded by -', () => { + const params = checkSetup('
text
'); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); - it('should work with xml:lang', function () { - var params = checkSetup('
text
'); + it('should work with xml:lang', () => { + const params = checkSetup('
text
'); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); - it('should accept options.attributes', function () { - var params = checkSetup('
text
', { - attributes: ['custom-lang'] - }); + it('should accept options.attributes', () => { + const params = checkSetup( + '
text
', + { + attributes: ['custom-lang'] + } + ); assert.isTrue(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, ['custom-lang="en_US"']); }); - it('should return true if lang value is just whitespace', function () { - var params = checkSetup('
text
'); + it('should return true if lang value is just whitespace', () => { + const params = checkSetup('
text
'); assert.isTrue(validLangEvaluate.apply(checkContext, params)); }); - it('should return false if a lang attribute element has no content', function () { - var params = checkSetup('
'); + it('should return false if a lang attribute element has no content', () => { + const params = checkSetup('
'); assert.isFalse(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, null); diff --git a/test/checks/language/xml-lang-mismatch.js b/test/checks/language/xml-lang-mismatch.js index 7c47213cf..0256aab94 100644 --- a/test/checks/language/xml-lang-mismatch.js +++ b/test/checks/language/xml-lang-mismatch.js @@ -1,14 +1,12 @@ -describe('xml-lang-mismatch', function () { - 'use strict'; +describe('xml-lang-mismatch', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const queryFixture = axe.testUtils.queryFixture; - var checkContext = axe.testUtils.MockCheckContext(); - var queryFixture = axe.testUtils.queryFixture; - - beforeEach(function () { + beforeEach(() => { // using a div element (instead of html), as the check is agnostic of element type }); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); @@ -16,8 +14,8 @@ describe('xml-lang-mismatch', function () { // hence below tests are only for HTML element, although the logic in the check looks for matches in value os lang and xml:lang // rather than node type match - hence the check can be re-used. - it('should return false if a only lang is supplied', function () { - var vNode = queryFixture('
'); + it('should return false if a only lang is supplied', () => { + const vNode = queryFixture('
'); assert.isFalse( axe.testUtils .getCheckEvaluate('xml-lang-mismatch') @@ -25,8 +23,8 @@ describe('xml-lang-mismatch', function () { ); }); - it('should return false if a only xml:lang is supplied albeit with region', function () { - var vNode = queryFixture('
'); + it('should return false if a only xml:lang is supplied albeit with region', () => { + const vNode = queryFixture('
'); assert.isFalse( axe.testUtils .getCheckEvaluate('xml-lang-mismatch') @@ -34,10 +32,10 @@ describe('xml-lang-mismatch', function () { ); }); - it('should return false if lang is undefined', function () { - var node = document.createElement('div'); + it('should return false if lang is undefined', () => { + const node = document.createElement('div'); node.setAttribute('lang', undefined); - var tree = axe.testUtils.flatTreeSetup(node); + const tree = axe.testUtils.flatTreeSetup(node); assert.isFalse( axe.testUtils .getCheckEvaluate('xml-lang-mismatch') @@ -45,8 +43,8 @@ describe('xml-lang-mismatch', function () { ); }); - it('should return true if lang and xml:lang is identical', function () { - var vNode = queryFixture( + it('should return true if lang and xml:lang is identical', () => { + const vNode = queryFixture( '
' ); assert.isTrue( @@ -56,8 +54,8 @@ describe('xml-lang-mismatch', function () { ); }); - it('should return true if lang and xml:lang have identical primary sub tag', function () { - var vNode = queryFixture( + it('should return true if lang and xml:lang have identical primary sub tag', () => { + const vNode = queryFixture( '
' ); assert.isTrue( @@ -67,11 +65,11 @@ describe('xml-lang-mismatch', function () { ); }); - it('should return false if lang and xml:lang are not identical', function () { - var vNode = queryFixture( + it('should return false if lang and xml:lang are not identical', () => { + const vNode = queryFixture( '
' ); - var actual = axe.testUtils + const actual = axe.testUtils .getCheckEvaluate('xml-lang-mismatch') .call(checkContext, null, {}, vNode); assert.isFalse(actual); diff --git a/test/checks/lists/dlitem.js b/test/checks/lists/dlitem.js index f9853dfd6..162bd9774 100644 --- a/test/checks/lists/dlitem.js +++ b/test/checks/lists/dlitem.js @@ -1,162 +1,136 @@ -describe('dlitem', function () { - 'use strict'; +describe('dlitem', () => { + const html = axe.testUtils.html; - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var shadowSupport = axe.testUtils.shadowSupport; + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should pass if the dlitem has a parent
', function () { - var checkArgs = checkSetup('
My list item
'); + it('should pass if the dlitem has a parent
', () => { + const checkArgs = checkSetup('
My list item
'); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should fail if the dt element has an incorrect parent', function () { - var checkArgs = checkSetup( + it('should fail if the dt element has an incorrect parent', () => { + const checkArgs = checkSetup( '' ); assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should pass if the dt element has a parent
with role="list"', function () { - var checkArgs = checkSetup( + it('should pass if the dt element has a parent
with role="list"', () => { + const checkArgs = checkSetup( '
My list item
' ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should pass if the dt element has a parent
with role="presentation"', function () { - var checkArgs = checkSetup( + it('should pass if the dt element has a parent
with role="presentation"', () => { + const checkArgs = checkSetup( '
My list item
' ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should fail if the dt element has a parent
with a changed role', function () { - var checkArgs = checkSetup( + it('should fail if the dt element has a parent
with a changed role', () => { + const checkArgs = checkSetup( '
My list item<
/dl>' ); assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should pass if the dt element has a parent
with an abstract role', function () { - var checkArgs = checkSetup( + it('should pass if the dt element has a parent
with an abstract role', () => { + const checkArgs = checkSetup( '
My list item
' ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should pass if the dt element has a parent
with an invalid role', function () { - var checkArgs = checkSetup( + it('should pass if the dt element has a parent
with an invalid role', () => { + const checkArgs = checkSetup( '
My list item
' ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('should fail if the dt element has a parent
with a changed role', function () { - var checkArgs = checkSetup( + it('should fail if the dt element has a parent
with a changed role', () => { + const checkArgs = checkSetup( '
My list item
' ); assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); }); - it('returns true if the dd/dt is in a div with a dl as grandparent', function () { - var nodeNames = ['dd', 'dt']; - nodeNames.forEach(function (nodeName) { - var checkArgs = checkSetup( - '
<' + - nodeName + - ' id="target">My list item
' + it('returns true if the dd/dt is in a div with a dl as grandparent', () => { + const nodeNames = ['dd', 'dt']; + nodeNames.forEach(nodeName => { + const checkArgs = checkSetup( + html`
<${nodeName} id="target">My list item
` ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); }); - it('returns false if the dd/dt is in a div with a role with a dl as grandparent with a list role', function () { - var nodeNames = ['dd', 'dt']; - nodeNames.forEach(function (nodeName) { - var checkArgs = checkSetup( - '
<' + - nodeName + - ' id="target">My list item
' + it('returns false if the dd/dt is in a div with a role with a dl as grandparent with a list role', () => { + const nodeNames = ['dd', 'dt']; + nodeNames.forEach(nodeName => { + const checkArgs = checkSetup( + html`
<${nodeName} id="target">My list item
` ); assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); }); }); - it('returns false if the dd/dt is in a div[role=presentation] with a dl as grandparent', function () { - var nodeNames = ['dd', 'dt']; - nodeNames.forEach(function (nodeName) { - var checkArgs = checkSetup( - '
<' + - nodeName + - ' id="target">My list item
' + it('returns false if the dd/dt is in a div[role=presentation] with a dl as grandparent', () => { + const nodeNames = ['dd', 'dt']; + nodeNames.forEach(nodeName => { + const checkArgs = checkSetup( + html`
<${nodeName} id="target">My list item
` ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); }); - it('returns false if the dd/dt is in a div[role=none] with a dl as grandparent', function () { - var nodeNames = ['dd', 'dt']; - nodeNames.forEach(function (nodeName) { - var checkArgs = checkSetup( - '
<' + - nodeName + - ' id="target">My list item
' + it('returns false if the dd/dt is in a div[role=none] with a dl as grandparent', () => { + const nodeNames = ['dd', 'dt']; + nodeNames.forEach(nodeName => { + const checkArgs = checkSetup( + html`
<${nodeName} id="target">My list item
` ); assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); }); }); - (shadowSupport.v1 ? it : xit)( - 'should return true in a shadow DOM pass', - function () { - var node = document.createElement('div'); - node.innerHTML = '
My list item
'; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
'; + it('should return true in a shadow DOM pass', () => { + const node = document.createElement('div'); + node.innerHTML = '
My list item
'; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; - var checkArgs = checkSetup(node, 'dt'); - assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); - } - ); - - (shadowSupport.v1 ? it : xit)( - 'should return false in a shadow DOM fail', - function () { - var node = document.createElement('div'); - node.innerHTML = '
My list item
'; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
'; - - var checkArgs = checkSetup(node, 'dt'); - assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); - } - ); - - (shadowSupport.v1 ? it : xit)( - 'should return true when the item is grouped in dl > div in a shadow DOM', - function () { - var node = document.createElement('div'); - node.innerHTML = '
My list item
'; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
'; - - var checkArgs = checkSetup(node, 'dt'); - assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); - } - ); + const checkArgs = checkSetup(node, 'dt'); + assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); + }); + + it('should return false in a shadow DOM fail', () => { + const node = document.createElement('div'); + node.innerHTML = '
My list item
'; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + + const checkArgs = checkSetup(node, 'dt'); + assert.isFalse(checks.dlitem.evaluate.apply(null, checkArgs)); + }); + + it('should return true when the item is grouped in dl > div in a shadow DOM', () => { + const node = document.createElement('div'); + node.innerHTML = '
My list item
'; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + + const checkArgs = checkSetup(node, 'dt'); + assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); + }); }); diff --git a/test/checks/lists/listitem.js b/test/checks/lists/listitem.js index 24b6adeb5..b277709c0 100644 --- a/test/checks/lists/listitem.js +++ b/test/checks/lists/listitem.js @@ -1,118 +1,127 @@ -describe('listitem', function () { - 'use strict'; +describe('listitem', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const fixtureSetup = axe.testUtils.fixtureSetup; + const checkEvaluate = axe.testUtils.getCheckEvaluate('listitem'); - var shadowSupport = axe.testUtils.shadowSupport; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var fixtureSetup = axe.testUtils.fixtureSetup; - var checkEvaluate = axe.testUtils.getCheckEvaluate('listitem'); - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should pass if the listitem has a parent
    ', function () { - var params = checkSetup('
    1. My list item
    '); - var result = checkEvaluate.apply(checkContext, params); + it('should pass if the listitem has a parent
      ', () => { + const params = checkSetup('
      1. My list item
      '); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should pass if the listitem has a parent
        ', function () { - var params = checkSetup('
        • My list item
        '); - var result = checkEvaluate.apply(checkContext, params); + it('should pass if the listitem has a parent
          ', () => { + const params = checkSetup('
          • My list item
          '); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should pass if the listitem has a parent role=list', function () { - var params = checkSetup( + it('should pass if the listitem has a parent role=list', () => { + const params = checkSetup( '
        • My list item
        • ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should pass if the listitem has a parent role=none', function () { - var params = checkSetup( + it('should pass if the listitem has a parent role=none', () => { + const params = checkSetup( '
          • My list item
          ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should pass if the listitem has a parent role=presentation', function () { - var params = checkSetup( + it('should pass if the listitem has a parent role=presentation', () => { + const params = checkSetup( '
          • My list item
          ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should fail if the listitem has an incorrect parent', function () { - var params = checkSetup('
        • My list item
        • '); - var result = checkEvaluate.apply(checkContext, params); + it('should fail if the listitem has an incorrect parent', () => { + const params = checkSetup('
        • My list item
        • '); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); }); - it('should fail if the listitem has a parent
            with changed role', function () { - var params = checkSetup( + it('should fail if the listitem has a parent
              with changed role', () => { + const params = checkSetup( '
              1. My list item
              ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.equal(checkContext._data.messageKey, 'roleNotValid'); }); - it('should pass if the listitem has a parent
                with an invalid role', function () { - var params = checkSetup( + it('should pass if the listitem has a parent
                  with an invalid role', () => { + const params = checkSetup( '
                  1. My list item
                  ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); }); - it('should pass if the listitem has a parent
                    with an abstract role', function () { - var params = checkSetup( + it('should pass if the listitem has a parent
                      with an abstract role', () => { + const params = checkSetup( '
                      1. My list item
                      ' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); + assert.isTrue(result); + }); + + it('should return true in a shadow DOM pass', () => { + const node = document.createElement('div'); + node.innerHTML = '
                    1. My list item
                    2. '; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
                      '; + fixtureSetup(node); + const target = node.querySelector('#target'); + const virtualTarget = axe.utils.getNodeFromTree(target); + const result = checkEvaluate.apply(checkContext, [ + target, + {}, + virtualTarget + ]); assert.isTrue(result); }); - (shadowSupport.v1 ? it : xit)( - 'should return true in a shadow DOM pass', - function () { - var node = document.createElement('div'); - node.innerHTML = '
                    3. My list item
                    4. '; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
                      '; - fixtureSetup(node); - var target = node.querySelector('#target'); - var virtualTarget = axe.utils.getNodeFromTree(target); - var result = checkEvaluate.apply(checkContext, [ - target, - {}, - virtualTarget - ]); + it('should return false in a shadow DOM fail', () => { + const node = document.createElement('div'); + node.innerHTML = '
                    5. My list item
                    6. '; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
                      '; + fixtureSetup(node); + const target = node.querySelector('#target'); + const virtualTarget = axe.utils.getNodeFromTree(target); + const result = checkEvaluate.apply(checkContext, [ + target, + {}, + virtualTarget + ]); + assert.isFalse(result); + }); + + describe('ElementInternals', () => { + it('should pass for element that uses elementInternals', () => { + const params = checkSetup( + '
                    7. My list item
                    8. ' + ); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); - } - ); + }); - (shadowSupport.v1 ? it : xit)( - 'should return false in a shadow DOM fail', - function () { - var node = document.createElement('div'); - node.innerHTML = '
                    9. My list item
                    10. '; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
                      '; - fixtureSetup(node); - var target = node.querySelector('#target'); - var virtualTarget = axe.utils.getNodeFromTree(target); - var result = checkEvaluate.apply(checkContext, [ - target, - {}, - virtualTarget - ]); + it('returns false for element with unallowed elementInternals role', () => { + const params = checkSetup( + '
                    11. My list item
                    12. ' + ); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); - } - ); + }); + }); }); diff --git a/test/checks/lists/only-listitems.js b/test/checks/lists/only-listitems.js index 29e59c4de..6162ff098 100644 --- a/test/checks/lists/only-listitems.js +++ b/test/checks/lists/only-listitems.js @@ -244,4 +244,23 @@ describe('only-listitems', () => { }); }); }); + + describe('ElementInternals', () => { + it('returns false for element that uses elementInternals', () => { + const checkArgs = checkSetup( + '
                      • An item
                      • custom item
                      ' + ); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('returns true for element with unallowed elementInternals role', () => { + const checkArgs = checkSetup( + '
                      • An item
                      • custom item
                      ' + ); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + assert.deepEqual(checkContext._data, { + values: 'testutils-element' + }); + }); + }); }); diff --git a/test/checks/lists/structured-dlitems.js b/test/checks/lists/structured-dlitems.js index 4060fb8dc..285552491 100644 --- a/test/checks/lists/structured-dlitems.js +++ b/test/checks/lists/structured-dlitems.js @@ -1,16 +1,13 @@ -describe('structured-dlitems', function () { - 'use strict'; +describe('structured-dlitems', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; - var shadowSupport = axe.testUtils.shadowSupport; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should return false if the list has no contents', function () { - var checkArgs = checkSetup('
                      '); + it('should return false if the list has no contents', () => { + const checkArgs = checkSetup('
                      '); assert.isFalse( axe.testUtils .getCheckEvaluate('structured-dlitems') @@ -18,8 +15,8 @@ describe('structured-dlitems', function () { ); }); - it('should return true if the list has only a dd', function () { - var checkArgs = checkSetup('
                      A list
                      '); + it('should return true if the list has only a dd', () => { + const checkArgs = checkSetup('
                      A list
                      '); assert.isTrue( axe.testUtils .getCheckEvaluate('structured-dlitems') @@ -27,8 +24,8 @@ describe('structured-dlitems', function () { ); }); - it('should return true if the list has only a dt', function () { - var checkArgs = checkSetup('
                      A list
                      '); + it('should return true if the list has only a dt', () => { + const checkArgs = checkSetup('
                      A list
                      '); assert.isTrue( axe.testUtils @@ -37,8 +34,8 @@ describe('structured-dlitems', function () { ); }); - it('should return true if the list has dt and dd in the incorrect order', function () { - var checkArgs = checkSetup( + it('should return true if the list has dt and dd in the incorrect order', () => { + const checkArgs = checkSetup( '
                      A list
                      An item
                      ' ); @@ -49,8 +46,8 @@ describe('structured-dlitems', function () { ); }); - it('should return true if the list has dt and dd in the correct order as non-child descendants', function () { - var checkArgs = checkSetup( + it('should return true if the list has dt and dd in the correct order as non-child descendants', () => { + const checkArgs = checkSetup( '
                      An item
                      A list
                      ' ); @@ -61,8 +58,8 @@ describe('structured-dlitems', function () { ); }); - it('should return false if the list has dt and dd in the correct order', function () { - var checkArgs = checkSetup( + it('should return false if the list has dt and dd in the correct order', () => { + const checkArgs = checkSetup( '
                      An item
                      A list
                      ' ); @@ -73,8 +70,8 @@ describe('structured-dlitems', function () { ); }); - it('should return false if the list has a correctly-ordered dt and dd with other content', function () { - var checkArgs = checkSetup( + it('should return false if the list has a correctly-ordered dt and dd with other content', () => { + const checkArgs = checkSetup( '
                      Stuff
                      Item one
                      Description

                      Not a list

                      ' ); @@ -85,37 +82,31 @@ describe('structured-dlitems', function () { ); }); - (shadowSupport.v1 ? it : xit)( - 'should return false in a shadow DOM pass', - function () { - var node = document.createElement('div'); - node.innerHTML = '
                      Grayhound bus
                      at dawn
                      '; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
                      '; - - var checkArgs = checkSetup(node, 'dl'); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('structured-dlitems') - .apply(null, checkArgs) - ); - } - ); - - (shadowSupport.v1 ? it : xit)( - 'should return true in a shadow DOM fail', - function () { - var node = document.createElement('div'); - node.innerHTML = '
                      Galileo
                      Figaro
                      '; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = '
                      '; - - var checkArgs = checkSetup(node, 'dl'); - assert.isTrue( - axe.testUtils - .getCheckEvaluate('structured-dlitems') - .apply(null, checkArgs) - ); - } - ); + it('should return false in a shadow DOM pass', () => { + const node = document.createElement('div'); + node.innerHTML = '
                      Grayhound bus
                      at dawn
                      '; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
                      '; + + const checkArgs = checkSetup(node, 'dl'); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('structured-dlitems') + .apply(null, checkArgs) + ); + }); + + it('should return true in a shadow DOM fail', () => { + const node = document.createElement('div'); + node.innerHTML = '
                      Galileo
                      Figaro
                      '; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
                      '; + + const checkArgs = checkSetup(node, 'dl'); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('structured-dlitems') + .apply(null, checkArgs) + ); + }); }); diff --git a/test/checks/media/caption.js b/test/checks/media/caption.js index 88fce5310..8fe5baf34 100644 --- a/test/checks/media/caption.js +++ b/test/checks/media/caption.js @@ -1,42 +1,39 @@ -describe('caption', function () { - 'use strict'; +describe('caption', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; - var fixture = document.getElementById('fixture'); - var shadowSupport = axe.testUtils.shadowSupport; - var checkSetup = axe.testUtils.checkSetup; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should return undefined if there is no track element', function () { - var checkArgs = checkSetup('', 'audio'); + it('should return undefined if there is no track element', () => { + const checkArgs = checkSetup('', 'audio'); assert.isUndefined(checks.caption.evaluate.apply(null, checkArgs)); }); - it('should return undefined if there is no kind=captions attribute', function () { - var checkArgs = checkSetup( + it('should return undefined if there is no kind=captions attribute', () => { + const checkArgs = checkSetup( '', 'audio' ); assert.isUndefined(checks.caption.evaluate.apply(null, checkArgs)); }); - it('should pass if there is a kind=captions attribute', function () { - var checkArgs = checkSetup('', 'audio'); + it('should pass if there is a kind=captions attribute', () => { + const checkArgs = checkSetup( + '', + 'audio' + ); assert.isFalse(checks.caption.evaluate.apply(null, checkArgs)); }); - (shadowSupport.v1 ? it : xit)( - 'should get track from composed tree', - function () { - var node = document.createElement('div'); - node.innerHTML = ''; - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = ''; + it('should get track from composed tree', () => { + const node = document.createElement('div'); + node.innerHTML = ''; + const shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; - var checkArgs = checkSetup(node, {}, 'audio'); - assert.isFalse(checks.caption.evaluate.apply(null, checkArgs)); - } - ); + const checkArgs = checkSetup(node, {}, 'audio'); + assert.isFalse(checks.caption.evaluate.apply(null, checkArgs)); + }); }); diff --git a/test/checks/media/frame-tested.js b/test/checks/media/frame-tested.js index 9addbfdcc..986c7223c 100644 --- a/test/checks/media/frame-tested.js +++ b/test/checks/media/frame-tested.js @@ -1,22 +1,20 @@ -describe('frame-tested', function () { - 'use strict'; +describe('frame-tested', () => { + const checkEvaluate = axe.testUtils.getCheckEvaluate('frame-tested'); + const frameTestedAfter = checks['frame-tested'].after; - var checkEvaluate = axe.testUtils.getCheckEvaluate('frame-tested'); - var frameTestedAfter = checks['frame-tested'].after; - - describe('evaluate', function () { - it('returns undefined', function () { + describe('evaluate', () => { + it('returns undefined', () => { assert.isUndefined(checkEvaluate()); }); - it('returns false if passed isViolation:true', function () { + it('returns false if passed isViolation:true', () => { assert.isFalse(checkEvaluate(null, { isViolation: true })); }); }); - describe('after', function () { - it('changes result to true if frame has been tested', function () { - var results = [ + describe('after', () => { + it('changes result to true if frame has been tested', () => { + const results = [ { result: undefined, node: { @@ -49,7 +47,7 @@ describe('frame-tested', function () { } ]; - var afterResults = frameTestedAfter(results); + const afterResults = frameTestedAfter(results); assert.lengthOf(afterResults, 2); assert.isTrue(afterResults[0].result); @@ -63,8 +61,8 @@ describe('frame-tested', function () { ]); }); - it('does not change result when iframe has not been tested', function () { - var results = [ + it('does not change result when iframe has not been tested', () => { + const results = [ { result: undefined, node: { @@ -97,7 +95,7 @@ describe('frame-tested', function () { } ]; - var afterResults = frameTestedAfter(results); + const afterResults = frameTestedAfter(results); assert.lengthOf(afterResults, 3); assert.isTrue(afterResults[0].result); @@ -116,8 +114,8 @@ describe('frame-tested', function () { ]); }); - it('works with shadow DOM', function () { - var results = [ + it('works with shadow DOM', () => { + const results = [ { result: undefined, node: { @@ -144,7 +142,7 @@ describe('frame-tested', function () { } ]; - var afterResults = frameTestedAfter(results); + const afterResults = frameTestedAfter(results); assert.lengthOf(afterResults, 2); assert.isTrue(afterResults[0].result); @@ -158,8 +156,8 @@ describe('frame-tested', function () { ]); }); - it('works with nested shadow DOM and iframes', function () { - var results = [ + it('works with nested shadow DOM and iframes', () => { + const results = [ { result: undefined, node: { @@ -220,7 +218,7 @@ describe('frame-tested', function () { } ]; - var afterResults = frameTestedAfter(results); + const afterResults = frameTestedAfter(results); assert.lengthOf(afterResults, 4); assert.isTrue(afterResults[0].result); diff --git a/test/checks/media/no-autoplay-audio.js b/test/checks/media/no-autoplay-audio.js index d0b24475c..251ebdf37 100644 --- a/test/checks/media/no-autoplay-audio.js +++ b/test/checks/media/no-autoplay-audio.js @@ -1,4 +1,5 @@ describe('no-autoplay-audio', () => { + const html = axe.testUtils.html; const check = checks['no-autoplay-audio']; const checkSetup = axe.testUtils.checkSetup; const checkContext = axe.testUtils.MockCheckContext(); @@ -29,7 +30,7 @@ describe('no-autoplay-audio', () => { }); it('returns false when