diff --git a/.circleci/config.yml b/.circleci/config.yml index 03753bbf89..9b27ebbca4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -327,42 +327,42 @@ workflows: # Run linting - lint: requires: - - build_unix + - dependencies_unix # Run tests on all commits, but after installing dependencies - test_chrome: requires: - - lint + - build_unix - test_firefox: requires: - - test_chrome + - build_unix - test_examples: requires: - - test_chrome + - build_unix - test_act: requires: - - test_chrome + - build_unix - test_aria_practices: requires: - - test_chrome + - build_unix - test_locales: requires: - - test_chrome + - build_unix - test_virtual_rules: requires: - - test_chrome + - build_unix - build_api_docs: requires: - - test_chrome + - build_unix - test_rule_help_version: requires: - - test_chrome + - build_unix - test_node: requires: - - test_chrome + - build_unix # Verify the sri history is correct - verify_sri: requires: - - dependencies_unix + - build_unix filters: branches: only: @@ -372,6 +372,7 @@ workflows: - hold_release: type: approval requires: + - test_chrome - test_firefox - test_examples - test_act @@ -389,6 +390,7 @@ workflows: # Run a next release on "develop" commits, but only after the tests pass and dependencies are installed - next_release: requires: + - test_chrome - test_firefox - test_examples - test_act @@ -435,12 +437,15 @@ workflows: - develop jobs: - dependencies_unix - - test_nightly_browsers: + - build_unix: requires: - dependencies_unix + - test_nightly_browsers: + requires: + - build_unix - test_nightly_act: requires: - - dependencies_unix + - build_unix - test_nightly_aria_practices: requires: - - dependencies_unix + - build_unix diff --git a/CHANGELOG.md b/CHANGELOG.md index aec179b7a7..a7a16dcec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [4.7.0](https://github.com/dequelabs/axe-core/compare/v4.6.3...v4.7.0) (2023-04-17) + +### Features + +- **aria-roledescription:** deprecate rule ([#3948](https://github.com/dequelabs/axe-core/issues/3948)) ([1913a9e](https://github.com/dequelabs/axe-core/commit/1913a9eaf0e669927c57d56710053303cda193f8)) +- **aria-roles:** deprecate the ARIA directory role ([#3952](https://github.com/dequelabs/axe-core/issues/3952)) ([893fdd0](https://github.com/dequelabs/axe-core/commit/893fdd0901f9218d9add39c16b2e6b77227fbdcd)) +- **d.ts:** setup/teardown, reporters & metadata definitions ([#3966](https://github.com/dequelabs/axe-core/issues/3966)) ([78264ee](https://github.com/dequelabs/axe-core/commit/78264ee663d528bc3fbfc9ea7dbba180259f01af)) +- deprecate bower ([#3889](https://github.com/dequelabs/axe-core/issues/3889)) ([651d811](https://github.com/dequelabs/axe-core/commit/651d811f0f1d1dfc5ab899568eaeb83931011f34)) +- deprecate color.filteredRectStack, color.getRectStack, and dom.visuallyContains ([#3859](https://github.com/dequelabs/axe-core/issues/3859)) ([3be2bad](https://github.com/dequelabs/axe-core/commit/3be2bad2a896e72a92fe70810500fc1ef67b7027)) +- **dom.focusDisabled,dom.isVisibleForScreenreader:** support the inert attribute ([#3857](https://github.com/dequelabs/axe-core/issues/3857)) ([273c971](https://github.com/dequelabs/axe-core/commit/273c97199bd596a288378427becba672b4482678)) +- **metadata:** add Trusted Tester tag ([#3986](https://github.com/dequelabs/axe-core/issues/3986)) ([1f6a2a6](https://github.com/dequelabs/axe-core/commit/1f6a2a68ac10c770091741b328de7efb2ccc6687)) +- support the dialog element ([#3902](https://github.com/dequelabs/axe-core/issues/3902)) ([d4522cd](https://github.com/dequelabs/axe-core/commit/d4522cdd7a90018336098f9b2119e353bd5a71ee)) + +### Bug Fixes + +- **aria-allowed-attrs:** allow aria-description and aria-braille\* attrs ([#3956](https://github.com/dequelabs/axe-core/issues/3956)) ([2842395](https://github.com/dequelabs/axe-core/commit/2842395f9a8990f670e7025749ff8301b68a15ae)) +- **aria-input-field-name:** skip combobox popups ([#3886](https://github.com/dequelabs/axe-core/issues/3886)) ([3dcdd42](https://github.com/dequelabs/axe-core/commit/3dcdd42d9ce52817d0931aa4fea1ec2b1fc9d650)) +- **aria-required-children:** allow separator in menu(bar) ([#3868](https://github.com/dequelabs/axe-core/issues/3868)) ([ec9f507](https://github.com/dequelabs/axe-core/commit/ec9f50708a233acfa4f9b851618077d6637e6582)) +- **aria-required-children:** do not fail for children with aria-hidden ([#3949](https://github.com/dequelabs/axe-core/issues/3949)) ([8714d6b](https://github.com/dequelabs/axe-core/commit/8714d6ba6debec93d095f5f12385d92c55b0d4b3)) +- **aria-required-children:** list elements that are not allowed ([#3951](https://github.com/dequelabs/axe-core/issues/3951)) ([cce7586](https://github.com/dequelabs/axe-core/commit/cce75869be032006dc505a2af853886943973319)) +- **autocomplete-valid:** allow webauthn token ([#3866](https://github.com/dequelabs/axe-core/issues/3866)) ([fd3010f](https://github.com/dequelabs/axe-core/commit/fd3010ff74eb677d4a84198bb1ca314d54393cb7)) +- **color-contrast:** correcly apply opacity to foreground color ([#3973](https://github.com/dequelabs/axe-core/issues/3973)) ([d7db279](https://github.com/dequelabs/axe-core/commit/d7db279549c443c1e2f43a6b33ebee0968c64325)) +- **color-contrast:** correctly calculate contrast of flex/grid items with z-index ([#3884](https://github.com/dequelabs/axe-core/issues/3884)) ([cef96be](https://github.com/dequelabs/axe-core/commit/cef96be6740657047031c2019908822f957e6c63)) +- **color-contrast:** correctly compute background color for elements with opacity ([#3944](https://github.com/dequelabs/axe-core/issues/3944)) ([c051fe8](https://github.com/dequelabs/axe-core/commit/c051fe851fb5eaa75e6dc0205c4db5e75d80f3a4)), closes [#3932](https://github.com/dequelabs/axe-core/issues/3932) +- **color-contrast:** correctly compute color contrast of elements ([#3847](https://github.com/dequelabs/axe-core/issues/3847)) ([4c3a00c](https://github.com/dequelabs/axe-core/commit/4c3a00c7bd6de68b2795be37113a59d804d0a313)) +- **color-contrast:** do not check contrast on elemets that are inerted ([#3894](https://github.com/dequelabs/axe-core/issues/3894)) ([da19946](https://github.com/dequelabs/axe-core/commit/da19946db610c3ab8898431645274a8a76d61a33)) +- **color-contrast:** skip ligature icons ([#3867](https://github.com/dequelabs/axe-core/issues/3867)) ([9486288](https://github.com/dequelabs/axe-core/commit/948628894e3119e7f6ad45a230fbee23ffe64ef2)) +- **create-grid:** correctly compute stack order for non-positioned stacking contexts ([#3930](https://github.com/dequelabs/axe-core/issues/3930)) ([8db2c24](https://github.com/dequelabs/axe-core/commit/8db2c2492d55a903b7903ed71f8b792e58dc2e8c)), closes [#3932](https://github.com/dequelabs/axe-core/issues/3932) +- **css-orientation-lock:** support the css rotate property ([#3958](https://github.com/dequelabs/axe-core/issues/3958)) ([c51f8bf](https://github.com/dequelabs/axe-core/commit/c51f8bfea87b57c269e509f88d64855368a25493)) +- **focus-order-semantics:** Add ARIA role article to list of valid roles for scrollable regions ([#3927](https://github.com/dequelabs/axe-core/issues/3927)) ([f029271](https://github.com/dequelabs/axe-core/commit/f0292714b94a1483f4148f3ca7206897cfb21318)) +- **is-icon-ligature:** prevent canvas2d warning willReadFrequently ([#3931](https://github.com/dequelabs/axe-core/issues/3931)) ([b3c52bb](https://github.com/dequelabs/axe-core/commit/b3c52bbb6eccda67dabcbf4183d7c201a227a0ac)) +- **link-in-text-block:** allow links with identical colors ([#3861](https://github.com/dequelabs/axe-core/issues/3861)) ([5f90040](https://github.com/dequelabs/axe-core/commit/5f900402470f925686a0d8b41ac01731492bbd30)) +- **respondable:** work with CDP `Page.setDocumentContent` ([#3921](https://github.com/dequelabs/axe-core/issues/3921)) ([66f23e5](https://github.com/dequelabs/axe-core/commit/66f23e59b6deddd3b95035545d622d761abe5825)) +- **scrollable-region-focusable:** change impact to serious ([#3959](https://github.com/dequelabs/axe-core/issues/3959)) ([e3a5c21](https://github.com/dequelabs/axe-core/commit/e3a5c211fe007736d98a16d69995318c2c651f2d)) +- **scrollable-region-focusable:** skip native controls ([#3862](https://github.com/dequelabs/axe-core/issues/3862)) ([b0bdefa](https://github.com/dequelabs/axe-core/commit/b0bdefa34b85363e742ff04e319c014fe95f31f7)) + ### [4.6.3](https://github.com/dequelabs/axe-core/compare/v4.6.2...v4.6.3) (2023-01-23) ### Bug Fixes diff --git a/axe.d.ts b/axe.d.ts index e9a447e4ae..c174b24c0f 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -190,12 +190,13 @@ declare namespace axe { help: string; }; } + interface CheckMessages { + pass: string | { [key: string]: string }; + fail: string | { [key: string]: string }; + incomplete: string | { [key: string]: string }; + } interface CheckLocale { - [key: string]: { - pass: string | { [key: string]: string }; - fail: string | { [key: string]: string }; - incomplete: string | { [key: string]: string }; - }; + [key: string]: CheckMessages; } interface Locale { lang?: string; @@ -237,7 +238,7 @@ declare namespace axe { } interface Spec { branding?: string | Branding; - reporter?: ReporterVersion; + reporter?: ReporterVersion | string | AxeReporter; checks?: Check[]; rules?: Rule[]; standards?: Standards; @@ -263,6 +264,10 @@ declare namespace axe { options?: any; matches?: string; enabled?: boolean; + metadata?: { + impact?: ImpactValue; + messages?: CheckMessages; + }; } interface Rule { id: string; @@ -277,6 +282,7 @@ declare namespace axe { tags?: string[]; matches?: string; reviewOnFail?: boolean; + metadata?: Omit; } interface AxePlugin { id: string; @@ -319,6 +325,40 @@ declare namespace axe { frameSelector: CrossTreeSelector; frameContext: FrameContextObject; } + + interface RawNodeResult { + any: CheckResult[]; + all: CheckResult[]; + none: CheckResult[]; + impact: ImpactValue | null; + result: T; + } + + interface RawResult extends Omit { + inapplicable: []; + passes: RawNodeResult<'passed'>[]; + incomplete: RawNodeResult<'incomplete'>[]; + violations: RawNodeResult<'failed'>[]; + pageLevel: boolean; + result: 'failed' | 'passed' | 'incomplete' | 'inapplicable'; + } + + type AxeReporter = ( + rawResults: RawResult[], + option: RunOptions, + callback: (report: T) => void + ) => void; + + interface VirtualNode { + actualNode?: Node; + shadowId?: string; + children?: VirtualNode[]; + parent?: VirtualNode; + attr(attr: string): string | null; + hasAttr(attr: string): boolean; + props: { [key: string]: unknown }; + } + interface Utils { getFrameContexts: ( context?: ElementContext, @@ -326,7 +366,9 @@ declare namespace axe { ) => FrameContext[]; shadowSelect: (selector: CrossTreeSelector) => Element | null; shadowSelectAll: (selector: CrossTreeSelector) => Element[]; + getStandards(): Required; } + interface EnvironmentData { testEngine: TestEngine; testRunner: TestRunner; @@ -436,6 +478,35 @@ declare namespace axe { */ function frameMessenger(frameMessenger: FrameMessenger): void; + /** + * Setup axe-core so axe.common functions can work properly. + */ + function setup(node?: Element | Document): VirtualNode; + + /** + * Clean up axe-core tree and caches. `axe.run` will call this function at the end of the run so there's no need to call it yourself afterwards. + */ + function teardown(): void; + + /** + * Check if a reporter is registered + */ + function hasReporter(reporterName: string): boolean; + + /** + * Get a reporter based the name it is registered with + */ + function getReporter(reporterName: string): AxeReporter; + + /** + * Register a new reporter, optionally setting it as the default + */ + function addReporter( + reporterName: string, + reporter: AxeReporter, + isDefault?: boolean + ): void; + // axe.frameMessenger type FrameMessenger = { open: (topicHandler: TopicHandler) => Close | void; diff --git a/bower.json b/bower.json index 939aeb2504..955de0f8e3 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,7 @@ { "name": "axe-core", - "version": "4.6.3", + "version": "4.7.0", + "deprecated": true, "contributors": [ { "name": "David Sturley", diff --git a/build/cherry-pick.js b/build/cherry-pick.js index 0ed8a80af1..2b3dc2afe3 100644 --- a/build/cherry-pick.js +++ b/build/cherry-pick.js @@ -45,7 +45,11 @@ let targetVersion = releaseType === 'patch' ? version : `${major}.${minor}.0`; // get all commits from a branch function getCommits(branch) { - const stdout = execSync(`git log ${branch || ''} --abbrev-commit`).toString(); + // 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()}` + ).toString(); const allCommits = stdout .split(/commit (?=[\w\d]{8}[\n\r])/) .filter(commit => !!commit); diff --git a/doc/API.md b/doc/API.md index 63b531cd87..9355005820 100644 --- a/doc/API.md +++ b/doc/API.md @@ -74,7 +74,7 @@ For a full listing of API offered by axe, clone the repository and run `npm run Each rule in axe-core has a number of tags. These provide metadata about the rule. Each rule has one tag that indicates which WCAG version / level it belongs to, or if it doesn't it have the `best-practice` tag. If the rule is required by WCAG, there is a tag that references the success criterion number. For example, the `wcag111` tag means a rule is required for WCAG 2 success criterion 1.1.1. -The `experimental`, `ACT` and `section508` tags are only added to some rules. Each rule with a `section508` tag also has a tag to indicate what requirement in old Section 508 the rule is required by. For example `section508.22.a`. +The `experimental`, `ACT`, `TT`, and `section508` tags are only added to some rules. Each rule with a `section508` tag also has a tag to indicate what requirement in old Section 508 the rule is required by. For example `section508.22.a`. | Tag Name | Accessibility Standard / Purpose | | ---------------- | ---------------------------------------------------- | @@ -89,6 +89,8 @@ The `experimental`, `ACT` and `section508` tags are only added to some rules. Ea | `ACT` | W3C approved Accessibility Conformance Testing rules | | `section508` | Old Section 508 rules | | `section508.*.*` | Requirement in old Section 508 | +| `TTv5` | Trusted Tester v5 rules | +| `TT*.*` | Test ID in Trusted Tester | | `experimental` | Cutting-edge rules, disabled by default | | `cat.*` | Category mappings used by Deque (see below) | diff --git a/doc/examples/puppeteer/package.json b/doc/examples/puppeteer/package.json index c3f7767446..eb4f1c4469 100644 --- a/doc/examples/puppeteer/package.json +++ b/doc/examples/puppeteer/package.json @@ -4,10 +4,12 @@ "private": true, "main": "axe-puppeteer.js", "scripts": { - "test": "node axe-puppeteer.js https://deque.com" + "test": "npm run test:url && npm run test:set-content", + "test:url": "node axe-puppeteer.js https://deque.com", + "test:set-content": "node set-content.js" }, "devDependencies": { "axe-core": "^4.6.2", - "puppeteer": "^19.5.0" + "puppeteer": "^19.8.2" } } diff --git a/doc/examples/puppeteer/set-content.js b/doc/examples/puppeteer/set-content.js new file mode 100644 index 0000000000..60e85f8efe --- /dev/null +++ b/doc/examples/puppeteer/set-content.js @@ -0,0 +1,34 @@ +const assert = require('assert'); +const puppeteer = require('puppeteer'); +const axe = require('axe-core'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.setContent(` + + + Test Page + + +
+

Hello World

+ + +
+ + + `); + + await page.evaluate(axe.source); + const frames = page.frames(); + for (let i = 0; i < frames.length; i++) { + await frames[i].evaluate(axe.source); + } + + const results = await page.evaluate(`window.axe.run()`); + assert(results.violations.length); + + await browser.close(); +})(); diff --git a/doc/projects.md b/doc/projects.md index 8663e98a65..5dcd800666 100644 --- a/doc/projects.md +++ b/doc/projects.md @@ -50,7 +50,7 @@ Add your project/integration to this file and submit a pull request. 1. [axe-TestCafe](https://github.com/helen-dikareva/axe-testcafe) 1. [Web Audit University of Nebraska-Lincoln](https://webaudit.unl.edu/) 1. [Vorlon.js Remote Debugger](https://github.com/MicrosoftDX/Vorlonjs) -1. [Terra's Webdriver.io Accessibility Service](https://github.com/cerner/terra-toolkit/blob/master/docs/AxeService.md) +1. [Terra Toolkit](https://github.com/cerner/terra-toolkit) 1. [axe-sarif-converter](https://github.com/microsoft/axe-sarif-converter) 1. [Selenium.Axe for .NET](https://github.com/TroyWalshProf/SeleniumAxeDotnet) 1. [vue-axe](https://github.com/vue-a11y/vue-axe-next) diff --git a/doc/release-and-support.md b/doc/release-and-support.md index a21b37aa83..1a076540e7 100644 --- a/doc/release-and-support.md +++ b/doc/release-and-support.md @@ -2,14 +2,16 @@ ## Release Cadence -Axe-core is used in lots of [projects and environments](./projects.md). Not all of these are able to upgrade at a rapid pace. Because of this, updates in axe-core are limited in the following ways. For details on what types of changes can come in these releases see [backward compatibility](./backwards-compatibility-doc.md). +Axe-core is used in many [projects and environments](./projects.md). Not all of these are able to upgrade at a rapid pace. Because of this, updates in axe-core are limited in the following ways. For details on what types of changes can come in these releases see [backward compatibility](./backwards-compatibility-doc.md). -- **Major releases**: Axe-core strives to have a major release every 18 to 24 months. These may include breaking changes, and provide opportunities for Deque to remove previously deprecated features. As an absolute minimum, there will be a 12 month period between major releases of axe-core, except if this is necessary for security. +- **Major releases**: Major axe-core releases likely include breaking changes, and provide opportunities for Deque to remove previously deprecated features. As an absolute minimum, there will be a 12 month period between major releases of axe-core, except if this is necessary for security. -- **Minor Releases**; Axe-core strives to publish three minor releases every year. There will be at least 6 weeks between each minor release, except if this is necessary for security. +- **Minor Releases**; Axe-core strives to publish three to five minor releases every year. There will be at least 3 weeks between each minor release, except if this is necessary for security. - **Patch Releases**: There are no restrictions on the number of patches released for axe-core. +For all major and minor releases a milestone will be created at least three weeks ahead of the release. The axe-core team strives to complete all issues in that milestone, although on occasion issues lower in the milestone may be dropped, and high priority issues may be added. Axe-core will observe a code freeze one week before releasing a major or minor version. Only documentation, metadata, and localizations may be modified during code freeze. + ## Security Updates Once a new major or minor version is released, the prior versions will no longer be updated, except if this is necessary for security. Security updates will be provided for major and minor versions **up to 18 months** old. For example, if version 4.0.0 was released 17 months ago, and a security issue is discovered a new patch will be released on the 4.0 line. However if 3.5.0 was released 20 months ago, even if 3.5.2 was released 17 months ago, a security patch for the 3.5 line may **not** be provided. @@ -20,6 +22,6 @@ The axe-core team considered security its very highest priority. While security In order to ensure the best quality from axe-core, we encourage everyone to regularly upgrade their version of axe-core, to try to stay as close to the latest release as possible. Depending on how axe-core is used, upgrading to a new minor or major version may result in new issues getting reported. To handle this, we recommend that you plan time to upgrade your version of axe-core at least twice a year. -Additionally, we recommend that you always use the latest patch version of whatever minor version you are on. For example if you are using axe-core 3.5.5, and 3.5.6 it is best to upgrade immediately. Patch releases of axe-core should should not find new issues, although they occasionally resolve issues in the case of false positives. +Additionally, we recommend that you always use the latest patch version of whatever minor version you are on. For example if you are using axe-core 3.5.5, and 3.5.6 is released it is best to upgrade immediately. Patch releases of axe-core should not find new issues, although they occasionally resolve issues in the case of false positives. Ensuring you always use the latest available patch version of axe-core on any minor line guarantees you always the most secure version of axe-core. This minor line must have been released within the last 18 months. See [security updates](#security-updates). diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 77afb94ca5..9a632695f0 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -12,73 +12,71 @@ ## WCAG 2.0 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :-------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [area-alt](https://dequeuniversity.com/rules/axe/4.6/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, ACT | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.6/aria-allowed-attr?application=RuleDescription) | Ensures ARIA attributes are allowed for an element's role | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-command-name](https://dequeuniversity.com/rules/axe/4.6/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.6/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | failure | | -| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.6/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | -| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.6/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.6/aria-meter-name?application=RuleDescription) | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | -| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.6/aria-progressbar-name?application=RuleDescription) | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | -| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.6/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | -| [aria-required-children](https://dequeuniversity.com/rules/axe/4.6/aria-required-children?application=RuleDescription) | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | 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.6/aria-required-parent?application=RuleDescription) | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.6/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | | -| [aria-roles](https://dequeuniversity.com/rules/axe/4.6/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Minor, Serious, Critical | cat.aria, wcag2a, wcag412 | failure | [674b10](https://act-rules.github.io/rules/674b10) | -| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.6/aria-toggle-field-name?application=RuleDescription) | Ensures every ARIA toggle field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.6/aria-tooltip-name?application=RuleDescription) | Ensures every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | | -| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.6/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | -| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.6/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | -| [audio-caption](https://dequeuniversity.com/rules/axe/4.6/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, section508, section508.22.a | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | -| [blink](https://dequeuniversity.com/rules/axe/4.6/blink?application=RuleDescription) | Ensures <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j | failure | | -| [button-name](https://dequeuniversity.com/rules/axe/4.6/button-name?application=RuleDescription) | Ensures buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT | 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.6/bypass?application=RuleDescription) | Ensures 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 | 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.6/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, ACT | 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.6/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [dlitem](https://dequeuniversity.com/rules/axe/4.6/dlitem?application=RuleDescription) | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [document-title](https://dequeuniversity.com/rules/axe/4.6/document-title?application=RuleDescription) | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, ACT | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | -| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.6/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.6/duplicate-id-aria?application=RuleDescription) | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id](https://dequeuniversity.com/rules/axe/4.6/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.6/form-field-multiple-labels?application=RuleDescription) | Ensures form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332 | needs review | | -| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.6/frame-focusable-content?application=RuleDescription) | Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.6/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag412, wcag2a | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | -| [frame-title](https://dequeuniversity.com/rules/axe/4.6/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | -| [html-has-lang](https://dequeuniversity.com/rules/axe/4.6/html-has-lang?application=RuleDescription) | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, ACT | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | -| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.6/html-lang-valid?application=RuleDescription) | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, ACT | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | -| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.6/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, ACT | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | -| [image-alt](https://dequeuniversity.com/rules/axe/4.6/image-alt?application=RuleDescription) | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [input-button-name](https://dequeuniversity.com/rules/axe/4.6/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [input-image-alt](https://dequeuniversity.com/rules/axe/4.6/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, ACT | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | -| [label](https://dequeuniversity.com/rules/axe/4.6/label?application=RuleDescription) | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.6/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 | failure, needs review | | -| [link-name](https://dequeuniversity.com/rules/axe/4.6/link-name?application=RuleDescription) | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a, ACT | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [list](https://dequeuniversity.com/rules/axe/4.6/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [listitem](https://dequeuniversity.com/rules/axe/4.6/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [marquee](https://dequeuniversity.com/rules/axe/4.6/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222 | failure | | -| [meta-refresh](https://dequeuniversity.com/rules/axe/4.6/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221 | 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.6/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, wcag2aa, wcag144, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | -| [nested-interactive](https://dequeuniversity.com/rules/axe/4.6/nested-interactive?application=RuleDescription) | Ensures 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 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | -| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.6/no-autoplay-audio?application=RuleDescription) | Ensures <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, ACT | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | -| [object-alt](https://dequeuniversity.com/rules/axe/4.6/object-alt?application=RuleDescription) | Ensures <object> elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | -| [role-img-alt](https://dequeuniversity.com/rules/axe/4.6/role-img-alt?application=RuleDescription) | Ensures [role='img'] elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.6/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard | Moderate | cat.keyboard, wcag2a, wcag211 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | -| [select-name](https://dequeuniversity.com/rules/axe/4.6/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.6/server-side-image-map?application=RuleDescription) | Ensures that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f | needs review | | -| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.6/svg-img-alt?application=RuleDescription) | Ensures <svg> elements with an img, graphics-document or graphics-symbol role have an accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | -| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.6/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | -| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.6/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 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | -| [valid-lang](https://dequeuniversity.com/rules/axe/4.6/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, ACT | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | -| [video-caption](https://dequeuniversity.com/rules/axe/4.6/video-caption?application=RuleDescription) | Ensures <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :--------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [area-alt](https://dequeuniversity.com/rules/axe/4.7/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, ACT, TTv5, TT6.a | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-attr?application=RuleDescription) | Ensures ARIA attributes are allowed for an element's role | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-command-name](https://dequeuniversity.com/rules/axe/4.7/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT6.a | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | failure | | +| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | +| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.7/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.7/aria-meter-name?application=RuleDescription) | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | +| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.7/aria-progressbar-name?application=RuleDescription) | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | +| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.7/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | +| [aria-required-children](https://dequeuniversity.com/rules/axe/4.7/aria-required-children?application=RuleDescription) | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | 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.7/aria-required-parent?application=RuleDescription) | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-roles](https://dequeuniversity.com/rules/axe/4.7/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Minor, Serious, Critical | cat.aria, wcag2a, wcag412 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.7/aria-toggle-field-name?application=RuleDescription) | Ensures every ARIA toggle field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.7/aria-tooltip-name?application=RuleDescription) | Ensures every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | | +| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | +| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | +| [blink](https://dequeuniversity.com/rules/axe/4.7/blink?application=RuleDescription) | Ensures <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b | failure | | +| [button-name](https://dequeuniversity.com/rules/axe/4.7/button-name?application=RuleDescription) | Ensures buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT, TTv5, TT6.a | 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.7/bypass?application=RuleDescription) | Ensures 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 | 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.7/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, ACT, TTv5, TT13.c | 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.7/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [dlitem](https://dequeuniversity.com/rules/axe/4.7/dlitem?application=RuleDescription) | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [document-title](https://dequeuniversity.com/rules/axe/4.7/document-title?application=RuleDescription) | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, ACT, TTv5, TT12.a | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | +| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.7/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.7/duplicate-id-aria?application=RuleDescription) | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [duplicate-id](https://dequeuniversity.com/rules/axe/4.7/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.7/form-field-multiple-labels?application=RuleDescription) | Ensures form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c | needs review | | +| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.7/frame-focusable-content?application=RuleDescription) | Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.7/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag412, wcag2a, TTv5, TT12.c | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | +| [frame-title](https://dequeuniversity.com/rules/axe/4.7/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.c | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | +| [html-has-lang](https://dequeuniversity.com/rules/axe/4.7/html-has-lang?application=RuleDescription) | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, ACT, TTv5, TT11.a | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | +| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.7/html-lang-valid?application=RuleDescription) | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, ACT, TTv5, TT11.a | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | +| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.7/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, ACT | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | +| [image-alt](https://dequeuniversity.com/rules/axe/4.7/image-alt?application=RuleDescription) | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a, TT7.b | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [input-button-name](https://dequeuniversity.com/rules/axe/4.7/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT, TTv5, TT5.c | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [input-image-alt](https://dequeuniversity.com/rules/axe/4.7/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | +| [label](https://dequeuniversity.com/rules/axe/4.7/label?application=RuleDescription) | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.7/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 | failure, needs review | | +| [link-name](https://dequeuniversity.com/rules/axe/4.7/link-name?application=RuleDescription) | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a, ACT, TTv5, TT6.a | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [list](https://dequeuniversity.com/rules/axe/4.7/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [listitem](https://dequeuniversity.com/rules/axe/4.7/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [marquee](https://dequeuniversity.com/rules/axe/4.7/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b | failure | | +| [meta-refresh](https://dequeuniversity.com/rules/axe/4.7/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT2.c | 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.7/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, wcag2aa, wcag144, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | +| [nested-interactive](https://dequeuniversity.com/rules/axe/4.7/nested-interactive?application=RuleDescription) | Ensures 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, TT4.a | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | +| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.7/no-autoplay-audio?application=RuleDescription) | Ensures <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, ACT, TTv5, TT2.a | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | +| [object-alt](https://dequeuniversity.com/rules/axe/4.7/object-alt?application=RuleDescription) | Ensures <object> elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | +| [role-img-alt](https://dequeuniversity.com/rules/axe/4.7/role-img-alt?application=RuleDescription) | Ensures [role='img'] elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.7/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard | Serious | cat.keyboard, wcag2a, wcag211 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | +| [select-name](https://dequeuniversity.com/rules/axe/4.7/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.7/server-side-image-map?application=RuleDescription) | Ensures that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f | needs review | | +| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.7/svg-img-alt?application=RuleDescription) | Ensures <svg> elements with an img, graphics-document or graphics-symbol role have an accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | +| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.7/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | +| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.7/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, 14.b | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | +| [valid-lang](https://dequeuniversity.com/rules/axe/4.7/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, ACT, TTv5, TT11.b | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | +| [video-caption](https://dequeuniversity.com/rules/axe/4.7/video-caption?application=RuleDescription) | Ensures <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a | 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 | | :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :------ | :------------------------------------- | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.6/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, ACT | failure | [73f2c2](https://act-rules.github.io/rules/73f2c2) | -| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.6/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | +| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.7/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, ACT | failure | [73f2c2](https://act-rules.github.io/rules/73f2c2) | +| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.7/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | ## WCAG 2.2 Level A & AA Rules @@ -86,7 +84,7 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r | Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | | :----------------------------------------------------------------------------------------------- | :------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :-------- | -| [target-size](https://dequeuniversity.com/rules/axe/4.6/target-size?application=RuleDescription) | Ensure touch target have sufficient size and space | Serious | wcag22aa, wcag258, cat.sensory-and-visual-cues | failure, needs review | | +| [target-size](https://dequeuniversity.com/rules/axe/4.7/target-size?application=RuleDescription) | Ensure touch target have sufficient size and space | Serious | wcag22aa, wcag258, cat.sensory-and-visual-cues | failure, needs review | | ## Best Practices Rules @@ -94,61 +92,64 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- | :----------------------------------------- | :------------------------- | :------------------------------------------------- | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.6/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.6/aria-allowed-role?application=RuleDescription) | Ensures 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.6/aria-dialog-name?application=RuleDescription) | Ensures 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.6/aria-text?application=RuleDescription) | Ensures "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.6/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.6/empty-heading?application=RuleDescription) | Ensures 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.6/empty-table-header?application=RuleDescription) | Ensures table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.6/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | -| [heading-order](https://dequeuniversity.com/rules/axe/4.6/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.6/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.6/label-title-only?application=RuleDescription) | Ensures 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.6/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.6/landmark-complementary-is-top-level?application=RuleDescription) | Ensures 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.6/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.6/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.6/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.6/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.6/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.6/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.6/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.6/meta-viewport-large?application=RuleDescription) | Ensures <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.6/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.6/presentation-role-conflict?application=RuleDescription) | Elements marked as presentational should not have global ARIA or tabindex to ensure 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.6/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.6/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | -| [skip-link](https://dequeuniversity.com/rules/axe/4.6/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | -| [tabindex](https://dequeuniversity.com/rules/axe/4.6/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.6/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure, needs review | | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.7/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-role?application=RuleDescription) | Ensures 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.7/aria-dialog-name?application=RuleDescription) | Ensures 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.7/aria-text?application=RuleDescription) | Ensures "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.7/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.7/empty-heading?application=RuleDescription) | Ensures 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.7/empty-table-header?application=RuleDescription) | Ensures table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.7/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | +| [heading-order](https://dequeuniversity.com/rules/axe/4.7/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.7/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.7/label-title-only?application=RuleDescription) | Ensures 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.7/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-complementary-is-top-level?application=RuleDescription) | Ensures 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.7/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.7/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.7/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.7/meta-viewport-large?application=RuleDescription) | Ensures <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.7/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.7/presentation-role-conflict?application=RuleDescription) | Elements marked as presentational should not have global ARIA or tabindex to ensure 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.7/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.7/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | +| [skip-link](https://dequeuniversity.com/rules/axe/4.7/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | +| [tabindex](https://dequeuniversity.com/rules/axe/4.7/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.7/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure, needs review | | ## WCAG 2.x level AAA rules Rules that check for conformance to WCAG AAA success criteria that can be fully automated. These are disabled by default in axe-core. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :--------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :------------------------------------------------- | -| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.6/color-contrast-enhanced?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AAA contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.6/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | -| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.6/meta-refresh-no-exceptions?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :--------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :------------------------------------------------- | +| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.7/color-contrast-enhanced?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.7/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | +| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.7/meta-refresh-no-exceptions?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | ## Experimental Rules Rules we are still testing and developing. They are disabled by default in axe-core, but are enabled for the axe browser extensions. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------- | :------------------------- | :------------------------------------------------- | -| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.6/css-orientation-lock?application=RuleDescription) | Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | -| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.6/focus-order-semantics?application=RuleDescription) | Ensures elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, experimental | failure | | -| [hidden-content](https://dequeuniversity.com/rules/axe/4.6/hidden-content?application=RuleDescription) | Informs users about hidden content. | Minor | cat.structure, experimental, review-item, best-practice | failure, needs review | | -| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.6/label-content-name-mismatch?application=RuleDescription) | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | -| [p-as-heading](https://dequeuniversity.com/rules/axe/4.6/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, experimental | failure, needs review | | -| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.6/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g | failure | | -| [td-has-header](https://dequeuniversity.com/rules/axe/4.6/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g | failure | | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------- | +| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.7/css-orientation-lock?application=RuleDescription) | Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | +| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.7/focus-order-semantics?application=RuleDescription) | Ensures elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, experimental | failure | | +| [hidden-content](https://dequeuniversity.com/rules/axe/4.7/hidden-content?application=RuleDescription) | Informs users about hidden content. | Minor | cat.structure, experimental, review-item, best-practice | failure, needs review | | +| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.7/label-content-name-mismatch?application=RuleDescription) | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | +| [p-as-heading](https://dequeuniversity.com/rules/axe/4.7/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, experimental | failure, needs review | | +| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.7/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g | failure | | +| [td-has-header](https://dequeuniversity.com/rules/axe/4.7/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b | failure | | ## Deprecated Rules Deprecated rules are disabled by default and will be removed in the next major release. -_There are no matching rules_ +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :----------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | +| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.7/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, deprecated | failure, needs review | | +| [audio-caption](https://dequeuniversity.com/rules/axe/4.7/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | diff --git a/lib/checks/aria/aria-busy.json b/lib/checks/aria/aria-busy.json index 0de2031ce8..e97624d046 100644 --- a/lib/checks/aria/aria-busy.json +++ b/lib/checks/aria/aria-busy.json @@ -5,7 +5,7 @@ "impact": "serious", "messages": { "pass": "Element has an aria-busy attribute", - "fail": "Element has no aria-busy=\"true\" attribute" + "fail": "Element uses aria-busy=\"true\" while showing a loader" } } } diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index 3ba1c2b757..940c824b62 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -5,7 +5,77 @@ import { getOwnedVirtual } from '../../commons/aria'; import { getGlobalAriaAttrs } from '../../commons/standards'; -import { hasContentVirtual, idrefs, isFocusable } from '../../commons/dom'; +import { + hasContentVirtual, + idrefs, + isFocusable, + isVisibleToScreenReaders +} from '../../commons/dom'; + +/** + * Check that an element owns all required children for its explicit role. + * + * Required roles are taken from the `ariaRoles` standards object from the roles `requiredOwned` property. + * + * @memberof checks + * @param {Boolean} options.reviewEmpty List of ARIA roles that should be flagged as "Needs Review" rather than a violation if the element has no owned children. + * @data {String[]} List of all missing owned roles. + * @returns {Mixed} True if the element owns all required roles. Undefined if `options.reviewEmpty=true` and the element has no owned children. False otherwise. + */ +export default function ariaRequiredChildrenEvaluate( + node, + options, + virtualNode +) { + const reviewEmpty = + options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : []; + const role = getExplicitRole(virtualNode, { dpub: true }); + const required = requiredOwned(role); + if (required === null) { + return true; + } + + const ownedRoles = getOwnedRoles(virtualNode, required); + const unallowed = ownedRoles.filter(({ role }) => !required.includes(role)); + + if (unallowed.length) { + this.relatedNodes(unallowed.map(({ ownedElement }) => ownedElement)); + this.data({ + messageKey: 'unallowed', + values: unallowed + .map(({ ownedElement, attr }) => + getUnallowedSelector(ownedElement, attr) + ) + .filter((selector, index, array) => array.indexOf(selector) === index) + .join(', ') + }); + return false; + } + + const missing = missingRequiredChildren( + virtualNode, + role, + required, + ownedRoles + ); + if (!missing) { + return true; + } + + this.data(missing); + + // Only review empty nodes when a node is both empty and does not have an aria-owns relationship + if ( + reviewEmpty.includes(role) && + !hasContentVirtual(virtualNode, false, true) && + !ownedRoles.length && + (!virtualNode.hasAttr('aria-owns') || !idrefs(node, 'aria-owns').length) + ) { + return undefined; + } + + return false; +} /** * Get all owned roles of an element @@ -15,25 +85,33 @@ function getOwnedRoles(virtualNode, required) { const ownedElements = getOwnedVirtual(virtualNode); for (let i = 0; i < ownedElements.length; i++) { const ownedElement = ownedElements[i]; + if (ownedElement.props.nodeType !== 1) { + continue; + } + const role = getRole(ownedElement, { noPresentational: true }); - const hasGlobalAria = getGlobalAriaAttrs().some(attr => - ownedElement.hasAttr(attr) - ); - const hasGlobalAriaOrFocusable = hasGlobalAria || isFocusable(ownedElement); + const globalAriaAttr = getGlobalAriaAttr(ownedElement); + const hasGlobalAriaOrFocusable = + !!globalAriaAttr || isFocusable(ownedElement); // if owned node has no role or is presentational, or if role // allows group or rowgroup, we keep parsing the descendant tree. // this means intermediate roles between a required parent and // child will fail the check if ( + !isVisibleToScreenReaders(ownedElement) || (!role && !hasGlobalAriaOrFocusable) || (['group', 'rowgroup'].includes(role) && required.some(requiredRole => requiredRole === role)) ) { ownedElements.push(...ownedElement.children); } else if (role || hasGlobalAriaOrFocusable) { - ownedRoles.push({ role, ownedElement }); + ownedRoles.push({ + role, + attr: globalAriaAttr || 'tabindex', + ownedElement + }); } } @@ -61,58 +139,35 @@ function missingRequiredChildren(virtualNode, role, required, ownedRoles) { } /** - * Check that an element owns all required children for its explicit role. - * - * Required roles are taken from the `ariaRoles` standards object from the roles `requiredOwned` property. - * - * @memberof checks - * @param {Boolean} options.reviewEmpty List of ARIA roles that should be flagged as "Needs Review" rather than a violation if the element has no owned children. - * @data {String[]} List of all missing owned roles. - * @returns {Mixed} True if the element owns all required roles. Undefined if `options.reviewEmpty=true` and the element has no owned children. False otherwise. + * Get the first global ARIA attribute the element has. + * @param {VirtualNode} vNode + * @return {String|null} */ -function ariaRequiredChildrenEvaluate(node, options, virtualNode) { - const reviewEmpty = - options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : []; - const role = getExplicitRole(virtualNode, { dpub: true }); - const required = requiredOwned(role); - if (required === null) { - return true; - } +function getGlobalAriaAttr(vNode) { + return getGlobalAriaAttrs().find(attr => vNode.hasAttr(attr)); +} - const ownedRoles = getOwnedRoles(virtualNode, required); - const unallowed = ownedRoles.filter(({ role }) => !required.includes(role)); +/** + * Return a simple selector for an unallowed element. + * @param {VirtualNode} vNode + * @param {String} [attr] - Optional attribute which made the element unallowed + * @return {String} + */ +function getUnallowedSelector(vNode, attr) { + const { nodeName, nodeType } = vNode.props; - if (unallowed.length) { - this.relatedNodes(unallowed.map(({ ownedElement }) => ownedElement)); - this.data({ - messageKey: 'unallowed' - }); - return false; + if (nodeType === 3) { + return `#text`; } - const missing = missingRequiredChildren( - virtualNode, - role, - required, - ownedRoles - ); - if (!missing) { - return true; + const role = getExplicitRole(vNode, { dpub: true }); + if (role) { + return `[role=${role}]`; } - this.data(missing); - - // Only review empty nodes when a node is both empty and does not have an aria-owns relationship - if ( - reviewEmpty.includes(role) && - !hasContentVirtual(virtualNode, false, true) && - !ownedRoles.length && - (!virtualNode.hasAttr('aria-owns') || !idrefs(node, 'aria-owns').length) - ) { - return undefined; + if (attr) { + return nodeName + `[${attr}]`; } - return false; + return nodeName; } - -export default ariaRequiredChildrenEvaluate; diff --git a/lib/checks/aria/aria-required-children.json b/lib/checks/aria/aria-required-children.json index 38618dbc19..db1e2bf630 100644 --- a/lib/checks/aria/aria-required-children.json +++ b/lib/checks/aria/aria-required-children.json @@ -24,7 +24,7 @@ "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 (see related nodes)" + "unallowed": "Element has children which are not allowed: ${data.values}" }, "incomplete": { "singular": "Expecting ARIA child role to be added: ${data.values}", diff --git a/lib/checks/aria/valid-scrollable-semantics-evaluate.js b/lib/checks/aria/valid-scrollable-semantics-evaluate.js index 1eb53b6d7a..7c3615dbcc 100644 --- a/lib/checks/aria/valid-scrollable-semantics-evaluate.js +++ b/lib/checks/aria/valid-scrollable-semantics-evaluate.js @@ -17,6 +17,7 @@ const VALID_TAG_NAMES_FOR_SCROLLABLE_REGIONS = { */ const VALID_ROLES_FOR_SCROLLABLE_REGIONS = { application: true, + article: true, banner: false, complementary: true, contentinfo: true, diff --git a/lib/checks/keyboard/focusable-content.json b/lib/checks/keyboard/focusable-content.json index 9a91fff842..11db612fb3 100644 --- a/lib/checks/keyboard/focusable-content.json +++ b/lib/checks/keyboard/focusable-content.json @@ -2,7 +2,7 @@ "id": "focusable-content", "evaluate": "focusable-content-evaluate", "metadata": { - "impact": "moderate", + "impact": "serious", "messages": { "pass": "Element contains focusable elements", "fail": "Element should have focusable content" diff --git a/lib/checks/keyboard/focusable-element.json b/lib/checks/keyboard/focusable-element.json index 9045e78f92..da108b768d 100644 --- a/lib/checks/keyboard/focusable-element.json +++ b/lib/checks/keyboard/focusable-element.json @@ -2,7 +2,7 @@ "id": "focusable-element", "evaluate": "focusable-element-evaluate", "metadata": { - "impact": "moderate", + "impact": "serious", "messages": { "pass": "Element is focusable", "fail": "Element should be focusable" diff --git a/lib/checks/mobile/css-orientation-lock-evaluate.js b/lib/checks/mobile/css-orientation-lock-evaluate.js index bb42f0a3ef..798235cacb 100644 --- a/lib/checks/mobile/css-orientation-lock-evaluate.js +++ b/lib/checks/mobile/css-orientation-lock-evaluate.js @@ -103,22 +103,15 @@ function cssOrientationLockEvaluate(node, options, virtualNode, context) { const transformStyle = style.transform || style.webkitTransform || style.msTransform || false; - if (!transformStyle) { + if (!transformStyle && !style.rotate) { return false; } - /** - * get last match/occurrence of a transformation function that can affect rotation along Z axis - */ - const matches = transformStyle.match( - /(rotate|rotateZ|rotate3d|matrix|matrix3d)\(([^)]+)\)(?!.*(rotate|rotateZ|rotate3d|matrix|matrix3d))/ - ); - if (!matches) { - return false; - } + const transformDegrees = getTransformDegrees(transformStyle); + const rotateDegrees = getRotationInDegrees('rotate', style.rotate); - const [, transformFn, transformFnValue] = matches; - let degrees = getRotationInDegrees(transformFn, transformFnValue); + // `transform: rotate` and `rotate` are additive + let degrees = transformDegrees + rotateDegrees; if (!degrees) { return false; } @@ -134,6 +127,30 @@ function cssOrientationLockEvaluate(node, options, virtualNode, context) { return Math.abs(degrees - 90) % 90 <= degreeThreshold; } + /** + * Get the degree value of a transform. + * @property {Object} cssRule.style style + * @return {Number} + */ + function getTransformDegrees(transformStyle) { + if (!transformStyle) { + return 0; + } + + /** + * get last match/occurrence of a transformation function that can affect rotation along Z axis + */ + const matches = transformStyle.match( + /(rotate|rotateZ|rotate3d|matrix|matrix3d)\(([^)]+)\)(?!.*(rotate|rotateZ|rotate3d|matrix|matrix3d))/ + ); + if (!matches) { + return 0; + } + + const [, transformFn, transformFnValue] = matches; + return getRotationInDegrees(transformFn, transformFnValue); + } + /** * Interpolate rotation along the z axis from a given value to a transform function * @param {String} transformFunction CSS transformation function @@ -158,7 +175,7 @@ function cssOrientationLockEvaluate(node, options, virtualNode, context) { case 'matrix3d': return getAngleInDegreesFromMatrixTransform(transformFnValue); default: - return; + return 0; } } @@ -170,7 +187,7 @@ function cssOrientationLockEvaluate(node, options, virtualNode, context) { function getAngleInDegrees(angleWithUnit) { const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/) || []; if (!unit) { - return; + return 0; } const angle = parseFloat(angleWithUnit.replace(unit, ``)); diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index fb5fc7347f..3a3cc1a79b 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -3,7 +3,7 @@ import getImplicitRole from './implicit-role'; import getGlobalAriaAttrs from '../standards/get-global-aria-attrs'; import isFocusable from '../dom/is-focusable'; import { getNodeFromTree } from '../../core/utils'; -import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; // when an element inherits the presentational role from a parent // is not defined in the spec, but through testing it seems to be @@ -126,7 +126,7 @@ function hasConflictResolution(vNode) { */ function resolveRole(node, { noImplicit, ...roleOptions } = {}) { const vNode = - node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); + node instanceof AbstractVirtualNode ? node : getNodeFromTree(node); if (vNode.props.nodeType !== 1) { return null; } diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index d9f7bd4805..9eb5bb3a88 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -19,6 +19,7 @@ export { default as implicitNodes } from './implicit-nodes'; export { default as implicitRole } from './implicit-role'; export { default as isAccessibleRef } from './is-accessible-ref'; export { default as isAriaRoleAllowedOnElement } from './is-aria-role-allowed-on-element'; +export { default as isComboboxPopup } from './is-combobox-popup'; export { default as isUnsupportedRole } from './is-unsupported-role'; export { default as isValidRole } from './is-valid-role'; export { default as labelVirtual } from './label-virtual'; diff --git a/lib/commons/aria/is-combobox-popup.js b/lib/commons/aria/is-combobox-popup.js new file mode 100644 index 0000000000..bef80f83be --- /dev/null +++ b/lib/commons/aria/is-combobox-popup.js @@ -0,0 +1,55 @@ +import getRole from './get-role'; +import ariaAttrs from '../../standards/aria-attrs'; +import { getRootNode } from '../../core/utils'; + +/** + * Whether an element is the popup for a combobox + * @method isComboboxPopup + * @memberof axe.commons.aria + * @instance + * @param {VirtualNode} virtualNode + * @param {Object} options + * @property {String[]} popupRoles Overrides which roles can be popup. Defaults to aria-haspopup values + * @returns {boolean} + */ +export default function isComboboxPopup(virtualNode, { popupRoles } = {}) { + const role = getRole(virtualNode); + popupRoles ??= ariaAttrs['aria-haspopup'].values; + if (!popupRoles.includes(role)) { + return false; + } + + // in ARIA 1.1 the container has role=combobox + const vParent = nearestParentWithRole(virtualNode); + if (isCombobox(vParent)) { + return true; + } + + const { id } = virtualNode.props; + if (!id) { + return false; + } + + if (!virtualNode.actualNode) { + throw new Error('Unable to determine combobox popup without an actualNode'); + } + const root = getRootNode(virtualNode.actualNode); + const ownedCombobox = root.querySelectorAll( + // aria-owns was from ARIA 1.0, aria-controls was from ARIA 1.2 + `[aria-owns~="${id}"][role~="combobox"]:not(select), + [aria-controls~="${id}"][role~="combobox"]:not(select)` + ); + + return Array.from(ownedCombobox).some(isCombobox); +} + +const isCombobox = node => node && getRole(node) === 'combobox'; + +function nearestParentWithRole(vNode) { + while ((vNode = vNode.parent)) { + if (getRole(vNode, { noPresentational: true }) !== null) { + return vNode; + } + } + return null; +} diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index d4cdae2285..3077931839 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -1,68 +1,7 @@ import standards from '../../standards'; -/** - * Convert a CSS color value into a number - */ -function convertColorVal(colorFunc, value, index) { - if (/%$/.test(value)) { - // - if (index === 3) { - // alpha - return parseFloat(value) / 100; - } - return (parseFloat(value) * 255) / 100; - } - if (colorFunc[index] === 'h') { - // hue - if (/turn$/.test(value)) { - return parseFloat(value) * 360; - } - if (/rad$/.test(value)) { - return parseFloat(value) * 57.3; - } - } - return parseFloat(value); -} - -/** - * Convert HSL to RGB - */ -function hslToRgb([hue, saturation, lightness, alpha]) { - // Must be fractions of 1 - saturation /= 255; - lightness /= 255; - - const high = (1 - Math.abs(2 * lightness - 1)) * saturation; - const low = high * (1 - Math.abs(((hue / 60) % 2) - 1)); - const base = lightness - high / 2; - - let colors; - if (hue < 60) { - // red - yellow - colors = [high, low, 0]; - } else if (hue < 120) { - // yellow - green - colors = [low, high, 0]; - } else if (hue < 180) { - // green - cyan - colors = [0, high, low]; - } else if (hue < 240) { - // cyan - blue - colors = [0, low, high]; - } else if (hue < 300) { - // blue - purple - colors = [low, 0, high]; - } else { - // purple - red - colors = [high, 0, low]; - } - - return colors - .map(color => { - return Math.round((color + base) * 255); - }) - .concat(alpha); -} +const hexRegex = /^#[0-9a-f]{3,8}$/i; +const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i; /** * @class Color @@ -72,18 +11,20 @@ function hslToRgb([hue, saturation, lightness, alpha]) { * @param {number} blue * @param {number} alpha */ -function Color(red, green, blue, alpha = 1) { - /** @type {number} */ - this.red = red; +export default class Color { + constructor(red, green, blue, alpha = 1) { + /** @type {number} */ + this.red = red; - /** @type {number} */ - this.green = green; + /** @type {number} */ + this.green = green; - /** @type {number} */ - this.blue = blue; + /** @type {number} */ + this.blue = blue; - /** @type {number} */ - this.alpha = alpha; + /** @type {number} */ + this.alpha = alpha; + } /** * Provide the hex string value for the color @@ -92,7 +33,7 @@ function Color(red, green, blue, alpha = 1) { * @instance * @return {string} */ - this.toHexString = function toHexString() { + toHexString() { var redString = Math.round(this.red).toString(16); var greenString = Math.round(this.green).toString(16); var blueString = Math.round(this.blue).toString(16); @@ -102,15 +43,12 @@ function Color(red, green, blue, alpha = 1) { (this.green > 15.5 ? greenString : '0' + greenString) + (this.blue > 15.5 ? blueString : '0' + blueString) ); - }; + } - this.toJSON = function toJSON() { + toJSON() { const { red, green, blue, alpha } = this; return { red, green, blue, alpha }; - }; - - const hexRegex = /^#[0-9a-f]{3,8}$/i; - const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i; + } /** * Parse any valid color string and assign its values to "this" @@ -118,7 +56,7 @@ function Color(red, green, blue, alpha = 1) { * @memberof axe.commons.color.Color * @instance */ - this.parseString = function parseString(colorString) { + parseString(colorString) { // IE occasionally returns named colors instead of RGB(A) values if (standards.cssColors[colorString] || colorString === 'transparent') { const [red, green, blue] = standards.cssColors[colorString] || [0, 0, 0]; @@ -139,7 +77,7 @@ function Color(red, green, blue, alpha = 1) { return this; } throw new Error(`Unable to parse color "${colorString}"`); - }; + } /** * Set the color value based on a CSS RGB/RGBA string @@ -149,7 +87,7 @@ function Color(red, green, blue, alpha = 1) { * @instance * @param {string} rgb The string value */ - this.parseRgbString = function parseRgbString(colorString) { + parseRgbString(colorString) { // IE can pass transparent as value instead of rgba if (colorString === 'transparent') { this.red = 0; @@ -159,7 +97,7 @@ function Color(red, green, blue, alpha = 1) { return; } this.parseColorFnString(colorString); - }; + } /** * Set the color value based on a CSS RGB/RGBA string @@ -169,7 +107,7 @@ function Color(red, green, blue, alpha = 1) { * @instance * @param {string} rgb The string value */ - this.parseHexString = function parseHexString(colorString) { + parseHexString(colorString) { if (!colorString.match(hexRegex) || [6, 8].includes(colorString.length)) { return; } @@ -191,7 +129,7 @@ function Color(red, green, blue, alpha = 1) { } else { this.alpha = 1; } - }; + } /** * Set the color value based on a CSS RGB/RGBA string @@ -201,7 +139,7 @@ function Color(red, green, blue, alpha = 1) { * @instance * @param {string} rgb The string value */ - this.parseColorFnString = function parseColorFnString(colorString) { + parseColorFnString(colorString) { const [, colorFunc, colorValStr] = colorString.match(colorFnRegex) || []; if (!colorFunc || !colorValStr) { return; @@ -226,7 +164,7 @@ function Color(red, green, blue, alpha = 1) { this.green = colorNums[1]; this.blue = colorNums[2]; this.alpha = typeof colorNums[3] === 'number' ? colorNums[3] : 1; - }; + } /** * Get the relative luminance value @@ -236,7 +174,7 @@ function Color(red, green, blue, alpha = 1) { * @instance * @return {number} The luminance value, ranges from 0 to 1 */ - this.getRelativeLuminance = function getRelativeLuminance() { + getRelativeLuminance() { var rSRGB = this.red / 255; var gSRGB = this.green / 255; var bSRGB = this.blue / 255; @@ -249,7 +187,69 @@ function Color(red, green, blue, alpha = 1) { bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; - }; + } +} + +/** + * Convert a CSS color value into a number + */ +function convertColorVal(colorFunc, value, index) { + if (/%$/.test(value)) { + // + if (index === 3) { + // alpha + return parseFloat(value) / 100; + } + return (parseFloat(value) * 255) / 100; + } + if (colorFunc[index] === 'h') { + // hue + if (/turn$/.test(value)) { + return parseFloat(value) * 360; + } + if (/rad$/.test(value)) { + return parseFloat(value) * 57.3; + } + } + return parseFloat(value); } -export default Color; +/** + * Convert HSL to RGB + */ +function hslToRgb([hue, saturation, lightness, alpha]) { + // Must be fractions of 1 + saturation /= 255; + lightness /= 255; + + const high = (1 - Math.abs(2 * lightness - 1)) * saturation; + const low = high * (1 - Math.abs(((hue / 60) % 2) - 1)); + const base = lightness - high / 2; + + let colors; + if (hue < 60) { + // red - yellow + colors = [high, low, 0]; + } else if (hue < 120) { + // yellow - green + colors = [low, high, 0]; + } else if (hue < 180) { + // green - cyan + colors = [0, high, low]; + } else if (hue < 240) { + // cyan - blue + colors = [0, low, high]; + } else if (hue < 300) { + // blue - purple + colors = [low, 0, high]; + } else { + // purple - red + colors = [high, 0, low]; + } + + return colors + .map(color => { + return Math.round((color + base) * 255); + }) + .concat(alpha); +} diff --git a/lib/commons/color/filtered-rect-stack.js b/lib/commons/color/filtered-rect-stack.js index bca9f3b43d..f07cc0579c 100644 --- a/lib/commons/color/filtered-rect-stack.js +++ b/lib/commons/color/filtered-rect-stack.js @@ -3,6 +3,7 @@ import incompleteData from './incomplete-data'; /** * Get filtered stack of block and inline elements, excluding line breaks + * @deprecated use color.getBackgroundStack instead * @method filteredRectStack * @memberof axe.commons.color * @param {Element} elm diff --git a/lib/commons/color/flatten-colors.js b/lib/commons/color/flatten-colors.js index 2a780c57c3..9ac19f0d11 100644 --- a/lib/commons/color/flatten-colors.js +++ b/lib/commons/color/flatten-colors.js @@ -88,37 +88,41 @@ function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) { * @method flattenColors * @memberof axe.commons.color.Color * @instance - * @param {Color} fgColor Foreground color - * @param {Color} bgColor Background color + * @param {Color} sourceColor Foreground color + * @param {Color} backdrop Background color * @return {Color} Blended color */ -function flattenColors(fgColor, bgColor, blendMode = 'normal') { +function flattenColors(sourceColor, backdrop, blendMode = 'normal') { // foreground is the "source" color and background is the "backdrop" color const r = simpleAlphaCompositing( - fgColor.red, - fgColor.alpha, - bgColor.red, - bgColor.alpha, + sourceColor.red, + sourceColor.alpha, + backdrop.red, + backdrop.alpha, blendMode ); const g = simpleAlphaCompositing( - fgColor.green, - fgColor.alpha, - bgColor.green, - bgColor.alpha, + sourceColor.green, + sourceColor.alpha, + backdrop.green, + backdrop.alpha, blendMode ); const b = simpleAlphaCompositing( - fgColor.blue, - fgColor.alpha, - bgColor.blue, - bgColor.alpha, + sourceColor.blue, + sourceColor.alpha, + backdrop.blue, + backdrop.alpha, blendMode ); // formula: αo = αs + αb x (1 - αs) // clamp alpha between 0 and 1 - const αo = clamp(fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha), 0, 1); + const αo = clamp( + sourceColor.alpha + backdrop.alpha * (1 - sourceColor.alpha), + 0, + 1 + ); if (αo === 0) { return new Color(r, g, b, αo); } diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index f564554517..262f90fe1b 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -8,6 +8,7 @@ import flattenShadowColors from './flatten-shadow-colors'; import getTextShadowColors from './get-text-shadow-colors'; import getVisibleChildTextRects from '../dom/get-visible-child-text-rects'; import { getNodeFromTree } from '../../core/utils'; +import { getStackingContext, stackingContextToColor } from './stacking-context'; /** * Returns background color for element @@ -47,29 +48,32 @@ export default function getBackgroundColor( } function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) { + const elmStack = getBackgroundStack(elm); + if (!elmStack) { + return null; + } + + const textRects = getVisibleChildTextRects(elm); let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); if (bgColors.length) { bgColors = [{ color: bgColors.reduce(flattenShadowColors) }]; } - const elmStack = getBackgroundStack(elm); - const textRects = getVisibleChildTextRects(elm); - // Search the stack until we have an alpha === 1 background - (elmStack || []).some(bgElm => { + for (let i = 0; i < elmStack.length; i++) { + const bgElm = elmStack[i]; const bgElmStyle = window.getComputedStyle(bgElm); if (elementHasImage(bgElm, bgElmStyle)) { - bgColors = null; bgElms.push(bgElm); - return true; + return null; } // Get the background color const bgColor = getOwnBackgroundColor(bgElmStyle); if (bgColor.alpha === 0) { - return false; + continue; } // abort if a node is partially obscured and obscuring element has a background @@ -77,29 +81,24 @@ function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) { bgElmStyle.getPropertyValue('display') !== 'inline' && !fullyEncompasses(bgElm, textRects) ) { - bgColors = null; bgElms.push(bgElm); incompleteData.set('bgColor', 'elmPartiallyObscured'); - return true; + return null; } // store elements contributing to the bg color. bgElms.push(bgElm); - const blendMode = bgElmStyle.getPropertyValue('mix-blend-mode'); - bgColors.unshift({ - color: bgColor, - blendMode: normalizeBlendMode(blendMode) - }); // Exit if the background is opaque - return bgColor.alpha === 1; - }); - - if (bgColors === null || elmStack === null) { - return null; + if (bgColor.alpha === 1) { + break; + } } + const stackingContext = getStackingContext(elm, elmStack); + bgColors = stackingContext.map(stackingContextToColor).concat(bgColors); + const pageBgs = getPageBackgroundColors( elm, elmStack.includes(document.body) @@ -166,6 +165,7 @@ function fullyEncompasses(node, rects) { function normalizeBlendMode(blendmode) { return !!blendmode ? blendmode : undefined; } + /** * Get the page background color. * @private diff --git a/lib/commons/color/get-foreground-color.js b/lib/commons/color/get-foreground-color.js index 91eb784be2..dca800421e 100644 --- a/lib/commons/color/get-foreground-color.js +++ b/lib/commons/color/get-foreground-color.js @@ -3,7 +3,7 @@ import getBackgroundColor from './get-background-color'; import incompleteData from './incomplete-data'; import flattenColors from './flatten-colors'; import getTextShadowColors from './get-text-shadow-colors'; -import { getNodeFromTree } from '../../core/utils'; +import { getStackingContext, stackingContextToColor } from './stacking-context'; /** * Returns the flattened foreground color of an element, or null if it can't be determined because @@ -21,42 +21,49 @@ import { getNodeFromTree } from '../../core/utils'; */ export default function getForegroundColor(node, _, bgColor, options = {}) { const nodeStyle = window.getComputedStyle(node); - const opacity = getOpacity(node, nodeStyle); - // Start with -webkit-text-stroke, it is rendered on top - const strokeColor = getStrokeColor(nodeStyle, options); - if (strokeColor && strokeColor.alpha * opacity === 1) { - strokeColor.alpha = 1; - return strokeColor; - } + const colorStack = [ + // Start with -webkit-text-stroke, it is rendered on top + () => getStrokeColor(nodeStyle, options), + // Next color / -webkit-text-fill-color + () => getTextColor(nodeStyle), + // If text is (semi-)transparent shadows are visible through it + () => getTextShadowColors(node, { minRatio: 0 }) + ]; + let fgColors = []; - // Next color / -webkit-text-fill-color - const textColor = getTextColor(nodeStyle); - let fgColor = strokeColor ? flattenColors(strokeColor, textColor) : textColor; - if (fgColor.alpha * opacity === 1) { - fgColor.alpha = 1; - return fgColor; - } + for (const colorFn of colorStack) { + const color = colorFn(); + if (!color) { + continue; + } + + fgColors = fgColors.concat(color); - // If text is (semi-)transparent shadows are visible through it. - const textShadowColors = getTextShadowColors(node, { minRatio: 0 }); - fgColor = textShadowColors.reduce((colorA, colorB) => { - return flattenColors(colorA, colorB); - }, fgColor); - if (fgColor.alpha * opacity === 1) { - fgColor.alpha = 1; - return fgColor; + if (color.alpha === 1) { + break; + } } - // Lastly, if text opacity still isn't at 1, blend the background + const fgColor = fgColors.reduce((source, backdrop) => { + return flattenColors(source, backdrop); + }); + + // Lastly blend the background bgColor ??= getBackgroundColor(node, []); if (bgColor === null) { const reason = incompleteData.get('bgColor'); incompleteData.set('fgColor', reason); return null; } - fgColor.alpha = fgColor.alpha * opacity; - return flattenColors(fgColor, bgColor); + + const stackingContexts = getStackingContext(node); + const context = findNodeInContexts(stackingContexts, node); + return flattenColors( + calculateBlendedForegroundColor(fgColor, context, stackingContexts), + // default page background + new Color(255, 255, 255, 1) + ); } function getTextColor(nodeStyle) { @@ -83,26 +90,73 @@ function getStrokeColor(nodeStyle, { textStrokeEmMin = 0 }) { return new Color().parseString(strokeColor); } -function getOpacity(node, nodeStyle) { - if (!node) { - return 1; - } +/** + * Blend a foreground color into the background stacking context, taking into account opacity at each step. + * @param {Color} fgColor + * @param {Object} context - The nodes stacking context + * @param {Object[]} stackingContexts - Array of all stacking contexts + * @return {Color} + */ +function calculateBlendedForegroundColor(fgColor, context, stackingContexts) { + while (context) { + // find the nearest ancestor that has opacity < 1 + if (context.opacity === 1 && context.ancestor) { + context = context.ancestor; + continue; + } - const vNode = getNodeFromTree(node); - if (vNode && vNode._opacity !== undefined && vNode._opacity !== null) { - return vNode._opacity; - } + fgColor.alpha *= context.opacity; - nodeStyle ??= window.getComputedStyle(node); - const opacity = nodeStyle.getPropertyValue('opacity'); - const finalOpacity = opacity * getOpacity(node.parentElement); + // when blending the foreground color to a background color with opacity, + // we ignore the background color of the node itself and instead blend + // with the stack behind it + let stack = context.ancestor?.descendants || stackingContexts; + if (context.opacity !== 1) { + stack = stack.slice(0, stack.indexOf(context)); + } - // cache the results of the getOpacity check on the parent tree - // so we don't have to look at the parent tree again for all its - // descendants - if (vNode) { - vNode._opacity = finalOpacity; + const bgColors = stack.map(stackingContextToColor); + + if (!bgColors.length) { + context = context.ancestor; + continue; + } + + const bgColor = bgColors.reduce( + (backdrop, source) => { + return flattenColors( + source.color, + backdrop.color instanceof Color ? backdrop.color : backdrop + ); + }, + { + color: new Color(0, 0, 0, 0), + blendMode: 'normal' + } + ); + + fgColor = flattenColors(fgColor, bgColor); + context = context.ancestor; } - return finalOpacity; + return fgColor; +} + +/** + * Find the stacking context that belongs to the passed in node + * @param {Object} contexts - Array of stacking contexts + * @param {Element} node + * @returns {Object} + */ +function findNodeInContexts(contexts, node) { + for (const context of contexts) { + if (context.vNode?.actualNode === node) { + return context; + } + + const found = findNodeInContexts(context.descendants, node); + if (found) { + return found; + } + } } diff --git a/lib/commons/color/get-rect-stack.js b/lib/commons/color/get-rect-stack.js index c608f34463..0f2274228f 100644 --- a/lib/commons/color/get-rect-stack.js +++ b/lib/commons/color/get-rect-stack.js @@ -3,6 +3,7 @@ import getTextElementStack from '../dom/get-text-element-stack'; /** * Get relevant stacks of block and inline elements, excluding line breaks + * @deprecated use color.getBackgroundStack instead * @method getRectStack * @memberof axe.commons.color * @param {Element} elm diff --git a/lib/commons/color/index.js b/lib/commons/color/index.js index 156cb3eded..78cc26c68b 100644 --- a/lib/commons/color/index.js +++ b/lib/commons/color/index.js @@ -19,3 +19,4 @@ export { default as getRectStack } from './get-rect-stack'; export { default as hasValidContrastRatio } from './has-valid-contrast-ratio'; export { default as incompleteData } from './incomplete-data'; export { default as getTextShadowColors } from './get-text-shadow-colors'; +export { getStackingContext, stackingContextToColor } from './stacking-context'; diff --git a/lib/commons/color/stacking-context.js b/lib/commons/color/stacking-context.js new file mode 100644 index 0000000000..05c49e5745 --- /dev/null +++ b/lib/commons/color/stacking-context.js @@ -0,0 +1,230 @@ +import { getNodeFromTree } from '../../core/utils'; +import getBackgroundStack from './get-background-stack'; +import Color from './color'; +import flattenColors from './flatten-colors'; + +/** + * Create a stacking context hierarchy tree for an element. This structure closely mimics the painting order of a page. + * @see https://www.w3.org/TR/CSS22/zindex.html#painting-order + * + * @example + * Given the following HTML structure: + * + *
+ *
Text
+ *
+ * + * Produces the following stacking context tree. Since the #parent element creates a stacking context due to `opacity`, the #target element's stacking context belongs under the #parent's context. + * + * [ + * { + * vNode: #parent, + * opacity: 0.8, + * blendMode: 'normal', + * bgColor: Color(255,0,0,1), + * descendants: [ + * { + * vNode: #target, + * opacity: 1, + * blendMode: 'normal', + * bgColor: Color(0,255,0,0.5), + * descendants: [] + * } + * ] + * } + * ] + * + * The stacking context hierarchy tree does not mimic the HTML structure. That is, elements that are on the same context level are siblings in the stacking context tree even if they have a parent/child HTML relationship. + * + * For example, given the following HTML structure: + * + *
+ *
+ *

Hello World

+ *
+ *
+ *

Lorium ipsum dolores...

+ * + * Produces the following tree structure: + * + * body + * - main + * - header + * - h1 + * - span + * - p + * - a + * + * @param {Node} elm + * @param {Node[]} [elmStack] - Optional element stack array to save on computing it again. + * @return {Object} + */ +export function getStackingContext(elm, elmStack) { + const vNode = getNodeFromTree(elm); + if (vNode._stackingContext) { + return vNode._stackingContext; + } + + const stackingContext = []; + const contextMap = new Map(); + elmStack = elmStack ?? getBackgroundStack(elm); + + elmStack.forEach(bgElm => { + const bgVNode = getNodeFromTree(bgElm); + const bgColor = getOwnBackgroundColor(bgVNode); + + /* + remove the ROOT_ORDER element to treat all root stacks and first-order + stacks at the same level (instead of nesting the first-order stack inside + the root stack) + + e.g. an element that creates a non-positioned stacking context at the + root level should be a sibling to root level elements that do not create + a stacking context + */ + const stackingOrder = bgVNode._stackingOrder.filter(({ vNode }) => !!vNode); + + // create a stacking context for each node in the stacking order + stackingOrder.forEach(({ vNode }, index) => { + const ancestorVNode = stackingOrder[index - 1]?.vNode; + const context = addToStackingContext(contextMap, vNode, ancestorVNode); + + if (index === 0 && !contextMap.get(vNode)) { + stackingContext.unshift(context); + } + contextMap.set(vNode, context); + }); + + // create a stacking context for the current node + const ancestorVNode = stackingOrder[stackingOrder.length - 1]?.vNode; + const context = addToStackingContext(contextMap, bgVNode, ancestorVNode); + if (!stackingOrder.length) { + stackingContext.unshift(context); + } + + // only assign the color to the current node so we don't apply any + // background colors from ancestor nodes that are not part of the element + // stack + context.bgColor = bgColor; + }); + + vNode._stackingContext = stackingContext; + return stackingContext; +} + +/** + * Transform a stacking context object into a Color. + * @param {Object} context + * @return {Object} + */ +export function stackingContextToColor(context) { + if (!context.descendants?.length) { + const color = context.bgColor; + color.alpha *= context.opacity; + + return { + color, + blendMode: context.blendMode + }; + } + + const sourceColor = context.descendants.reduce( + reduceToColor, + // ensure an array with a single context is reduced to a color by passing + // in an empty stacking context + createStackingContext() + ); + const color = flattenColors( + sourceColor, + context.bgColor, + context.descendants[0].blendMode + ); + color.alpha *= context.opacity; + + // carry forward the mix-blind-mode property so background color algorithm + // can use it to flatten multiple contexts together + return { + color, + blendMode: context.blendMode + }; +} + +/** + * Reduce two context objects into a Color by blending them together + * @param {Object} backdropContext + * @param {Object} sourceContext + * @return {Color} + */ +function reduceToColor(backdropContext, sourceContext) { + let backdrop; + if (backdropContext instanceof Color) { + backdrop = backdropContext; + } else { + backdrop = stackingContextToColor(backdropContext).color; + } + + const sourceColor = stackingContextToColor(sourceContext).color; + return flattenColors(sourceColor, backdrop, sourceContext.blendMode); +} + +/** + * Create a stacking context object for a virtual node. + * @param {VirtualNode} vNode + * @param {Object} ancestorContext + * @return {Object} + */ +function createStackingContext(vNode, ancestorContext) { + return { + vNode: vNode, + ancestor: ancestorContext, + opacity: parseFloat(vNode?.getComputedStylePropertyValue('opacity') ?? 1), + bgColor: new Color(0, 0, 0, 0), + blendMode: normalizeBlendMode( + vNode?.getComputedStylePropertyValue('mix-blend-mode') + ), + descendants: [] + }; +} + +/** + * Normalize a mix-blend-mode CSS value + * @param {String} blendmode + * @return {String|undefined} + */ +function normalizeBlendMode(blendmode) { + return !!blendmode ? blendmode : undefined; +} + +/** + * Create a stacking context for a virtual node and add it as a descendant of an ancestor's context. + * @param {Map} contextMap + * @param {VirtualNode} vNode + * @param {VirtualNode} ancestorVNode + * @return {Object} + */ +function addToStackingContext(contextMap, vNode, ancestorVNode) { + const ancestorContext = contextMap.get(ancestorVNode); + const context = + contextMap.get(vNode) ?? createStackingContext(vNode, ancestorContext); + if ( + ancestorContext && + ancestorVNode !== vNode && + !ancestorContext.descendants.includes(context) + ) { + ancestorContext.descendants.unshift(context); + } + + return context; +} + +/** + * Get the background color for a virtual node + * @param {VirtualNode} vNode + * @return {Color} + */ +function getOwnBackgroundColor(vNode) { + const bgColor = new Color(); + bgColor.parseString(vNode.getComputedStylePropertyValue('background-color')); + + return bgColor; +} diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 509fbba5f9..465acdc0f2 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -7,6 +7,12 @@ import constants from '../../core/constants'; import cache from '../../core/base/cache'; import assert from '../../core/utils/assert'; +const ROOT_ORDER = 0; +const DEFAULT_ORDER = 0.1; +const FLOAT_ORDER = 0.2; +const POSITION_STATIC_ORDER = 0.3; +let nodeIndex = 0; + /** * Setup the 2d grid and add every element to it, even elements not * included in the flat tree @@ -32,7 +38,8 @@ export default function createGrid( vNode = new VirtualNode(document.documentElement); } - vNode._stackingOrder = [0]; + nodeIndex = 0; + vNode._stackingOrder = [createContext(ROOT_ORDER, null)]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -57,11 +64,11 @@ export default function createGrid( if (vNode && vNode.parent) { parentVNode = vNode.parent; } - // elements with an assigned slot need to be a child of the slot element + // Elements with an assigned slot need to be a child of the slot element else if (node.assignedSlot) { parentVNode = getNodeFromTree(node.assignedSlot); } - // an svg in IE11 does not have a parentElement but instead has a + // An SVG in IE11 does not have a parentElement but instead has a // parentNode. but parentNode could be a shadow root so we need to // verify it's in the tree first else if (node.parentElement) { @@ -74,7 +81,7 @@ export default function createGrid( vNode = new axe.VirtualNode(node, parentVNode); } - vNode._stackingOrder = getStackingOrder(vNode, parentVNode); + vNode._stackingOrder = createStackingOrder(vNode, parentVNode, nodeIndex++); const scrollRegionParent = findScrollRegionParent(vNode, parentVNode); const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; @@ -221,69 +228,106 @@ function isStackingContext(vNode, parentVNode) { } // a flex item or gird item with a z-index value other than "auto", that is the parent element display: flex|inline-flex|grid|inline-grid, - if (zIndex !== 'auto' && parentVNode) { - const parentDsiplay = parentVNode.getComputedStylePropertyValue('display'); - if ( - [ - 'flex', - 'inline-flex', - 'inline flex', - 'grid', - 'inline-grid', - 'inline grid' - ].includes(parentDsiplay) - ) { - return true; - } + if (zIndex !== 'auto' && isFlexOrGridContainer(parentVNode)) { + return true; } return false; } +/** + * Determine if element is a flex or grid container. + * @param {VirtualNode} vNode + * @return {Boolean} + */ +function isFlexOrGridContainer(vNode) { + if (!vNode) { + return false; + } + + const display = vNode.getComputedStylePropertyValue('display'); + return ['flex', 'inline-flex', 'grid', 'inline-grid'].includes(display); +} + /** * Determine the stacking order of an element. The stacking order is an array of * zIndex values for each stacking context parent. - * @param {VirtualNode} + * @param {VirtualNode} vNode + * @param {VirtualNode} parentVNode + * @param {Number} nodeIndex * @return {Number[]} */ -function getStackingOrder(vNode, parentVNode) { +function createStackingOrder(vNode, parentVNode, nodeIndex) { const stackingOrder = parentVNode._stackingOrder.slice(); - const zIndex = vNode.getComputedStylePropertyValue('z-index'); - const positioned = - vNode.getComputedStylePropertyValue('position') !== 'static'; - const floated = vNode.getComputedStylePropertyValue('float') !== 'none'; - - if (positioned && !['auto', '0'].includes(zIndex)) { - // if a positioned element has a z-index > 0, find the first - // true stack (not a "fake" stack created from positioned or - // floated elements without a z-index) and create a new stack at - // that point (step #5 and step #8) - // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - while (stackingOrder.find(value => value % 1 !== 0)) { - const index = stackingOrder.findIndex(value => value % 1 !== 0); - stackingOrder.splice(index, 1); - } - stackingOrder[stackingOrder.length - 1] = parseInt(zIndex); - } - if (isStackingContext(vNode, parentVNode)) { - stackingOrder.push(0); - } + // if a positioned element has z-index: auto or 0 (step #8), or if // a non-positioned floating element (step #5), treat it as its // own stacking context // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - else if (positioned) { - // Put positioned elements above floated elements - stackingOrder.push(0.5); - } else if (floated) { - // Put floated elements above z-index: 0 - // (step #5 floating get sorted below step #8 positioned) - stackingOrder.push(0.25); + if (!isStackingContext(vNode, parentVNode)) { + if (vNode.getComputedStylePropertyValue('position') !== 'static') { + // Put positioned elements above floated elements + stackingOrder.push(createContext(POSITION_STATIC_ORDER, vNode)); + } else if (vNode.getComputedStylePropertyValue('float') !== 'none') { + // Put floated elements above z-index: 0 + // (step #5 floating get sorted below step #8 positioned) + stackingOrder.push(createContext(FLOAT_ORDER, vNode)); + } + return stackingOrder; + } + + // if an element creates a stacking context, find the first + // true stack (not a "fake" stack created from positioned or + // floated elements without a z-index) and create a new stack at + // that point (step #5 and step #8) + // @see https://www.w3.org/Style/css2-updates/css2/zindex.html + const index = stackingOrder.findIndex(({ value }) => + [ROOT_ORDER, FLOAT_ORDER, POSITION_STATIC_ORDER].includes(value) + ); + if (index !== -1) { + stackingOrder.splice(index, stackingOrder.length - index); + } + + const zIndex = getRealZIndex(vNode, parentVNode); + if (!['auto', '0'].includes(zIndex)) { + stackingOrder.push(createContext(parseInt(zIndex), vNode)); + return stackingOrder; } + // since many things can create a new stacking context without position or + // z-index, we need to know the order in the dom to sort them by. Use the + // nodeIndex property to create a number less than the "fake" stacks from + // positioned or floated elements but still larger than 0 + // 10 pad gives us the ability to sort up to 1B nodes (padStart does not + // exist in ie11) + let float = nodeIndex.toString(); + while (float.length < 10) { + float = '0' + float; + } + stackingOrder.push( + createContext(parseFloat(`${DEFAULT_ORDER}${float}`), vNode) + ); return stackingOrder; } +function createContext(value, vNode) { + return { + value, + vNode + }; +} + +function getRealZIndex(vNode, parentVNode) { + const position = vNode.getComputedStylePropertyValue('position'); + if (position === 'static' && !isFlexOrGridContainer(parentVNode)) { + // z-index is ignored on position:static, except if on a flex or grid + // @see https://www.w3.org/TR/css-flexbox-1/#painting + // @see https://www.w3.org/TR/css-grid-1/#z-order + return 'auto'; + } + return vNode.getComputedStylePropertyValue('z-index'); +} + /** * Return the parent node that is a scroll region. * @param {VirtualNode} diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index 427c1aa0c0..8ba7be8855 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -1,6 +1,8 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree } from '../../core/utils'; import isHiddenForEveryone from './is-hidden-for-everyone'; +import isInert from './is-inert'; + // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled const allowedDisabledNodeNames = [ 'button', @@ -27,8 +29,9 @@ function focusDisabled(el) { const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); if ( - isDisabledAttrAllowed(vNode.props.nodeName) && - vNode.hasAttr('disabled') + (isDisabledAttrAllowed(vNode.props.nodeName) && + vNode.hasAttr('disabled')) || + isInert(vNode) ) { return true; } diff --git a/lib/commons/dom/get-modal-dialog.js b/lib/commons/dom/get-modal-dialog.js new file mode 100644 index 0000000000..df9a87f9ba --- /dev/null +++ b/lib/commons/dom/get-modal-dialog.js @@ -0,0 +1,120 @@ +import memoize from '../../core/utils/memoize'; +import { querySelectorAllFilter } from '../../core/utils'; +import isVisibleOnScreen from './is-visible-on-screen'; +import createGrid from './create-grid'; +import getIntersectionRect from '../math/get-intersection-rect'; + +/** + * Determine if a dialog element is opened as a modal. Currently there are no APIs to determine this so we'll use a bit of a hacky solution that has known issues. + * This can tell us that a dialog element is open but it cannot tell us which one is the top layer, nor which one is visually on top. Nested dialogs that are opened using both `.show` and`.showModal` can cause issues as well. + * @see https://github.com/dequelabs/axe-core/issues/3463 + * @return {VirtualNode|Null} The modal dialog virtual node or null if none are found + */ +const getModalDialog = memoize(function getModalDialogMemoized() { + // this is here for tests so we don't have + // to set up the virtual tree when code + // isn't testing this bit + if (!axe._tree) { + return null; + } + + const dialogs = querySelectorAllFilter( + // TODO: es-module-_tree + axe._tree[0], + 'dialog[open]', + vNode => { + const rect = vNode.boundingClientRect; + const stack = document.elementsFromPoint(rect.left + 1, rect.top + 1); + return stack.includes(vNode.actualNode) && isVisibleOnScreen(vNode); + } + ); + + if (!dialogs.length) { + return null; + } + + // for Chrome and Firefox, look to see if + // elementsFromPoint returns the dialog + // when checking outside its bounds + const modalDialog = dialogs.find(dialog => { + const rect = dialog.boundingClientRect; + const stack = document.elementsFromPoint(rect.left - 10, rect.top - 10); + + return stack.includes(dialog.actualNode); + }); + + if (modalDialog) { + return modalDialog; + } + + // fallback for Safari, look at the grid to + // find a node to check as elementsFromPoint + // does not return inert nodes + return ( + dialogs.find(dialog => { + const { vNode, rect } = getNodeFromGrid(dialog) ?? {}; + if (!vNode) { + return false; + } + + const stack = document.elementsFromPoint(rect.left + 1, rect.top + 1); + return !stack.includes(vNode.actualNode); + }) ?? null + ); +}); +export default getModalDialog; + +/** + * Find the first non-html from the grid to use as a test for elementsFromPoint + * @return {Object} + */ +function getNodeFromGrid(dialog) { + createGrid(); + // TODO: es-module-_tree + const grid = axe._tree[0]._grid; + const viewRect = new window.DOMRect( + 0, + 0, + window.innerWidth, + window.innerHeight + ); + + if (!grid) { + return; + } + + for (let row = 0; row < grid.cells.length; row++) { + const cols = grid.cells[row]; + if (!cols) { + continue; + } + + for (let col = 0; col < cols.length; col++) { + const cells = cols[col]; + if (!cells) { + continue; + } + + for (let i = 0; i < cells.length; i++) { + const vNode = cells[i]; + const rect = vNode.boundingClientRect; + const intersection = getIntersectionRect(rect, viewRect); + + if ( + // html is always returned from + // elementsFromPoint + vNode.props.nodeName !== 'html' && + vNode !== dialog && + vNode.getComputedStylePropertyValue('pointer-events') !== 'none' && + // ensure the element is visible in + // the current viewport for + // elementsFromPoint so we don't have + // to scroll + intersection + ) { + return { vNode, rect: intersection }; + } + } + } + } +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 20a972cb47..9e5f16a741 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -12,6 +12,7 @@ export { default as getComposedParent } from './get-composed-parent'; export { default as getElementByReference } from './get-element-by-reference'; export { default as getElementCoordinates } from './get-element-coordinates'; export { default as getElementStack } from './get-element-stack'; +export { default as getModalDialog } from './get-modal-dialog'; export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-ancestors'; export { default as getRootNode } from './get-root-node'; export { default as getScrollOffset } from './get-scroll-offset'; @@ -31,6 +32,7 @@ export { default as isHiddenForEveryone } from './is-hidden-for-everyone'; export { default as isHTML5 } from './is-html5'; export { default as isInTabOrder } from './is-in-tab-order'; export { default as isInTextBlock } from './is-in-text-block'; +export { default as isInert } from './is-inert'; export { default as isModalOpen } from './is-modal-open'; export { default as isMultiline } from './is-multiline'; export { default as isNativelyFocusable } from './is-natively-focusable'; @@ -38,7 +40,7 @@ export { default as isNode } from './is-node'; export { default as isOffscreen } from './is-offscreen'; export { default as isOpaque } from './is-opaque'; export { default as isSkipLink } from './is-skip-link'; -export { default as isVisibleToScreenReaders } from './is-visible-for-screenreader'; +export { default as isVisibleToScreenReaders } from './is-visible-to-screenreader'; export { default as isVisibleOnScreen } from './is-visible-on-screen'; export { default as isVisible } from './is-visible'; export { default as isVisualContent } from './is-visual-content'; diff --git a/lib/commons/dom/is-inert.js b/lib/commons/dom/is-inert.js new file mode 100644 index 0000000000..e0a0270908 --- /dev/null +++ b/lib/commons/dom/is-inert.js @@ -0,0 +1,56 @@ +import memoize from '../../core/utils/memoize'; +import getModalDialog from './get-modal-dialog'; +import { contains } from '../../core/utils'; + +/** + * Determines if an element is inside an inert subtree. + * @param {VirtualNode} vNode + * @param {Boolean} [options.skipAncestors] If the ancestor tree should not be used + * @return {Boolean} The element's inert state + */ +export default function isInert(vNode, { skipAncestors, isAncestor } = {}) { + if (skipAncestors) { + return isInertSelf(vNode, isAncestor); + } + + return isInertAncestors(vNode, isAncestor); +} + +/** + * Check the element for inert + */ +const isInertSelf = memoize(function isInertSelfMemoized(vNode, isAncestor) { + if (vNode.hasAttr('inert')) { + return true; + } + + if (!isAncestor && vNode.actualNode) { + // elements outside of an opened modal + // dialog are treated as inert by the + // browser + const modalDialog = getModalDialog(); + if (modalDialog && !contains(modalDialog, vNode)) { + return true; + } + } + + return false; +}); + +/** + * Check the element and ancestors for inert + */ +const isInertAncestors = memoize(function isInertAncestorsMemoized( + vNode, + isAncestor +) { + if (isInertSelf(vNode, isAncestor)) { + return true; + } + + if (!vNode.parent) { + return false; + } + + return isInertAncestors(vNode.parent, true); +}); diff --git a/lib/commons/dom/is-visible-for-screenreader.js b/lib/commons/dom/is-visible-to-screenreader.js similarity index 90% rename from lib/commons/dom/is-visible-for-screenreader.js rename to lib/commons/dom/is-visible-to-screenreader.js index 712acbda37..8687c99e20 100644 --- a/lib/commons/dom/is-visible-for-screenreader.js +++ b/lib/commons/dom/is-visible-to-screenreader.js @@ -3,6 +3,7 @@ import { getNodeFromTree } from '../../core/utils'; import memoize from '../../core/utils/memoize'; import isHiddenForEveryone from './is-hidden-for-everyone'; import { ariaHidden, areaHidden } from './visibility-methods'; +import isInert from './is-inert'; /** * Determine if an element is visible to a screen reader @@ -21,7 +22,10 @@ export default function isVisibleToScreenReaders(vNode) { */ const isVisibleToScreenReadersVirtual = memoize( function isVisibleToScreenReadersMemoized(vNode, isAncestor) { - if (ariaHidden(vNode)) { + if ( + ariaHidden(vNode) || + isInert(vNode, { skipAncestors: true, isAncestor }) + ) { return false; } diff --git a/lib/commons/dom/visually-contains.js b/lib/commons/dom/visually-contains.js index c46f8b0fdc..84df0fec1e 100644 --- a/lib/commons/dom/visually-contains.js +++ b/lib/commons/dom/visually-contains.js @@ -3,6 +3,7 @@ import { getNodeFromTree, getScroll } from '../../core/utils'; /** * Checks whether a parent element visually contains its child, either directly or via scrolling. * Assumes that |parent| is an ancestor of |node|. + * @deprecated * @method visuallyContains * @memberof axe.commons.dom * @instance diff --git a/lib/commons/dom/visually-sort.js b/lib/commons/dom/visually-sort.js index 9dd22bc015..1ceaf8245e 100644 --- a/lib/commons/dom/visually-sort.js +++ b/lib/commons/dom/visually-sort.js @@ -19,12 +19,12 @@ export default function visuallySort(a, b) { } // 7. the child stacking contexts with positive stack levels (least positive first). - if (b._stackingOrder[i] > a._stackingOrder[i]) { + if (b._stackingOrder[i].value > a._stackingOrder[i].value) { return 1; } // 2. the child stacking contexts with negative stack levels (most negative first). - if (b._stackingOrder[i] < a._stackingOrder[i]) { + if (b._stackingOrder[i].value < a._stackingOrder[i].value) { return -1; } } diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js index 4acecb0c25..e47c259134 100644 --- a/lib/commons/text/accessible-text-virtual.js +++ b/lib/commons/text/accessible-text-virtual.js @@ -5,7 +5,7 @@ import formControlValue from './form-control-value'; import subtreeText from './subtree-text'; import titleText from './title-text'; import sanitize from './sanitize'; -import isVisibleToScreenReaders from '../dom/is-visible-for-screenreader'; +import isVisibleToScreenReaders from '../dom/is-visible-to-screenreader'; import isIconLigature from '../text/is-icon-ligature'; /** diff --git a/lib/commons/text/is-icon-ligature.js b/lib/commons/text/is-icon-ligature.js index 6a01cbd645..5e17c55984 100644 --- a/lib/commons/text/is-icon-ligature.js +++ b/lib/commons/text/is-icon-ligature.js @@ -85,7 +85,9 @@ function isIconLigature( } const canvasContext = cache.get('canvasContext', () => - document.createElement('canvas').getContext('2d') + document + .createElement('canvas') + .getContext('2d', { willReadFrequently: true }) ); const canvas = canvasContext.canvas; @@ -107,8 +109,9 @@ function isIconLigature( } const font = fonts[fontFamily]; - // improve the performance by only comparing the image data of a font - // a certain number of times + // improve the performance by only comparing the image data of a fon a certain number of times + // NOTE: This MIGHT cause an issue if someone uses an icon font to render actual text. + // We're leaving this as-is, unless someone reports a false positive over it. if (font.occurrences >= occurrenceThreshold) { // if the font has always been a ligature assume it's a ligature font if (font.numLigatures / font.occurrences === 1) { diff --git a/lib/commons/text/visible-virtual.js b/lib/commons/text/visible-virtual.js index 02942b6775..5862a84e94 100644 --- a/lib/commons/text/visible-virtual.js +++ b/lib/commons/text/visible-virtual.js @@ -1,6 +1,6 @@ import sanitize from './sanitize'; import isVisibleOnScreen from '../dom/is-visible-on-screen'; -import isVisibleToScreenReaders from '../dom/is-visible-for-screenreader'; +import isVisibleToScreenReaders from '../dom/is-visible-to-screenreader'; import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree } from '../../core/utils'; diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index a56eb51b45..b8aee7d4e1 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -18,11 +18,16 @@ const dotRegex = /\{\{.+?\}\}/g; function getDefaultOrigin() { // @see https://html.spec.whatwg.org/multipage/webappapis.html#dom-origin-dev // window.origin does not exist in ie11 - if (window.origin) { + // prevent origin default "null" string on CDP `Page.setDocumentContent` https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-setDocumentContent + if (window.origin && window.origin !== 'null') { return window.origin; } // window.location does not exist in node when we run the build - if (window.location && window.location.origin) { + if ( + window.location && + window.location.origin && + window.location.origin !== 'null' + ) { return window.location.origin; } } diff --git a/lib/rules/area-alt.json b/lib/rules/area-alt.json index 4deca349b4..afe0ddbe3c 100644 --- a/lib/rules/area-alt.json +++ b/lib/rules/area-alt.json @@ -9,7 +9,9 @@ "wcag412", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT6.a" ], "actIds": ["c487ae"], "metadata": { diff --git a/lib/rules/aria-command-name.json b/lib/rules/aria-command-name.json index aa578eba89..636da2914f 100644 --- a/lib/rules/aria-command-name.json +++ b/lib/rules/aria-command-name.json @@ -2,7 +2,7 @@ "id": "aria-command-name", "selector": "[role=\"link\"], [role=\"button\"], [role=\"menuitem\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT"], + "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT6.a"], "actIds": ["97a4e1"], "metadata": { "description": "Ensures every ARIA button, link and menuitem has an accessible name", diff --git a/lib/rules/aria-input-field-name.json b/lib/rules/aria-input-field-name.json index 4efe53b556..042d5d77f9 100644 --- a/lib/rules/aria-input-field-name.json +++ b/lib/rules/aria-input-field-name.json @@ -2,7 +2,7 @@ "id": "aria-input-field-name", "selector": "[role=\"combobox\"], [role=\"listbox\"], [role=\"searchbox\"], [role=\"slider\"], [role=\"spinbutton\"], [role=\"textbox\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT"], + "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT5.c"], "actIds": ["e086e5"], "metadata": { "description": "Ensures every ARIA input field has an accessible name", diff --git a/lib/rules/aria-roledescription.json b/lib/rules/aria-roledescription.json index 64a43b4346..73013d9e98 100644 --- a/lib/rules/aria-roledescription.json +++ b/lib/rules/aria-roledescription.json @@ -1,7 +1,8 @@ { "id": "aria-roledescription", "selector": "[aria-roledescription]", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "deprecated"], + "enabled": false, "metadata": { "description": "Ensure aria-roledescription is only used on elements with an implicit or explicit role", "help": "aria-roledescription must be on elements with a semantic role" diff --git a/lib/rules/aria-toggle-field-name.json b/lib/rules/aria-toggle-field-name.json index c2dc04cc01..b50cf20869 100644 --- a/lib/rules/aria-toggle-field-name.json +++ b/lib/rules/aria-toggle-field-name.json @@ -2,7 +2,7 @@ "id": "aria-toggle-field-name", "selector": "[role=\"checkbox\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"], [role=\"radio\"], [role=\"switch\"], [role=\"option\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT"], + "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT5.c"], "actIds": ["e086e5"], "metadata": { "description": "Ensures every ARIA toggle field has an accessible name", diff --git a/lib/rules/audio-caption.json b/lib/rules/audio-caption.json index 026a5277a7..cbcf650dfe 100644 --- a/lib/rules/audio-caption.json +++ b/lib/rules/audio-caption.json @@ -8,7 +8,8 @@ "wcag2a", "wcag121", "section508", - "section508.22.a" + "section508.22.a", + "deprecated" ], "actIds": ["2eb176", "afb423"], "metadata": { diff --git a/lib/rules/blink.json b/lib/rules/blink.json index ed21e49ef7..bca2f0405c 100644 --- a/lib/rules/blink.json +++ b/lib/rules/blink.json @@ -7,7 +7,9 @@ "wcag2a", "wcag222", "section508", - "section508.22.j" + "section508.22.j", + "TTv5", + "TT2.b" ], "metadata": { "description": "Ensures elements are not used", diff --git a/lib/rules/button-name.json b/lib/rules/button-name.json index 1dbc340df4..6ea4703975 100644 --- a/lib/rules/button-name.json +++ b/lib/rules/button-name.json @@ -8,7 +8,9 @@ "wcag412", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT6.a" ], "actIds": ["97a4e1", "m6b1q3"], "metadata": { diff --git a/lib/rules/bypass.json b/lib/rules/bypass.json index ca8e58f155..62c3626393 100644 --- a/lib/rules/bypass.json +++ b/lib/rules/bypass.json @@ -9,7 +9,9 @@ "wcag2a", "wcag241", "section508", - "section508.22.o" + "section508.22.o", + "TTv5", + "TT9.a" ], "actIds": ["cf77f2", "047fe0", "b40fd1", "3e12e1", "ye5d6e"], "metadata": { diff --git a/lib/rules/color-contrast-enhanced.json b/lib/rules/color-contrast-enhanced.json index 1c80d60715..0cc2751582 100644 --- a/lib/rules/color-contrast-enhanced.json +++ b/lib/rules/color-contrast-enhanced.json @@ -6,8 +6,8 @@ "tags": ["cat.color", "wcag2aaa", "wcag146", "ACT"], "actIds": ["09o5cg"], "metadata": { - "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AAA contrast ratio thresholds", - "help": "Elements must have sufficient color contrast" + "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds", + "help": "Elements must meet enhanced color contrast ratio thresholds" }, "all": [], "any": ["color-contrast-enhanced"], diff --git a/lib/rules/color-contrast-matches.js b/lib/rules/color-contrast-matches.js index 9a75b04b2f..d95b1c3890 100644 --- a/lib/rules/color-contrast-matches.js +++ b/lib/rules/color-contrast-matches.js @@ -1,6 +1,11 @@ /* global document */ import { getAccessibleRefs } from '../commons/aria'; -import { findUpVirtual, visuallyOverlaps, getRootNode } from '../commons/dom'; +import { + findUpVirtual, + visuallyOverlaps, + getRootNode, + isInert +} from '../commons/dom'; import { visibleVirtual, removeUnicode, @@ -35,7 +40,7 @@ function colorContrastMatches(node, virtualNode) { return false; } - if (isDisabled(virtualNode)) { + if (isDisabled(virtualNode) || isInert(virtualNode)) { return false; } diff --git a/lib/rules/color-contrast.json b/lib/rules/color-contrast.json index ab0287ab3e..5abd09598d 100644 --- a/lib/rules/color-contrast.json +++ b/lib/rules/color-contrast.json @@ -2,11 +2,11 @@ "id": "color-contrast", "matches": "color-contrast-matches", "excludeHidden": false, - "tags": ["cat.color", "wcag2aa", "wcag143", "ACT"], + "tags": ["cat.color", "wcag2aa", "wcag143", "ACT", "TTv5", "TT13.c"], "actIds": ["afw4f7", "09o5cg"], "metadata": { - "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds", - "help": "Elements must have sufficient color contrast" + "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds" }, "all": [], "any": ["color-contrast"], diff --git a/lib/rules/document-title.json b/lib/rules/document-title.json index 1de86ae86f..4bd5992cbb 100644 --- a/lib/rules/document-title.json +++ b/lib/rules/document-title.json @@ -2,7 +2,14 @@ "id": "document-title", "selector": "html", "matches": "is-initiator-matches", - "tags": ["cat.text-alternatives", "wcag2a", "wcag242", "ACT"], + "tags": [ + "cat.text-alternatives", + "wcag2a", + "wcag242", + "ACT", + "TTv5", + "TT12.a" + ], "actIds": ["2779a5"], "metadata": { "description": "Ensures each HTML document contains a non-empty element", diff --git a/lib/rules/form-field-multiple-labels.json b/lib/rules/form-field-multiple-labels.json index ea8cde33e6..3d4d95a1f7 100644 --- a/lib/rules/form-field-multiple-labels.json +++ b/lib/rules/form-field-multiple-labels.json @@ -2,7 +2,7 @@ "id": "form-field-multiple-labels", "selector": "input, select, textarea", "matches": "label-matches", - "tags": ["cat.forms", "wcag2a", "wcag332"], + "tags": ["cat.forms", "wcag2a", "wcag332", "TTv5", "TT5.c"], "metadata": { "description": "Ensures form field does not have multiple label elements", "help": "Form field must not have multiple label elements" diff --git a/lib/rules/frame-focusable-content.json b/lib/rules/frame-focusable-content.json index 04c51f7df8..903a1361f1 100644 --- a/lib/rules/frame-focusable-content.json +++ b/lib/rules/frame-focusable-content.json @@ -2,7 +2,7 @@ "id": "frame-focusable-content", "selector": "html", "matches": "frame-focusable-content-matches", - "tags": ["cat.keyboard", "wcag2a", "wcag211"], + "tags": ["cat.keyboard", "wcag2a", "wcag211", "TTv5", "TT4.a"], "actIds": ["akn7bn"], "metadata": { "description": "Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1", diff --git a/lib/rules/frame-title-unique.json b/lib/rules/frame-title-unique.json index 342b4ba43f..158feee30a 100644 --- a/lib/rules/frame-title-unique.json +++ b/lib/rules/frame-title-unique.json @@ -2,7 +2,7 @@ "id": "frame-title-unique", "selector": "frame[title], iframe[title]", "matches": "frame-title-has-text-matches", - "tags": ["cat.text-alternatives", "wcag412", "wcag2a"], + "tags": ["cat.text-alternatives", "wcag412", "wcag2a", "TTv5", "TT12.c"], "actIds": ["4b1c6c"], "metadata": { "description": "Ensures <iframe> and <frame> elements contain a unique title attribute", diff --git a/lib/rules/frame-title.json b/lib/rules/frame-title.json index a954ab9020..79207c90d6 100644 --- a/lib/rules/frame-title.json +++ b/lib/rules/frame-title.json @@ -7,7 +7,9 @@ "wcag2a", "wcag412", "section508", - "section508.22.i" + "section508.22.i", + "TTv5", + "TT12.c" ], "actIds": ["cae760"], "metadata": { diff --git a/lib/rules/html-has-lang.json b/lib/rules/html-has-lang.json index cc399a6a7e..b27ea7a787 100644 --- a/lib/rules/html-has-lang.json +++ b/lib/rules/html-has-lang.json @@ -2,7 +2,7 @@ "id": "html-has-lang", "selector": "html", "matches": "is-initiator-matches", - "tags": ["cat.language", "wcag2a", "wcag311", "ACT"], + "tags": ["cat.language", "wcag2a", "wcag311", "ACT", "TTv5", "TT11.a"], "actIds": ["b5c3f8"], "metadata": { "description": "Ensures every HTML document has a lang attribute", diff --git a/lib/rules/html-lang-valid.json b/lib/rules/html-lang-valid.json index 5cf81f4325..b761f400f0 100644 --- a/lib/rules/html-lang-valid.json +++ b/lib/rules/html-lang-valid.json @@ -1,7 +1,7 @@ { "id": "html-lang-valid", "selector": "html[lang]:not([lang=\"\"]), html[xml\\:lang]:not([xml\\:lang=\"\"])", - "tags": ["cat.language", "wcag2a", "wcag311", "ACT"], + "tags": ["cat.language", "wcag2a", "wcag311", "ACT", "TTv5", "TT11.a"], "actIds": ["bf051a"], "metadata": { "description": "Ensures the lang attribute of the <html> element has a valid value", diff --git a/lib/rules/image-alt.json b/lib/rules/image-alt.json index 05eeb093f2..84408c6103 100644 --- a/lib/rules/image-alt.json +++ b/lib/rules/image-alt.json @@ -8,7 +8,10 @@ "wcag111", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT7.a", + "TT7.b" ], "actIds": ["23a2a8"], "metadata": { diff --git a/lib/rules/input-button-name.json b/lib/rules/input-button-name.json index adb7cea02d..c7fee8c4de 100644 --- a/lib/rules/input-button-name.json +++ b/lib/rules/input-button-name.json @@ -8,7 +8,9 @@ "wcag412", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT5.c" ], "actIds": ["97a4e1"], "metadata": { diff --git a/lib/rules/input-image-alt.json b/lib/rules/input-image-alt.json index 5405aca264..7525bb4532 100644 --- a/lib/rules/input-image-alt.json +++ b/lib/rules/input-image-alt.json @@ -9,7 +9,9 @@ "wcag412", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT7.a" ], "actIds": ["59796f"], "metadata": { diff --git a/lib/rules/label.json b/lib/rules/label.json index 33c39b63cb..0ecd18324a 100644 --- a/lib/rules/label.json +++ b/lib/rules/label.json @@ -8,7 +8,9 @@ "wcag412", "section508", "section508.22.n", - "ACT" + "ACT", + "TTv5", + "TT5.c" ], "actIds": ["e086e5"], "metadata": { diff --git a/lib/rules/link-in-text-block.json b/lib/rules/link-in-text-block.json index 09f53df166..c047fcaa4e 100644 --- a/lib/rules/link-in-text-block.json +++ b/lib/rules/link-in-text-block.json @@ -3,7 +3,7 @@ "selector": "a[href], [role=link]", "matches": "link-in-text-block-matches", "excludeHidden": false, - "tags": ["cat.color", "wcag2a", "wcag141"], + "tags": ["cat.color", "wcag2a", "wcag141", "TTv5", "TT13.a"], "metadata": { "description": "Ensure links are distinguished from surrounding text in a way that does not rely on color", "help": "Links must be distinguishable without relying on color" diff --git a/lib/rules/link-name.json b/lib/rules/link-name.json index 1f516dbfb7..5b8a8a3d42 100644 --- a/lib/rules/link-name.json +++ b/lib/rules/link-name.json @@ -8,7 +8,9 @@ "wcag244", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT6.a" ], "actIds": ["c487ae"], "metadata": { diff --git a/lib/rules/marquee.json b/lib/rules/marquee.json index 7acb5f11a8..995719cc71 100644 --- a/lib/rules/marquee.json +++ b/lib/rules/marquee.json @@ -2,7 +2,7 @@ "id": "marquee", "selector": "marquee", "excludeHidden": false, - "tags": ["cat.parsing", "wcag2a", "wcag222"], + "tags": ["cat.parsing", "wcag2a", "wcag222", "TTv5", "TT2.b"], "metadata": { "description": "Ensures <marquee> elements are not used", "help": "<marquee> elements are deprecated and must not be used" diff --git a/lib/rules/meta-refresh.json b/lib/rules/meta-refresh.json index d6c3747bfe..7fd460be21 100644 --- a/lib/rules/meta-refresh.json +++ b/lib/rules/meta-refresh.json @@ -2,7 +2,7 @@ "id": "meta-refresh", "selector": "meta[http-equiv=\"refresh\"][content]", "excludeHidden": false, - "tags": ["cat.time-and-media", "wcag2a", "wcag221"], + "tags": ["cat.time-and-media", "wcag2a", "wcag221", "TTv5", "TT2.c"], "actIds": ["bc659a", "bisz58"], "metadata": { "description": "Ensures <meta http-equiv=\"refresh\"> is not used for delayed refresh", diff --git a/lib/rules/nested-interactive.json b/lib/rules/nested-interactive.json index 857aea60f2..eac7278cd4 100644 --- a/lib/rules/nested-interactive.json +++ b/lib/rules/nested-interactive.json @@ -1,7 +1,7 @@ { "id": "nested-interactive", "matches": "nested-interactive-matches", - "tags": ["cat.keyboard", "wcag2a", "wcag412"], + "tags": ["cat.keyboard", "wcag2a", "wcag412", "TTv5", "TT4.a"], "actIds": ["307n5z"], "metadata": { "description": "Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies", diff --git a/lib/rules/no-autoplay-audio.json b/lib/rules/no-autoplay-audio.json index b6490f28b5..25522e1be2 100644 --- a/lib/rules/no-autoplay-audio.json +++ b/lib/rules/no-autoplay-audio.json @@ -4,7 +4,7 @@ "selector": "audio[autoplay], video[autoplay]", "matches": "no-autoplay-audio-matches", "reviewOnFail": true, - "tags": ["cat.time-and-media", "wcag2a", "wcag142", "ACT"], + "tags": ["cat.time-and-media", "wcag2a", "wcag142", "ACT", "TTv5", "TT2.a"], "actIds": ["80f0bf"], "metadata": { "description": "Ensures <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio", diff --git a/lib/rules/no-naming-method-matches.js b/lib/rules/no-naming-method-matches.js index 2986f8247b..cd8c973f3b 100644 --- a/lib/rules/no-naming-method-matches.js +++ b/lib/rules/no-naming-method-matches.js @@ -1,6 +1,7 @@ -import { getExplicitRole } from '../commons/aria'; -import { querySelectorAll } from '../core/utils'; +import getExplicitRole from '../commons/aria/get-explicit-role'; +import isComboboxPopup from '../commons/aria/is-combobox-popup'; import getElementSpec from '../commons/standards/get-element-spec'; +import querySelectorAll from '../core/utils/query-selector-all'; /** * Filter out elements that have a naming method (i.e. img[alt], table > caption, etc.) @@ -10,7 +11,6 @@ function noNamingMethodMatches(node, virtualNode) { if (namingMethods && namingMethods.length !== 0) { return false; } - // Additionally, ignore combobox that get their name from a descendant input: if ( getExplicitRole(virtualNode) === 'combobox' && @@ -18,6 +18,11 @@ function noNamingMethodMatches(node, virtualNode) { ) { return false; } + // Ignore listboxes that are referenced by a combobox + // Other roles don't require a name at all, or require one anyway + if (isComboboxPopup(virtualNode, { popupRoles: ['listbox'] })) { + return false; + } return true; } diff --git a/lib/rules/role-img-alt.json b/lib/rules/role-img-alt.json index d7c87e5dd8..2e55b1c631 100644 --- a/lib/rules/role-img-alt.json +++ b/lib/rules/role-img-alt.json @@ -8,7 +8,9 @@ "wcag111", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT7.a" ], "actIds": ["23a2a8"], "metadata": { diff --git a/lib/rules/scrollable-region-focusable-matches.js b/lib/rules/scrollable-region-focusable-matches.js index 71acf09d24..34c4b08d77 100644 --- a/lib/rules/scrollable-region-focusable-matches.js +++ b/lib/rules/scrollable-region-focusable-matches.js @@ -1,75 +1,21 @@ -import { hasContentVirtual } from '../commons/dom'; -import { getExplicitRole } from '../commons/aria'; -import { - querySelectorAll, - getScroll, - closest, - getRootNode, - tokenList -} from '../core/utils'; -import ariaAttrs from '../standards/aria-attrs'; - -function scrollableRegionFocusableMatches(node, virtualNode) { - /** - * Note: - * `excludeHidden=true` for this rule, thus considering only elements in the accessibility tree. - */ - - /** - * if not scrollable -> `return` - */ - if (!!getScroll(node, 13) === false) { - return false; - } - - /** - * ignore scrollable regions owned by combobox. limit to roles - * ownable by combobox so we don't keep calling closest for every - * node (which would be slow) - * @see https://github.com/dequelabs/axe-core/issues/1763 - */ - const role = getExplicitRole(virtualNode); - if (ariaAttrs['aria-haspopup'].values.includes(role)) { - // in ARIA 1.1 the container has role=combobox - if (closest(virtualNode, '[role~="combobox"]')) { - return false; - } - - // in ARIA 1.0 and 1.2 the combobox owns (1.0) or controls (1.2) - // the listbox - const id = virtualNode.attr('id'); - if (id) { - const doc = getRootNode(node); - const owned = Array.from( - doc.querySelectorAll(`[aria-owns~="${id}"], [aria-controls~="${id}"]`) - ); - const comboboxOwned = owned.some(el => { - const roles = tokenList(el.getAttribute('role')); - return roles.includes('combobox'); - }); - - if (comboboxOwned) { - return false; - } - } - } - - /** - * check if node has visible contents - */ - const nodeAndDescendents = querySelectorAll(virtualNode, '*'); - const hasVisibleChildren = nodeAndDescendents.some(elm => - hasContentVirtual( - elm, - true, // noRecursion - true // ignoreAria - ) +import hasContentVirtual from '../commons/dom/has-content-virtual'; +import isComboboxPopup from '../commons/aria/is-combobox-popup'; +import { querySelectorAll, getScroll } from '../core/utils'; + +export default function scrollableRegionFocusableMatches(node, virtualNode) { + return ( + // The element scrolls + getScroll(node, 13) !== undefined && + // It's not a combobox popup, which commonly has keyboard focus added + isComboboxPopup(virtualNode) === false && + // And there's something actually worth scrolling to + isNoneEmptyElement(virtualNode) ); - if (!hasVisibleChildren) { - return false; - } - - return true; } -export default scrollableRegionFocusableMatches; +function isNoneEmptyElement(vNode) { + return querySelectorAll(vNode, '*').some(elm => + // (elm, noRecursion, ignoreAria) + hasContentVirtual(elm, true, true) + ); +} diff --git a/lib/rules/select-name.json b/lib/rules/select-name.json index 34a8d0f566..38d0657eeb 100644 --- a/lib/rules/select-name.json +++ b/lib/rules/select-name.json @@ -7,7 +7,9 @@ "wcag412", "section508", "section508.22.n", - "ACT" + "ACT", + "TTv5", + "TT5.c" ], "actIds": ["e086e5"], "metadata": { diff --git a/lib/rules/svg-img-alt.json b/lib/rules/svg-img-alt.json index a5a655137f..0920dfb584 100644 --- a/lib/rules/svg-img-alt.json +++ b/lib/rules/svg-img-alt.json @@ -8,7 +8,9 @@ "wcag111", "section508", "section508.22.a", - "ACT" + "ACT", + "TTv5", + "TT7.a" ], "actIds": ["7d6734"], "metadata": { diff --git a/lib/rules/td-has-header.json b/lib/rules/td-has-header.json index 41774d1330..ac7a232dc9 100644 --- a/lib/rules/td-has-header.json +++ b/lib/rules/td-has-header.json @@ -8,7 +8,9 @@ "wcag2a", "wcag131", "section508", - "section508.22.g" + "section508.22.g", + "TTv5", + "TT14.b" ], "metadata": { "description": "Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers", diff --git a/lib/rules/th-has-data-cells.json b/lib/rules/th-has-data-cells.json index cfe14ce698..023efc871c 100644 --- a/lib/rules/th-has-data-cells.json +++ b/lib/rules/th-has-data-cells.json @@ -2,7 +2,15 @@ "id": "th-has-data-cells", "selector": "table", "matches": "data-table-matches", - "tags": ["cat.tables", "wcag2a", "wcag131", "section508", "section508.22.g"], + "tags": [ + "cat.tables", + "wcag2a", + "wcag131", + "section508", + "section508.22.g", + "TTv5", + "14.b" + ], "actIds": ["d0f69e"], "metadata": { "description": "Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe", diff --git a/lib/rules/valid-lang.json b/lib/rules/valid-lang.json index 382b15f949..a6e50b80b6 100644 --- a/lib/rules/valid-lang.json +++ b/lib/rules/valid-lang.json @@ -1,7 +1,7 @@ { "id": "valid-lang", "selector": "[lang]:not(html), [xml\\:lang]:not(html)", - "tags": ["cat.language", "wcag2aa", "wcag312", "ACT"], + "tags": ["cat.language", "wcag2aa", "wcag312", "ACT", "TTv5", "TT11.b"], "actIds": ["de46e4"], "metadata": { "description": "Ensures lang attributes have valid values", diff --git a/lib/rules/video-caption.json b/lib/rules/video-caption.json index 2bbd98c24f..27cea4010c 100644 --- a/lib/rules/video-caption.json +++ b/lib/rules/video-caption.json @@ -6,7 +6,9 @@ "wcag2a", "wcag122", "section508", - "section508.22.a" + "section508.22.a", + "TTv5", + "TT17.a" ], "actIds": ["eac66b"], "metadata": { diff --git a/lib/standards/aria-attrs.js b/lib/standards/aria-attrs.js index 85bf547e74..1143ca1fc7 100644 --- a/lib/standards/aria-attrs.js +++ b/lib/standards/aria-attrs.js @@ -12,6 +12,14 @@ const ariaAttrs = { type: 'nmtoken', values: ['inline', 'list', 'both', 'none'] }, + 'aria-braillelabel': { + type: 'string', + global: true + }, + 'aria-brailleroledescription': { + type: 'string', + global: true + }, 'aria-busy': { type: 'boolean', global: true @@ -48,6 +56,11 @@ const ariaAttrs = { allowEmpty: true, global: true }, + 'aria-description': { + type: 'string', + allowEmpty: true, + global: true + }, 'aria-details': { type: 'idref', allowEmpty: true, diff --git a/lib/standards/aria-roles.js b/lib/standards/aria-roles.js index eaa4d5e751..c6c0f67c4e 100644 --- a/lib/standards/aria-roles.js +++ b/lib/standards/aria-roles.js @@ -167,6 +167,7 @@ const ariaRoles = { }, directory: { type: 'structure', + deprecated: true, allowedAttrs: ['aria-expanded'], superclassRole: ['list'], // Note: spec difference diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 8f67b9d833..7e99be92a9 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -182,6 +182,7 @@ const htmlElms = { datalist: { contentTypes: ['phrasing', 'flow'], allowedRoles: false, + noAriaAttrs: true, implicitAttrs: { // Note: even though the value of aria-multiselectable is based // on the attributes, we don't currently need to know the diff --git a/locales/_template.json b/locales/_template.json index 54ae98ef55..fd028d34e2 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -114,12 +114,12 @@ "help": "Page must have means to bypass repeated blocks" }, "color-contrast-enhanced": { - "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AAA contrast ratio thresholds", - "help": "Elements must have sufficient color contrast" + "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds", + "help": "Elements must meet enhanced color contrast ratio thresholds" }, "color-contrast": { - "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds", - "help": "Elements must have sufficient color contrast" + "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds" }, "css-orientation-lock": { "description": "Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations", @@ -427,7 +427,7 @@ }, "aria-busy": { "pass": "Element has an aria-busy attribute", - "fail": "Element has no aria-busy=\"true\" attribute" + "fail": "Element uses aria-busy=\"true\" while showing a loader" }, "aria-errormessage": { "pass": "aria-errormessage exists and references elements visible to screen readers that use a supported aria-errormessage technique", @@ -477,7 +477,7 @@ "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 (see related nodes)" + "unallowed": "Element has children which are not allowed: ${data.values}" }, "incomplete": { "singular": "Expecting ARIA child role to be added: ${data.values}", diff --git a/locales/ja.json b/locales/ja.json index bd90933079..3efa3adccb 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -289,6 +289,10 @@ "description": "<marquee>要素が使用されていないことを確認します", "help": "<marquee>要素は非推奨のため、使用してはなりません" }, + "meta-refresh-no-exceptions": { + "description": "<meta http-equiv=\"refresh\">が使用されていないことを確認します", + "help": "制限時間のある更新が存在してはなりません" + }, "meta-refresh": { "description": "<meta http-equiv=\"refresh\">が使用されていないことを確認します", "help": "制限時間のある更新が存在してはなりません" @@ -369,6 +373,10 @@ "description": "キャプション付きのテーブルが<caption>要素を用いていることを確認します", "help": "データテーブルにキャプションをつけるためにデータまたはヘッダーセルを用いるべきではありません" }, + "target-size": { + "description": "タッチターゲットのサイズとスペースが十分にあることを確認します", + "help": "すべてのタッチターゲットは24pxの大きさか、十分なスペースが必要です" + }, "td-has-header": { "description": "大きなテーブルの空ではないデータセルに1つかそれ以上のテーブルヘッダーが存在することを確認します", "help": "3×3より大きいテーブルの空ではないtd要素はテーブルヘッダーと関連づいていなければなりません" @@ -403,7 +411,8 @@ "fail": { "singular": "ARIA属性は許可されていません: ${data.values}", "plural": "ARIA属性は許可されていません: ${data.values}" - } + }, + "incomplete": "次の要素のARIA属性が無視されても問題ないことを確認します: ${data.values}" }, "aria-allowed-role": { "pass": "ARIAロールは指定された要素に対して許可されています", @@ -416,6 +425,10 @@ "plural": "ARIA ロール ${data.values} この要素に許可されていないため、要素が表示されたときはARIAロールを削除する必要があります" } }, + "aria-busy": { + "pass": "要素にはaria-busy属性が存在しています", + "fail": "要素に aria-busy=\"true\" が設定されていません" + }, "aria-errormessage": { "pass": "サポートされているaria-errormessageの技術を使用しています", "fail": { @@ -439,8 +452,18 @@ }, "aria-prohibited-attr": { "pass": "ARIA属性は使用できます", - "fail": "ARIA属性は使用できません: ${data.values}", - "incomplete": "要素でのARIA属性のサポートが不充分なため、代わりにコンテンツのテキストが用いられます: ${data.values}" + "fail": { + "hasRolePlural": "${data.prohibited} 属性は \"${data.role}\" ロールでは使用できません", + "hasRoleSingular": "${data.prohibited} 属性は \"${data.role}\" ロールでは使用できません", + "noRolePlural": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では使用できません", + "noRoleSingular": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では使用できません" + }, + "incomplete": { + "hasRoleSingular": "${data.prohibited} 属性はロール \"${data.role}\" では十分にサポートされていません", + "hasRolePlural": "${data.prohibited} 属性はロール \"${data.role}\" では十分にサポートされていません", + "noRoleSingular": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では十分にサポートされていません", + "noRolePlural": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では十分にサポートされていません" + } }, "aria-required-attr": { "pass": "すべての必須のARIA属性が存在しています", @@ -453,7 +476,8 @@ "pass": "必須のARIA子ロールが存在しています", "fail": { "singular": "必須のARIA子ロールが提供されていません: ${data.values}", - "plural": "必須のARIA子ロールが提供されていません: ${data.values}" + "plural": "必須のARIA子ロールが提供されていません: ${data.values}", + "unallowed": "要素には許可されていないARIA子ロールがあります (関連ノードを参照)" }, "incomplete": { "singular": "ARIAの子ロールを追加することが求められます: ${data.values}", @@ -484,8 +508,10 @@ }, "incomplete": { "noId": "ARIA属性で指定されている要素のIDがページ上に存在しません: ${data.needsReview}", - "ariaCurrent": "ARIA 属性値が無効であるため、\"aria-current=true\" として扱われます: ${data.needsReview}", - "idrefs": "ARIA属性で指定されている要素のIDがページ上に存在するかどうか判定できません: ${data.needsReview}" + "noIdShadow": "ARIA属性で指定されている要素のIDがページ上に存在しないか、別のshadow DOMツリーの小要素です: ${data.needsReview}", + "ariaCurrent": "ARIA属性値が無効であるため、\"aria-current=true\" として扱われます: ${data.needsReview}", + "idrefs": "ARIA属性で指定されている要素のIDがページ上に存在するかどうか判定できません: ${data.needsReview}", + "empty": "ARIA属性値が空のときは無視されます: ${data.needsReview}" } }, "aria-valid-attr": { @@ -497,7 +523,7 @@ }, "deprecatedrole": { "pass": "推奨されていないARIAロールではありません", - "fail": "非推奨のロールが使用されています: ${data.values}" + "fail": "非推奨のロールが使用されています: ${data}" }, "fallbackrole": { "pass": "1つのロール値のみ使用されています", @@ -562,7 +588,10 @@ } }, "color-contrast": { - "pass": "要素には${data.contrastRatio}の十分なコントラスト比があります", + "pass": { + "default": "要素には${data.contrastRatio}の十分なコントラスト比があります", + "hidden": "要素は非表示です" + }, "fail": { "default": "要素のコントラスト比が不十分です ${data.contrastRatio}(前景色: ${data.fgColor}、背景色: ${data.bgColor}、フォントサイズ: ${data.fontSize}、フォントの太さ: ${data.fontWeight})。コントラスト比${data.expectedContrastRatio}を必要とします", "fgOnShadowColor": "要素の前景色と影(text-shadow)の色のコントラスト比が不十分です ${data.contrastRatio}(前景色: ${data.fgColor}、影(text-shadow)の色: ${data.shadowColor}、フォントサイズ: ${data.fontSize}、フォントの太さ: ${data.fontWeight})。コントラスト比${data.expectedContrastRatio}を必要とします", @@ -584,9 +613,16 @@ "pseudoContent": "擬似要素のため、要素の背景色を判定することができませんでした" } }, + "link-in-text-block-style": { + "pass": "リンクは視覚的なスタイル設定によって周囲のテキストと区別できます", + "fail": "リンクには、周囲のテキストと区別するためのスタイル (下線など) がありません" + }, "link-in-text-block": { "pass": "リンクは色以外の方法で周囲のテキストと区別できます", - "fail": "リンクは色以外の方法で周囲のテキストと区別させる必要があります", + "fail": { + "fgContrast": "このリンクは、周囲のテキストとの${data.contrastRatio}:1の色のコントラストが不十分です。(最小コントラストは ${data.requiredContrastRatio}:1、リンクテキスト: ${data.nodeColor}、周囲のテキスト: ${data.parentColor})", + "bgContrast": "リンクの背景の色コントラストが ${data.contrastRatio} で十分ではありません (最小コントラストは ${data.requiredContrastRatio}:1、リンク背景色: ${data.nodeBackgroundColor}、周囲の背景色: ${data.parentBackgroundColor})" + }, "incomplete": { "default": "コントラスト比を判定できません", "bgContrast": "要素のコントラスト比を判定できません。明確なホバー/フォーカススタイルを確認します", @@ -614,6 +650,7 @@ }, "focusable-disabled": { "pass": "要素内にフォーカス不可能な要素は含まれていません", + "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認します", "fail": "フォーカス可能なコンテンツは無効にするか、DOMから削除するべきです" }, "focusable-element": { @@ -631,6 +668,7 @@ }, "focusable-not-tabbable": { "pass": "要素内にフォーカス不可能な要素は含まれていません", + "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認します", "fail": "フォーカス可能なコンテンツは tabindex='-1' を指定するか、DOMから削除するべきです" }, "frame-focusable-content": { @@ -785,6 +823,30 @@ "pass": "<meta>タグはモバイルデバイスでの拡大を無効にしません", "fail": "<meta>タグの${data}がモバイルデバイスでの拡大を無効にします" }, + "target-offset": { + "pass": "ターゲットに最も近い隣接要素からのオフセットが十分にあリます (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)", + "fail": "ターゲットの最も近い隣接要素からのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)", + "incomplete": { + "default": "tabindexが負の要素において、最も近い隣接要素からのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)。これがターゲットである要素か確認します", + "nonTabbableNeighbor": "ターゲットのtabindexが負の隣接ノードからのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)。隣接要素がターゲットか確認します" + } + }, + "target-size": { + "pass": { + "default": "コントロールには十分なサイズがあります (${data.width} px x ${data.height} pxであり、${data.minSize} px x ${data.minSize} px 以上です)", + "obscured": "コントロールは完全に隠されていてクリックできないため無視されます" + }, + "fail": { + "default": "ターゲットのサイズが不十分です (${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)", + "partiallyObscured": "ターゲットは部分的に隠れているためサイズが不十分です (最小スペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)" + }, + "incomplete": { + "default": "tabindex が負の要素のサイズが不十分です (${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。これがターゲットの要素であることを確認します", + "contentOverflow": "コンテンツがオーバーフローしたため、要素のサイズを正確に決定できませんでした", + "partiallyObscured": "tabindex が負の要素は、部分的に隠れているためサイズが不十分です (最小のスペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。これがターゲットの要素であることを確認します", + "partiallyObscuredNonTabbable": "ターゲットのサイズが不十分です。これは tabindex が負の隣接オブジェクトによって部分的に隠されているためです (最小のスペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。隣接要素がターゲットの要素であることを確認します" + } + }, "header-present": { "pass": "ページにheaderが存在しています", "fail": "ページにheaderが存在していません" @@ -806,6 +868,10 @@ "pass": "ページにランドマーク領域が存在しています", "fail": "ページにランドマーク領域が存在していません" }, + "meta-refresh-no-exceptions": { + "pass": "<meta>タグはすぐにページを更新しません", + "fail": "<meta>タグは時限によるページの更新を強制します" + }, "meta-refresh": { "pass": "<meta>タグはすぐにページを更新しません", "fail": "<meta>タグは時限によるページの更新を強制します" @@ -878,6 +944,18 @@ "fail": "要素にスクリーン・リーダーが認識可能なテキストが存在していません", "incomplete": "要素に子ノードがあるか判定できません" }, + "important-letter-spacing": { + "pass": "文字の間隔(letter-spacing)の属性が '!important' に設定されていない、または最低条件を満たしています", + "fail": "文字の間隔(letter-spacing)の属性には '!important' を設定しない、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + }, + "important-line-height": { + "pass": "行間(line-height)に '!important' が設定されていません、または最低条件を満たしています", + "fail": "行間(line-height)には '!important' を使用しないでください、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + }, + "important-word-spacing": { + "pass": "単語の間隔(word-spacing)に '!important' が設定されていません、または最低条件を満たしています", + "fail": "単語の間隔(word-spacing)には '!important' を使用しないでください、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + }, "is-on-screen": { "pass": "要素が表示されていません", "fail": "要素が表示されています" @@ -923,7 +1001,8 @@ "default": "要素のデフォルトのセマンティクスはrole=\"none\"またはrole=\"presentation\"で上書きされませんでした", "globalAria": "要素にはARIAのグローバル属性が指定されているため、role=\"none\"またはrole=\"presentation\"にはなりません", "focusable": "要素がフォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません", - "both": "要素にはARIAのグローバル属性が指定されており、フォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません" + "both": "要素にはARIAのグローバル属性が指定されており、フォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません", + "iframe": "${data.nodeName} 要素にプレゼンテーション用のroleを持つ 'title' 属性を使用すると、スクリーンリーダー間で動作が一貫しません" } }, "role-none": { @@ -952,7 +1031,8 @@ }, "same-caption-summary": { "pass": "summary属性および<caption>のコンテンツは重複していません", - "fail": "summary属性および<caption>のコンテンツが同一です" + "fail": "summary属性および<caption>のコンテンツが同一です", + "incomplete": "<table>にキャプションがあるかどうかを確認できません" }, "scope-value": { "pass": "scope属性は正しく使用されています", diff --git a/package-lock.json b/package-lock.json index 207751b012..05c44338c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "axe-core", - "version": "4.6.3", + "version": "4.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "axe-core", - "version": "4.6.3", + "version": "4.7.0", "license": "MPL-2.0", "devDependencies": { "@axe-core/webdriverjs": "^4.5.2", @@ -15,12 +15,12 @@ "@babel/preset-env": "^7.20.2", "@babel/runtime-corejs3": "^7.20.7", "@deque/dot": "^1.1.5", - "aria-practices": "github:w3c/aria-practices#edbf534", + "aria-practices": "github:w3c/aria-practices#ce0336bd82d7d3651abcbde86af644197ddbc629", "aria-query": "^5.1.3", "browser-driver-manager": "1.0.4", "chai": "^4.3.7", "chalk": "^4.x", - "chromedriver": "latest", + "chromedriver": "^111.0.0", "clone": "^2.1.2", "conventional-commits-parser": "^3.2.4", "core-js": "^3.27.1", @@ -74,7 +74,7 @@ "typedarray": "^0.0.7", "typescript": "^4.9.4", "uglify-js": "^3.17.4", - "wcag-act-rules": "github:w3c/wcag-act-rules#9416ea6", + "wcag-act-rules": "github:w3c/wcag-act-rules#2341a1b", "weakmap-polyfill": "^2.0.4" }, "engines": { @@ -2466,13 +2466,14 @@ }, "node_modules/aria-practices": { "version": "0.0.0", - "resolved": "git+ssh://git@github.com/w3c/aria-practices.git#edbf534c7d4decd5d52d518b703a2463a2342e08", + "resolved": "git+ssh://git@github.com/w3c/aria-practices.git#ce0336bd82d7d3651abcbde86af644197ddbc629", + "integrity": "sha512-YeAp8bT7jWtWQMW5svlRzvHp0STw1cGW0xgy/MrapgA+oaW85NDnz7/YFrBGZbWrOp3xLujTSHSv+YGgnFT4KQ==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/rest": "^18.9.1", - "dotenv": "^8.2.0", - "node-html-parser": "^3.1.4" + "@octokit/rest": "^18.12.0", + "dotenv": "^16.0.3", + "node-html-parser": "^5.2.0" } }, "node_modules/aria-query": { @@ -2578,9 +2579,9 @@ } }, "node_modules/axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -3057,14 +3058,14 @@ } }, "node_modules/chromedriver": { - "version": "107.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz", - "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==", + "version": "111.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-111.0.0.tgz", + "integrity": "sha512-XavNYNBBfKIrT8Oi/ywJ0DoOOU+jHF5bQWTkqStFsAXvfCV9VzYN3J+TGAvZdrpXeoixqPRGUxQ2yZhD2iowdQ==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.3", - "axios": "^1.1.3", + "axios": "^1.2.1", "compare-versions": "^5.0.1", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", @@ -4177,12 +4178,12 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/dotgitignore": { @@ -8644,12 +8645,12 @@ } }, "node_modules/node-html-parser": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.3.6.tgz", - "integrity": "sha512-VkWDHvNgFGB3mbQGMyzqRE1i/BG7TKX9wRXC8e/v8kL0kZR/Oy6RjYxXH91K6/+m3g8iQ8dTqRy75lTYoA2Cjg==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", "dev": true, "dependencies": { - "css-select": "^4.1.3", + "css-select": "^4.2.1", "he": "1.2.0" } }, @@ -11614,7 +11615,7 @@ } }, "node_modules/wcag-act-rules": { - "resolved": "git+ssh://git@github.com/w3c/wcag-act-rules.git#9416ea60714ed6c916435c7b38db76062f3ef004", + "resolved": "git+ssh://git@github.com/w3c/wcag-act-rules.git#2341a1ba4d47bc4cdccd5a3b7534e67b52c59002", "dev": true }, "node_modules/weakmap-polyfill": { @@ -13824,13 +13825,14 @@ "dev": true }, "aria-practices": { - "version": "git+ssh://git@github.com/w3c/aria-practices.git#edbf534c7d4decd5d52d518b703a2463a2342e08", + "version": "git+ssh://git@github.com/w3c/aria-practices.git#ce0336bd82d7d3651abcbde86af644197ddbc629", + "integrity": "sha512-YeAp8bT7jWtWQMW5svlRzvHp0STw1cGW0xgy/MrapgA+oaW85NDnz7/YFrBGZbWrOp3xLujTSHSv+YGgnFT4KQ==", "dev": true, - "from": "aria-practices@github:w3c/aria-practices#edbf534", + "from": "aria-practices@github:w3c/aria-practices#ce0336bd82d7d3651abcbde86af644197ddbc629", "requires": { - "@octokit/rest": "^18.9.1", - "dotenv": "^8.2.0", - "node-html-parser": "^3.1.4" + "@octokit/rest": "^18.12.0", + "dotenv": "^16.0.3", + "node-html-parser": "^5.2.0" } }, "aria-query": { @@ -13909,9 +13911,9 @@ "dev": true }, "axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "dev": true, "requires": { "follow-redirects": "^1.15.0", @@ -14273,13 +14275,13 @@ } }, "chromedriver": { - "version": "107.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz", - "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==", + "version": "111.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-111.0.0.tgz", + "integrity": "sha512-XavNYNBBfKIrT8Oi/ywJ0DoOOU+jHF5bQWTkqStFsAXvfCV9VzYN3J+TGAvZdrpXeoixqPRGUxQ2yZhD2iowdQ==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.3", - "axios": "^1.1.3", + "axios": "^1.2.1", "compare-versions": "^5.0.1", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", @@ -15138,9 +15140,9 @@ } }, "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", "dev": true }, "dotgitignore": { @@ -18519,12 +18521,12 @@ } }, "node-html-parser": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.3.6.tgz", - "integrity": "sha512-VkWDHvNgFGB3mbQGMyzqRE1i/BG7TKX9wRXC8e/v8kL0kZR/Oy6RjYxXH91K6/+m3g8iQ8dTqRy75lTYoA2Cjg==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", "dev": true, "requires": { - "css-select": "^4.1.3", + "css-select": "^4.2.1", "he": "1.2.0" } }, @@ -20786,9 +20788,9 @@ } }, "wcag-act-rules": { - "version": "git+ssh://git@github.com/w3c/wcag-act-rules.git#9416ea60714ed6c916435c7b38db76062f3ef004", + "version": "git+ssh://git@github.com/w3c/wcag-act-rules.git#2341a1ba4d47bc4cdccd5a3b7534e67b52c59002", "dev": true, - "from": "wcag-act-rules@github:w3c/wcag-act-rules#9416ea6" + "from": "wcag-act-rules@github:w3c/wcag-act-rules#2341a1b" }, "weakmap-polyfill": { "version": "2.0.4", diff --git a/package.json b/package.json index 1d2f4d872b..6e19596def 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "axe-core", "description": "Accessibility engine for automated Web UI testing", - "version": "4.6.3", + "version": "4.7.0", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -83,14 +83,14 @@ "test:unit:integration": "npm run test:unit -- testDirs=integration", "test:unit:virtual-rules": "npm run test:unit -- testDirs=virtual-rules", "integration": "node test/integration/full/test-webdriver.js", - "integration:apg": "mocha test/aria-practices/*.spec.js", + "integration:apg": "mocha --fail-zero test/aria-practices/*.spec.js", "integration:chrome": "npm run integration -- browser=Chrome", "integration:firefox": "npm run integration -- browser=Firefox", "test:integration": "npm run test:integration:chrome", "test:integration:chrome": "start-server-and-test 9876 integration:chrome", "test:integration:firefox": "start-server-and-test 9876 integration:firefox", "test:examples": "node ./doc/examples/test-examples", - "test:act": "mocha test/act-rules/*.spec.js", + "test:act": "mocha --fail-zero test/act-rules/*.spec.js", "test:apg": "start-server-and-test 9876 integration:apg", "test:locales": "mocha test/test-locales.js", "test:virtual-rules": "mocha test/test-virtual-rules.js", @@ -115,7 +115,7 @@ "@babel/preset-env": "^7.20.2", "@babel/runtime-corejs3": "^7.20.7", "@deque/dot": "^1.1.5", - "aria-practices": "github:w3c/aria-practices#edbf534", + "aria-practices": "github:w3c/aria-practices#ce0336bd82d7d3651abcbde86af644197ddbc629", "aria-query": "^5.1.3", "browser-driver-manager": "1.0.4", "chai": "^4.3.7", @@ -174,7 +174,7 @@ "typedarray": "^0.0.7", "typescript": "^4.9.4", "uglify-js": "^3.17.4", - "wcag-act-rules": "github:w3c/wcag-act-rules#9416ea6", + "wcag-act-rules": "github:w3c/wcag-act-rules#2341a1b", "weakmap-polyfill": "^2.0.4" }, "lint-staged": { diff --git a/sri-history.json b/sri-history.json index 3df9ad31c0..c3bf0eb6ea 100644 --- a/sri-history.json +++ b/sri-history.json @@ -338,5 +338,9 @@ "4.6.3": { "axe.js": "sha256-N8N7PwDRFNyzsL80UoXAE5ibkfu/KJ71Ixw0zWYX+yM=", "axe.min.js": "sha256-vOhlk9sgryrpUZOv6801a9uYhuG6Z/NBJmOJb0aAQQQ=" + }, + "4.7.0": { + "axe.js": "sha256-JxLtVRh3EvdwPhr6ipXNnoN2ugUYvpwIAx4usr5jKcU=", + "axe.min.js": "sha256-q5YvHv5paIlrWyys5xKDb79XtmXYpiAHFVZcg61Qick=" } } diff --git a/test/act-rules/act-runner.js b/test/act-rules/act-runner.js index 024beb6dec..75251f0ac6 100644 --- a/test/act-rules/act-runner.js +++ b/test/act-rules/act-runner.js @@ -2,15 +2,10 @@ const path = require('path'); const fs = require('fs'); const http = require('http'); const handler = require('serve-handler'); -const chromedriver = require('chromedriver'); const AxeBuilder = require('@axe-core/webdriverjs'); -const { - getWebdriver, - connectToChromeDriver -} = require('../aria-practices/run-server'); +const { getWebdriver } = require('../get-webdriver'); const { assert } = require('chai'); -const driverPort = 9515; const serverPort = 9898; const axePath = require.resolve('../../axe.js'); const axeSource = fs.readFileSync(axePath, 'utf8'); @@ -34,9 +29,6 @@ module.exports = ({ id, title, axeRules, skipTests = [] }) => { this.retries(3); before(async () => { - chromedriver.start([`--port=${driverPort}`]); - await new Promise(r => setTimeout(r, 500)); - await connectToChromeDriver(driverPort); driver = getWebdriver(); }); @@ -59,7 +51,6 @@ module.exports = ({ id, title, axeRules, skipTests = [] }) => { after(async () => { await driver.close(); - chromedriver.stop(); await new Promise(r => server.close(r)); }); diff --git a/test/aria-practices/apg.spec.js b/test/aria-practices/apg.spec.js index 58e6748c6f..05708a1cb5 100644 --- a/test/aria-practices/apg.spec.js +++ b/test/aria-practices/apg.spec.js @@ -1,19 +1,20 @@ const path = require('path'); const fs = require('fs'); -const chromedriver = require('chromedriver'); const AxeBuilder = require('@axe-core/webdriverjs'); -const { getWebdriver, connectToChromeDriver } = require('./run-server'); +const { getWebdriver } = require('../get-webdriver'); const { assert } = require('chai'); const globby = require('globby'); describe('aria-practices', function () { - // Use path.resolve rather than require.resolve because APG has no package.json + // Use path.resolve rather than require.resolve because APG package.json main file does not exist const apgPath = path.resolve(__dirname, '../../node_modules/aria-practices/'); - const filePaths = globby.sync(`${apgPath}/examples/**/*.html`); + const filePaths = globby.sync( + `${apgPath}/content/patterns/*/**/examples/*.html` + ); const testFiles = filePaths.map( - fileName => fileName.split('/aria-practices/examples/')[1] + fileName => fileName.split('/aria-practices/content/patterns/')[1] ); - const port = 9515; + const addr = `http://localhost:9876/node_modules/aria-practices/`; let driver, axeSource; this.timeout(50000); @@ -22,15 +23,11 @@ describe('aria-practices', function () { before(async () => { const axePath = require.resolve('../../axe.js'); axeSource = fs.readFileSync(axePath, 'utf8'); - chromedriver.start([`--port=${port}`]); - await new Promise(r => setTimeout(r, 500)); - await connectToChromeDriver(port); driver = getWebdriver(); }); after(async () => { await driver.close(); - chromedriver.stop(); }); const disabledRules = { @@ -38,56 +35,40 @@ describe('aria-practices', function () { 'color-contrast', 'target-size', 'heading-order', // w3c/aria-practices#2119 - 'list', // w3c/aria-practices#2118 'scrollable-region-focusable' // w3c/aria-practices#2114 - ], - 'feed/feedDisplay.html': ['page-has-heading-one'], // w3c/aria-practices#2120 - // "page within a page" type thing going on - 'menubar/menubar-navigation.html': [ - 'aria-allowed-role', - 'landmark-banner-is-top-level', - 'landmark-contentinfo-is-top-level' - ], - // "page within a page" type thing going on - 'treeview/treeview-navigation.html': [ - 'aria-allowed-role', - 'landmark-banner-is-top-level', - 'landmark-contentinfo-is-top-level' - ], - // https://github.com/w3c/aria-practices/issues/2199 - 'button/button_idl.html': ['aria-allowed-attr'], - // https://github.com/w3c/aria-practices/issues/2285 - 'checkbox/checkbox.html': ['empty-table-header'], - 'dialog-modal/datepicker-dialog.html': ['empty-table-header'], - // https://github.com/w3c/aria-practices/issues/2505 - 'landmarks/search.html': ['link-in-text-block'] + ] }; // Not an actual content file const skippedPages = [ - 'index.html', // Not an example, just an index file - 'js/notice.html', // Embedded into another page - 'toolbar/help.html' // Embedded into another page + 'toolbar/examples/help.html' // Embedded into another page ]; + it('finds examples', () => { + assert.isTrue(testFiles.length > 0); + }); + testFiles .filter(filePath => !skippedPages.includes(filePath)) .forEach(filePath => { it(`finds no issue in "${filePath}"`, async () => { - await driver.get(`${addr}/examples/${filePath}`); + await driver.get(`${addr}content/patterns/${filePath}`); - const builder = new AxeBuilder(driver, axeSource); - builder.disableRules([ - ...disabledRules['*'], - ...(disabledRules[filePath] || []) - ]); + const builder = new AxeBuilder(driver, axeSource) + // Support table has no title and has duplicate ids + .exclude('#at-support') + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .disableRules([ + ...disabledRules['*'], + ...(disabledRules[filePath] || []) + ]); const { violations } = await builder.analyze(); const issues = violations.map(({ id, nodes }) => ({ id, issues: nodes.length })); - assert.lengthOf(issues, 0); + assert.lengthOf(issues, 0, issues.map(({ id }) => id).join(', ')); }); }); }); diff --git a/test/aria-practices/run-server.js b/test/aria-practices/run-server.js deleted file mode 100644 index 7ff38e4ecc..0000000000 --- a/test/aria-practices/run-server.js +++ /dev/null @@ -1,43 +0,0 @@ -const net = require('net'); -const { Builder } = require('selenium-webdriver'); -const chrome = require('selenium-webdriver/chrome'); - -const getWebdriver = () => { - const webdriver = new Builder() - .setChromeOptions(new chrome.Options().headless()) - .forBrowser('chrome') - .build(); - return webdriver; -}; - -const connectToChromeDriver = port => { - let socket; - return new Promise((resolve, reject) => { - // Give up after 1s - const timer = setTimeout(() => { - socket.destroy(); - reject(new Error('Unable to connect to ChromeDriver')); - }, 1000); - - const connectionListener = () => { - clearTimeout(timer); - socket.destroy(); - return resolve(); - }; - - socket = net.createConnection( - { host: 'localhost', port }, - connectionListener - ); - - // Fail on error - socket.once('error', err => { - clearTimeout(timer); - socket.destroy(); - return reject(err); - }); - }); -}; - -module.exports.getWebdriver = getWebdriver; -module.exports.connectToChromeDriver = connectToChromeDriver; diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index ab39ead842..47191efb09 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -1,19 +1,17 @@ -describe('aria-required-children', function () { - 'use strict'; +describe('aria-required-children', () => { + const fixture = document.getElementById('fixture'); + const shadowSupported = axe.testUtils.shadowSupport.v1; + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; - var fixture = document.getElementById('fixture'); - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; checkContext.reset(); }); - it('should detect missing sole required child', function () { - var params = checkSetup( + it('should detect missing sole required child', () => { + const params = checkSetup( '<div role="list" id="target"><p>Nothing here.</p></div>' ); @@ -27,17 +25,17 @@ describe('aria-required-children', function () { (shadowSupported ? it : xit)( 'should detect missing sole required child in shadow tree', - function () { + () => { fixture.innerHTML = '<div id="target" role="list"></div>'; - var target = document.querySelector('#target'); - var shadowRoot = target.attachShadow({ mode: 'open' }); + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '<p>Nothing here.</p>'; axe.testUtils.flatTreeSetup(fixture); - var virtualTarget = axe.utils.getNodeFromTree(target); + const virtualTarget = axe.utils.getNodeFromTree(target); - var params = [target, undefined, virtualTarget]; + const params = [target, undefined, virtualTarget]; assert.isFalse( axe.testUtils .getCheckEvaluate('aria-required-children') @@ -47,8 +45,8 @@ describe('aria-required-children', function () { } ); - it('should detect multiple missing required children when one required', function () { - var params = checkSetup( + it('should detect multiple missing required children when one required', () => { + const params = checkSetup( '<div role="grid" id="target"><p>Nothing here.</p></div>' ); @@ -62,17 +60,17 @@ describe('aria-required-children', function () { (shadowSupported ? it : xit)( 'should detect missing multiple required children in shadow tree when one required', - function () { + () => { fixture.innerHTML = '<div role="grid" id="target"></div>'; - var target = document.querySelector('#target'); - var shadowRoot = target.attachShadow({ mode: 'open' }); + const target = document.querySelector('#target'); + const shadowRoot = target.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '<p>Nothing here.</p>'; axe.testUtils.flatTreeSetup(fixture); - var virtualTarget = axe.utils.getNodeFromTree(target); + const virtualTarget = axe.utils.getNodeFromTree(target); - var params = [target, undefined, virtualTarget]; + const params = [target, undefined, virtualTarget]; assert.isFalse( axe.testUtils .getCheckEvaluate('aria-required-children') @@ -82,8 +80,8 @@ describe('aria-required-children', function () { } ); - it('should pass all existing required children when all required', function () { - var params = checkSetup( + it('should pass all existing required children when all required', () => { + const params = checkSetup( '<div id="target" role="menu"><li role="none"></li><li role="menuitem">Item 1</li><div role="menuitemradio">Item 2</div><div role="menuitemcheckbox">Item 3</div></div>' ); assert.isTrue( @@ -93,8 +91,8 @@ describe('aria-required-children', function () { ); }); - it('should return undefined when element is empty and is in reviewEmpty options', function () { - var params = checkSetup('<div role="list" id="target"></div>', { + it('should return undefined when element is empty and is in reviewEmpty options', () => { + const params = checkSetup('<div role="list" id="target"></div>', { reviewEmpty: ['list'] }); assert.isUndefined( @@ -104,8 +102,8 @@ describe('aria-required-children', function () { ); }); - it('should return false when children do not have correct role and is in reviewEmpty options', function () { - var params = checkSetup( + it('should return false when children do not have correct role and is in reviewEmpty options', () => { + const params = checkSetup( '<div role="list" id="target"><div role="menuitem"></div></div>', { reviewEmpty: ['list'] } ); @@ -116,8 +114,8 @@ describe('aria-required-children', function () { ); }); - it('should return false when owned children do not have correct role and is in reviewEmpty options', function () { - var params = checkSetup( + it('should return false when owned children do not have correct role and is in reviewEmpty options', () => { + const params = checkSetup( '<div role="list" id="target" aria-owns="ownedchild"></div><div id="ownedchild" role="menuitem"></div>', { reviewEmpty: ['list'] } ); @@ -128,8 +126,8 @@ describe('aria-required-children', function () { ); }); - it('should fail when list does not have required children listitem', function () { - var params = checkSetup( + it('should fail when list does not have required children listitem', () => { + const params = checkSetup( '<div id="target" role="list"><span>Item 1</span></div>' ); assert.isFalse( @@ -141,8 +139,8 @@ describe('aria-required-children', function () { assert.deepEqual(checkContext._data, ['listitem']); }); - it('should fail when list has intermediate child with role that is not a required role', function () { - var params = checkSetup( + it('should fail when list has intermediate child with role that is not a required role', () => { + const params = checkSetup( '<div id="target" role="list"><div role="tabpanel"><div role="listitem">List item 1</div></div></div>' ); assert.isFalse( @@ -151,16 +149,19 @@ describe('aria-required-children', function () { .apply(checkContext, params) ); - var unallowed = axe.utils.querySelectorAll( + const unallowed = axe.utils.querySelectorAll( axe._tree, '[role="tabpanel"]' )[0]; - assert.deepEqual(checkContext._data, { messageKey: 'unallowed' }); + assert.deepEqual(checkContext._data, { + messageKey: 'unallowed', + values: '[role=tabpanel]' + }); assert.deepEqual(checkContext._relatedNodes, [unallowed]); }); - it('should fail when list has child with global aria attribute but no role', function () { - var params = checkSetup( + it('should fail when list has child with global aria attribute but no role', () => { + const params = checkSetup( '<div id="target" role="list"><div aria-live="polite"><div role="listitem">List item 1</div></div></div>' ); assert.isFalse( @@ -169,16 +170,19 @@ describe('aria-required-children', function () { .apply(checkContext, params) ); - var unallowed = axe.utils.querySelectorAll( + const unallowed = axe.utils.querySelectorAll( axe._tree, '[aria-live="polite"]' )[0]; - assert.deepEqual(checkContext._data, { messageKey: 'unallowed' }); + assert.deepEqual(checkContext._data, { + messageKey: 'unallowed', + values: 'div[aria-live]' + }); assert.deepEqual(checkContext._relatedNodes, [unallowed]); }); - it('should fail when list has child with tabindex but no role', function () { - var params = checkSetup( + it('should fail when list has child with tabindex but no role', () => { + const params = checkSetup( '<div id="target" role="list"><div tabindex="0"><div role="listitem">List item 1</div></div></div>' ); assert.isFalse( @@ -187,13 +191,67 @@ describe('aria-required-children', function () { .apply(checkContext, params) ); - var unallowed = axe.utils.querySelectorAll(axe._tree, '[tabindex="0"]')[0]; - assert.deepEqual(checkContext._data, { messageKey: 'unallowed' }); + const unallowed = axe.utils.querySelectorAll( + axe._tree, + '[tabindex="0"]' + )[0]; + assert.deepEqual(checkContext._data, { + messageKey: 'unallowed', + values: 'div[tabindex]' + }); assert.deepEqual(checkContext._relatedNodes, [unallowed]); }); - it('should fail when nested child with role row does not have required child role cell', function () { - var params = checkSetup( + it('should remove duplicate unallowed selectors', () => { + const params = checkSetup(` + <div id="target" role="list"> + <div role="tabpanel"></div> + <div role="listitem">List item 1</div> + <div role="tabpanel"></div> + </div> + `); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-required-children') + .apply(checkContext, params) + ); + + assert.deepEqual(checkContext._data, { + messageKey: 'unallowed', + values: '[role=tabpanel]' + }); + }); + + it('should pass when list has child with aria-hidden', () => { + const params = checkSetup( + '<div id="target" role="list">' + + '<div aria-hidden="true">Ignore item</div>' + + '<div role="listitem">List item 1</div>' + + '</div>' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-required-children') + .apply(checkContext, params) + ); + }); + + it('should pass when list has child with aria-hidden and is focusable', () => { + const params = checkSetup( + '<div id="target" role="list">' + + '<div aria-hidden="true" tabindex="0">Ignore item</div>' + + '<div role="listitem">List item 1</div>' + + '</div>' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-required-children') + .apply(checkContext, params) + ); + }); + + it('should fail when nested child with role row does not have required child role cell', () => { + const params = checkSetup( '<div role="grid"><div role="row" id="target"><span>Item 1</span></div></div>' ); assert.isFalse( @@ -205,8 +263,8 @@ describe('aria-required-children', function () { assert.includeMembers(checkContext._data, ['cell']); }); - it('should pass one indirectly aria-owned child when one required', function () { - var params = checkSetup( + it('should pass one indirectly aria-owned child when one required', () => { + const params = checkSetup( '<div role="grid" id="target" aria-owns="r"></div><div id="r"><div role="row">Nothing here.</div></div>' ); assert.isTrue( @@ -216,8 +274,8 @@ describe('aria-required-children', function () { ); }); - it('should not break if aria-owns points to non-existent node', function () { - var params = checkSetup( + it('should not break if aria-owns points to non-existent node', () => { + const params = checkSetup( '<div role="grid" id="target" aria-owns="nonexistent"></div>' ); assert.isFalse( @@ -227,8 +285,8 @@ describe('aria-required-children', function () { ); }); - it('should pass one existing aria-owned child when one required', function () { - var params = checkSetup( + it('should pass one existing aria-owned child when one required', () => { + const params = checkSetup( '<div role="grid" id="target" aria-owns="r"></div><p id="r" role="row">Nothing here.</p>' ); assert.isTrue( @@ -238,8 +296,8 @@ describe('aria-required-children', function () { ); }); - it('should fail one existing aria-owned child when an intermediate child with role that is not a required role exists', function () { - var params = checkSetup( + it('should fail one existing aria-owned child when an intermediate child with role that is not a required role exists', () => { + const params = checkSetup( '<div id="target" role="list" aria-owns="list"></div><div id="list"><div role="tabpanel"><div role="listitem"></div></div></div>' ); assert.isFalse( @@ -249,8 +307,8 @@ describe('aria-required-children', function () { ); }); - it('should pass one existing required child when one required (has explicit role of tab)', function () { - var params = checkSetup( + it('should pass one existing required child when one required (has explicit role of tab)', () => { + const params = checkSetup( '<ul id="target" role="tablist"><li role="tab">Tab 1</li><li role="tab">Tab 2</li></ul>' ); assert.isTrue( @@ -260,8 +318,8 @@ describe('aria-required-children', function () { ); }); - it('should pass required child roles (grid contains row, which contains cell)', function () { - var params = checkSetup( + it('should pass required child roles (grid contains row, which contains cell)', () => { + const params = checkSetup( '<table id="target" role="grid"><tr role="row"><td role="cell">Item 1</td></tr></table>' ); assert.isTrue( @@ -271,8 +329,8 @@ describe('aria-required-children', function () { ); }); - it('should pass one existing required child when one required', function () { - var params = checkSetup( + it('should pass one existing required child when one required', () => { + const params = checkSetup( '<div role="grid" id="target"><p role="row">Nothing here.</p></div>' ); assert.isTrue( @@ -282,8 +340,8 @@ describe('aria-required-children', function () { ); }); - it('should pass one existing required child when one required because of implicit role', function () { - var params = checkSetup( + it('should pass one existing required child when one required because of implicit role', () => { + const params = checkSetup( '<table id="target"><p role="row">Nothing here.</p></table>' ); assert.isTrue( @@ -293,8 +351,8 @@ describe('aria-required-children', function () { ); }); - it('should pass when a child with an implicit role is present', function () { - var params = checkSetup( + it('should pass when a child with an implicit role is present', () => { + const params = checkSetup( '<table role="grid" id="target"><tr><td>Nothing here.</td></tr></table>' ); assert.isTrue( @@ -304,8 +362,8 @@ describe('aria-required-children', function () { ); }); - it('should pass direct existing required children', function () { - var params = checkSetup( + it('should pass direct existing required children', () => { + const params = checkSetup( '<div role="list" id="target"><p role="listitem">Nothing here.</p></div>' ); assert.isTrue( @@ -315,8 +373,8 @@ describe('aria-required-children', function () { ); }); - it('should pass indirect required children', function () { - var params = checkSetup( + it('should pass indirect required children', () => { + const params = checkSetup( '<div role="list" id="target"><p>Just a regular ol p that contains a... <p role="listitem">Nothing here.</p></p></div>' ); assert.isTrue( @@ -326,8 +384,8 @@ describe('aria-required-children', function () { ); }); - it('should return true when a role has no required owned', function () { - var params = checkSetup( + it('should return true when a role has no required owned', () => { + const params = checkSetup( '<div role="listitem" id="target"><p>Nothing here.</p></div>' ); assert.isTrue( @@ -337,8 +395,8 @@ describe('aria-required-children', function () { ); }); - it('should pass when role allows group and group has required child', function () { - var params = checkSetup( + it('should pass when role allows group and group has required child', () => { + const params = checkSetup( '<div role="menu" id="target"><ul role="group"><li role="menuitem">Menuitem</li></ul></div>' ); assert.isTrue( @@ -348,8 +406,8 @@ describe('aria-required-children', function () { ); }); - it('should fail when role allows group and group does not have required child', function () { - var params = checkSetup( + it('should fail when role allows group and group does not have required child', () => { + const params = checkSetup( '<div role="menu" id="target"><ul role="group"><li>Menuitem</li></ul></div>' ); assert.isFalse( @@ -359,8 +417,8 @@ describe('aria-required-children', function () { ); }); - it('should fail when role does not allow group', function () { - var params = checkSetup( + it('should fail when role does not allow group', () => { + const params = checkSetup( '<div role="list" id="target"><ul role="group"><li role="listitem">Item</li></ul></div>' ); assert.isFalse( @@ -370,8 +428,8 @@ describe('aria-required-children', function () { ); }); - it('should pass when role allows rowgroup and rowgroup has required child', function () { - var params = checkSetup( + it('should pass when role allows rowgroup and rowgroup has required child', () => { + const params = checkSetup( '<div role="table" id="target"><ul role="rowgroup"><li role="row">Row</li></ul></div>' ); assert.isTrue( @@ -381,8 +439,8 @@ describe('aria-required-children', function () { ); }); - it('should fail when role allows rowgroup and rowgroup does not have required child', function () { - var params = checkSetup( + it('should fail when role allows rowgroup and rowgroup does not have required child', () => { + const params = checkSetup( '<div role="table" id="target"><ul role="rowgroup"><li>Row</li></ul></div>' ); assert.isFalse( @@ -392,8 +450,8 @@ describe('aria-required-children', function () { ); }); - it('should fail when role does not allow rowgroup', function () { - var params = checkSetup( + it('should fail when role does not allow rowgroup', () => { + const params = checkSetup( '<div role="listbox" id="target"><ul role="rowgroup"><li role="option">Option</li></ul></div>' ); assert.isFalse( @@ -403,9 +461,9 @@ describe('aria-required-children', function () { ); }); - describe('options', function () { - it('should return undefined instead of false when the role is in options.reviewEmpty', function () { - var params = checkSetup('<div role="grid" id="target"></div>', { + describe('options', () => { + it('should return undefined instead of false when the role is in options.reviewEmpty', () => { + const params = checkSetup('<div role="grid" id="target"></div>', { reviewEmpty: [] }); assert.isFalse( @@ -425,8 +483,8 @@ describe('aria-required-children', function () { ); }); - it('should not throw when options is incorrect', function () { - var params = checkSetup('<div role="row" id="target"></div>'); + it('should not throw when options is incorrect', () => { + const params = checkSetup('<div role="row" id="target"></div>'); // Options: (incorrect) params[1] = ['menu']; @@ -453,8 +511,8 @@ describe('aria-required-children', function () { ); }); - it('should return undefined when the element has empty children', function () { - var params = checkSetup( + it('should return undefined when the element has empty children', () => { + const params = checkSetup( '<div role="listbox" id="target"><div></div></div>' ); params[1] = { @@ -467,8 +525,8 @@ describe('aria-required-children', function () { ); }); - it('should return false when the element has empty child with role', function () { - var params = checkSetup( + it('should return false when the element has empty child with role', () => { + const params = checkSetup( '<div role="listbox" id="target"><div role="grid"></div></div>' ); params[1] = { @@ -481,8 +539,8 @@ describe('aria-required-children', function () { ); }); - it('should return undefined when the element has empty child with role=presentation', function () { - var params = checkSetup( + it('should return undefined when the element has empty child with role=presentation', () => { + const params = checkSetup( '<div role="listbox" id="target"><div role="presentation"></div></div>' ); params[1] = { @@ -495,8 +553,8 @@ describe('aria-required-children', function () { ); }); - it('should return undefined when the element has empty child with role=none', function () { - var params = checkSetup( + it('should return undefined when the element has empty child with role=none', () => { + const params = checkSetup( '<div role="listbox" id="target"><div role="none"></div></div>' ); params[1] = { @@ -509,8 +567,8 @@ describe('aria-required-children', function () { ); }); - it('should return undefined when the element has empty child and aria-label', function () { - var params = checkSetup( + it('should return undefined when the element has empty child and aria-label', () => { + const params = checkSetup( '<div role="listbox" id="target" aria-label="listbox"><div></div></div>' ); params[1] = { diff --git a/test/checks/aria/valid-scrollable-semantics.js b/test/checks/aria/valid-scrollable-semantics.js index c4aca6578e..26ea72533a 100644 --- a/test/checks/aria/valid-scrollable-semantics.js +++ b/test/checks/aria/valid-scrollable-semantics.js @@ -106,6 +106,18 @@ describe('valid-scrollable-semantics', function () { ); }); + it('should return true for role=article', function () { + var node = document.createElement('div'); + node.setAttribute('role', 'article'); + fixture.appendChild(node); + flatTreeSetup(fixture); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('valid-scrollable-semantics') + .call(checkContext, node) + ); + }); + it('should return true for nav elements', function () { var node = document.createElement('nav'); fixture.appendChild(node); diff --git a/test/checks/mobile/css-orientation-lock.js b/test/checks/mobile/css-orientation-lock.js index 46aa3ba71e..62175eccc2 100644 --- a/test/checks/mobile/css-orientation-lock.js +++ b/test/checks/mobile/css-orientation-lock.js @@ -302,6 +302,51 @@ describe('css-orientation-lock tests', function () { assert.isFalse(actual); }); + it('returns false when CSSOM has Orientation CSS media features with rotate property', function () { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { rotate: 90deg; } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + + it('returns false when CSSOM has Orientation CSS media features with rotate property matrix', function () { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { rotate: 0 0 1 1.5708rad; } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + + it('returns false when CSSOM has Orientation CSS media features with transform: rotate and rotate property', function () { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { rotate: 45deg; transform: rotate(45deg); -webkit-transform: rotate(45deg); } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + // Note: // external stylesheets is tested in integration tests // shadow DOM is tested in integration tests diff --git a/test/commons/aria/is-combobox-popup.js b/test/commons/aria/is-combobox-popup.js new file mode 100644 index 0000000000..65ff04ac2b --- /dev/null +++ b/test/commons/aria/is-combobox-popup.js @@ -0,0 +1,145 @@ +describe('isComboboxPopup', () => { + const { isComboboxPopup } = axe.commons.aria; + const { queryFixture } = axe.testUtils; + + it('does not match non-popup roles', () => { + const roles = ['main', 'combobox', 'textbox', 'button']; + for (const role of roles) { + const vNode = queryFixture( + `<div role="combobox" aria-controls="target"></div> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + } + }); + + for (const role of ['menu', 'listbox', 'tree', 'grid', 'dialog']) { + describe(role, () => { + it('is false when not related to the combobox', () => { + const vNode = queryFixture( + `<div role="combobox"></div> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + + describe('using aria-controls (ARIA 1.2 pattern)', () => { + it('is true when referenced', () => { + const vNode = queryFixture( + `<div role="combobox" aria-controls="target"></div> + <div role="${role}" id="target"></div>` + ); + assert.isTrue(isComboboxPopup(vNode)); + }); + + it('is false when controlled by a select element', () => { + const vNode = queryFixture( + `<select role="combobox" aria-controls="target"></select> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + + it('is false when not controlled by a combobox', () => { + const vNode = queryFixture( + `<div role="button combobox" aria-controls="target"></div> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + }); + + describe('using parent owned (ARIA 1.1 pattern)', () => { + it('is true when its a child of the combobox', () => { + const vNode = queryFixture( + `<div role="combobox"> + <div role="${role}" id="target"></div> + </div>` + ); + assert.isTrue(isComboboxPopup(vNode)); + }); + + it('is false when its not a child of a real combobox', () => { + const vNode = queryFixture( + `<div role="button combobox"> + <div role="${role}" id="target"></div> + </div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + + it('is false when its nearest parent with a role is not a combobox', () => { + const vNode = queryFixture( + `<div role="combobox"> + <div role="region"> + <div role="${role}" id="target"></div> + </div> + </div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + + it('is true when its nearest parent with a role is not a combobox', () => { + const vNode = queryFixture( + `<div role="combobox"> + <div> + <div role="none"> + <div role="presentation"> + <div role="${role}" id="target"></div> + </div> + </div> + </div> + </div>` + ); + assert.isTrue(isComboboxPopup(vNode)); + }); + }); + + describe('when using aria-owns (ARIA 1.0 pattern)', () => { + it('is true when referenced', () => { + const vNode = queryFixture( + `<div role="combobox" aria-owns="target"></div> + <div role="${role}" id="target"></div>` + ); + assert.isTrue(isComboboxPopup(vNode)); + }); + + it('is false when owned by a select element', () => { + const vNode = queryFixture( + `<select role="combobox" aria-owns="target"></select> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + + it('is false when not owned by a combobox', () => { + const vNode = queryFixture( + `<div role="button combobox" aria-owns="target"></div> + <div role="${role}" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + }); + }); + }); + } + + describe('options.popupRoles', () => { + it('allows custom popup roles', () => { + const vNode = queryFixture( + `<div role="combobox" aria-controls="target"></div> + <div role="button" id="target"></div>` + ); + assert.isFalse(isComboboxPopup(vNode)); + assert.isTrue(isComboboxPopup(vNode, { popupRoles: ['button'] })); + }); + + it('overrides the default popup roles', () => { + const vNode = queryFixture( + `<div role="combobox" aria-controls="target"></div> + <div role="listbox" id="target"></div>` + ); + assert.isTrue(isComboboxPopup(vNode)); + assert.isFalse(isComboboxPopup(vNode, { popupRoles: ['button'] })); + }); + }); +}); diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index 7bb84a894b..fb76e89328 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -145,6 +145,40 @@ describe('color.getBackgroundColor', function () { assert.deepEqual(bgNodes, [target, parent]); }); + it('should apply opacity after blending', function () { + fixture.innerHTML = ` + <div id="parent" style="height: 40px; width: 30px; background-color: rgba(128,0,0,1); opacity: 0.8;"> + <div id="target" style="height: 20px; width: 15px; background-color: rgba(0,255,0,0.5);"></div> + </div>`; + var target = fixture.querySelector('#target'); + var bgNodes = []; + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, bgNodes); + var expected = new axe.commons.color.Color(102, 153, 51, 1); + assert.equal(actual.red, expected.red); + assert.equal(actual.green, expected.green); + assert.equal(actual.blue, expected.blue); + assert.equal(actual.alpha, expected.alpha); + }); + + it('should apply opacity from an ancestor not in the element stack', function () { + fixture.innerHTML = ` + <div style="opacity: 0.8;"> + <div id="parent" style="position: absolute; height: 40px; width: 30px; background-color: rgba(128,0,0,1);"> + <div id="target" style="height: 20px; width: 15px; background-color: rgba(0,255,0,0.5);"></div> + </div> + </div>`; + var target = fixture.querySelector('#target'); + var bgNodes = []; + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, bgNodes); + var expected = new axe.commons.color.Color(102, 153, 51, 1); + assert.equal(actual.red, expected.red); + assert.equal(actual.green, expected.green); + assert.equal(actual.blue, expected.blue); + assert.equal(actual.alpha, expected.alpha); + }); + it('should return null if containing parent has a background image and is non-opaque', function () { fixture.innerHTML = '<div id="parent" style="height: 40px; width: 30px;' + @@ -802,6 +836,25 @@ describe('color.getBackgroundColor', function () { document.body.style.height = originalHeight; }); + it('should apply mix-blend-mode', function () { + fixture.innerHTML = ` + <div style="background-color: rgba(255, 255, 255, 1)"> + <div style="background-color: rgba(0, 128, 0, 0.25)"> + <div id="target" style="background-color: rgba(255, 0, 0, 0.5); mix-blend-mode: exclusion;">exclusion1</div> + </div> + </div> + `; + + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var actual = axe.commons.color.getBackgroundColor(target, []); + + assert.closeTo(actual.red, 128, 0); + assert.closeTo(actual.green, 223, 0); + assert.closeTo(actual.blue, 191, 0); + assert.closeTo(actual.alpha, 1, 0); + }); + (shadowSupported ? it : xit)( 'finds colors in shadow boundaries', function () { diff --git a/test/commons/color/get-foreground-color.js b/test/commons/color/get-foreground-color.js index bbfc3cdd37..50b7ea5526 100644 --- a/test/commons/color/get-foreground-color.js +++ b/test/commons/color/get-foreground-color.js @@ -17,7 +17,7 @@ describe('color.getForegroundColor', () => { it('returns the CSS color property', () => { const target = queryFixture( - '<div id="target" style="color: rgb(0 0 128)"></div>' + '<div id="target" style="color: rgb(0 0 128)">Hello World</div>' ).actualNode; const fgColor = getForegroundColor(target); assertSameColor(fgColor, new Color(0, 0, 128)); @@ -26,7 +26,7 @@ describe('color.getForegroundColor', () => { it('returns the CSS color from inside of Shadow DOM', () => { const target = queryShadowFixture( '<div id="shadow" style="height: 40px; width: 30px; background-color: red;"></div>', - '<div id="target" style="height:20px; width:15px; color:rgb(0 0 128); background-color:green;"></div>' + '<div id="target" style="height:20px; width:15px; color:rgb(0 0 128); background-color:green;">Hello World</div>' ).actualNode; const fgColor = getForegroundColor(target); @@ -38,28 +38,16 @@ describe('color.getForegroundColor', () => { '<div style="height: 40px; width: 30px;' + 'background-color: #800000; background-image: url(image.png);">' + '<div id="target" style="height: 20px; width: 15px; color: blue; background-color: green; opacity: 0.5;">' + + 'Hello World' + '</div></div>' ).actualNode; assert.isNull(getForegroundColor(target)); assert.equal(axe.commons.color.incompleteData.get('fgColor'), 'bgImage'); }); - it('does not recalculate bgColor if passed in', () => { - const target = queryFixture( - '<div style="height: 40px; background-color: #000000;">' + - '<div id="target" style="height: 40px; color: rgba(0, 0, 128, 0.5);">' + - 'This is my text' + - '</div></div>' - ).actualNode; - - const bgColor = new Color(64, 64, 0); - const fgColor = getForegroundColor(target, false, bgColor); - assertSameColor(fgColor, new Color(32, 32, 64), 0.8); - }); - it('returns `-webkit-text-fill-color` over `color`', () => { const target = queryFixture( - '<div id="target" style="-webkit-text-fill-color: rgb(0 0 255); color: rgb(0 0 128)"></div>' + '<div id="target" style="-webkit-text-fill-color: rgb(0 0 255); color: rgb(0 0 128)">Hello World</div>' ).actualNode; const fgColor = getForegroundColor(target); assertSameColor(fgColor, new Color(0, 0, 255)); @@ -68,7 +56,7 @@ describe('color.getForegroundColor', () => { describe('text-stroke', () => { it('ignores stroke when equal to 0', () => { const target = queryFixture( - '<div style="color: rgb(0 0 128); -webkit-text-stroke: 0 #CCC" id="target"></div>' + '<div style="color: rgb(0 0 128); -webkit-text-stroke: 0 #CCC" id="target">Hello World</div>' ).actualNode; const options = { textStrokeEmMin: 0 }; const fgColor = getForegroundColor(target, null, null, options); @@ -77,7 +65,7 @@ describe('color.getForegroundColor', () => { it('ignores stroke when less then the minimum', () => { const target = queryFixture( - '<div style="color: rgb(0 0 128); -webkit-text-stroke: 0.1em #CCC" id="target"></div>' + '<div style="color: rgb(0 0 128); -webkit-text-stroke: 0.1em #CCC" id="target">Hello World</div>' ).actualNode; const options = { textStrokeEmMin: 0.2 }; const fgColor = getForegroundColor(target, null, null, options); @@ -86,7 +74,7 @@ describe('color.getForegroundColor', () => { it('uses stroke color when thickness is equal to the minimum', () => { const target = queryFixture( - '<div style="color: #CCC; -webkit-text-stroke: 0.2em rgb(0 0 128);" id="target"></div>' + '<div style="color: #CCC; -webkit-text-stroke: 0.2em rgb(0 0 128);" id="target">Hello World</div>' ).actualNode; const options = { textStrokeEmMin: 0.2 }; const fgColor = getForegroundColor(target, null, null, options); @@ -95,7 +83,7 @@ describe('color.getForegroundColor', () => { it('blends the stroke color with `color`', () => { const target = queryFixture( - '<div style="color: rgb(0 0 55); -webkit-text-stroke: 0.2em rgb(0 0 255 / 50%);" id="target"></div>' + '<div style="color: rgb(0 0 55); -webkit-text-stroke: 0.2em rgb(0 0 255 / 50%);" id="target">Hello World</div>' ).actualNode; const options = { textStrokeEmMin: 0.1 }; const fgColor = getForegroundColor(target, null, null, options); @@ -125,7 +113,15 @@ describe('color.getForegroundColor', () => { '</div></div>' ).actualNode; const fgColor = getForegroundColor(target); - assertSameColor(fgColor, new Color(32, 32, 64)); + assertSameColor(fgColor, new Color(64, 0, 64)); + }); + + it('does not apply opacity to node background', () => { + const target = queryFixture( + '<div id="target" style="color: #fff; background-color: #00633D; opacity: 0.65"><span>Hello World</span></div>' + ).actualNode; + const fgColor = getForegroundColor(target); + assertSameColor(fgColor, new Color(255, 255, 255)); }); it('combines opacity with text stroke alpha color', () => { diff --git a/test/commons/color/stacking-context.js b/test/commons/color/stacking-context.js new file mode 100644 index 0000000000..d02d62e48c --- /dev/null +++ b/test/commons/color/stacking-context.js @@ -0,0 +1,278 @@ +describe('color.stackingContext', () => { + const { Color, getStackingContext, stackingContextToColor } = + axe.commons.color; + const { getElementStack } = axe.commons.dom; + const { querySelectorAll } = axe.utils; + const { queryFixture } = axe.testUtils; + const fixture = document.querySelector('#fixture'); + + beforeEach(() => { + // remove html, body, and fixture from the + // element stack to make testing easier + document.documentElement.style.height = 0; + document.body.style.height = 0; + fixture.style.height = 0; + }); + + afterEach(() => { + document.documentElement.removeAttribute('style'); + document.body.removeAttribute('style'); + fixture.removeAttribute('style'); + }); + + describe('color.getStackingContexts', () => { + it('creates a context for a single element', () => { + const vNode = queryFixture('<div id="target">Hello World</div>'); + // html is always added as the last element + // so we'll remove it to make testing easier + const elmStack = getElementStack(vNode.actualNode).slice(0, -1); + const stackingContext = getStackingContext(vNode.actualNode, elmStack); + + assert.deepEqual(stackingContext, [ + { + vNode, + ancestor: undefined, + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + } + ]); + }); + + it('creates a context for every node in the element stack', () => { + const vNode = queryFixture(` + <div id="elm1"> + <div id="elm2"> + <div id="target">Hello World</div> + </div> + </div> + `); + const elmStack = getElementStack(vNode.actualNode).slice(0, -1); + const stackingContext = getStackingContext(vNode.actualNode, elmStack); + + assert.deepEqual(stackingContext, [ + { + vNode: querySelectorAll(axe._tree[0], '#elm1')[0], + ancestor: undefined, + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + }, + { + vNode: querySelectorAll(axe._tree[0], '#elm2')[0], + ancestor: undefined, + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + }, + { + vNode, + ancestor: undefined, + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + } + ]); + }); + + it('nests contexts', () => { + const vNode = queryFixture(` + <div id="elm1" style="position: absolute; z-index: 2"> + <div id="elm2"> + <div id="target">Hello World</div> + </div> + </div> + `); + const elmStack = getElementStack(vNode.actualNode).slice(0, -1); + const stackingContext = getStackingContext(vNode.actualNode, elmStack); + + assert.deepEqual(stackingContext, [ + { + vNode: querySelectorAll(axe._tree[0], '#elm1')[0], + ancestor: undefined, + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [ + { + vNode: querySelectorAll(axe._tree[0], '#elm2')[0], + ancestor: stackingContext[0], + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + }, + { + vNode, + ancestor: stackingContext[0], + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + } + ] + } + ]); + }); + + it('sets context properties', () => { + const vNode = queryFixture( + '<div id="target" style="background-color: rgba(255,0,0,0.5); opacity: 0.8; mix-blend-mode: difference;">Hello World</div>' + ); + const elmStack = getElementStack(vNode.actualNode).slice(0, -1); + const stackingContext = getStackingContext(vNode.actualNode, elmStack); + + assert.deepEqual(stackingContext, [ + { + vNode, + ancestor: undefined, + opacity: 0.8, + bgColor: new Color(255, 0, 0, 0.5), + blendMode: 'difference', + descendants: [] + } + ]); + }); + + it('creates a context for ancestors that create a stacking context but are not in the element stack', () => { + const vNode = queryFixture(` + <div id="elm1" style="opacity: 0.8"> + <div id="target" style="position: absolute; z-index: 2">Hello World</div> + </div> + `); + const elmStack = getElementStack(vNode.actualNode).slice(0, -1); + const stackingContext = getStackingContext(vNode.actualNode, elmStack); + + assert.lengthOf(elmStack, 1); + assert.deepEqual(stackingContext, [ + { + vNode: querySelectorAll(axe._tree[0], '#elm1')[0], + ancestor: undefined, + opacity: 0.8, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [ + { + vNode, + ancestor: stackingContext[0], + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [] + } + ] + } + ]); + }); + }); + + describe('color.stackingContextToColor', () => { + it('reduces a context to a color', () => { + const context = { + opacity: 1, + bgColor: new Color(255, 128, 0, 1), + blendMode: 'normal', + descendants: [] + }; + + assert.deepEqual(stackingContextToColor(context), { + color: new Color(255, 128, 0, 1), + blendMode: 'normal' + }); + }); + + it('reduces a nested context to a color', () => { + const context = { + opacity: 1, + bgColor: new Color(255, 128, 0, 1), + blendMode: 'normal', + descendants: [ + { + opacity: 1, + bgColor: new Color(0, 255, 128, 0.5), + blendMode: 'normal', + descendants: [] + } + ] + }; + + assert.deepEqual(stackingContextToColor(context), { + color: new Color(128, 192, 64, 1), + blendMode: 'normal' + }); + }); + + it('applies opacity after blending', () => { + const context = { + opacity: 0.8, + bgColor: new Color(255, 128, 0, 1), + blendMode: 'normal', + descendants: [ + { + opacity: 1, + bgColor: new Color(0, 255, 128, 0.5), + blendMode: 'normal', + descendants: [] + } + ] + }; + + assert.deepEqual(stackingContextToColor(context), { + color: new Color(128, 192, 64, 0.8), + blendMode: 'normal' + }); + }); + + it('applies mix-blend-mode from a sibling context', () => { + const context = { + opacity: 1, + bgColor: new Color(0, 0, 0, 0), + blendMode: 'normal', + descendants: [ + { + opacity: 1, + bgColor: new Color(255, 128, 0, 1), + blendMode: 'normal', + descendants: [] + }, + { + opacity: 1, + bgColor: new Color(0, 255, 128, 0.5), + blendMode: 'difference', + descendants: [] + } + ] + }; + + assert.deepEqual(stackingContextToColor(context), { + color: new Color(255, 128, 64, 1), + blendMode: 'normal' + }); + }); + + it('applies mix-blend-mode from a nested context', () => { + const context = { + opacity: 1, + bgColor: new Color(255, 128, 0, 1), + blendMode: 'normal', + descendants: [ + { + opacity: 1, + bgColor: new Color(0, 255, 128, 0.5), + blendMode: 'difference', + descendants: [] + } + ] + }; + + assert.deepEqual(stackingContextToColor(context), { + color: new Color(255, 128, 64, 1), + blendMode: 'normal' + }); + }); + }); +}); diff --git a/test/commons/dom/focus-disabled.js b/test/commons/dom/focus-disabled.js index 2f8a242dc4..998dd60f9d 100644 --- a/test/commons/dom/focus-disabled.js +++ b/test/commons/dom/focus-disabled.js @@ -51,6 +51,29 @@ describe('dom.focus-disabled', () => { assert.isTrue(focusDisabled(vNode)); }); + it('returns true for element with inert', () => { + const vNode = queryFixture('<button id="target" inert></button>'); + + assert.isTrue(focusDisabled(vNode)); + }); + + it('returns true for ancestor with inert', () => { + const vNode = queryFixture( + '<div inert><div><button id="target"></button></div></div>' + ); + + assert.isTrue(focusDisabled(vNode)); + }); + + it('returns true for ancestor with inert outside shadow tree', () => { + const vNode = queryShadowFixture( + '<div inert><div id="shadow"></div></div>', + '<input id="target"/>' + ); + + assert.isTrue(focusDisabled(vNode)); + }); + describe('SerialVirtualNode', () => { it('returns false if element is hidden for everyone', () => { const vNode = new axe.SerialVirtualNode({ diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index fb33e7f2da..df466da016 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -1,9 +1,9 @@ -describe('dom.getElementStack', function () { +describe('dom.getElementStack', () => { 'use strict'; - var fixture = document.getElementById('fixture'); - var getElementStack = axe.commons.dom.getElementStack; - var shadowSupported = axe.testUtils.shadowSupport.v1; + const fixture = document.getElementById('fixture'); + const getElementStack = axe.commons.dom.getElementStack; + const shadowSupported = axe.testUtils.shadowSupport.v1; function mapToIDs(stack) { return stack @@ -15,12 +15,12 @@ describe('dom.getElementStack', function () { }); } - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - describe('stack order', function () { - it('should return stack in DOM order of non-positioned elements', function () { + describe('stack order', () => { + it('should return stack in DOM order of non-positioned elements', () => { fixture.innerHTML = '<main id="1">' + '<div id="2">' + @@ -28,12 +28,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); - it('should not return elements outside of the stack', function () { + it('should not return elements outside of the stack', () => { fixture.innerHTML = '<main id="1">' + '<div id="2">' + @@ -42,12 +42,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); - it('should return stack in DOM order of non-positioned elements with z-index', function () { + it('should return stack in DOM order of non-positioned elements with z-index', () => { fixture.innerHTML = '<div id="1" style=";width:40px;height:40px;">' + '<div id="target" style="width:40px;height:40px;z-index:100">hello world</div>' + @@ -56,15 +56,15 @@ describe('dom.getElementStack', function () { '<div id="3" style="width:40px;height:40px;">Some text</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); // Browsers seem to be buggy, which suggest [3, target, 2, 1, fixture] // We're following the spec in this. // @see https://codepen.io/straker/pen/gOxpJyE assert.deepEqual(stack, ['3', '2', 'target', '1', 'fixture']); }); - it('should should handle positioned elements without z-index', function () { + it('should should handle positioned elements without z-index', () => { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index fixture.innerHTML = '<div id="1" style="width:40px;height:40px;position:absolute;top:0;">' + @@ -78,12 +78,12 @@ describe('dom.getElementStack', function () { '<div id="target" style="width:40px;height:40px;margin-top:-80px;">' + 'DIV #5<br />position:static;</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '3', '2', '1', 'target', 'fixture']); }); - it('should handle floating and positioned elements without z-index', function () { + it('should handle floating and positioned elements without z-index', () => { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float fixture.innerHTML = '<div id="1" style="width:40px;height:40px;position:absolute;top:0;">' + @@ -95,12 +95,12 @@ describe('dom.getElementStack', function () { '<div id="4" style="width:40px;height:40px;position:absolute;top:0;">' + 'DIV #4<br />position:absolute;</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '1', '2', 'target', 'fixture']); }); - it('should handle floating parent elements', function () { + it('should handle floating parent elements', () => { fixture.innerHTML = '<div id="1" style="float: left; background: #000000; color: #fff;">' + '<div id="2"><span id="target">whole picture</span></div>' + @@ -110,12 +110,12 @@ describe('dom.getElementStack', function () { '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', '4', '3', 'fixture']); }); - it('should handle z-index positioned elements in the same stacking context', function () { + it('should handle z-index positioned elements in the same stacking context', () => { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_1 fixture.innerHTML = '<div id="target" style="width:40px;height:40px;position:relative;">' + @@ -138,12 +138,12 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '2', '3', 'target', 'fixture']); }); - it('should handle z-index positioned elements in different stacking contexts', function () { + it('should handle z-index positioned elements in different stacking contexts', () => { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_2 fixture.innerHTML = '<div id="target" style="width:40px;height:40px;position:relative;">' + @@ -166,12 +166,12 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['2', '4', '3', 'target', 'fixture']); }); - it('should handle complex stacking context', function () { + it('should handle complex stacking context', () => { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context fixture.innerHTML = '<div id="1" style="position:absolute;top:0;left:0;width:40px;height:40px;z-index:5;">' + @@ -206,34 +206,49 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['1', '4', 'target', '5', '3', '2']); }); - it('should correctly order children of position elements without z-index', function () { + it('should correctly order children of position elements without z-index', () => { fixture.innerHTML = '<div id="1" style="position:relative;width:40px;height:40px;">' + '<div id="target" style="width:40px;height:40px;"></div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); - it('should correctly order children of position elements with z-index', function () { + it('should correctly order children of position elements with z-index', () => { fixture.innerHTML = '<div id="1" style="position:relative;width:40px;height:40px;z-index:1">' + '<div id="target" style="width:40px;height:40px;"></div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); - it('should handle modals on top of the stack', function () { + ['flex', 'inline-flex', 'grid', 'inline-grid'].forEach(type => { + it(`should correctly order "${type}" items with z-index`, () => { + fixture.innerHTML = ` + <div id="1" style="position:absolute;width:40px;height:40px;z-index:1"></div> + <div id="2" style="display: ${type}"> + <div id="target" style="width:40px;height:40px;z-index:1"></div> + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['target', '1', '2', 'fixture']); + }); + }); + + it('should handle modals on top of the stack', () => { fixture.innerHTML = '<main id="1">' + '<div id="2">' + @@ -242,12 +257,12 @@ describe('dom.getElementStack', function () { '</main>' + '<div id="3" style="position:absolute;top:0;left:0;right:0;height:100px"></div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); }); - it('should handle "pointer-events:none"', function () { + it('should handle "pointer-events:none"', () => { fixture.innerHTML = '<main id="1">' + '<div id="2">' + @@ -256,12 +271,12 @@ describe('dom.getElementStack', function () { '</main>' + '<div id="3" style="position:absolute;top:0;left:0;right:0;height:100px;pointer-events:none"></div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); }); - it('should return elements left out by document.elementsFromPoint', function () { + it('should return elements left out by document.elementsFromPoint', () => { fixture.innerHTML = '<main id="1">' + '<div id="2">' + @@ -269,47 +284,47 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); - it('should not return elements that do not fully cover the target', function () { + it('should not return elements that do not fully cover the target', () => { fixture.innerHTML = '<div id="1" style="position:relative;">' + '<div id="2" style="position:absolute;width:300px;height:19px;"></div>' + '<p id="target" style="position:relative;z-index:1;width:300px;height:40px;">Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); - it('should not return parent elements that do not fully cover the target', function () { + it('should not return parent elements that do not fully cover the target', () => { fixture.innerHTML = '<div id="1" style="height:20px;position:relative;">' + '<div id="target" style="position:absolute;top:21px;">Text</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target']); }); - it('should return elements that partially cover the target', function () { + it('should return elements that partially cover the target', () => { fixture.innerHTML = '<div id="1" style="height:40px;position:relative;">' + '<div id="2" style="height:20px;"></div>' + '<div id="target" style="position:absolute;margin-top:-11px;">Text</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); - it('should handle negative z-index', function () { + it('should handle negative z-index', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="position:relative;z-index:-10">' + @@ -317,12 +332,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['1', 'fixture', 'target', '2']); }); - it('should not add hidden elements', function () { + it('should not add hidden elements', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="position: absolute; display: none;">Some text</div>' + @@ -331,12 +346,12 @@ describe('dom.getElementStack', function () { '<span id="target">Hello World</span>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); - it('should correctly position children of positioned parents', function () { + it('should correctly position children of positioned parents', () => { fixture.innerHTML = '<div id="1" style="position: relative; padding: 60px 0;">Some text</div>' + '<section id="2" style="position: relative; padding: 60px 0;">' + @@ -345,12 +360,12 @@ describe('dom.getElementStack', function () { '</div>' + '</section>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '1', 'fixture']); }); - it('should correctly position siblings with positioned children correctly', function () { + it('should correctly position siblings with positioned children correctly', () => { fixture.innerHTML = '<div id="1">Some text</div>' + '<div id="2" style="position: absolute; top: 0; left: 0;">Some text</div>' + @@ -361,12 +376,12 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', 'target', '5', '3', '2', '1', 'fixture']); }); - it('should correctly position children of float elements with position elements', function () { + it('should correctly position children of float elements with position elements', () => { fixture.innerHTML = '<div id="1" style="width: 50px; height: 50px; position: relative;">' + '<div id="2" style="width: 50px; height: 50px; float: left;">' + @@ -379,12 +394,50 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['5', 'target', '4', '3', '2', '1', 'fixture']); }); - it('should return empty array for hidden elements', function () { + it('should correctly position opacity elements and positioned elements', () => { + fixture.innerHTML = ` + <div id="1" style="opacity: 0.9;"> + <div id="2" style="position: relative; z-index: 2"> + <h1 id="target">Hello World</h1> + </div> + </div> + <div id="3" style="opacity: 0.8;"> + <div id="4" style="position: absolute; top: 20px; z-index: -1;"> + <div id="5" style="height: 40px; width: 100vw; background: red"></div> + </div> + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['5', '4', 'target', '2', '1', 'fixture']); + }); + + it('should correctly order elements outside of the axe tree', () => { + fixture.innerHTML = ` + <div id="1" style="opacity: 0.9;"> + <div id="2" style="position: relative; z-index: 2"> + <h1 id="target">Hello World</h1> + </div> + </div> + <div id="tree" style="opacity: 0.8;"> + <div id="4" style="position: absolute; top: 20px; z-index: -1;"> + <div id="5" style="height: 40px; width: 100vw; background: red"></div> + </div> + </div> + `; + axe.testUtils.flatTreeSetup(fixture.querySelector('#tree')); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['5', '4', 'target', '2', '1', 'fixture']); + }); + + it('should return empty array for hidden elements', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="position: absolute; display: none">' + @@ -393,12 +446,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, []); }); - it('should return empty array for children of 0 height scrollable regions', function () { + it('should return empty array for children of 0 height scrollable regions', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="overflow: scroll; height: 0">' + @@ -407,18 +460,18 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, []); }); - it('should throw error if element midpoint-x exceeds the grid', function () { + it('should throw error if element midpoint-x exceeds the grid', () => { fixture.innerHTML = '<div id="target">Hello World</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var vNode = axe.utils.getNodeFromTree(target); + const target = fixture.querySelector('#target'); + const vNode = axe.utils.getNodeFromTree(target); Object.defineProperty(vNode, 'boundingClientRect', { - get: function () { + get: () => { return { left: 0, top: 10, @@ -427,18 +480,18 @@ describe('dom.getElementStack', function () { }; } }); - assert.throws(function () { + assert.throws(() => { getElementStack(target); }, 'Element midpoint exceeds the grid bounds'); }); - it('should throw error if element midpoint-y exceeds the grid', function () { + it('should throw error if element midpoint-y exceeds the grid', () => { fixture.innerHTML = '<div id="target">Hello World</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var vNode = axe.utils.getNodeFromTree(target); + const target = fixture.querySelector('#target'); + const vNode = axe.utils.getNodeFromTree(target); Object.defineProperty(vNode, 'boundingClientRect', { - get: function () { + get: () => { return { left: 0, top: 10, @@ -447,12 +500,12 @@ describe('dom.getElementStack', function () { }; } }); - assert.throws(function () { + assert.throws(() => { getElementStack(target); }, 'Element midpoint exceeds the grid bounds'); }); - it('should ignore element which exactly overlaps midpoint of target element', function () { + it('should ignore element which exactly overlaps midpoint of target element', () => { fixture.innerHTML = '<div id="target" style="width: 100%; height: 50px;">' + '<h4 id="1" style="margin: 0; width: 100%; height: 25px;">Foo</h4>' + @@ -460,56 +513,56 @@ describe('dom.getElementStack', function () { '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', 'fixture']); }); // IE11 either only supports clip paths defined by url() or not at all, // MDN and caniuse.com give different results... - it('should not add hidden elements using clip-path', function () { + it('should not add hidden elements using clip-path', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="position: absolute; clip: rect(1px, 1px, 1px, 1px);">Some text</div>' + '<span id="target">Hello World</span>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); (shadowSupported ? it : xit)( 'should sort shadow dom elements correctly', - function () { + () => { fixture.innerHTML = '<div id="container"></div>'; - var container = fixture.querySelector('#container'); - var shadow = container.attachShadow({ mode: 'open' }); + const container = fixture.querySelector('#container'); + const shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '<span id="shadowTarget">Text</span>'; axe.testUtils.flatTreeSetup(fixture); - var target = shadow.querySelector('#shadowTarget'); - var stack = mapToIDs(getElementStack(target)); + const target = shadow.querySelector('#shadowTarget'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['shadowTarget', 'container', 'fixture']); } ); (shadowSupported ? it : xit)( 'should sort nested shadow dom elements correctly', - function () { + () => { fixture.innerHTML = '<div id="container"></div>'; - var container = fixture.querySelector('#container'); - var shadow = container.attachShadow({ mode: 'open' }); + const container = fixture.querySelector('#container'); + const shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '<div id="shadowContainer"></div>'; - var nestedContainer = shadow.querySelector('#shadowContainer'); - var nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); + const nestedContainer = shadow.querySelector('#shadowContainer'); + const nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); nestedShadow.innerHTML = '<span id="shadowTarget">Text</span>'; axe.testUtils.flatTreeSetup(fixture); - var target = nestedShadow.querySelector('#shadowTarget'); - var stack = mapToIDs(getElementStack(target)); + const target = nestedShadow.querySelector('#shadowTarget'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, [ 'shadowTarget', 'shadowContainer', @@ -521,21 +574,21 @@ describe('dom.getElementStack', function () { (shadowSupported ? it : xit)( 'should sort positioned shadow elements correctly', - function () { + () => { fixture.innerHTML = '<div id="container"></div>'; - var container = fixture.querySelector('#container'); - var shadow = container.attachShadow({ mode: 'open' }); + const container = fixture.querySelector('#container'); + const shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '<div id="shadowContainer" style="position: relative; z-index: -1;"></div>'; - var nestedContainer = shadow.querySelector('#shadowContainer'); - var nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); + const nestedContainer = shadow.querySelector('#shadowContainer'); + const nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); nestedShadow.innerHTML = '<span id="shadowTarget">Text</span>'; axe.testUtils.flatTreeSetup(fixture); - var target = nestedShadow.querySelector('#shadowTarget'); - var stack = mapToIDs(getElementStack(target)); + const target = nestedShadow.querySelector('#shadowTarget'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, [ 'container', 'fixture', @@ -547,20 +600,20 @@ describe('dom.getElementStack', function () { (shadowSupported ? it : xit)( 'should sort shadow elements with different trees correctly', - function () { + () => { fixture.innerHTML = '<div id="container1"></div><div id="container2" style="position: absolute; top: 0;">'; - var container1 = fixture.querySelector('#container1'); - var shadow1 = container1.attachShadow({ mode: 'open' }); + const container1 = fixture.querySelector('#container1'); + const shadow1 = container1.attachShadow({ mode: 'open' }); shadow1.innerHTML = '<span id="shadowTarget">Text</span>'; - var container2 = fixture.querySelector('#container2'); - var shadow2 = container2.attachShadow({ mode: 'open' }); + const container2 = fixture.querySelector('#container2'); + const shadow2 = container2.attachShadow({ mode: 'open' }); shadow2.innerHTML = '<span id="1">Container 2 text</span>'; axe.testUtils.flatTreeSetup(fixture); - var target = shadow1.querySelector('#shadowTarget'); - var stack = mapToIDs(getElementStack(target)); + const target = shadow1.querySelector('#shadowTarget'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, [ '1', 'container2', @@ -592,16 +645,16 @@ describe('dom.getElementStack', function () { }); }); - describe('scroll regions', function () { - var origHeight = document.documentElement.style.height; - var origOverflow = document.documentElement.style.overflowY; + describe('scroll regions', () => { + const origHeight = document.documentElement.style.height; + const origOverflow = document.documentElement.style.overflowY; - afterEach(function () { + afterEach(() => { document.documentElement.style.height = origHeight; document.documentElement.style.overflowY = origOverflow; }); - it('should return stack of scroll regions', function () { + it('should return stack of scroll regions', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="overflow:auto">' + @@ -611,12 +664,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); - it('should return stack when scroll region is larger than parent', function () { + it('should return stack when scroll region is larger than parent', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="overflow:auto;height:40px">' + @@ -626,12 +679,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); - it('should return stack of recursive scroll regions', function () { + it('should return stack of recursive scroll regions', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="overflow:auto;height:40px">' + @@ -645,12 +698,12 @@ describe('dom.getElementStack', function () { '</div>' + '</main>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '5', '4', '3', '2', '1', 'fixture']); }); - it('should handle html as a scroll region', function () { + it('should handle html as a scroll region', () => { fixture.innerHTML = '<main id="1">' + '<div id="2" style="overflow:auto">' + @@ -662,12 +715,12 @@ describe('dom.getElementStack', function () { document.documentElement.style.height = '5000px'; document.documentElement.style.overflowY = 'scroll'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); - it('should use correct scroll region parent', function () { + it('should use correct scroll region parent', () => { fixture.innerHTML = '<div id="1" style="overflow: scroll; height: 50px;">' + '<div id="2" style="overflow: scroll; height: 100px;">' + @@ -677,8 +730,8 @@ describe('dom.getElementStack', function () { '</div>' + '</div>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var stack = mapToIDs(getElementStack(target)); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); }); diff --git a/test/commons/dom/get-modal-dialog.js b/test/commons/dom/get-modal-dialog.js new file mode 100644 index 0000000000..08a5a09c2f --- /dev/null +++ b/test/commons/dom/get-modal-dialog.js @@ -0,0 +1,129 @@ +describe('dom.getModalDialog', () => { + const fixture = document.querySelector('#fixture'); + const getModalDialog = axe.commons.dom.getModalDialog; + const { flatTreeSetup } = axe.testUtils; + + it('returns a modal dialog', () => { + fixture.innerHTML = ` + <dialog id="target"><span>Hello</span></dialog> + `; + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.equal(getModalDialog(), vNode); + }); + + it('returns null for an opened dialog', () => { + fixture.innerHTML = ` + <dialog open><span>Hello</span></dialog> + `; + flatTreeSetup(fixture); + + assert.isNull(getModalDialog()); + }); + + it('returns null for a closed dialog', () => { + fixture.innerHTML = ` + <dialog><span>Hello</span></dialog> + `; + flatTreeSetup(fixture); + + assert.isNull(getModalDialog()); + }); + + it('returns null when there is no dialog', () => { + fixture.innerHTML = ` + <div>World</div> + `; + flatTreeSetup(fixture); + + assert.isNull(getModalDialog()); + }); + + it('returns null if the modal dialog is not visible', () => { + fixture.innerHTML = ` + <style>dialog[open] { display: none }</style> + <dialog id="target"><span>Hello</span></dialog> + `; + document.querySelector('#target').showModal(); + flatTreeSetup(fixture); + + assert.isNull(getModalDialog()); + }); + + describe('fallback', () => { + it('returns true for modal dialog when elementsFromPoint does not return the dialog', () => { + fixture.innerHTML = ` + <style>dialog::backdrop { display: none; }</style> + <dialog id="target"><span>Hello</span></dialog> + <div>World</div> + `; + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.equal(getModalDialog(), vNode); + }); + + it('skips checking elements with pointer-events: none', () => { + fixture.innerHTML = ` + <style>body { pointer-events: none; } dialog::backdrop { display: none; }</style> + <dialog id="target"><span>Hello</span></dialog> + <div>World</div> + `; + document.querySelector('#target').showModal(); + flatTreeSetup(fixture); + + assert.isNull(getModalDialog()); + }); + + it('takes into account a scrolled page', () => { + fixture.innerHTML = ` + <style> + dialog::backdrop { display: none; } + #large-scroll { height: 200vh; margin-bottom: 100px; } + </style> + <div id="large-scroll"></div> + <dialog id="target"><span>Hello</span></dialog> + <div id="scroll-target">World</div> + `; + document.querySelector('#scroll-target').scrollIntoView(); + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.equal(getModalDialog(), vNode); + }); + + it('returns the modal dialog when two dialogs are open', () => { + fixture.innerHTML = ` + <style>dialog::backdrop { display: none; }</style> + <dialog id="not-modal" open><span>Hello</span></dialog> + <dialog id="target"><span>Hello</span></dialog> + <div>World</div> + `; + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.equal(getModalDialog(), vNode); + }); + + it('returns the outer modal when a dialog modal contains a non-dialog modal', () => { + fixture.innerHTML = ` + <style>dialog::backdrop { display: none; }</style> + <dialog id="target"> + <span>Hello</span> + <dialog open>Open modal</dialog> + </dialog> + <div>World</div> + `; + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.equal(getModalDialog(), vNode); + }); + }); +}); diff --git a/test/commons/dom/is-inert.js b/test/commons/dom/is-inert.js new file mode 100644 index 0000000000..ca4ba92e8b --- /dev/null +++ b/test/commons/dom/is-inert.js @@ -0,0 +1,107 @@ +describe('dom.isInert', () => { + const fixture = document.querySelector('#fixture'); + const isInert = axe.commons.dom.isInert; + const { queryFixture, flatTreeSetup } = axe.testUtils; + + it('returns true for element with "inert=false`', () => { + const vNode = queryFixture('<div id="target" inert="false"></div>'); + + assert.isTrue(isInert(vNode)); + }); + + it('returns true for element with "inert`', () => { + const vNode = queryFixture('<div id="target" inert></div>'); + + assert.isTrue(isInert(vNode)); + }); + + it('returns false for element without inert', () => { + const vNode = queryFixture('<div id="target"></div>'); + + assert.isFalse(isInert(vNode)); + }); + + it('returns true for ancestor with inert', () => { + const vNode = queryFixture( + '<div inert><div><div id="target"></div></div></div>' + ); + + assert.isTrue(isInert(vNode)); + }); + + it('returns false for closed dialog', () => { + const vNode = queryFixture(` + <dialog><span>Hello</span></dialog> + <div id="target">World</div> + `); + + assert.isFalse(isInert(vNode)); + }); + + it('returns false for non-modal dialog', () => { + const vNode = queryFixture(` + <dialog open><span>Hello</span></dialog> + <div id="target">World</div> + `); + + assert.isFalse(isInert(vNode)); + }); + + it('returns true for modal dialog', () => { + fixture.innerHTML = ` + <dialog id="modal"><span>Hello</span></dialog> + <div id="target">World</div> + `; + document.querySelector('#modal').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.isTrue(isInert(vNode)); + }); + + it('returns false for the modal dialog element', () => { + fixture.innerHTML = ` + <dialog id="target"><span>Hello</span></dialog> + `; + document.querySelector('#target').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.isFalse(isInert(vNode)); + }); + + it('returns false for a descendant of the modal dialog', () => { + fixture.innerHTML = ` + <dialog id="modal"><span id="target">Hello</span></dialog> + `; + document.querySelector('#modal').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.isFalse(isInert(vNode)); + }); + + describe('options.skipAncestors', () => { + it('returns false for ancestor with inert', () => { + const vNode = queryFixture( + '<div inert><div><div id="target"></div></div></div>' + ); + + assert.isFalse(isInert(vNode, { skipAncestors: true })); + }); + }); + + describe('options.isAncestor', () => { + it('return false for modal dialog', () => { + fixture.innerHTML = ` + <dialog id="modal"><span>Hello</span></dialog> + <div id="target">World</div> + `; + document.querySelector('#modal').showModal(); + const tree = flatTreeSetup(fixture); + const vNode = axe.utils.querySelectorAll(tree, '#target')[0]; + + assert.isFalse(isInert(vNode, { isAncestor: true })); + }); + }); +}); diff --git a/test/commons/dom/is-visible-for-screenreader.js b/test/commons/dom/is-visible-to-screenreader.js similarity index 90% rename from test/commons/dom/is-visible-for-screenreader.js rename to test/commons/dom/is-visible-to-screenreader.js index 354b1becd8..3586366a2a 100644 --- a/test/commons/dom/is-visible-for-screenreader.js +++ b/test/commons/dom/is-visible-to-screenreader.js @@ -62,6 +62,13 @@ describe('dom.isVisibleToScreenReaders', function () { assert.isFalse(isVisibleToScreenReaders(vNode)); }); + it('should return false if `inert` is set', function () { + var vNode = queryFixture( + '<div id="target" inert>Hidden from screen readers</div>' + ); + assert.isFalse(isVisibleToScreenReaders(vNode)); + }); + it('should return false if `display: none` is set', function () { var vNode = queryFixture( '<div id="target" style="display: none">Hidden from screen readers</div>' @@ -230,5 +237,30 @@ describe('dom.isVisibleToScreenReaders', function () { vNode.parent = parentVNode; assert.isFalse(isVisibleToScreenReaders(vNode)); }); + + it('should return false if `inert` is set', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + inert: true + } + }); + assert.isFalse(isVisibleToScreenReaders(vNode)); + }); + + it('should return false if `inert` is set on parent', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + var parentVNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + inert: true + } + }); + parentVNode.children = [vNode]; + vNode.parent = parentVNode; + assert.isFalse(isVisibleToScreenReaders(vNode)); + }); }); }); diff --git a/test/commons/standards/get-global-aria-attrs.js b/test/commons/standards/get-global-aria-attrs.js index a1bbf28408..0141f255a3 100644 --- a/test/commons/standards/get-global-aria-attrs.js +++ b/test/commons/standards/get-global-aria-attrs.js @@ -14,10 +14,13 @@ describe('standards.getGlobalAriaAttrs', function () { var globalAttrs = getGlobalAriaAttrs(); assert.deepEqual(globalAttrs, [ 'aria-atomic', + 'aria-braillelabel', + 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby', + 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', diff --git a/test/get-webdriver.js b/test/get-webdriver.js new file mode 100644 index 0000000000..98ca2e2d37 --- /dev/null +++ b/test/get-webdriver.js @@ -0,0 +1,16 @@ +const { Builder } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); +const chromedriverPath = require('chromedriver').path; + +const getWebdriver = () => { + const service = new chrome.ServiceBuilder(chromedriverPath); + + const webdriver = new Builder() + .setChromeOptions(new chrome.Options().headless()) + .forBrowser('chrome') + .setChromeService(service) + .build(); + return webdriver; +}; + +module.exports.getWebdriver = getWebdriver; diff --git a/test/integration/full/css-orientation-lock/violations.css b/test/integration/full/css-orientation-lock/violations.css index e5702b6deb..ebed686f25 100644 --- a/test/integration/full/css-orientation-lock/violations.css +++ b/test/integration/full/css-orientation-lock/violations.css @@ -2,6 +2,9 @@ .thatDiv { transform: rotate(90deg); } + .rotateDiv { + rotate: 90deg; + } } @media screen and (min-width: 10px) and (max-width: 3000px) and (orientation: landscape) { @@ -11,4 +14,7 @@ .someDiv { transform: matrix3d(0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } + .rotateMatrix { + rotate: 0 0 1 1.5708rad; + } } diff --git a/test/integration/full/css-orientation-lock/violations.html b/test/integration/full/css-orientation-lock/violations.html index b80196b5fa..4359e17058 100644 --- a/test/integration/full/css-orientation-lock/violations.html +++ b/test/integration/full/css-orientation-lock/violations.html @@ -23,6 +23,8 @@ <div id="mocha"></div> <div class="someDiv">some div content</div> <div class="thatDiv">that div content</div> + <div class="rotateDiv">that div content</div> + <div class="rotateMatrix">that div content</div> <div id="shadow-fixture"></div> <script src="/test/testutils.js"></script> <script src="violations.js"></script> diff --git a/test/integration/full/css-orientation-lock/violations.js b/test/integration/full/css-orientation-lock/violations.js index d32c84c335..ff38cfd788 100644 --- a/test/integration/full/css-orientation-lock/violations.js +++ b/test/integration/full/css-orientation-lock/violations.js @@ -41,26 +41,32 @@ describe('css-orientation-lock violations test', function () { } }, function (err, res) { - assert.isNull(err); - assert.isDefined(res); + try { + assert.isNull(err); + assert.isDefined(res); - // check for violation - assert.property(res, 'violations'); - assert.lengthOf(res.violations, 1); + // check for violation + assert.property(res, 'violations'); + assert.lengthOf(res.violations, 1); - // assert the node - var checkedNode = res.violations[0].nodes[0]; - assert.isTrue(/html/i.test(checkedNode.html)); + // assert the node + var checkedNode = res.violations[0].nodes[0]; + assert.isTrue(/html/i.test(checkedNode.html)); - // assert the relatedNodes - var checkResult = checkedNode.all[0]; - assert.lengthOf(checkResult.relatedNodes, 2); - assertViolatedSelectors(checkResult.relatedNodes, [ - '.someDiv', - '.thatDiv' - ]); + // assert the relatedNodes + var checkResult = checkedNode.all[0]; + assert.lengthOf(checkResult.relatedNodes, 4); + assertViolatedSelectors(checkResult.relatedNodes, [ + '.someDiv', + '.thatDiv', + '.rotateDiv', + '.rotateMatrix' + ]); - done(); + done(); + } catch (err) { + done(err); + } } ); }); @@ -83,27 +89,33 @@ describe('css-orientation-lock violations test', function () { } }, function (err, res) { - assert.isNull(err); - assert.isDefined(res); + try { + assert.isNull(err); + assert.isDefined(res); - // check for violation - assert.property(res, 'violations'); - assert.lengthOf(res.violations, 1); + // check for violation + assert.property(res, 'violations'); + assert.lengthOf(res.violations, 1); - // assert the node - var checkedNode = res.violations[0].nodes[0]; - assert.isTrue(/html/i.test(checkedNode.html)); + // assert the node + var checkedNode = res.violations[0].nodes[0]; + assert.isTrue(/html/i.test(checkedNode.html)); - // assert the relatedNodes - var checkResult = checkedNode.all[0]; - assert.lengthOf(checkResult.relatedNodes, 3); - assertViolatedSelectors(checkResult.relatedNodes, [ - '.someDiv', - '.thatDiv', - '.shadowDiv' - ]); + // assert the relatedNodes + var checkResult = checkedNode.all[0]; + assert.lengthOf(checkResult.relatedNodes, 5); + assertViolatedSelectors(checkResult.relatedNodes, [ + '.someDiv', + '.thatDiv', + '.rotateDiv', + '.rotateMatrix', + '.shadowDiv' + ]); - done(); + done(); + } catch (err) { + done(err); + } } ); } diff --git a/test/integration/full/dialog/dialog.html b/test/integration/full/dialog/dialog.html new file mode 100644 index 0000000000..e1168db4cf --- /dev/null +++ b/test/integration/full/dialog/dialog.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>dialog test + + + + + + + + +
+ +
+ Contrast failure +
+ + +
+ Contrast failure +
+
+
+
+ + + + + diff --git a/test/integration/full/dialog/dialog.js b/test/integration/full/dialog/dialog.js new file mode 100644 index 0000000000..228c2abed8 --- /dev/null +++ b/test/integration/full/dialog/dialog.js @@ -0,0 +1,51 @@ +describe('dialog tests', () => { + const dialog = document.querySelector('dialog'); + const target = document.querySelector('#target'); + + async function getViolations() { + const results = await axe.run(target); + const buttonName = results.violations.find( + ({ id }) => id === 'button-name' + ); + const colorContrast = results.violations.find( + ({ id }) => id === 'color-contrast' + ); + + return { buttonName, colorContrast }; + } + + afterEach(function () { + dialog.close(); + }); + + it('should not find violations inside a closed dialog', async () => { + const { buttonName, colorContrast } = await getViolations(); + + assert.lengthOf(buttonName.nodes, 1); + assert.deepEqual(buttonName.nodes[0].target, ['#root-button']); + assert.lengthOf(colorContrast.nodes, 1); + assert.deepEqual(colorContrast.nodes[0].target, ['#root-color']); + }); + + it('should not find violations outside a modal dialog', async () => { + dialog.showModal(); + const { buttonName, colorContrast } = await getViolations(); + + assert.lengthOf(buttonName.nodes, 1); + assert.deepEqual(buttonName.nodes[0].target, ['#dialog-button']); + assert.lengthOf(colorContrast.nodes, 1); + assert.deepEqual(colorContrast.nodes[0].target, ['#dialog-color']); + }); + + it('should find violations inside and outside an open dialog', async () => { + dialog.show(); + const { buttonName, colorContrast } = await getViolations(); + + assert.lengthOf(buttonName.nodes, 2); + assert.deepEqual(buttonName.nodes[0].target, ['#root-button']); + assert.deepEqual(buttonName.nodes[1].target, ['#dialog-button']); + assert.lengthOf(colorContrast.nodes, 2); + assert.deepEqual(colorContrast.nodes[0].target, ['#root-color']); + assert.deepEqual(colorContrast.nodes[1].target, ['#dialog-color']); + }); +}); diff --git a/test/integration/rules/aria-allowed-attr/passes.html b/test/integration/rules/aria-allowed-attr/passes.html index fe562556bc..9bee42b07c 100644 --- a/test/integration/rules/aria-allowed-attr/passes.html +++ b/test/integration/rules/aria-allowed-attr/passes.html @@ -7,9 +7,12 @@ id="pass1" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -33,9 +36,12 @@ id="pass2" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -61,9 +67,12 @@ aria-activedescendant="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -89,9 +98,12 @@ aria-posinset="value" aria-setsize="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -115,9 +127,12 @@ id="pass5" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -142,9 +157,12 @@ aria-expanded="value" aria-pressed="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -168,9 +186,12 @@ id="pass7" aria-checked="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -203,9 +224,12 @@ aria-selected="value" aria-required="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -233,9 +257,12 @@ aria-expanded="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -259,9 +286,12 @@ id="pass10" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -285,9 +315,12 @@ id="pass11" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -311,9 +344,12 @@ id="pass12" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -337,9 +373,12 @@ id="pass13" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -364,9 +403,12 @@ id="pass14" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -390,9 +432,12 @@ id="pass15" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -416,9 +461,12 @@ id="pass16" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -442,9 +490,12 @@ id="pass17" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -472,9 +523,12 @@ aria-activedescendant="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -507,9 +561,12 @@ aria-expanded="value" aria-required="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -534,9 +591,12 @@ aria-activedescendant="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -560,9 +620,12 @@ id="pass21" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -586,9 +649,12 @@ id="pass22" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -612,9 +678,12 @@ id="pass23" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -638,9 +707,12 @@ id="pass24" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -668,9 +740,12 @@ aria-expanded="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -698,9 +773,12 @@ aria-setsize="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -724,9 +802,12 @@ id="pass27" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -750,9 +831,12 @@ id="pass28" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -776,9 +860,12 @@ id="pass29" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -802,9 +889,12 @@ id="pass30" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -830,9 +920,12 @@ aria-expanded="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -858,9 +951,12 @@ aria-expanded="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -886,9 +982,12 @@ aria-expanded="value" aria-setsize="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -914,9 +1013,12 @@ aria-posinset="value" aria-setsize="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -942,9 +1044,12 @@ aria-setsize="value" aria-checked="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -968,9 +1073,12 @@ id="pass36" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -994,9 +1102,12 @@ id="pass37" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1023,9 +1134,12 @@ aria-setsize="value" aria-checked="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1052,9 +1166,12 @@ aria-valuemax="value" aria-valuemin="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1081,9 +1198,12 @@ aria-setsize="value" aria-checked="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1109,9 +1229,12 @@ aria-required="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1136,9 +1259,12 @@ id="pass42" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1167,9 +1293,12 @@ aria-activedescendant="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1192,9 +1321,12 @@ role="rowgroup" id="pass44" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1226,9 +1358,12 @@ aria-expanded="value" aria-selected="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1257,8 +1392,11 @@ aria-valuemax="value" aria-valuemin="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1282,9 +1420,12 @@ id="pass47" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1308,9 +1449,12 @@ id="pass48" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1342,9 +1486,12 @@ aria-valuemax="value" aria-valuemin="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1373,9 +1520,12 @@ aria-valuemax="value" aria-valuemin="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1400,9 +1550,12 @@ id="pass51" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1429,9 +1582,12 @@ aria-posinset="value" aria-setsize="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1459,9 +1615,12 @@ aria-multiselectable="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1485,9 +1644,12 @@ id="pass54" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1515,9 +1677,12 @@ aria-readonly="value" aria-required="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1542,9 +1707,12 @@ id="pass56" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1569,9 +1737,12 @@ aria-activedescendant="value" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1596,9 +1767,12 @@ id="pass58" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1626,9 +1800,12 @@ aria-expanded="value" aria-orientation="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1660,9 +1837,12 @@ aria-required="value" aria-rowcount="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1691,9 +1871,12 @@ aria-posinset="value" aria-setsize="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1717,9 +1900,12 @@ id="pass62" aria-checked="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1746,9 +1932,12 @@ aria-rowindex="value" aria-rowspan="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1776,9 +1965,12 @@ aria-readonly="value" aria-required="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1803,9 +1995,12 @@ id="pass65" aria-expanded="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1828,9 +2023,12 @@ role="text" id="pass66" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" @@ -1855,9 +2053,12 @@ aria-colcount="value" aria-rowcount="value" aria-atomic="value" + aria-braillelabel="value" + aria-brailleroledescription="value" aria-busy="value" aria-controls="value" aria-describedby="value" + aria-description="value" aria-details="value" aria-disabled="value" aria-dropeffect="value" diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html index 59d9532886..b43814699a 100644 --- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html @@ -27,6 +27,12 @@
+ + diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json index 1c6640ebd9..908a0e7178 100644 --- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json @@ -16,7 +16,8 @@ ["#pass3"], ["#pass4"], ["#pass5"], - ["#pass6"] + ["#pass6"], + ["#pass7"] ], "incomplete": [["#incomplete1"], ["#incomplete2"]] } diff --git a/test/integration/rules/aria-input-field-name/aria-input-field-name.html b/test/integration/rules/aria-input-field-name/aria-input-field-name.html index bfd15eb387..a83a78cd27 100644 --- a/test/integration/rules/aria-input-field-name/aria-input-field-name.html +++ b/test/integration/rules/aria-input-field-name/aria-input-field-name.html @@ -1,6 +1,21 @@ -
England
+
+ England +
+ + +
    +
  • Zebra
  • +
  • Zoom
  • +
+

Select a color:

@@ -87,13 +102,13 @@ - - + - + diff --git a/test/integration/rules/aria-required-children/aria-required-children.html b/test/integration/rules/aria-required-children/aria-required-children.html index 274527e339..14a52d2d10 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.html +++ b/test/integration/rules/aria-required-children/aria-required-children.html @@ -135,3 +135,8 @@ Item 3 + + diff --git a/test/integration/rules/aria-required-children/aria-required-children.json b/test/integration/rules/aria-required-children/aria-required-children.json index c1d7f46d4b..23f2822f0a 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.json +++ b/test/integration/rules/aria-required-children/aria-required-children.json @@ -32,7 +32,8 @@ ["#pass14"], ["#pass15"], ["#pass16"], - ["#pass17"] + ["#pass17"], + ["#pass18"] ], "incomplete": [ ["#incomplete1"], diff --git a/test/integration/rules/aria-roles/aria-roles.html b/test/integration/rules/aria-roles/aria-roles.html index ef071398f0..0416d2dbbe 100644 --- a/test/integration/rules/aria-roles/aria-roles.html +++ b/test/integration/rules/aria-roles/aria-roles.html @@ -12,7 +12,6 @@
ok
-
ok
ok
ok
ok
@@ -135,6 +134,7 @@
fail
fail
+
ok
diff --git a/test/integration/rules/aria-roles/aria-roles.json b/test/integration/rules/aria-roles/aria-roles.json index 7cef76c13f..e444afd603 100644 --- a/test/integration/rules/aria-roles/aria-roles.json +++ b/test/integration/rules/aria-roles/aria-roles.json @@ -16,7 +16,8 @@ ["#fail12"], ["#fail13"], ["#fail14"], - ["#fail15"] + ["#fail15"], + ["#fail16"] ], "passes": [ ["#pass1"], @@ -32,7 +33,6 @@ ["#pass11"], ["#pass12"], ["#pass13"], - ["#pass14"], ["#pass15"], ["#pass16"], ["#pass17"], diff --git a/test/integration/rules/color-contrast/color-contrast.html b/test/integration/rules/color-contrast/color-contrast.html index d3c97e5a6f..dd3a114312 100644 --- a/test/integration/rules/color-contrast/color-contrast.html +++ b/test/integration/rules/color-contrast/color-contrast.html @@ -424,3 +424,27 @@

Text-stroke 0.02em

+ +
+
+
+ This div will be on top of the stack +
+
+ +
hello
+ +
+
+
hello
+
+
diff --git a/test/integration/rules/color-contrast/color-contrast.json b/test/integration/rules/color-contrast/color-contrast.json index b3801df32d..0cd24e9ad7 100644 --- a/test/integration/rules/color-contrast/color-contrast.json +++ b/test/integration/rules/color-contrast/color-contrast.json @@ -35,7 +35,8 @@ ["#pass18"], ["#pass19"], ["#pass20"], - ["#pass21"] + ["#pass21"], + ["#pass22"] ], "incomplete": [ ["#canttell1"], diff --git a/test/integration/rules/focus-order-semantics/focus-order-semantics.html b/test/integration/rules/focus-order-semantics/focus-order-semantics.html index 3588fe1f95..7ac09790ae 100644 --- a/test/integration/rules/focus-order-semantics/focus-order-semantics.html +++ b/test/integration/rules/focus-order-semantics/focus-order-semantics.html @@ -30,6 +30,7 @@

Valid landmark roles for scrollable containers

+

Valid scrollable HTML tags for scrollable regions, not selected by this diff --git a/test/integration/rules/focus-order-semantics/focus-order-semantics.json b/test/integration/rules/focus-order-semantics/focus-order-semantics.json index c682ed88e4..dac1ba8f13 100644 --- a/test/integration/rules/focus-order-semantics/focus-order-semantics.json +++ b/test/integration/rules/focus-order-semantics/focus-order-semantics.json @@ -10,7 +10,8 @@ ["#pass6"], ["#pass7"], ["#pass8"], - ["#pass9"] + ["#pass9"], + ["#pass10"] ], "violations": [ ["#violation1"], diff --git a/test/integration/rules/frame-focusable-content/frame-focusable-content.html b/test/integration/rules/frame-focusable-content/frame-focusable-content.html index f61fdba0be..8928fc6800 100644 --- a/test/integration/rules/frame-focusable-content/frame-focusable-content.html +++ b/test/integration/rules/frame-focusable-content/frame-focusable-content.html @@ -46,3 +46,8 @@ height="0" id="inapplicable-3" > + diff --git a/test/rule-matches/color-contrast-matches.js b/test/rule-matches/color-contrast-matches.js index c9afeba4d8..382457119c 100644 --- a/test/rule-matches/color-contrast-matches.js +++ b/test/rule-matches/color-contrast-matches.js @@ -404,6 +404,20 @@ describe('color-contrast-matches', function () { assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); }); + it('should not match inert', function () { + fixture.innerHTML = '
hi
'; + var target = fixture.querySelector('div'); + axe.testUtils.flatTreeSetup(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); + }); + + it('should not match a descendant of inert', function () { + fixture.innerHTML = '
hi
'; + var target = fixture.querySelector('span'); + axe.testUtils.flatTreeSetup(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); + }); + if (shadowSupport) { it('should match a descendant of an element across a shadow boundary', function () { fixture.innerHTML = diff --git a/test/rule-matches/no-naming-method-matches.js b/test/rule-matches/no-naming-method-matches.js index fd00c1eed8..c92213817e 100644 --- a/test/rule-matches/no-naming-method-matches.js +++ b/test/rule-matches/no-naming-method-matches.js @@ -69,6 +69,24 @@ describe('no-naming-method-matches', function () { assert.isFalse(actual); }); + it('returns false for the listbox popup of a role=`combobox`', function () { + var vNode = queryFixture( + '
' + + '
' + ); + var actual = rule.matches(null, vNode); + assert.isFalse(actual); + }); + + it('returns true for the dialog popup of a role=`combobox`', function () { + var vNode = queryFixture( + '
' + + '' + ); + var actual = rule.matches(null, vNode); + assert.isTrue(actual); + }); + it('returns true for a div with role=`button`', function () { var vNode = queryFixture('
'); var actual = rule.matches(null, vNode); diff --git a/typings/axe-core/axe-core-tests.ts b/typings/axe-core/axe-core-tests.ts index ba83d9fd5f..309dc9e75a 100644 --- a/typings/axe-core/axe-core-tests.ts +++ b/typings/axe-core/axe-core-tests.ts @@ -223,10 +223,22 @@ var spec: axe.Spec = { id: 'custom-check', evaluate: function () { return true; + }, + metadata: { + impact: 'minor', + messages: { + pass: 'yes', + fail: 'nope', + incomplete: { + maybe: 'maybe', + or: 'maybe not' + } + } } } ], standards: { + ...axe.utils.getStandards(), ariaRoles: { 'custom-role': { type: 'widget', @@ -251,7 +263,13 @@ var spec: axe.Spec = { rules: [ { id: 'custom-rule', - any: ['custom-check'] + any: ['custom-check'], + metadata: { + description: 'custom rule', + help: 'different help', + helpUrl: 'https://example.com', + tags: ['custom'] + } } ] }; @@ -270,24 +288,6 @@ rules.forEach(rule => { rule.ruleId.substr(1234); }); -// Plugins -var pluginSrc: axe.AxePlugin = { - id: 'doStuff', - run: (data: any, callback: Function) => { - callback(); - }, - commands: [ - { - id: 'run-doStuff', - callback: (data: any, callback: Function) => { - axe.plugins['doStuff'].run(data, callback); - } - } - ] -}; -axe.registerPlugin(pluginSrc); -axe.cleanup(); - axe.configure({ locale: { checks: { @@ -322,3 +322,41 @@ axe.configure({ } } }); + +// Reporters +let fooReporter = ( + results: axe.RawResult[], + options: axe.RunOptions, + cb: (out: 'foo') => void +) => { + cb('foo'); +}; + +axe.addReporter<'foo'>('foo', fooReporter, true); +axe.configure({ reporter: fooReporter }); +fooReporter = axe.getReporter<'foo'>('foo'); +const hasFoo: boolean = axe.hasReporter('foo'); + +// setup & teardown +axe.setup(); +axe.setup(document); +axe.setup(document.createElement('div')); +axe.teardown(); + +// Plugins +var pluginSrc: axe.AxePlugin = { + id: 'doStuff', + run: (data: any, callback: Function) => { + callback(); + }, + commands: [ + { + id: 'run-doStuff', + callback: (data: any, callback: Function) => { + axe.plugins['doStuff'].run(data, callback); + } + } + ] +}; +axe.registerPlugin(pluginSrc); +axe.cleanup();