diff --git a/.circleci/config.yml b/.circleci/config.yml index fc17df756b..9b4741ec9e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,11 @@ defaults: &defaults unix_box: &unix_box docker: - - image: circleci/node:10-browsers + - image: circleci/node:16.13-browsers + +unix_nightly_box: &unix_nightly_box + docker: + - image: cimg/node:lts-browsers win_box: &win_box executor: @@ -15,7 +19,7 @@ win_box: &win_box orbs: win: circleci/windows@1.0.0 puppeteer: threetreeslight/puppeteer@0.1.2 - + browser-tools: circleci/browser-tools@1.1.0 set_npm_auth: &set_npm_auth run: npm config set "//registry.npmjs.org/:_authToken" $NPM_AUTH @@ -32,6 +36,20 @@ restore_dependency_cache_win: &restore_dependency_cache_win - v9-cache-win-{{ checksum "package-lock.json" }} - v9-cache-win- +# install the version of chromedriver that matches the currently installed version of Chrome (taken from CircleCI chromedriver script) +install_linux_chromedriver: &install_linux_chromedriver + run: + name: Install Chromedriver + command: | + CHROME_VERSION="$(google-chrome --version)" + CHROME_VERSION_STRING="$(echo $CHROME_VERSION | sed 's/^Google Chrome //' | sed 's/^Chromium //')" + echo "Installed version of Google Chrome is $CHROME_VERSION_STRING" + + CHROMEDRIVER_RELEASE="${CHROME_VERSION_STRING%%.*}" + + echo "ChromeDriver $CHROMEDRIVER_RELEASE will be installed" + npm install --no-save "chromedriver@$CHROMEDRIVER_RELEASE" + jobs: # Fetch and cache dependencies. dependencies_unix: @@ -42,6 +60,7 @@ jobs: - <<: *set_npm_auth - <<: *restore_dependency_cache_unix - run: npm ci + - <<: *install_linux_chromedriver - save_cache: key: v9-cache-unix-{{ checksum "package-lock.json" }} paths: @@ -119,6 +138,16 @@ jobs: - run: npm run build - run: npm run test:act + # Run ARIA practices test cases + test_aria_practices: + <<: *defaults + <<: *unix_box + steps: + - checkout + - <<: *restore_dependency_cache_unix + - run: npm run build + - run: npm run test:apg + # Test locale files test_locales: <<: *defaults @@ -140,20 +169,41 @@ jobs: - run: npm run test:virtual-rules # Run the test suite for nightly builds. - test_nightly: + test_nightly_browsers: <<: *defaults - <<: *unix_box + <<: *unix_nightly_box steps: - checkout - <<: *restore_dependency_cache_unix - run: npm run build - # install Chrome Beta - - run: | - wget https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb - sudo apt install ./google-chrome-beta_current_amd64.deb - - run: | - CHROME_BIN="$(which google-chrome-beta)" && echo "CHROME_BIN: $CHROME_BIN" - npm run test -- --browsers Chrome,FirefoxNightly + - run: + name: Install Chrome Beta + command: | + wget https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb + sudo apt install ./google-chrome-beta_current_amd64.deb + - <<: *install_linux_chromedriver + - run: + name: Install Firefox Nightly + command: | + wget -O firefox-nightly.tar.bz2 "https://download.mozilla.org/?product=firefox-nightly-latest-ssl&os=linux64&lang=en-US" + tar xf firefox-nightly.tar.bz2 + - run: + name: Set Environment Variable + command: echo "export FIREFOX_NIGHTLY_BIN=$(pwd)/firefox/firefox-bin" >> $BASH_ENV + - run: npm run test -- --browsers Chrome,FirefoxNightly + + # Run the test suite for nightly builds. + test_nightly_aria_practices: + <<: *defaults + <<: *unix_nightly_box + steps: + - browser-tools/install-browser-tools + - checkout + - <<: *restore_dependency_cache_unix + - run: npm run build + # install ARIA practices + - run: npm install w3c/aria-practices#main + - run: npm run test:apg # Test api docs can be built build_api_docs: @@ -257,7 +307,6 @@ workflows: # Run tests on all commits, but after installing dependencies - test_unix: requires: - - dependencies_unix - lint # Run IE/ Windows test on all commits - test_win: @@ -269,6 +318,9 @@ workflows: - test_act: requires: - test_unix + - test_aria_practices: + requires: + - test_unix - test_locales: requires: - test_unix @@ -354,6 +406,9 @@ workflows: - develop jobs: - dependencies_unix - - test_nightly: + - test_nightly_browsers: + requires: + - dependencies_unix + - test_nightly_aria_practices: requires: - dependencies_unix \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 45e80130bd..e7d24a2e38 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -109,8 +109,17 @@ module.exports = { 'no-use-before-define': 'off' } }, + { + files: [ + 'test/aria-practices/**/*.js' + ], + env: { + mocha: true + } + }, { files: ['test/**/*.js'], + excludedFiles: 'test/aria-practices/**/*.js', parserOptions: { ecmaVersion: 5 }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f50ed1d93..a7f0d09c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,74 @@ 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.4.0](https://github.com/dequelabs/axe-core/compare/v4.3.5...v4.4.0) (2022-01-26) + +### Features + +- add new ARIA roles ([#3138](https://github.com/dequelabs/axe-core/issues/3138)) ([61be7e5](https://github.com/dequelabs/axe-core/commit/61be7e555152d89c6770679fd6fdac10038f7cd3)) +- **aria-allowed-attr:** report violation for non-global ARIA attrs on element without role ([#3342](https://github.com/dequelabs/axe-core/issues/3342)) ([fb5d990](https://github.com/dequelabs/axe-core/commit/fb5d99005cf5e989a51b276b88ad67011cc02d49)) +- **aria-allowed-attr:** report violations for non-global ARIA attributes on elements without a role ([#3102](https://github.com/dequelabs/axe-core/issues/3102)) ([87cfc0b](https://github.com/dequelabs/axe-core/commit/87cfc0b4f4d998a88a2d534438e4f2ccf9427a86)) +- **color-contrast:** add support for CSS mix-blend-mode ([#3226](https://github.com/dequelabs/axe-core/issues/3226)) ([d497f40](https://github.com/dequelabs/axe-core/commit/d497f40026ba2266e31e7e8802511eb242ef0066)) +- **commons:** deprecate shadowElementsFromPoint ([#3311](https://github.com/dequelabs/axe-core/issues/3311)) ([c3a7d16](https://github.com/dequelabs/axe-core/commit/c3a7d1648d0e319003f573f6b4cfe94a1a043808)) +- **configure:** Deprecate branding: Object, use a string instead. ([#3278](https://github.com/dequelabs/axe-core/issues/3278)) ([1f01309](https://github.com/dequelabs/axe-core/commit/1f0130993b64e2016fcc9ea17f63f8da380de513)) +- **dpub:** upgrade to DPUB 1.1 and report deprecated roles ([#3280](https://github.com/dequelabs/axe-core/issues/3280)) ([034a846](https://github.com/dequelabs/axe-core/commit/034a846cf38bff4ff5836b22c78c0f44e2cb3f6d)) +- **options:** make axe.ping configurable with pingWaitTime ([#3273](https://github.com/dequelabs/axe-core/issues/3273)) ([ce4dfaf](https://github.com/dequelabs/axe-core/commit/ce4dfaff7b98c69e15290b71f76e1c523c78ed8d)) +- **rule:** add new color-contrast-enhanced rule (WCAG AAA) ([#3235](https://github.com/dequelabs/axe-core/issues/3235)) ([bec20fc](https://github.com/dequelabs/axe-core/commit/bec20fcbf2f407ecab2fc6d0d829d23525989d48)), closes [#2934](https://github.com/dequelabs/axe-core/issues/2934) + +### Bug Fixes + +- **allowed-role:** area without href can have a button or link role ([#3275](https://github.com/dequelabs/axe-core/issues/3275)) ([bf7e60a](https://github.com/dequelabs/axe-core/commit/bf7e60aad9b00844c2b18691d463d5478b53aa2a)) +- **aria-allowed-attr:** check for invalid `aria-attributes` for `role="row"` ([#3160](https://github.com/dequelabs/axe-core/issues/3160)) ([cfa900d](https://github.com/dequelabs/axe-core/commit/cfa900d57265907b638dad36ba405a5b40dbde9c)) +- **aria-allowed-attr:** revert violation for non-global ARIA attrs on element without role ([#3243](https://github.com/dequelabs/axe-core/issues/3243)) ([112b960](https://github.com/dequelabs/axe-core/commit/112b960ee95b6a6abfb38a15b7092d9847512f0f)) +- **aria-allowed-children,aria-allowed-parent:** allow group role in listbox ([#3195](https://github.com/dequelabs/axe-core/issues/3195)) ([cb01975](https://github.com/dequelabs/axe-core/commit/cb019755d9cb52b997aae340f406ac26d0cf90e5)) +- **aria-allowed-role:** allow title, aria-label and aria-labelledby on a img element with a supported role ([#3224](https://github.com/dequelabs/axe-core/issues/3224)) ([006a681](https://github.com/dequelabs/axe-core/commit/006a681395422bbd0603bab346dbdc6b38087d83)) +- **aria-allowed-role:** landmark roles banner on header and contentinfo on footer to only report on top-level rule ([#3142](https://github.com/dequelabs/axe-core/issues/3142)) ([1fd4b00](https://github.com/dequelabs/axe-core/commit/1fd4b004b2543727d4a3775f355934327765baa1)) +- **aria-allowed-roles:** allow role=radio on img with non-empty name ([#3320](https://github.com/dequelabs/axe-core/issues/3320)) ([accafdf](https://github.com/dequelabs/axe-core/commit/accafdfe170b8ad4b3706134d60808a614e40b00)) +- **aria-allowed-roles:** update role allowances for section element ([#3238](https://github.com/dequelabs/axe-core/issues/3238)) ([99676ec](https://github.com/dequelabs/axe-core/commit/99676ece547be39d71e776a5b9cae2da41c31572)), closes [#3237](https://github.com/dequelabs/axe-core/issues/3237) +- **aria-allowed-role:** Update allowed roles based on ARIA spec updates ([#3124](https://github.com/dequelabs/axe-core/issues/3124)) ([00f6efc](https://github.com/dequelabs/axe-core/commit/00f6efcd55eb0a4c56cc3ca1acc7c79e3d22f58d)) +- **aria-allowed-role:** updates the allowed roles for the wbr element to none and presentation ([#3192](https://github.com/dequelabs/axe-core/issues/3192)) ([2f439b3](https://github.com/dequelabs/axe-core/commit/2f439b3fdb7e7fa3228e663c5313af0f08aa4327)), closes [#3177](https://github.com/dequelabs/axe-core/issues/3177) +- **aria-prohibited-attr:** update metadata message ([#3206](https://github.com/dequelabs/axe-core/issues/3206)) ([d1a768e](https://github.com/dequelabs/axe-core/commit/d1a768eaefe6d1c95e925174bc979bc7a95ee7d9)) +- **autocomplete-valid:** Allow custom autocomplete attribute values ([#3225](https://github.com/dequelabs/axe-core/issues/3225)) ([6076ee8](https://github.com/dequelabs/axe-core/commit/6076ee8a7ba7527c9886916db1eda5d90cd26259)) +- **axe.configure:** do not remove newline characters from locale doT strings ([#3216](https://github.com/dequelabs/axe-core/issues/3216)) ([ea2ce17](https://github.com/dequelabs/axe-core/commit/ea2ce171fd7562e6b85471e72dddc84be23a4297)) +- **axe.d.ts:** allow Node for include/exclude object ([#3338](https://github.com/dequelabs/axe-core/issues/3338)) ([e699939](https://github.com/dequelabs/axe-core/commit/e699939bfd43fcc66b357d0e7329adce6f29cd6b)) +- **axe.run:** add option to increase iframe ping timeout ([#3233](https://github.com/dequelabs/axe-core/issues/3233)) ([ec937e3](https://github.com/dequelabs/axe-core/commit/ec937e3e147274cbdbba2b046a651c90623130e4)) +- check for hidden elements on `aria-errormessage` ([#3156](https://github.com/dequelabs/axe-core/issues/3156)) ([95d37dd](https://github.com/dequelabs/axe-core/commit/95d37dd794dc8552d731fabf45244b260da53d8f)) +- **color-contrast:** account for 0 width scroll regions with children ([#3172](https://github.com/dequelabs/axe-core/issues/3172)) ([5908f0d](https://github.com/dequelabs/axe-core/commit/5908f0d644c20e7091329bd8bbeb191837d27feb)) +- **color-contrast:** account for elements that do not fill entire bounding size ([#3186](https://github.com/dequelabs/axe-core/issues/3186)) ([699697b](https://github.com/dequelabs/axe-core/commit/699697bc237b6c69050e4572ba5cfdc5f338f450)) +- **color-contrast:** check bg on fg contrast for thin text-shadows ([#3350](https://github.com/dequelabs/axe-core/issues/3350)) ([d92a7e5](https://github.com/dequelabs/axe-core/commit/d92a7e527eb61e5c62a59019b024f288ebac3663)) +- **color-contrast:** correctly apply page background color ([#3207](https://github.com/dequelabs/axe-core/issues/3207)) ([fbc581d](https://github.com/dequelabs/axe-core/commit/fbc581d77e457fe092ecc2b95015e667292f1a08)) +- **color-contrast:** correctly compute color-contrast of truncated children ([#3203](https://github.com/dequelabs/axe-core/issues/3203)) ([ac7b2b5](https://github.com/dequelabs/axe-core/commit/ac7b2b5ec402e9de91c50ef39aefd5843f0d62bb)) +- **color-contrast:** correctly handle nested scroll regions ([#3212](https://github.com/dequelabs/axe-core/issues/3212)) ([22db29c](https://github.com/dequelabs/axe-core/commit/22db29ca7e9964a8447392fba45a09057a926ab9)) +- **color-contrast:** correctly work with positioned elements without z-index ([#3209](https://github.com/dequelabs/axe-core/issues/3209)) ([725a20c](https://github.com/dequelabs/axe-core/commit/725a20c91b9006e64009059f0ab9d1a0098d29df)) +- **color-contrast:** inconsistency of bgOverlap message based on scroll ([#3310](https://github.com/dequelabs/axe-core/issues/3310)) ([25eff98](https://github.com/dequelabs/axe-core/commit/25eff98e698f8dd00e5efd05a9b325a5202eae9b)) +- **color-contrast:** properly blend multiple alpha colors ([#3193](https://github.com/dequelabs/axe-core/issues/3193)) ([e930a70](https://github.com/dequelabs/axe-core/commit/e930a7081d4308549370f74e9d341badd9661584)) +- **core:** Incomplete fallback was missing, and could cause infinite loop ([#3302](https://github.com/dequelabs/axe-core/issues/3302)) ([f23d8c8](https://github.com/dequelabs/axe-core/commit/f23d8c8e305d27c8323547731b335d2900e03239)) +- **custom-elms:** Don't error on custom Element.children prop ([#3326](https://github.com/dequelabs/axe-core/issues/3326)) ([2ad92f6](https://github.com/dequelabs/axe-core/commit/2ad92f67205fd370c3ad5ba44274248c2b9fe6e5)) +- **d.ts:** Add PartialResults type ([#3126](https://github.com/dequelabs/axe-core/issues/3126)) ([544b6d5](https://github.com/dequelabs/axe-core/commit/544b6d579f3eecf8e102a53a911bbce0bd53b74f)) +- **get-selector:** do not URL encode or token escape attribute selectors ([#3215](https://github.com/dequelabs/axe-core/issues/3215)) ([6f7e183](https://github.com/dequelabs/axe-core/commit/6f7e183553206afa2ca21914bf388e019b4acfdc)) +- **getFrameContext:** option.iframe=false always returns an empty array ([#3279](https://github.com/dequelabs/axe-core/issues/3279)) ([dfa9725](https://github.com/dequelabs/axe-core/commit/dfa9725e39b8b4bca5a1856d44ff21c9894fc958)) +- greater consistency of help / description text ([#3204](https://github.com/dequelabs/axe-core/issues/3204)) ([0677565](https://github.com/dequelabs/axe-core/commit/0677565941486cf339e7267760d4e533d4a29a05)) +- **is-visible:** do not error if window.Node does not exist ([#3168](https://github.com/dequelabs/axe-core/issues/3168)) ([4046087](https://github.com/dequelabs/axe-core/commit/404608773abf7b4d069a64931adf4ac7e942b663)) +- **jsdoc:** typo Sting -> String ([d1cc205](https://github.com/dequelabs/axe-core/commit/d1cc205629cb159ca760b18ece1f1e9aea22ec3a)) +- **label-content-name-mismatch:** account for formatting elements ([#3349](https://github.com/dequelabs/axe-core/issues/3349)) ([53a6684](https://github.com/dequelabs/axe-core/commit/53a6684a6ebef004d451ff1be63bbfe4503e9447)) +- **label-title-only:** allow hidden labels ([#3183](https://github.com/dequelabs/axe-core/issues/3183)) ([cad3994](https://github.com/dequelabs/axe-core/commit/cad39949c29bc3f83863e3484feef82e89e12118)) +- **listitem:** allow as child of menu ([#3286](https://github.com/dequelabs/axe-core/issues/3286)) ([4bf7d35](https://github.com/dequelabs/axe-core/commit/4bf7d35a1f283a181205bb31f8f4c64c450772ca)) +- **nativeSelectValue:** update selected value on change ([#3154](https://github.com/dequelabs/axe-core/issues/3154)) ([1ee88cb](https://github.com/dequelabs/axe-core/commit/1ee88cb4bb557560f10eab136464c321d4dee81e)) +- **nested-interactive/aria-text:** allow "tabindex=-1" on elements with no role ([#3165](https://github.com/dequelabs/axe-core/issues/3165)) ([0ddc00b](https://github.com/dequelabs/axe-core/commit/0ddc00bb2d0eed457d9ce8ba5cd05606ef3bdc9e)), closes [#2466](https://github.com/dequelabs/axe-core/issues/2466) [#2934](https://github.com/dequelabs/axe-core/issues/2934) [#2934](https://github.com/dequelabs/axe-core/issues/2934) +- **nested-interactive:** add focusable descendants as related nodes ([#3261](https://github.com/dequelabs/axe-core/issues/3261)) ([3b2fdda](https://github.com/dequelabs/axe-core/commit/3b2fdda5ff90703dd20e9b19c4c0331a3d32cd5e)) +- **nested-interactive:** add message for negative tabindex ([#3194](https://github.com/dequelabs/axe-core/issues/3194)) ([b445291](https://github.com/dequelabs/axe-core/commit/b44529130568347816fa810c959b68f980161241)), closes [#2466](https://github.com/dequelabs/axe-core/issues/2466) [#2934](https://github.com/dequelabs/axe-core/issues/2934) [/github.com/dequelabs/axe-core/issues/3163#issuecomment-949804464](https://github.com/dequelabs//github.com/dequelabs/axe-core/issues/3163/issues/issuecomment-949804464) +- **nested-interactive:** update negative tabindex message to include asssistive technologies ([#3262](https://github.com/dequelabs/axe-core/issues/3262)) ([b985776](https://github.com/dequelabs/axe-core/commit/b985776b6fdb2c96f40df38cf86f7241039d4f5b)) +- **p-as-heading:** `p-as-heading` rule to account for `textContent` length ([#3145](https://github.com/dequelabs/axe-core/issues/3145)) ([400a230](https://github.com/dequelabs/axe-core/commit/400a2308510246d64d37fac3db375201610cd7e7)) +- **prohibited-attr:** always report incomplete if there is text in the subtree ([#3347](https://github.com/dequelabs/axe-core/issues/3347)) ([2e27dca](https://github.com/dequelabs/axe-core/commit/2e27dca551d1aee273ad8ac055f7dfd45578dad0)) +- **region:** Allow skip menu buttons ([#3277](https://github.com/dequelabs/axe-core/issues/3277)) ([6b6f2e3](https://github.com/dequelabs/axe-core/commit/6b6f2e36b09f70633b36da1cbcf2bcab59edebcf)) +- remove optional crypto dependency (webpack compatibility) ([#3358](https://github.com/dequelabs/axe-core/issues/3358)) ([aa9d095](https://github.com/dequelabs/axe-core/commit/aa9d0957cbe1a91d7491a27cdea643f800ec7384)) +- **reporter:** Run inside isolated contexts ([#3129](https://github.com/dequelabs/axe-core/issues/3129)) ([afe6675](https://github.com/dequelabs/axe-core/commit/afe6675d2452089602dcc6c9e931987936e9a55a)) +- **respondable:** avoid crashes if the data in `window.postMessage` is `null` ([#3249](https://github.com/dequelabs/axe-core/issues/3249)) ([b37b2f6](https://github.com/dequelabs/axe-core/commit/b37b2f67ac4f2204cf63be351a70cb8a680812a3)) +- **scrollable-region-focusable:** treat overflow:clip as hidden ([#3304](https://github.com/dequelabs/axe-core/issues/3304)) ([ef45377](https://github.com/dequelabs/axe-core/commit/ef453771b252a04fb5854f7d4d5b10281889f395)) +- Separate Level AAA rules from A and best-practices ([#3191](https://github.com/dequelabs/axe-core/issues/3191)) ([828e526](https://github.com/dequelabs/axe-core/commit/828e526fd06ee596df73f4825e750aad459ca75e)) +- **skip-link:** work with absolute and relative paths ([#2875](https://github.com/dequelabs/axe-core/issues/2875)) ([ee49d3e](https://github.com/dequelabs/axe-core/commit/ee49d3e83e8c77159d22b475c7d6d801d921b114)) +- **typescript:** allow passing a NodeList to ElementContext ([#3161](https://github.com/dequelabs/axe-core/issues/3161)) ([ad4b165](https://github.com/dequelabs/axe-core/commit/ad4b165a0e019cd65f70fa5d085d83cea3e7338c)) + ### [4.3.5](https://github.com/dequelabs/axe-core/compare/v4.3.4...v4.3.5) (2021-10-29) ### Bug Fixes diff --git a/README.md b/README.md index d3e42431f4..d431b67213 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Commits](https://img.shields.io/github/commit-activity/y/dequelabs/axe-core.svg)](https://github.com/dequelabs/axe-core/commits/develop) [![GitHub contributors](https://img.shields.io/github/contributors/dequelabs/axe-core.svg)](https://github.com/dequelabs/axe-core/graphs/contributors) [![Join our Slack chat](https://img.shields.io/badge/slack-chat-purple.svg?logo=slack)](https://accessibility.deque.com/axe-community) -[![Package Quality](http://npm.packagequality.com/shield/axe-core.svg)](http://packagequality.com/#?package=axe-core) +[![Package Quality](https://npm.packagequality.com/shield/axe-core.svg)](https://packagequality.com/#?package=axe-core) Axe is an accessibility testing engine for websites and other HTML-based user interfaces. It's fast, secure, lightweight, and was built to seamlessly integrate with any existing test environment so you can automate accessibility testing alongside your regular functional testing. diff --git a/axe.d.ts b/axe.d.ts index 8554f4381d..1887c03b69 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -50,8 +50,8 @@ declare namespace axe { type CrossFrameSelector = CrossTreeSelector[]; type ContextObject = { - include?: BaseSelector | Array; - exclude?: BaseSelector | Array; + include?: Node | BaseSelector | Array; + exclude?: Node | BaseSelector | Array; }; type RunCallback = (error: Error, results: AxeResults) => void; @@ -95,6 +95,7 @@ declare namespace axe { frameWaitTime?: number; preload?: boolean; performanceTimer?: boolean; + pingWaitTime?: number; } interface AxeResults extends EnvironmentData { toolOptions: RunOptions; @@ -187,10 +188,7 @@ declare namespace axe { cssColors?: { [key: string]: number[] }; } interface Spec { - branding?: { - brand?: string; - application?: string; - }; + branding?: string | Branding; reporter?: ReporterVersion; checks?: Check[]; rules?: Rule[]; @@ -203,6 +201,13 @@ declare namespace axe { // Deprecated - do not use. ver?: string; } + /** + * @deprecated Use branding: string instead to set the application key in help URLs + */ + interface Branding { + brand?: string; + application?: string; + } interface Check { id: string; evaluate?: Function | string; @@ -259,13 +264,16 @@ declare namespace axe { results: PartialRuleResult[]; environmentData?: EnvironmentData; } - type PartialResults = Array + type PartialResults = Array; interface FrameContext { frameSelector: CrossTreeSelector; frameContext: ContextObject; } interface Utils { - getFrameContexts: (context?: ElementContext) => FrameContext[]; + getFrameContexts: ( + context?: ElementContext, + options?: RunOptions + ) => FrameContext[]; shadowSelect: (selector: CrossTreeSelector) => Element | null; } interface EnvironmentData { diff --git a/bower.json b/bower.json index aa7a303f2c..41e6dc7c6d 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.3.5", + "version": "4.4.0", "contributors": [ { "name": "David Sturley", diff --git a/build/configure.js b/build/configure.js index cec2b60744..1dfc2c7d6d 100644 --- a/build/configure.js +++ b/build/configure.js @@ -148,16 +148,10 @@ function buildRules(grunt, options, commons, callback) { } function getIncompleteMsg(summaries) { - var result = {}; - summaries.forEach(function(summary) { - if ( - summary.incompleteFallbackMessage && - doTRegex.test(summary.incompleteFallbackMessage) - ) { - result = doT.template(summary.incompleteFallbackMessage).toString(); - } + var summary = summaries.find(function(summary) { + return typeof summary.incompleteFallbackMessage === 'string'; }); - return result; + return summary ? summary.incompleteFallbackMessage : ''; } function replaceFunctions(string) { diff --git a/doc/API.md b/doc/API.md index 98ee922793..a6dc0dd2fc 100644 --- a/doc/API.md +++ b/doc/API.md @@ -186,10 +186,7 @@ User specifies the format of the JSON structure passed to the callback of `axe.r ```js axe.configure({ - branding: { - brand: String, - application: String - }, + branding: String, reporter: 'option' | Function, checks: [Object], rules: [Object], @@ -245,6 +242,8 @@ axe.configure({ **Returns:** Nothing +**Note**: The `branding` property accepts a `string`, which sets the application. Passing it an object is deprecated as of axe-core 4.4.0, as is the `branding.brand` property. + ##### Page level rules Page level rules split their evaluation into two phases. A 'data collection' phase which is done inside the 'evaluate' function and an assessment phase which is done inside the 'after' function. The evaluate function executes inside each individual frame and is responsible for collection data that is passed into the after function which inspects that data and makes a decision. @@ -328,7 +327,7 @@ By default, `axe.run` will test the entire document. The context object is an op The include exclude object is a JSON object with two attributes: include and exclude. Either include or exclude is required. If only `exclude` is specified; include will default to the entire `document`. - A node, or -- An array of arrays of [CSS selectors](./developer-guide.md#supported-css-selectors) +- An array of Nodes or an array of arrays of [CSS selectors](./developer-guide.md#supported-css-selectors) - If the nested array contains a single string, that string is the CSS selector - If the nested array contains multiple strings - The last string is the final CSS selector @@ -447,6 +446,7 @@ Additionally, there are a number or properties that allow configuration of diffe | `frameWaitTime` | `60000` | How long (in milliseconds) axe waits for a response from embedded frames before timing out | | `preload` | `true` | Any additional assets (eg: cssom) to preload before running rules. [See here for configuration details](#preload-configuration-details) | | `performanceTimer` | `false` | Log rule performance metrics to the console | +| `pingWaitTime` | `500` | Time before axe-core considers a frame unresponsive. [See frame messenger for details](frame-messenger.md) | ###### Options Parameter Examples diff --git a/doc/frame-messenger.md b/doc/frame-messenger.md index ca880ebc7d..53dd3a9b2b 100644 --- a/doc/frame-messenger.md +++ b/doc/frame-messenger.md @@ -105,3 +105,15 @@ If for some reason the frameMessenger fails to open, post, or close you should n Axe-core has a timeout mechanism built in, which pings frames to see if they respond before instructing them to run. There is no retry behavior in axe-core, which assumes that whatever channel is used is stable. If this isn't the case, this will need to be built into frameMessenger. The `message` passed to responder may be an `Error`. If axe-core passes an `Error`, this should be propagated "as is". If this is not possible because the message needs to be serialized, a new `Error` object must be constructed as part of deserialization. + +### pingWaitTime + +When axe-core tests frames, it first sends a ping to that frame, to check that the frame has a compatible version of axe-core in it that can respond to the message. If it gets no response, that frame will be skipped in the test. Axe-core does this to avoid a situation where it waits the full frame timeout, just to find out the frame didn't have axe-core in it in the first place. + +In situations where communication between frames can be slow, it may be necessary to increase the ping timeout. This can be done with the `pingWaitTime` option. By default, this is 500ms. This can be configured in the following way: + +```js +const results = await axe.run(context, { pingWaitTime: 1000 })); +``` + +It is possible to skip this ping altogether by setting `pingWaitTime` to `0`. This can slightly speed up performance, but should only be used when long wait times for unresponsive frames can be avoided. Axe-core handles timeout errors the same way it handles any other frame communication errors. Therefore if a custom frame messenger has a timeout, it can inform axe by calling `replyHandler` with an `Error` object. diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 5a949c8cc2..d48a85a535 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -11,62 +11,62 @@ ## WCAG 2.0 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | :---------------- | :----------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | -| [area-alt](https://dequeuniversity.com/rules/axe/4.3/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, 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.3/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.3/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, wcag131 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | -| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | | -| [aria-required-children](https://dequeuniversity.com/rules/axe/4.3/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 | [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.3/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 | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | | -| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.3/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 | | -| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea), [c487ae](https://act-rules.github.io/rules/c487ae) | -| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.3/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | | -| [audio-caption](https://dequeuniversity.com/rules/axe/4.3/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, section508, section508.22.a | needs review | [c3232f](https://act-rules.github.io/rules/c3232f), [e7aa44](https://act-rules.github.io/rules/e7aa44) | -| [blink](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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 | | -| [color-contrast](https://dequeuniversity.com/rules/axe/4.3/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143 | failure, needs review | | -| [definition-list](https://dequeuniversity.com/rules/axe/4.3/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [dlitem](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | failure | | -| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.3/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.3/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | failure | | -| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.3/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.3/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 | | -| [frame-title](https://dequeuniversity.com/rules/axe/4.3/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag241, wcag412, section508, section508.22.i | failure, needs review | | -| [html-has-lang](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/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.3/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a | failure, needs review | | -| [input-image-alt](https://dequeuniversity.com/rules/axe/4.3/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | -| [label](https://dequeuniversity.com/rules/axe/4.3/label?application=RuleDescription) | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag412, wcag131, section508, section508.22.n, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5), [307n5z](https://act-rules.github.io/rules/307n5z) | -| [link-name](https://dequeuniversity.com/rules/axe/4.3/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.3/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [listitem](https://dequeuniversity.com/rules/axe/4.3/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [marquee](https://dequeuniversity.com/rules/axe/4.3/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222 | failure | | -| [meta-refresh](https://dequeuniversity.com/rules/axe/4.3/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used | Critical | cat.time-and-media, wcag2a, wcag221, wcag224, wcag325 | failure | | -| [nested-interactive](https://dequeuniversity.com/rules/axe/4.3/nested-interactive?application=RuleDescription) | Ensure controls are not nested as they are not announced by screen readers | Serious | cat.keyboard, wcag2a, wcag412 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | -| [object-alt](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, Critical | cat.forms, wcag2a, wcag412, wcag131, 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.3/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.3/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.3/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.3/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.3/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312 | failure | | -| [video-caption](https://dequeuniversity.com/rules/axe/4.3/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.3/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, 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.3/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.3/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, wcag131 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | +| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | | +| [aria-required-children](https://dequeuniversity.com/rules/axe/4.3/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 | [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.3/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 | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Minor, Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | | +| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.3/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 | | +| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.3/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.3/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea), [c487ae](https://act-rules.github.io/rules/c487ae) | +| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.3/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | | +| [audio-caption](https://dequeuniversity.com/rules/axe/4.3/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, section508, section508.22.a | needs review | [c3232f](https://act-rules.github.io/rules/c3232f), [e7aa44](https://act-rules.github.io/rules/e7aa44) | +| [blink](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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 | | +| [color-contrast](https://dequeuniversity.com/rules/axe/4.3/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143 | failure, needs review | | +| [definition-list](https://dequeuniversity.com/rules/axe/4.3/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [dlitem](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | failure | | +| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.3/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.3/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | failure | | +| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.3/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.3/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 | | +| [frame-title](https://dequeuniversity.com/rules/axe/4.3/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag241, wcag412, section508, section508.22.i | failure, needs review | | +| [html-has-lang](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/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.3/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a | failure, needs review | | +| [input-image-alt](https://dequeuniversity.com/rules/axe/4.3/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | +| [label](https://dequeuniversity.com/rules/axe/4.3/label?application=RuleDescription) | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag412, wcag131, section508, section508.22.n, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5), [307n5z](https://act-rules.github.io/rules/307n5z) | +| [link-name](https://dequeuniversity.com/rules/axe/4.3/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.3/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [listitem](https://dequeuniversity.com/rules/axe/4.3/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131 | failure | | +| [marquee](https://dequeuniversity.com/rules/axe/4.3/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222 | failure | | +| [meta-refresh](https://dequeuniversity.com/rules/axe/4.3/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used | Critical | cat.time-and-media, wcag2a, wcag221, wcag224, wcag325 | failure | | +| [nested-interactive](https://dequeuniversity.com/rules/axe/4.3/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) | +| [object-alt](https://dequeuniversity.com/rules/axe/4.3/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.3/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.3/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.3/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, Critical | cat.forms, wcag2a, wcag412, wcag131, 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.3/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.3/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.3/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.3/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.3/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312 | failure | | +| [video-caption](https://dequeuniversity.com/rules/axe/4.3/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) | ## WCAG 2.1 Level A & AA Rules @@ -116,9 +116,10 @@ Rules that do not necessarily conform to WCAG success criterion but are industry Rules that check for conformance to WCAG AAA success criteria that can be fully automated. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :--------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | :----- | :------------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------- | -| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.3/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), [fd3a94](https://act-rules.github.io/rules/fd3a94) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :--------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :------ | :------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | +| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.3/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 | failure, needs review | | +| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.3/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), [fd3a94](https://act-rules.github.io/rules/fd3a94) | ## Experimental Rules diff --git a/doc/run-partial.md b/doc/run-partial.md index 011edf4a85..ae7114d8ea 100644 --- a/doc/run-partial.md +++ b/doc/run-partial.md @@ -7,9 +7,7 @@ To use these methods, call `axe.runPartial()` in the top window, and in all nest This results in code that looks something like the following. The `context` and `options` arguments used are the same as would be passed to `axe.run`. See [API.md](api.md) for details. ```js -const partialResults = await Promise.all( - runPartialRecursive(context, options) -) +const partialResults = await Promise.all(runPartialRecursive(context, options)); const axeResults = await axe.finishRun(partialResults, options); ``` @@ -23,12 +21,12 @@ When using `axe.runPartial()` it is important to keep in mind that the `context` function runPartialRecursive(context, options = {}, win = window) { const { axe, document } = win; // Find all frames in context, and determine what context object to use in that frame - const frameContexts = axe.utils.getFrameContexts(context); + const frameContexts = axe.utils.getFrameContexts(context, options); // Run the current context, in the current window. - const promiseResults = [ axe.runPartial(context, options) ]; + const promiseResults = [axe.runPartial(context, options)]; // Loop over all frames in context - frameContexts.forEach(({ frameSelector, frameContext }) => { + frameContexts.forEach(({ frameSelector, frameContext }) => { // Find the window of the frame const frame = axe.utils.shadowSelect(frameSelector); const frameWin = frame.contentWindow; @@ -37,7 +35,7 @@ function runPartialRecursive(context, options = {}, win = window) { promiseResults.push(...frameResults); }); return promiseResults; -}; +} ``` **important**: The order in which these methods are called matters for performance. Internally, axe-core constructs a flattened tree when `axe.utils.getFrameContexts` is called. This is fairly slow, and so should not happen more than once per frame. When `axe.runPartial` is called, that tree will be used if it still exists. Since this tree can get out of sync with the actual DOM, it is important to call `axe.runPartial` immediately after `axe.utils.getFrameContexts`. @@ -55,7 +53,7 @@ The `axe.finishRun` method does two things: It calls the `after` methods of chec // - frame_1a // - frame_2 // The partial results are passed in the following order: -axe.finishRun([ top, frame_1, frame_1a, frame_2 ]) +axe.finishRun([top, frame_1, frame_1a, frame_2]); ``` If for some reason `axe.runPartial` fails to run, the integration API **must** include `null` in the data in place of the results object, so that axe-core knows to skip over it. If a frame fails to run, results from any descending frames **must be omitted**. To illustrate this, consider the following: @@ -68,20 +66,20 @@ If for some reason `axe.runPartial` fails to run, the integration API **must** i // - frame_2 // If axe.runPartial throws an error, the results must be passed to finishRun like this: -axe.finishRun([ - top, null, /* nothing for frame 1a, */ frame_2 -]) +axe.finishRun([top, null, /* nothing for frame 1a, */ frame_2]); ``` **important**: Since `axe.finishRun` may have access to cross-origin information, it should only be called in an environment that is known not to have third-party scripts. When using a browser driver, this can for example by done in a blank page. -## axe.utils.getFrameContexts(context): FrameContext[] +## axe.utils.getFrameContexts(context, options): FrameContext[] The `axe.utils.getFrameContexts` method takes any valid context, and returns an array of objects. Each object represents a frame that is in the context. The object has the following properties: - `frameSelector`: This is a CSS selector, or array of CSS selectors in case of nodes in a shadow DOM tree to locate the frame element to be tested. - `frameContext`: This is an object is a context object that should be tested in the particular frame. +The `options` object takes the same RunOptions object that axe.run accepts. When the `iframes` property is `false`, it returns an empty array. + ## Custom Rulesets and Reporters Because `axe.finishRun` does not run inside the page, the `reporter` and `after` methods do not have access to the top-level `window` and `document` objects, and might not have access to common browser APIs. Axe-core reporter use the `environmentData` property that is set on the partialResult object of the initiator. diff --git a/doc/standards-object.md b/doc/standards-object.md index 3c2f4157df..a439b1ed41 100644 --- a/doc/standards-object.md +++ b/doc/standards-object.md @@ -20,7 +20,7 @@ The following properties are currently available in axe-core `standards`: 1. [ARIA Attrs](#aria-attrs) 1. [ARIA Roles](#aria-roles) 1. [DPUB Roles](#dpub-roles) -1. [Graphics Roles][#graphics-roles] +1. [Graphics Roles](#graphics-roles) 1. [HTML Elms](#html-elms) 1. [CSS Colors](#css-colors) diff --git a/lib/checks/aria/aria-allowed-attr-evaluate.js b/lib/checks/aria/aria-allowed-attr-evaluate.js index 9d309437bf..c72f7bb70a 100644 --- a/lib/checks/aria/aria-allowed-attr-evaluate.js +++ b/lib/checks/aria/aria-allowed-attr-evaluate.js @@ -1,5 +1,6 @@ -import { uniqueArray, closest } from '../../core/utils'; +import { uniqueArray, closest, isHtmlElement } from '../../core/utils'; import { getRole, allowedAttr, validateAttr } from '../../commons/aria'; +import { isFocusable } from '../../commons/dom'; import cache from '../../core/base/cache'; /** @@ -69,7 +70,7 @@ function ariaAllowedAttrEvaluate(node, options, virtualNode) { ariaAttr.forEach(attr => { preChecks[attr] = validateRowAttrs; }); - if (role && allowed) { + if (allowed) { for (let i = 0; i < attrs.length; i++) { const attrName = attrs[i]; if (validateAttr(attrName) && preChecks[attrName]?.()) { @@ -82,6 +83,11 @@ function ariaAllowedAttrEvaluate(node, options, virtualNode) { if (invalid.length) { this.data(invalid); + + if (!isHtmlElement(virtualNode) && !role && !isFocusable(virtualNode)) { + return undefined; + } + return false; } diff --git a/lib/checks/aria/aria-allowed-attr.json b/lib/checks/aria/aria-allowed-attr.json index 4079b3ebcb..cf39faf10f 100644 --- a/lib/checks/aria/aria-allowed-attr.json +++ b/lib/checks/aria/aria-allowed-attr.json @@ -16,7 +16,8 @@ "fail": { "singular": "ARIA attribute is not allowed: ${data.values}", "plural": "ARIA attributes are not allowed: ${data.values}" - } + }, + "incomplete": "Check that there is no problem if the ARIA attribute is ignored on this element: ${data.values}" } } } diff --git a/lib/checks/aria/aria-prohibited-attr-evaluate.js b/lib/checks/aria/aria-prohibited-attr-evaluate.js index ff843065fd..c4e7085761 100644 --- a/lib/checks/aria/aria-prohibited-attr-evaluate.js +++ b/lib/checks/aria/aria-prohibited-attr-evaluate.js @@ -26,14 +26,12 @@ import standards from '../../standards'; * @memberof checks * @return {Boolean} True if the element uses any prohibited ARIA attributes. False otherwise. */ -function ariaProhibitedAttrEvaluate(node, options = {}, virtualNode) { - const extraElementsAllowedAriaLabel = options.elementsAllowedAriaLabel || []; - - const prohibitedList = listProhibitedAttrs( - virtualNode, - extraElementsAllowedAriaLabel - ); +export default function ariaProhibitedAttrEvaluate(node, options = {}, virtualNode) { + const elementsAllowedAriaLabel = options?.elementsAllowedAriaLabel || []; + const { nodeName } = virtualNode.props; + const role = getRole(virtualNode, { chromium: true }); + const prohibitedList = listProhibitedAttrs(role, nodeName, elementsAllowedAriaLabel); const prohibited = prohibitedList.filter(attrName => { if (!virtualNode.attrNames.includes(attrName)) { return false; @@ -45,24 +43,26 @@ function ariaProhibitedAttrEvaluate(node, options = {}, virtualNode) { return false; } - this.data(prohibited); - const hasTextContent = sanitize(subtreeText(virtualNode)) !== ''; - // Don't fail if there is text content to announce - return hasTextContent ? undefined : true; + let messageKey = virtualNode.hasAttr('role') ? 'hasRole' : 'noRole'; + messageKey += prohibited.length > 1 ? 'Plural' : 'Singular'; + this.data({ role, nodeName, messageKey, prohibited }); + + // `subtreeDescendant` to override namedFromContents + const textContent = subtreeText(virtualNode, { subtreeDescendant: true }); + if (sanitize(textContent) !== '') { + // Don't fail if there is text content to announce + return undefined; + } + return true; } -function listProhibitedAttrs(virtualNode, elementsAllowedAriaLabel) { - const role = getRole(virtualNode, { chromium: true }); +function listProhibitedAttrs(role, nodeName, elementsAllowedAriaLabel) { const roleSpec = standards.ariaRoles[role]; if (roleSpec) { return roleSpec.prohibitedAttrs || []; } - - const { nodeName } = virtualNode.props; if (!!role || elementsAllowedAriaLabel.includes(nodeName)) { return []; } return ['aria-label', 'aria-labelledby']; } - -export default ariaProhibitedAttrEvaluate; diff --git a/lib/checks/aria/aria-prohibited-attr.json b/lib/checks/aria/aria-prohibited-attr.json index fd2f3e72bc..21e8a007f9 100644 --- a/lib/checks/aria/aria-prohibited-attr.json +++ b/lib/checks/aria/aria-prohibited-attr.json @@ -8,8 +8,18 @@ "impact": "serious", "messages": { "pass": "ARIA attribute is allowed", - "fail": "ARIA attribute: ${data.values} is not allowed. Use a different role attribute or element.", - "incomplete": "ARIA attribute: ${data.values} is not well supported. Use a different role attribute or element." + "fail": { + "hasRolePlural": "${data.prohibited} attributes cannot be used with role \"${data.role}\".", + "hasRoleSingular": "${data.prohibited} attribute cannot be used with role \"${data.role}\".", + "noRolePlural": "${data.prohibited} attributes cannot be used on a ${data.nodeName} with no valid role attribute.", + "noRoleSingular": "${data.prohibited} attribute cannot be used on a ${data.nodeName} with no valid role attribute." + }, + "incomplete": { + "hasRoleSingular": "${data.prohibited} attribute is not well supported with role \"${data.role}\".", + "hasRolePlural": "${data.prohibited} attributes are not well supported with role \"${data.role}\".", + "noRoleSingular": "${data.prohibited} attribute is not well supported on a ${data.nodeName} with no valid role attribute.", + "noRolePlural": "${data.prohibited} attributes are not well supported on a ${data.nodeName} with no valid role attribute." + } } } } diff --git a/lib/checks/aria/deprecatedrole-evaluate.js b/lib/checks/aria/deprecatedrole-evaluate.js new file mode 100644 index 0000000000..c2349df4c5 --- /dev/null +++ b/lib/checks/aria/deprecatedrole-evaluate.js @@ -0,0 +1,21 @@ +import standards from '../../standards'; +import { getRole } from '../../commons/aria'; + +/** + * Check that an elements semantic role is deprecated. + * + * Deprecated roles are taken from the `ariaRoles` standards object from the roles `deprecated` property. + * + * @memberof checks + * @return {Boolean} True if the elements semantic role is deprecated. False otherwise. + */ +export default function deprecatedroleEvaluate(node, options, virtualNode) { + const role = getRole(virtualNode, { dpub: true, fallback: true }); + const roleDefinition = standards.ariaRoles[role]; + if (!roleDefinition?.deprecated) { + return false; + } + + this.data(role); + return true; +} diff --git a/lib/checks/aria/deprecatedrole.json b/lib/checks/aria/deprecatedrole.json new file mode 100644 index 0000000000..8c9aee4c9c --- /dev/null +++ b/lib/checks/aria/deprecatedrole.json @@ -0,0 +1,11 @@ +{ + "id": "deprecatedrole", + "evaluate": "deprecatedrole-evaluate", + "metadata": { + "impact": "minor", + "messages": { + "pass": "ARIA role is not deprecated", + "fail": "The role used is deprecated: ${data.values}" + } + } +} diff --git a/lib/checks/color/color-contrast-enhanced.json b/lib/checks/color/color-contrast-enhanced.json new file mode 100644 index 0000000000..d2de46c476 --- /dev/null +++ b/lib/checks/color/color-contrast-enhanced.json @@ -0,0 +1,48 @@ +{ + "id": "color-contrast-enhanced", + "evaluate": "color-contrast-evaluate", + "options": { + "ignoreUnicode": true, + "ignoreLength": false, + "ignorePseudo": false, + "boldValue": 700, + "boldTextPt": 14, + "largeTextPt": 18, + "contrastRatio": { + "normal": { + "expected": 7 + }, + "large": { + "expected": 4.5 + } + }, + "pseudoSizeThreshold": 0.25, + "shadowOutlineEmMax": 0.1 + }, + "metadata": { + "impact": "serious", + "messages": { + "pass": "Element has sufficient color contrast of ${data.contrastRatio}", + "fail": { + "default": "Element has insufficient color contrast of ${data.contrastRatio} (foreground color: ${data.fgColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Expected contrast ratio of ${data.expectedContrastRatio}", + "fgOnShadowColor": "Element has insufficient color contrast of ${data.contrastRatio} between the foreground and shadow color (foreground color: ${data.fgColor}, text-shadow color: ${data.shadowColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Expected contrast ratio of ${data.expectedContrastRatio}", + "shadowOnBgColor": "Element has insufficient color contrast of ${data.contrastRatio} between the shadow color and background color (text-shadow color: ${data.shadowColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Expected contrast ratio of ${data.expectedContrastRatio}" + }, + "incomplete": { + "default": "Unable to determine contrast ratio", + "bgImage": "Element's background color could not be determined due to a background image", + "bgGradient": "Element's background color could not be determined due to a background gradient", + "imgNode": "Element's background color could not be determined because element contains an image node", + "bgOverlap": "Element's background color could not be determined because it is overlapped by another element", + "fgAlpha": "Element's foreground color could not be determined because of alpha transparency", + "elmPartiallyObscured": "Element's background color could not be determined because it's partially obscured by another element", + "elmPartiallyObscuring": "Element's background color could not be determined because it partially overlaps other elements", + "outsideViewport": "Element's background color could not be determined because it's outside the viewport", + "equalRatio": "Element has a 1:1 contrast ratio with the background", + "shortTextContent": "Element content is too short to determine if it is actual text content", + "nonBmp": "Element content contains only non-text characters", + "pseudoContent": "Element's background color could not be determined due to a pseudo element" + } + } + } +} diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index 8aa165d37d..e4afe201b9 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -71,14 +71,16 @@ export default function colorContrastEvaluate(node, options, virtualNode) { if (shadowColors.length === 0) { contrast = getContrast(bgColor, fgColor); } else if (fgColor && bgColor) { - // Thin shadows can pass either by contrasting with the text color - // or when contrasting with the background. shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors); - const bgContrast = getContrast(bgColor, shadowColor); - const fgContrast = getContrast(shadowColor, fgColor); - contrast = Math.max(bgContrast, fgContrast); - contrastContributor = - bgContrast > fgContrast ? 'shadowOnBgColor' : 'fgOnShadowColor'; + // Compare shadow, bgColor, textColor. Check passes if any is sufficient + const fgBgContrast = getContrast(bgColor, fgColor); + const bgShContrast = getContrast(bgColor, shadowColor); + const fgShContrast = getContrast(shadowColor, fgColor); + contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast); + if (contrast !== fgBgContrast) { + contrastContributor = + bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor'; + } } const ptSize = Math.ceil(fontSize * 72) / 96; diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index 52cd48361c..67fdd6c286 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -17,7 +17,7 @@ } }, "pseudoSizeThreshold": 0.25, - "shadowOutlineEmMax": 0.1 + "shadowOutlineEmMax": 0.2 }, "metadata": { "impact": "serious", diff --git a/lib/checks/forms/autocomplete-valid.json b/lib/checks/forms/autocomplete-valid.json index c70c31fe79..0906e4b89a 100644 --- a/lib/checks/forms/autocomplete-valid.json +++ b/lib/checks/forms/autocomplete-valid.json @@ -7,5 +7,16 @@ "pass": "the autocomplete attribute is correctly formatted", "fail": "the autocomplete attribute is incorrectly formatted" } + }, + "options": { + "stateTerms": [ + "none", + "false", + "true", + "disabled", + "enabled", + "undefined", + "null" + ] } } diff --git a/lib/checks/keyboard/frame-focusable-content-evaluate.js b/lib/checks/keyboard/frame-focusable-content-evaluate.js new file mode 100644 index 0000000000..0624167778 --- /dev/null +++ b/lib/checks/keyboard/frame-focusable-content-evaluate.js @@ -0,0 +1,35 @@ +import isFocusable from '../../commons/dom/is-focusable'; + +function focusableDescendants(vNode) { + if (isFocusable(vNode)) { + return true; + } + + if (!vNode.children) { + if (vNode.props.nodeType === 1) { + throw new Error('Cannot determine children'); + } + + return false; + } + + return vNode.children.some(child => { + return focusableDescendants(child); + }); +} + +function frameFocusableContentEvaluate(node, options, virtualNode) { + if (!virtualNode.children) { + return undefined; + } + + try { + return !virtualNode.children.some(child => { + return focusableDescendants(child); + }); + } catch (e) { + return undefined; + } +} + +export default frameFocusableContentEvaluate; diff --git a/lib/checks/keyboard/frame-focusable-content.json b/lib/checks/keyboard/frame-focusable-content.json index 5157665b2e..6926a2ccf2 100644 --- a/lib/checks/keyboard/frame-focusable-content.json +++ b/lib/checks/keyboard/frame-focusable-content.json @@ -1,6 +1,6 @@ { "id": "frame-focusable-content", - "evaluate": "no-focusable-content-evaluate", + "evaluate": "frame-focusable-content-evaluate", "metadata": { "impact": "serious", "messages": { diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index e45bdfde9f..f72bad79d7 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -1,35 +1,57 @@ import isFocusable from '../../commons/dom/is-focusable'; +import { getRole, getRoleType } from '../../commons/aria'; -function focusableDescendants(vNode) { - if (isFocusable(vNode)) { - return true; +export default function noFocusableContentEvaluate(node, options, virtualNode) { + if (!virtualNode.children) { + return undefined; + } + + try { + const focusableDescendants = getFocusableDescendants(virtualNode); + + if (!focusableDescendants.length) { + return true; + } + + const notHiddenElements = focusableDescendants.filter( + usesUnreliableHidingStrategy + ); + + if (notHiddenElements.length > 0) { + this.data({ messageKey: 'notHidden' }); + this.relatedNodes(notHiddenElements); + } else { + this.relatedNodes(focusableDescendants); + } + + return false; + } catch (e) { + return undefined; } +} +function getFocusableDescendants(vNode) { if (!vNode.children) { if (vNode.props.nodeType === 1) { throw new Error('Cannot determine children'); } - return false; + return []; } - return vNode.children.some(child => { - return focusableDescendants(child); + const retVal = []; + vNode.children.forEach(child => { + const role = getRole(child); + if (getRoleType(role) === 'widget' && isFocusable(child)) { + retVal.push(child); + } else { + retVal.push(...getFocusableDescendants(child)); + } }); + return retVal; } -function noFocusbleContentEvaluate(node, options, virtualNode) { - if (!virtualNode.children) { - return undefined; - } - - try { - return !virtualNode.children.some(child => { - return focusableDescendants(child); - }); - } catch (e) { - return undefined; - } +function usesUnreliableHidingStrategy(vNode) { + const tabIndex = parseInt(vNode.attr('tabindex'), 10); + return !isNaN(tabIndex) && tabIndex < 0; } - -export default noFocusbleContentEvaluate; diff --git a/lib/checks/keyboard/no-focusable-content.json b/lib/checks/keyboard/no-focusable-content.json index a67554e6a8..9608c74389 100644 --- a/lib/checks/keyboard/no-focusable-content.json +++ b/lib/checks/keyboard/no-focusable-content.json @@ -5,7 +5,10 @@ "impact": "serious", "messages": { "pass": "Element does not have focusable descendants", - "fail": "Element has focusable descendants", + "fail": { + "default": "Element has focusable descendants", + "notHidden": "Using a negative tabindex on an element inside an interactive control does not prevent assistive technologies from focusing the element (even with 'aria-hidden=true')" + }, "incomplete": "Could not determine if element has descendants" } } diff --git a/lib/checks/label/label-content-name-mismatch-evaluate.js b/lib/checks/label/label-content-name-mismatch-evaluate.js index ba25035c9c..5e77ce0867 100644 --- a/lib/checks/label/label-content-name-mismatch-evaluate.js +++ b/lib/checks/label/label-content-name-mismatch-evaluate.js @@ -1,8 +1,7 @@ import { accessibleText, isHumanInterpretable, - visibleTextNodes, - isIconLigature, + subtreeText, sanitize, removeUnicode } from '../../commons/text'; @@ -46,15 +45,14 @@ function labelContentNameMismatchEvaluate(node, options, virtualNode) { return undefined; } - const textVNodes = visibleTextNodes(virtualNode); - const nonLigatureText = textVNodes - .filter( - textVNode => - !isIconLigature(textVNode, pixelThreshold, occuranceThreshold) - ) - .map(textVNode => textVNode.actualNode.nodeValue) - .join(''); - const visibleText = sanitize(nonLigatureText).toLowerCase(); + const visibleText = sanitize( + subtreeText(virtualNode, { + subtreeDescendant: true, + ignoreIconLigature: true, + pixelThreshold, + occuranceThreshold + }) + ).toLowerCase(); if (!visibleText) { return true; } diff --git a/lib/checks/lists/listitem-evaluate.js b/lib/checks/lists/listitem-evaluate.js index 0fa6382506..b09c5414bb 100644 --- a/lib/checks/lists/listitem-evaluate.js +++ b/lib/checks/lists/listitem-evaluate.js @@ -1,15 +1,14 @@ -import { getComposedParent } from '../../commons/dom'; -import { isValidRole } from '../../commons/aria'; +import { isValidRole, getExplicitRole } from '../../commons/aria'; -function listitemEvaluate(node) { - const parent = getComposedParent(node); +export default function listitemEvaluate(node, options, virtualNode) { + const { parent } = virtualNode; if (!parent) { // Can only happen with detached DOM nodes and roots: return undefined; } - const parentTagName = parent.nodeName.toUpperCase(); - const parentRole = (parent.getAttribute('role') || '').toLowerCase(); + const parentNodeName = parent.props.nodeName; + const parentRole = getExplicitRole(parent); if (['presentation', 'none', 'list'].includes(parentRole)) { return true; @@ -21,8 +20,5 @@ function listitemEvaluate(node) { }); return false; } - - return ['UL', 'OL'].includes(parentTagName); + return ['ul', 'ol', 'menu'].includes(parentNodeName); } - -export default listitemEvaluate; diff --git a/lib/checks/navigation/internal-link-present-evaluate.js b/lib/checks/navigation/internal-link-present-evaluate.js index acf086d43f..d235bfbe2b 100644 --- a/lib/checks/navigation/internal-link-present-evaluate.js +++ b/lib/checks/navigation/internal-link-present-evaluate.js @@ -1,10 +1,10 @@ -import { querySelectorAll } from '../../core/utils'; - -function internalLinkPresentEvaluate(node, options, virtualNode) { - const links = querySelectorAll(virtualNode, 'a[href]'); - return links.some(vLink => { - return /^#[^/!]/.test(vLink.actualNode.getAttribute('href')); - }); -} - -export default internalLinkPresentEvaluate; +import { querySelectorAll } from '../../core/utils'; + +function internalLinkPresentEvaluate(node, options, virtualNode) { + const links = querySelectorAll(virtualNode, 'a[href]'); + return links.some(vLink => { + return /^#[^/!]/.test(vLink.attr('href')); + }); +} + +export default internalLinkPresentEvaluate; diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js index 0ff830f8b1..48b86e92f1 100644 --- a/lib/checks/navigation/region-evaluate.js +++ b/lib/checks/navigation/region-evaluate.js @@ -1,5 +1,5 @@ import * as dom from '../../commons/dom'; -import * as aria from '../../commons/aria'; +import { getRole } from '../../commons/aria'; import * as standards from '../../commons/standards'; import matches from '../../commons/matches'; import cache from '../../core/base/cache'; @@ -10,7 +10,7 @@ const implicitAriaLiveRoles = ['alert', 'log', 'status']; // Check if the current element is a landmark function isRegion(virtualNode, options) { const node = virtualNode.actualNode; - const role = aria.getRole(virtualNode); + const role = getRole(virtualNode); const ariaLive = (node.getAttribute('aria-live') || '').toLowerCase().trim(); // Ignore content inside of aria-live @@ -41,6 +41,7 @@ function findRegionlessElms(virtualNode, options) { const node = virtualNode.actualNode; // End recursion if the element is a landmark, skiplink, or hidden content if ( + getRole(virtualNode) === 'button' || isRegion(virtualNode, options) || ['iframe', 'frame'].includes(virtualNode.props.nodeName) || (dom.isSkipLink(virtualNode.actualNode) && diff --git a/lib/commons/aria/get-accessible-refs.js b/lib/commons/aria/get-accessible-refs.js index 0ff919dcbc..534f60ca1b 100644 --- a/lib/commons/aria/get-accessible-refs.js +++ b/lib/commons/aria/get-accessible-refs.js @@ -33,8 +33,10 @@ function cacheIdRefs(node, idRefs, refAttrs) { } } - for (let i = 0; i < node.children.length; i++) { - cacheIdRefs(node.children[i], idRefs, refAttrs); + for (let i = 0; i < node.childNodes.length; i++) { + if (node.childNodes[i].nodeType === 1) { + cacheIdRefs(node.childNodes[i], idRefs, refAttrs); + } } } diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index 9209443353..cbf7ede0ff 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -2,12 +2,7 @@ import isValidRole from './is-valid-role'; import getImplicitRole from './implicit-role'; import getRoleType from './get-role-type'; import isAriaRoleAllowedOnElement from './is-aria-role-allowed-on-element'; -import { - tokenList, - isHtmlElement, - matchesSelector, - getNodeFromTree -} from '../../core/utils'; +import { tokenList, isHtmlElement, getNodeFromTree } from '../../core/utils'; import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node'; // dpub roles which are subclassing roles that are implicit on some native @@ -22,6 +17,11 @@ const dpubRoles = [ 'doc-noteref' ]; +const landmarkRoles = { + header: 'banner', + footer: 'contentinfo' +}; + /** * Returns all roles applicable to element in a list * @@ -33,7 +33,6 @@ const dpubRoles = [ function getRoleSegments(vNode) { let roles = []; - if (!vNode) { return roles; } @@ -44,9 +43,7 @@ function getRoleSegments(vNode) { } // filter invalid roles - roles = roles.filter(role => isValidRole(role)); - - return roles; + return roles.filter(role => isValidRole(role)); } /** @@ -59,51 +56,32 @@ function getRoleSegments(vNode) { function getElementUnallowedRoles(node, allowImplicit = true) { const vNode = node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); - const { nodeName } = vNode.props; - // by pass custom elements if (!isHtmlElement(vNode)) { return []; } + // allow landmark roles to use their implicit role inside another landmark + // @see https://github.com/dequelabs/axe-core/pull/3142 + const { nodeName } = vNode.props; + const implicitRole = getImplicitRole(vNode) || landmarkRoles[nodeName]; const roleSegments = getRoleSegments(vNode); - const implicitRole = getImplicitRole(vNode); - - // stores all roles that are not allowed for a specific element most often an element only has one explicit role - const unallowedRoles = roleSegments.filter(role => { - // if role and implicit role are same, when allowImplicit: true - // ignore as it is a redundant role - if (allowImplicit && role === implicitRole) { - return false; - } - - // if role is a dpub role make sure it's used on an element with a valid - // implicit role fallback - if (allowImplicit && dpubRoles.includes(role)) { - const roleType = getRoleType(role); - if (implicitRole !== roleType) { - return true; - } - } - - // Edge case: - // setting implicit role row on tr element is allowed when child of table[role='grid'] - if ( - !allowImplicit && - !( - role === 'row' && - nodeName === 'tr' && - matchesSelector(vNode, 'table[role="grid"] > tr') - ) - ) { - return true; - } - - // check if role is allowed on element - return !isAriaRoleAllowedOnElement(vNode, role); + return roleSegments.filter(role => { + return !roleIsAllowed(role, vNode, allowImplicit, implicitRole); }); +} - return unallowedRoles; +function roleIsAllowed(role, vNode, allowImplicit, implicitRole) { + if (allowImplicit && role === implicitRole) { + return true; + } + // if role is a dpub role make sure it's used on an element with a valid + // implicit role fallback + if (dpubRoles.includes(role) && getRoleType(role) !== implicitRole) { + return false; + } + // check if role is allowed on element + return isAriaRoleAllowedOnElement(vNode, role); } export default getElementUnallowedRoles; diff --git a/lib/commons/aria/is-aria-role-allowed-on-element.js b/lib/commons/aria/is-aria-role-allowed-on-element.js index 0051c7ac42..b7c3bb9191 100644 --- a/lib/commons/aria/is-aria-role-allowed-on-element.js +++ b/lib/commons/aria/is-aria-role-allowed-on-element.js @@ -15,17 +15,17 @@ function isAriaRoleAllowedOnElement(node, role) { node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); const implicitRole = getImplicitRole(vNode); - // always allow the explicit role to match the implicit role - if (role === implicitRole) { - return true; - } - const spec = getElementSpec(vNode); if (Array.isArray(spec.allowedRoles)) { return spec.allowedRoles.includes(role); } + // By default, ARIA in HTML does not allow implicit roles to be the same as explicit ones + // aria-allowed-roles has an `allowedImplicit` option to bypass this. + if (role === implicitRole) { + return false; + } return !!spec.allowedRoles; } diff --git a/lib/commons/color/flatten-colors.js b/lib/commons/color/flatten-colors.js index 2c3de0e033..a87de6656f 100644 --- a/lib/commons/color/flatten-colors.js +++ b/lib/commons/color/flatten-colors.js @@ -1,11 +1,66 @@ import Color from './color'; +// clamp a value between two numbers (inclusive) +function clamp(value, min, max) { + return Math.min(Math.max(min, value), max); +} + // how to combine background and foreground colors together when using // the CSS property `mix-blend-mode`. Defaults to `normal` // @see https://www.w3.org/TR/compositing-1/#blendingseparable const blendFunctions = { normal(Cb, Cs) { return Cs; + }, + multiply(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingmultiply + return Cs * Cb; + }, + screen(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingscreen + return Cb + Cs - Cb * Cs; + }, + overlay(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingoverlay + return this['hard-light'](Cs, Cb); + }, + darken(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingdarken + return Math.min(Cb, Cs); + }, + lighten(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendinglighten + return Math.max(Cb, Cs); + }, + 'color-dodge'(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingcolordodge + return Cb === 0 ? 0 : Cs === 1 ? 1 : Math.min(1, Cb / (1 - Cs)); + }, + 'color-burn'(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingcolorburn + return Cb === 1 ? 1 : Cs === 0 ? 0 : 1 - Math.min(1, (1 - Cb) / Cs); + }, + 'hard-light'(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendinghardlight + + return Cs <= 0.5 ? this.multiply(Cb, 2 * Cs) : this.screen(Cb, 2 * Cs - 1); + }, + 'soft-light'(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingsoftlight + if (Cs <= 0.5) { + return Cb - (1 - 2 * Cs) * Cb * (1 - Cb); + } else { + const D = Cb <= 0.25 ? ((16 * Cb - 12) * Cb + 4) * Cb : Math.sqrt(Cb); + return Cb + (2 * Cs - 1) * (D - Cb); + } + }, + difference(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingdifference + return Math.abs(Cb - Cs); + }, + exclusion(Cb, Cs) { + // @see https://www.w3.org/TR/compositing-1/#blendingexclusion + return Cb + Cs - 2 * Cb * Cs; } }; @@ -19,12 +74,12 @@ const blendFunctions = { // @see https://www.w3.org/TR/compositing-1/#blending // @see https://ciechanow.ski/alpha-compositing/ function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) { - // RGB color space doesn't have decimal values so we will follow what browsers do and round - // e.g. rgb(255.2, 127.5, 127.8) === rgb(255, 128, 128) - return Math.round( + return ( αs * (1 - αb) * Cs + - αs * αb * blendFunctions[blendMode](Cb, Cs) + - (1 - αs) * αb * Cb + // Note: Cs and Cb values need to be between 0 and 1 inclusive for the blend function + // @see https://www.w3.org/TR/compositing-1/#simplealphacompositing + αs * αb * blendFunctions[blendMode](Cb / 255, Cs / 255) * 255 + + (1 - αs) * αb * Cb ); } @@ -63,12 +118,22 @@ function flattenColors(fgColor, bgColor, blendMode = 'normal') { // formula: αo = αs + αb x (1 - αs) // clamp alpha between 0 and 1 - const a = Math.max( - 0, - Math.min(fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha), 1) - ); + const αo = clamp(fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha), 0, 1); + + // simple alpha compositing gives premultiplied values, but our Color + // constructor takes unpremultiplied values. So we need to divide the + // final color values by the final alpha + // formula: Co = co / αo + // @see https://www.w3.org/TR/compositing-1/#simplealphacompositing + // @see https://github.com/w3c/fxtf-drafts/issues/440#issuecomment-956418953 + // + // RGB color space doesn't have decimal values so we will follow what browsers do and round + // e.g. rgb(255.2, 127.5, 127.8) === rgb(255, 128, 128) + const Cr = Math.round(r / αo); + const Cg = Math.round(g / αo); + const Cb = Math.round(b / αo); - return new Color(r, g, b, a); + return new Color(Cr, Cg, Cb, αo); } export default flattenColors; diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index ff2a28bc87..499511b770 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -27,8 +27,9 @@ export default function getBackgroundColor( ) { let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); if (bgColors.length) { - bgColors = [bgColors.reduce(flattenShadowColors)]; + bgColors = [{ color: bgColors.reduce(flattenShadowColors) }]; } + const elmStack = getBackgroundStack(elm); // Search the stack until we have an alpha === 1 background @@ -53,7 +54,11 @@ export default function getBackgroundColor( if (bgColor.alpha !== 0) { // store elements contributing to the br color. bgElms.push(bgElm); - bgColors.unshift(bgColor); + const blendMode = bgElmStyle.getPropertyValue('mix-blend-mode'); + bgColors.unshift({ + color: bgColor, + blendMode: normalizeBlendMode(blendMode) + }); // Exit if the background is opaque return bgColor.alpha === 1; @@ -72,14 +77,28 @@ export default function getBackgroundColor( ); bgColors.unshift(...pageBgs); + // default to white if bgColors is empty + if (bgColors.length === 0) { + return new Color(255, 255, 255, 1); + } // Mix the colors together. Colors must be mixed in bottom up // order (background to foreground order) to produce the correct // result. // @see https://github.com/dequelabs/axe-core/issues/2924 - var colors = bgColors.reduce((bgColor, fgColor) => { - return flattenColors(fgColor, bgColor); + const blendedColor = bgColors.reduce((bgColor, fgColor) => { + return flattenColors( + fgColor.color, + bgColor.color instanceof Color ? bgColor.color : bgColor, + fgColor.blendMode + ); }); - return colors; + + // default page background is white which must be mixed last + // @see https://www.w3.org/TR/compositing-1/#pagebackdrop + return flattenColors( + blendedColor.color instanceof Color ? blendedColor.color : blendedColor, + new Color(255, 255, 255, 1) + ); } /** @@ -99,6 +118,9 @@ function elmPartiallyObscured(elm, bgElm, bgColor) { return obscured; } +function normalizeBlendMode(blendmode) { + return !!blendmode ? blendmode : undefined; +} /** * Get the page background color. * @private @@ -131,24 +153,30 @@ function getPageBackgroundColors(elm, stackContainsBody) { const bodyBgColor = getOwnBackgroundColor(bodyStyle); const bodyBgColorApplies = bodyBgColor.alpha !== 0 && visuallyContains(elm, body); - if ( (bodyBgColor.alpha !== 0 && htmlBgColor.alpha === 0) || (bodyBgColorApplies && bodyBgColor.alpha !== 1) ) { - pageColors.unshift(bodyBgColor); + pageColors.unshift({ + color: bodyBgColor, + blendMode: normalizeBlendMode( + bodyStyle.getPropertyValue('mix-blend-mode') + ) + }); } if ( htmlBgColor.alpha !== 0 && (!bodyBgColorApplies || (bodyBgColorApplies && bodyBgColor.alpha !== 1)) ) { - pageColors.unshift(htmlBgColor); + pageColors.unshift({ + color: htmlBgColor, + blendMode: normalizeBlendMode( + htmlStyle.getPropertyValue('mix-blend-mode') + ) + }); } } - // default page background is white - pageColors.unshift(new Color(255, 255, 255, 1)); - return pageColors; } diff --git a/lib/commons/color/get-background-stack.js b/lib/commons/color/get-background-stack.js index d06d071564..10a54dccfe 100644 --- a/lib/commons/color/get-background-stack.js +++ b/lib/commons/color/get-background-stack.js @@ -2,39 +2,33 @@ import filteredRectStack from './filtered-rect-stack'; import elementHasImage from './element-has-image'; import getOwnBackgroundColor from './get-own-background-color'; import incompleteData from './incomplete-data'; -import shadowElementsFromPoint from '../dom/shadow-elements-from-point'; import reduceToElementsBelowFloating from '../dom/reduce-to-elements-below-floating'; /** - * Determines overlap of node's content with a bgNode. Used for inline elements + * Determine if element B is an inline descendant of A * @private - * @param {Element} targetElement - * @param {Element} bgNode + * @param {Element} node + * @param {Element} descendant * @return {Boolean} */ -function contentOverlapping(targetElement, bgNode) { - // get content box of target element - // check to see if the current bgNode is overlapping - var targetRect = targetElement.getClientRects()[0]; - var obscuringElements = shadowElementsFromPoint( - targetRect.left, - targetRect.top - ); - if (obscuringElements) { - for (var i = 0; i < obscuringElements.length; i++) { - if ( - obscuringElements[i] !== targetElement && - obscuringElements[i] === bgNode - ) { - return true; - } - } +function isInlineDescendant(node, descendant) { + const CONTAINED_BY = Node.DOCUMENT_POSITION_CONTAINED_BY; + // eslint-disable-next-line no-bitwise + if (!(node.compareDocumentPosition(descendant) & CONTAINED_BY)) { + return false; } - return false; + const style = window.getComputedStyle(descendant); + const display = style.getPropertyValue('display'); + if (!display.includes('inline')) { + return false; + } + // IE needs this; It doesn't set display:block when position is set + const position = style.getPropertyValue('position') + return position === 'static'; } /** - * Calculate alpha transparency of a background element obscuring the current node + * Determine if the element obscures / overlaps with the text * @private * @param {Number} elmIndex * @param {Array} elmStack @@ -42,19 +36,16 @@ function contentOverlapping(targetElement, bgNode) { * @return {Number|undefined} */ function calculateObscuringElement(elmIndex, elmStack, originalElm) { - if (elmIndex > 0) { - // there are elements above our element, check if they contribute to the background - for (var i = elmIndex - 1; i >= 0; i--) { - const bgElm = elmStack[i]; - if (contentOverlapping(originalElm, bgElm)) { - return true; - } else { - // remove elements not contributing to the background - elmStack.splice(i, 1); - } + // Reverse order, so that we can safely splice + for (let i = elmIndex - 1; i >= 0; i--) { + if (!isInlineDescendant(originalElm, elmStack[i])) { + return true; } + // Ignore inline descendants, for example: + //

text

; We don't care about the element, + // since it does not overlap the text inside of

+ elmStack.splice(i, 1); } - return false; } diff --git a/lib/commons/dom/get-element-by-reference.js b/lib/commons/dom/get-element-by-reference.js index 381d438dfd..dfd2c7f15c 100644 --- a/lib/commons/dom/get-element-by-reference.js +++ b/lib/commons/dom/get-element-by-reference.js @@ -1,3 +1,5 @@ +import isCurrentPageLink from './is-current-page-link'; + /** * Returns a reference to the element matching the attr URL fragment value * @method getElementByReference @@ -13,10 +15,12 @@ function getElementByReference(node, attr) { return null; } - if (fragment.charAt(0) === '#') { - fragment = decodeURIComponent(fragment.substring(1)); - } else if (fragment.substr(0, 2) === '/#') { - fragment = decodeURIComponent(fragment.substring(2)); + if (attr === 'href' && !isCurrentPageLink(node)) { + return null; + } + + if (fragment.indexOf('#') !== -1) { + fragment = decodeURIComponent(fragment.substr(fragment.indexOf('#') + 1)); } let candidate = document.getElementById(fragment); diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 025d2d0a67..5acd13c076 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -19,6 +19,7 @@ export { default as hasContentVirtual } from './has-content-virtual'; export { default as hasContent } from './has-content'; export { default as idrefs } from './idrefs'; export { default as insertedIntoFocusOrder } from './inserted-into-focus-order'; +export { default as isCurrentPageLink } from './is-current-page-link'; export { default as isFocusable } from './is-focusable'; export { default as isHiddenWithCSS } from './is-hidden-with-css'; export { default as isHTML5 } from './is-html5'; diff --git a/lib/commons/dom/is-current-page-link.js b/lib/commons/dom/is-current-page-link.js new file mode 100644 index 0000000000..91c80708ee --- /dev/null +++ b/lib/commons/dom/is-current-page-link.js @@ -0,0 +1,66 @@ +// angular skip links start with /# +const angularSkipLinkRegex = /^\/\#/; + +// angular router link uses #! or #/ +const angularRouterLinkRegex = /^#[!/]/; + +/** + * Determine if an anchor elements href attribute references the current page. + * @method isCurrentPageLink + * @memberof axe.commons.dom + * @param {HTMLAnchorElement} anchor + * @return {Boolean|null} + */ +export default function isCurrentPageLink(anchor) { + const href = anchor.getAttribute('href'); + if (!href || href === '#') { + return false; + } + + if (angularSkipLinkRegex.test(href)) { + return true; + } + + const { hash, protocol, hostname, port, pathname } = anchor; + if (angularRouterLinkRegex.test(hash)) { + return false; + } + + if (href.charAt(0) === '#') { + return true; + } + + // jsdom can have window.location.origin set to "null" (the string) + // if the url option is not set when parsing the dom string + if ( + typeof window.location?.origin !== 'string' || + window.location.origin.indexOf('://') === -1 + ) { + return null; + } + + // ie11 does not support window.origin + const currentPageUrl = window.location.origin + window.location.pathname; + + // ie11 does not have anchor.origin so we need to construct + // it ourselves + // also ie11 has empty protocol, hostname, and port when the + // link is relative, so use window.location.origin in these cases + let url; + if (!hostname) { + url = window.location.origin; + } else { + url = `${protocol}//${hostname}${port ? `:${port}` : ''}`; + } + + // ie11 has empty pathname if link is just a hash, so use + // window.location.pathname in these cases + if (!pathname) { + url += window.location.pathname; + } else { + // ie11 pathname does not start with / but chrome and firefox do + url += (pathname[0] !== '/' ? '/' : '') + pathname; + } + + return url === currentPageUrl; +} diff --git a/lib/commons/dom/is-skip-link.js b/lib/commons/dom/is-skip-link.js index 236b45155a..2ec3139e08 100644 --- a/lib/commons/dom/is-skip-link.js +++ b/lib/commons/dom/is-skip-link.js @@ -1,19 +1,23 @@ import cache from '../../core/base/cache'; import { querySelectorAll } from '../../core/utils'; - -// test for hrefs that start with # or /# (for angular) -const isInternalLinkRegex = /^\/?#[^/!]/; +import isCurrentPageLink from './is-current-page-link'; /** - * Determines if element is a skip link + * Determines if element is a skip link. + * + * Define a skip link as any anchor element whose resolved href + * resolves to the current page and uses a fragment identifier (#) + * and which precedes the first anchor element whose resolved href + * does not resolve to the current page or that doesn't use a + * fragment identifier. * @method isSkipLink * @memberof axe.commons.dom * @instance * @param {Element} element * @return {Boolean} */ -function isSkipLink(element) { - if (!isInternalLinkRegex.test(element.getAttribute('href'))) { +export default function isSkipLink(element) { + if (!element.href) { return false; } @@ -21,14 +25,19 @@ function isSkipLink(element) { if (typeof cache.get('firstPageLink') !== 'undefined') { firstPageLink = cache.get('firstPageLink'); } else { - // define a skip link as any anchor element whose href starts with `#...` - // and which precedes the first anchor element whose href doesn't start - // with `#...` (that is, a link to a page) - firstPageLink = querySelectorAll( - // TODO: es-module-_tree - axe._tree, - 'a:not([href^="#"]):not([href^="/#"]):not([href^="javascript"])' - )[0]; + // jsdom can have window.location.origin set to null + if (!window.location.origin) { + firstPageLink = querySelectorAll( + // TODO: es-module-_tree + axe._tree, + 'a:not([href^="#"]):not([href^="/#"]):not([href^="javascript:"])' + )[0]; + } else { + firstPageLink = querySelectorAll( + axe._tree, + 'a[href]:not([href^="javascript:"])' + ).find(link => !isCurrentPageLink(link.actualNode)); + } // null will signify no first page link cache.set('firstPageLink', firstPageLink || null); @@ -45,5 +54,3 @@ function isSkipLink(element) { element.DOCUMENT_POSITION_FOLLOWING ); } - -export default isSkipLink; diff --git a/lib/commons/dom/shadow-elements-from-point.js b/lib/commons/dom/shadow-elements-from-point.js index e20b7e3395..a13be216c0 100644 --- a/lib/commons/dom/shadow-elements-from-point.js +++ b/lib/commons/dom/shadow-elements-from-point.js @@ -4,6 +4,7 @@ import { isShadowRoot } from '../../core/utils'; /** * Get elements from point across shadow dom boundaries + * @deprecated As of axe-core 4.4. May be removed in axe-core 5.0 * @method shadowElementsFromPoint * @memberof axe.commons.dom * @instance diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 14564b99e5..7afefcac13 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -1,3 +1,4 @@ +import hasAccessibleName from './has-accessible-name'; import attributes from './attributes'; import condition from './condition'; import explicitRole from './explicit-role'; @@ -9,6 +10,7 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-n import { getNodeFromTree, matches } from '../../core/utils'; const matchers = { + hasAccessibleName, attributes, condition, explicitRole, diff --git a/lib/commons/matches/has-accessible-name.js b/lib/commons/matches/has-accessible-name.js new file mode 100644 index 0000000000..7a3cad27d0 --- /dev/null +++ b/lib/commons/matches/has-accessible-name.js @@ -0,0 +1,25 @@ +import accessibleTextVirtual from '../text/accessible-text-virtual'; +import fromPrimative from './from-primative'; + +/** + * Check if a virtual node has a non-empty accessible name + *`` + * Note: matches.hasAccessibleName(vNode, true) can be indirectly used through + * matches(vNode, { hasAccessibleName: boolean }) + * + * Example: + * ```js + * matches.hasAccessibleName(vNode, true); + * matches.hasAccessibleName(vNode, false); + * + * ``` + * + * @param {VirtualNode} vNode + * @param {Object} matcher + * @returns {Boolean} + */ +function hasAccessibleName(vNode, matcher) { + return fromPrimative(!!accessibleTextVirtual(vNode), matcher); +} + +export default hasAccessibleName; diff --git a/lib/commons/matches/index.js b/lib/commons/matches/index.js index f0434676fc..025381d5c4 100644 --- a/lib/commons/matches/index.js +++ b/lib/commons/matches/index.js @@ -3,6 +3,7 @@ * @namespace commons.matches * @memberof axe */ +import hasAccessibleName from './has-accessible-name'; import attributes from './attributes'; import condition from './condition'; import explicitRole from './explicit-role'; @@ -15,6 +16,7 @@ import nodeName from './node-name'; import properties from './properties'; import semanticRole from './semantic-role'; +matches.hasAccessibleName = hasAccessibleName; matches.attributes = attributes; matches.condition = condition; matches.explicitRole = explicitRole; diff --git a/lib/commons/standards/get-element-spec.js b/lib/commons/standards/get-element-spec.js index 10354dad56..218fb30fd6 100644 --- a/lib/commons/standards/get-element-spec.js +++ b/lib/commons/standards/get-element-spec.js @@ -6,7 +6,7 @@ import matchesFn from '../../commons/matches'; * @param {VirtualNode} vNode The VirtualNode to get the spec for. * @return {Object} The standard spec object */ -function getElementSpec(vNode) { +function getElementSpec(vNode, { noMatchAccessibleName = false } = {}) { const standard = standards.htmlElms[vNode.props.nodeName]; // invalid element name (could be an svg or custom element name) @@ -29,6 +29,13 @@ function getElementSpec(vNode) { } const { matches, ...props } = variant[variantName]; + const matchProperties = Array.isArray(matches) ? matches : [matches]; + for (let i = 0; i < matchProperties.length && noMatchAccessibleName; i++) { + if (matchProperties[i].hasOwnProperty('hasAccessibleName')) { + return standard; + } + } + if (matchesFn(vNode, matches)) { for (const propName in props) { if (props.hasOwnProperty(propName)) { diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js index c0407f6113..09a00e4d77 100644 --- a/lib/commons/text/accessible-text-virtual.js +++ b/lib/commons/text/accessible-text-virtual.js @@ -6,6 +6,7 @@ import subtreeText from './subtree-text'; import titleText from './title-text'; import sanitize from './sanitize'; import isVisible from '../dom/is-visible'; +import isIconLigature from '../text/is-icon-ligature'; /** * Finds virtual node and calls accessibleTextVirtual() @@ -26,6 +27,11 @@ function accessibleTextVirtual(virtualNode, context = {}) { return ''; } + // Ignore ligature icons + if (shouldIgnoreIconLigature(virtualNode, context)) { + return ''; + } + const computationSteps = [ arialabelledbyText, // Step 2B.1 arialabelText, // Step 2C @@ -91,6 +97,21 @@ function shouldIgnoreHidden({ actualNode }, context) { return !isVisible(actualNode, true); } +/** + * Check if a ligature icon should be ignored + * @param {VirtualNode} element + * @param {VirtualNode} element + * @param {Object} context + * @return {Boolean} + */ +function shouldIgnoreIconLigature(virtualNode, context) { + const { ignoreIconLigature, pixelThreshold, occuranceThreshold } = context; + if (virtualNode.props.nodeType !== 3 || !ignoreIconLigature) { + return false; + } + return isIconLigature(virtualNode, pixelThreshold, occuranceThreshold); +} + /** * Apply defaults to the context * @param {VirtualNode} element diff --git a/lib/commons/text/native-text-alternative.js b/lib/commons/text/native-text-alternative.js index 3fd64e1c68..ff2b12ad08 100644 --- a/lib/commons/text/native-text-alternative.js +++ b/lib/commons/text/native-text-alternative.js @@ -37,7 +37,7 @@ function nativeTextAlternative(virtualNode, context = {}) { * @return {Function[]} Array of native accessible name computation methods */ function findTextMethods(virtualNode) { - const elmSpec = getElementSpec(virtualNode); + const elmSpec = getElementSpec(virtualNode, { noMatchAccessibleName: true }); const methods = elmSpec.namingMethods || []; return methods.map(methodName => nativeTextMethods[methodName]); diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js index 2e8b1c4395..a3d669f342 100644 --- a/lib/commons/text/subtree-text.js +++ b/lib/commons/text/subtree-text.js @@ -2,7 +2,7 @@ import accessibleTextVirtual from './accessible-text-virtual'; import namedFromContents from '../aria/named-from-contents'; import getOwnedVirtual from '../aria/get-owned-virtual'; import getElementsByContentType from '../standards/get-elements-by-content-type'; -import getElementSpec from '../standards/get-element-spec' +import getElementSpec from '../standards/get-element-spec'; /** * Get the accessible text for an element that can get its name from content @@ -16,7 +16,9 @@ function subtreeText(virtualNode, context = {}) { const { alreadyProcessed } = accessibleTextVirtual; context.startNode = context.startNode || virtualNode; const { strict, inControlContext, inLabelledByContext } = context; - const { contentTypes } = getElementSpec(virtualNode); + const { contentTypes } = getElementSpec(virtualNode, { + noMatchAccessibleName: true + }); if ( alreadyProcessed(virtualNode, context) || virtualNode.props.nodeType !== 1 || diff --git a/lib/commons/text/visible-text-nodes.js b/lib/commons/text/visible-text-nodes.js index 5b3fb60bc2..b82b2b0d3e 100644 --- a/lib/commons/text/visible-text-nodes.js +++ b/lib/commons/text/visible-text-nodes.js @@ -8,6 +8,7 @@ import isVisible from '../dom/is-visible'; * @instance * @param {VirtualNode} vNode * @return {VitrualNode[]} + * @deprecated */ function visibleTextNodes(vNode) { const parentVisible = isVisible(vNode.actualNode); diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index 14d01cbaa2..a56eb51b45 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -592,11 +592,10 @@ class Audit { } else if (['tag', 'tags', undefined].includes(only.type)) { only.type = 'tag'; - const unmatchedTags = only.values.filter(tag => ( - !tags.includes(tag) && - !/wcag2[1-3]a{1,3}/.test(tag) - )); - if (unmatchedTags.length !== 0) { + const unmatchedTags = only.values.filter( + tag => !tags.includes(tag) && !/wcag2[1-3]a{1,3}/.test(tag) + ); + if (unmatchedTags.length !== 0) { axe.log('Could not find tags `' + unmatchedTags.join('`, `') + '`'); } } else { @@ -621,6 +620,9 @@ class Audit { brand: this.brand, application: this.application }; + if (typeof branding === 'string') { + this.application = branding; + } if ( branding && branding.hasOwnProperty('brand') && diff --git a/lib/core/base/metadata-function-map.js b/lib/core/base/metadata-function-map.js index a8d8b169cb..608d93dacb 100644 --- a/lib/core/base/metadata-function-map.js +++ b/lib/core/base/metadata-function-map.js @@ -13,6 +13,7 @@ import ariaRoledescriptionEvaluate from '../../checks/aria/aria-roledescription- import ariaUnsupportedAttrEvaluate from '../../checks/aria/aria-unsupported-attr-evaluate'; import ariaValidAttrEvaluate from '../../checks/aria/aria-valid-attr-evaluate'; import ariaValidAttrValueEvaluate from '../../checks/aria/aria-valid-attr-value-evaluate'; +import deprecatedroleEvaluate from '../../checks/aria/deprecatedrole-evaluate'; import fallbackroleEvaluate from '../../checks/aria/fallbackrole-evaluate'; import hasGlobalAriaAttributeEvaluate from '../../checks/aria/has-global-aria-attribute-evaluate'; import hasImplicitChromiumRoleMatches from '../../rules/has-implicit-chromium-role-matches'; @@ -96,6 +97,7 @@ import focusableModalOpenEvaluate from '../../checks/keyboard/focusable-modal-op import focusableNoNameEvaluate from '../../checks/keyboard/focusable-no-name-evaluate'; import focusableNotTabbableEvaluate from '../../checks/keyboard/focusable-not-tabbable-evaluate'; import landmarkIsTopLevelEvaluate from '../../checks/keyboard/landmark-is-top-level-evaluate'; +import frameFocusableContentEvaluate from '../../checks/keyboard/frame-focusable-content-evaluate'; import noFocusableContentEvaluate from '../../checks/keyboard/no-focusable-content-evaluate'; import tabindexEvaluate from '../../checks/keyboard/tabindex-evaluate'; @@ -190,6 +192,7 @@ const metadataFunctionMap = { 'aria-unsupported-attr-evaluate': ariaUnsupportedAttrEvaluate, 'aria-valid-attr-evaluate': ariaValidAttrEvaluate, 'aria-valid-attr-value-evaluate': ariaValidAttrValueEvaluate, + 'deprecatedrole-evaluate': deprecatedroleEvaluate, 'fallbackrole-evaluate': fallbackroleEvaluate, 'has-global-aria-attribute-evaluate': hasGlobalAriaAttributeEvaluate, 'has-implicit-chromium-role-matches': hasImplicitChromiumRoleMatches, @@ -273,6 +276,7 @@ const metadataFunctionMap = { 'focusable-no-name-evaluate': focusableNoNameEvaluate, 'focusable-not-tabbable-evaluate': focusableNotTabbableEvaluate, 'landmark-is-top-level-evaluate': landmarkIsTopLevelEvaluate, + 'frame-focusable-content-evaluate': frameFocusableContentEvaluate, 'no-focusable-content-evaluate': noFocusableContentEvaluate, 'tabindex-evaluate': tabindexEvaluate, diff --git a/lib/core/reporters/helpers/incomplete-fallback-msg.js b/lib/core/reporters/helpers/incomplete-fallback-msg.js index 02678c7b6e..53d1d96553 100644 --- a/lib/core/reporters/helpers/incomplete-fallback-msg.js +++ b/lib/core/reporters/helpers/incomplete-fallback-msg.js @@ -3,10 +3,13 @@ * This mechanism allows the string to be localized. * @return {String} */ -function incompleteFallbackMessage() { - return typeof axe._audit.data.incompleteFallbackMessage === 'function' - ? axe._audit.data.incompleteFallbackMessage() - : axe._audit.data.incompleteFallbackMessage; + export default function incompleteFallbackMessage() { + let { incompleteFallbackMessage } = axe._audit.data; + if (typeof incompleteFallbackMessage === 'function') { + incompleteFallbackMessage = incompleteFallbackMessage(); + } + if (typeof incompleteFallbackMessage !== 'string') { + return ''; + } + return incompleteFallbackMessage; } - -export default incompleteFallbackMessage; diff --git a/lib/core/utils/check-helper.js b/lib/core/utils/check-helper.js index 54bd539eb1..68e39f6a4b 100644 --- a/lib/core/utils/check-helper.js +++ b/lib/core/utils/check-helper.js @@ -25,7 +25,18 @@ function checkHelper(checkResult, options, resolve, reject) { checkResult.data = data; }, relatedNodes(nodes) { + if (!window.Node) { + return; + } + nodes = nodes instanceof window.Node ? [nodes] : toArray(nodes); + + if ( + !nodes.every(node => node instanceof window.Node || node.actualNode) + ) { + return; + } + checkResult.relatedNodes = nodes.map(element => { return new DqElement(element, options); }); diff --git a/lib/core/utils/contains.js b/lib/core/utils/contains.js index 08b25226f9..45d33e8d27 100644 --- a/lib/core/utils/contains.js +++ b/lib/core/utils/contains.js @@ -6,41 +6,32 @@ * @param {VirtualNode} otherVNode The vNode to test is contained by `vNode` * @return {Boolean} Whether `vNode` contains `otherVNode` */ -function contains(vNode, otherVNode) { +export default function contains(vNode, otherVNode) { /*eslint no-bitwise: 0*/ - - function containsShadowChild(vNode, otherVNode) { - if (vNode.shadowId === otherVNode.shadowId) { - return true; - } - return !!vNode.children.find(child => { - return containsShadowChild(child, otherVNode); - }); - } - if (vNode.shadowId || otherVNode.shadowId) { - return containsShadowChild(vNode, otherVNode); + do { + if (vNode.shadowId === otherVNode.shadowId) { + return true; + } + otherVNode = otherVNode.parent; + } while (otherVNode) + return false; } - if (vNode.actualNode) { - if (typeof vNode.actualNode.contains === 'function') { - return vNode.actualNode.contains(otherVNode.actualNode); - } - - return !!( - vNode.actualNode.compareDocumentPosition(otherVNode.actualNode) & 16 - ); - } else { - // fallback for virtualNode only contexts (e.g. linting) + if (!vNode.actualNode) { + // fallback for virtualNode only contexts // @see https://github.com/Financial-Times/polyfill-service/pull/183/files do { if (otherVNode === vNode) { return true; } - } while ((otherVNode = otherVNode && otherVNode.parent)); + otherVNode = otherVNode.parent + } while (otherVNode); } - return false; + if (typeof vNode.actualNode.contains !== 'function') { + const position = vNode.actualNode.compareDocumentPosition(otherVNode.actualNode); + return !!(position & 16); + } + return vNode.actualNode.contains(otherVNode.actualNode); } - -export default contains; diff --git a/lib/core/utils/frame-messenger/message-handler.js b/lib/core/utils/frame-messenger/message-handler.js index 51b148a384..486d9eb00d 100644 --- a/lib/core/utils/frame-messenger/message-handler.js +++ b/lib/core/utils/frame-messenger/message-handler.js @@ -23,29 +23,34 @@ export function messageHandler( { origin, data: dataString, source: win }, topicHandler ) { - const data = parseMessage(dataString) || {}; - const { channelId, message, messageId } = data; + try { + const data = parseMessage(dataString) || {}; + const { channelId, message, messageId } = data; - if (!originIsAllowed(origin) || !isNewMessage(messageId)) { - return; - } + if (!originIsAllowed(origin) || !isNewMessage(messageId)) { + return; + } - // An error should never come from a parent. Log it and exit. - if (message instanceof Error && win.parent !== window) { - axe.log(message); - return false; - } + // An error should never come from a parent. Log it and exit. + if (message instanceof Error && win.parent !== window) { + axe.log(message); + return false; + } - try { - if (data.topic) { - const responder = createResponder(win, channelId); - assertIsParentWindow(win); - topicHandler(data, responder); - } else { - callReplyHandler(win, data); + try { + if (data.topic) { + const responder = createResponder(win, channelId); + assertIsParentWindow(win); + topicHandler(data, responder); + } else { + callReplyHandler(win, data); + } + } catch (error) { + processError(win, error, channelId); } } catch (error) { - processError(win, error, channelId); + axe.log(error); + return false; } } diff --git a/lib/core/utils/frame-messenger/message-parser.js b/lib/core/utils/frame-messenger/message-parser.js index 65aea70f41..c7154f8e38 100644 --- a/lib/core/utils/frame-messenger/message-parser.js +++ b/lib/core/utils/frame-messenger/message-parser.js @@ -67,6 +67,7 @@ export function parseMessage(dataString) { */ function isRespondableMessage(postedMessage) { return ( + postedMessage !== null && typeof postedMessage === 'object' && typeof postedMessage.channelId === 'string' && postedMessage.source === getSource() diff --git a/lib/core/utils/get-frame-contexts.js b/lib/core/utils/get-frame-contexts.js index be88061a35..e12a69d485 100644 --- a/lib/core/utils/get-frame-contexts.js +++ b/lib/core/utils/get-frame-contexts.js @@ -1,7 +1,11 @@ import Context from '../base/context'; import getAncestry from './get-ancestry'; -export default function getFrameContexts(context) { +export default function getFrameContexts(context, options = {}) { + if (options.iframes === false) { + return []; + } + const { frames } = new Context(context); return frames.map(({ node, ...frameContext }) => { frameContext.initiator = false; diff --git a/lib/core/utils/get-scroll.js b/lib/core/utils/get-scroll.js index edbaf5310e..0771e71f3c 100644 --- a/lib/core/utils/get-scroll.js +++ b/lib/core/utils/get-scroll.js @@ -6,7 +6,7 @@ * @param {buffer} (Optional) allowed negligence in overflow * @returns {Object | undefined} */ -function getScroll(elm, buffer = 0) { + export default function getScroll(elm, buffer = 0) { const overflowX = elm.scrollWidth > elm.clientWidth + buffer; const overflowY = elm.scrollHeight > elm.clientHeight + buffer; @@ -19,12 +19,8 @@ function getScroll(elm, buffer = 0) { } const style = window.getComputedStyle(elm); - const overflowXStyle = style.getPropertyValue('overflow-x'); - const overflowYStyle = style.getPropertyValue('overflow-y'); - const scrollableX = - overflowXStyle !== 'visible' && overflowXStyle !== 'hidden'; - const scrollableY = - overflowYStyle !== 'visible' && overflowYStyle !== 'hidden'; + const scrollableX = isScrollable(style, 'overflow-x'); + const scrollableY = isScrollable(style, 'overflow-y'); /** * check direction of `overflow` and `scrollable` @@ -38,4 +34,7 @@ function getScroll(elm, buffer = 0) { } } -export default getScroll; +function isScrollable(style, prop) { + const overflowProp = style.getPropertyValue(prop); + return ['scroll', 'auto'].includes(overflowProp); +} diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index aa8e787ba2..02c192f8ec 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -26,6 +26,23 @@ const ignoredAttributes = [ 'aria-valuenow' ]; const MAXATTRIBUTELENGTH = 31; +const attrCharsRegex = /([\\"])/g; +const newlineChars = /(\r\n|\r|\n)/g; + +/** + * Escape an attribute selector string. + * @param {String} str + * @return {String} + */ +function escapeAttribute(str) { + return ( + str + // @see https://www.py4u.net/discuss/286669 + .replace(attrCharsRegex, '\\$1') + // @see https://stackoverflow.com/a/20354013/2124254 + .replace(newlineChars, '\\a ') + ); +} /** * get the attribute name and value as a string @@ -40,21 +57,16 @@ function getAttributeNameValue(node, at) { if (name.indexOf('href') !== -1 || name.indexOf('src') !== -1) { const friendly = getFriendlyUriEnd(node.getAttribute(name)); if (friendly) { - const value = encodeURI(friendly); - if (value) { - atnv = escapeSelector(at.name) + '$="' + escapeSelector(value) + '"'; - } else { - return; - } + atnv = escapeSelector(at.name) + '$="' + escapeAttribute(friendly) + '"'; } else { atnv = escapeSelector(at.name) + '="' + - escapeSelector(node.getAttribute(name)) + + escapeAttribute(node.getAttribute(name)) + '"'; } } else { - atnv = escapeSelector(name) + '="' + escapeSelector(at.value) + '"'; + atnv = escapeSelector(name) + '="' + escapeAttribute(at.value) + '"'; } return atnv; } diff --git a/lib/core/utils/select.js b/lib/core/utils/select.js index be7a4a683c..671ea31c22 100644 --- a/lib/core/utils/select.js +++ b/lib/core/utils/select.js @@ -31,10 +31,11 @@ function pushNode(result, nodes) { /** * reduces the includes list to only the outermost includes + * @private * @param {Array} the array of include nodes * @return {Array} the modified array of nodes */ -function reduceIncludes(includes) { +function getOuterIncludes(includes) { return includes.reduce((res, el) => { if (!res.length || !contains(res[res.length - 1], el)) { res.push(el); @@ -62,18 +63,13 @@ function select(selector, context) { } } } - const curried = (context => { - return node => { - return isNodeInContext(node, context); - }; - })(context); - const reducedIncludes = reduceIncludes(context.include); - for (let i = 0; i < reducedIncludes.length; i++) { - candidate = reducedIncludes[i]; - result = pushNode( - result, - querySelectorAllFilter(candidate, selector, curried) - ); + + const outerIncludes = getOuterIncludes(context.include); + const isInContext = getContextFilter(context); + for (let i = 0; i < outerIncludes.length; i++) { + candidate = outerIncludes[i]; + const nodes = querySelectorAllFilter(candidate, selector, isInContext); + result = pushNode(result, nodes); } if (axe._selectCache) { axe._selectCache.push({ @@ -85,3 +81,19 @@ function select(selector, context) { } export default select; + +/** + * Return a filter method to test if a node is in context; or + * null if the node is always included. + * @private + * @param {Context} + * @return {Function|null} + */ +function getContextFilter(context) { + // Since we're starting from included nodes, + // if nothing is excluded, we can skip the filter step. + if (!context.exclude || context.exclude.length === 0) { + return null; + } + return node => isNodeInContext(node, context); +} diff --git a/lib/core/utils/send-command-to-frame.js b/lib/core/utils/send-command-to-frame.js index fb17e30d69..7d7f5b57ea 100644 --- a/lib/core/utils/send-command-to-frame.js +++ b/lib/core/utils/send-command-to-frame.js @@ -2,31 +2,29 @@ import getSelector from './get-selector'; import respondable from './respondable'; import log from '../log'; -function err(message, node) { - var selector; - // TODO: es-modules_tree - if (axe._tree) { - selector = getSelector(node); - } - return new Error(message + ': ' + (selector || node)); -} - /** * Sends a command to an instance of axe in the specified frame * @param {Element} node The frame element to send the message to * @param {Object} parameters Parameters to pass to the frame * @param {Function} callback Function to call when results from the frame has returned */ -function sendCommandToFrame(node, parameters, resolve, reject) { - var win = node.contentWindow; +export default function sendCommandToFrame(node, parameters, resolve, reject) { + const win = node.contentWindow; + const pingWaitTime = parameters.options?.pingWaitTime ?? 500; if (!win) { log('Frame does not have a content window', node); resolve(null); return; } + // Skip ping + if (pingWaitTime === 0) { + callAxeStart(node, parameters, resolve, reject); + return; + } + // give the frame .5s to respond to 'axe.ping', else log failed response - var timeout = setTimeout(() => { + let timeout = setTimeout(() => { // This double timeout is important for allowing iframes to respond // DO NOT REMOVE timeout = setTimeout(() => { @@ -36,30 +34,39 @@ function sendCommandToFrame(node, parameters, resolve, reject) { reject(err('No response from frame', node)); } }, 0); - }, parameters.options?.pingWaitTime ?? 500); + }, pingWaitTime); // send 'axe.ping' to the frame respondable(win, 'axe.ping', null, undefined, () => { clearTimeout(timeout); + callAxeStart(node, parameters, resolve, reject); + }); +} - // Give axe 60s (or user-supplied value) to respond to 'axe.start' - var frameWaitTime = - (parameters.options && parameters.options.frameWaitTime) || 60000; - - timeout = setTimeout(function collectResultFramesTimeout() { - reject(err('Axe in frame timed out', node)); - }, frameWaitTime); +function callAxeStart(node, parameters, resolve, reject) { + // Give axe 60s (or user-supplied value) to respond to 'axe.start' + const frameWaitTime = parameters.options?.frameWaitTime ?? 60000; + const win = node.contentWindow; + const timeout = setTimeout(function collectResultFramesTimeout() { + reject(err('Axe in frame timed out', node)); + }, frameWaitTime); - // send 'axe.start' and send the callback if it responded - respondable(win, 'axe.start', parameters, undefined, data => { - clearTimeout(timeout); - if (data instanceof Error === false) { - resolve(data); - } else { - reject(data); - } - }); + // send 'axe.start' and send the callback if it responded + respondable(win, 'axe.start', parameters, undefined, data => { + clearTimeout(timeout); + if (data instanceof Error === false) { + resolve(data); + } else { + reject(data); + } }); } -export default sendCommandToFrame; +function err(message, node) { + var selector; + // TODO: es-modules_tree + if (axe._tree) { + selector = getSelector(node); + } + return new Error(message + ': ' + (selector || node)); +} diff --git a/lib/core/utils/uuid.js b/lib/core/utils/uuid.js index cf2d2e5f9b..de8a9eb66e 100644 --- a/lib/core/utils/uuid.js +++ b/lib/core/utils/uuid.js @@ -23,15 +23,6 @@ if (!_rng && _crypto && _crypto.getRandomValues) { }; } -try { - if (!_rng) { - const nodeCrypto = require('crypto'); - _rng = () => nodeCrypto.randomBytes(16); - } -} catch (e) { - /* do nothing */ -} - if (!_rng) { // Math.random()-based (RNG) // diff --git a/lib/core/utils/valid-langs.js b/lib/core/utils/valid-langs.js index 569d54e081..b52ec759d1 100644 --- a/lib/core/utils/valid-langs.js +++ b/lib/core/utils/valid-langs.js @@ -83,7 +83,7 @@ function isValidLang(lang) { * @deprecated * @method validLangs * @memberof axe.utils - * @return {Array} Valid language codes + * @return {Array} Valid language codes */ export function validLangs(langArray) { // account for our external API tests passing non-array things diff --git a/lib/rules/aria-roles.json b/lib/rules/aria-roles.json index 82d7749dde..ac36928643 100644 --- a/lib/rules/aria-roles.json +++ b/lib/rules/aria-roles.json @@ -9,5 +9,11 @@ }, "all": [], "any": [], - "none": ["fallbackrole", "invalidrole", "abstractrole", "unsupportedrole"] + "none": [ + "fallbackrole", + "invalidrole", + "abstractrole", + "unsupportedrole", + "deprecatedrole" + ] } diff --git a/lib/rules/color-contrast-enhanced.json b/lib/rules/color-contrast-enhanced.json new file mode 100644 index 0000000000..21860fc869 --- /dev/null +++ b/lib/rules/color-contrast-enhanced.json @@ -0,0 +1,14 @@ +{ + "id": "color-contrast-enhanced", + "matches": "color-contrast-matches", + "excludeHidden": false, + "enabled": false, + "tags": ["cat.color", "wcag2aaa", "wcag146"], + "metadata": { + "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AAA contrast ratio thresholds", + "help": "Elements must have sufficient color contrast" + }, + "all": [], + "any": ["color-contrast-enhanced"], + "none": [] +} diff --git a/lib/rules/nested-interactive.json b/lib/rules/nested-interactive.json index 5293a7794f..857aea60f2 100644 --- a/lib/rules/nested-interactive.json +++ b/lib/rules/nested-interactive.json @@ -4,7 +4,7 @@ "tags": ["cat.keyboard", "wcag2a", "wcag412"], "actIds": ["307n5z"], "metadata": { - "description": "Ensure controls are not nested as they are not announced by screen readers", + "description": "Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies", "help": "Interactive controls must not be nested" }, "all": [], diff --git a/lib/standards/aria-roles.js b/lib/standards/aria-roles.js index 8dc3c5b661..f1291d37f0 100644 --- a/lib/standards/aria-roles.js +++ b/lib/standards/aria-roles.js @@ -147,6 +147,11 @@ const ariaRoles = { allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, + comment: { + type: 'structure', + allowedAttrs: ['aria-level', 'aria-posinset', 'aria-setsize'], + superclassRole: ['article'] + }, definition: { type: 'structure', allowedAttrs: ['aria-expanded'], @@ -391,6 +396,11 @@ const ariaRoles = { accessibleNameRequired: true, childrenPresentational: true }, + mark: { + type: 'structure', + superclassRole: ['section'], + prohibitedAttrs: ['aria-label', 'aria-labelledby'] + }, navigation: { type: 'landmark', allowedAttrs: ['aria-expanded'], @@ -674,6 +684,12 @@ const ariaRoles = { nameFromContent: true, childrenPresentational: true }, + suggestion: { + type: 'structure', + requiredOwned: ['insertion', 'deletion'], + superclassRole: ['section'], + prohibitedAttrs: ['aria-label', 'aria-labelledby'] + }, tab: { type: 'widget', requiredContext: ['tablist'], diff --git a/lib/standards/dpub-roles.js b/lib/standards/dpub-roles.js index 8354fb1823..820553cef0 100644 --- a/lib/standards/dpub-roles.js +++ b/lib/standards/dpub-roles.js @@ -28,18 +28,17 @@ const dpubRoles = { }, 'doc-biblioentry': { type: 'listitem', - requiredContext: ['doc-bibliography'], allowedAttrs: [ 'aria-expanded', 'aria-level', 'aria-posinset', 'aria-setsize' ], - superclassRole: ['listitem'] + superclassRole: ['listitem'], + deprecated: true }, 'doc-bibliography': { type: 'landmark', - requiredOwned: ['doc-biblioentry'], allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -86,7 +85,6 @@ const dpubRoles = { }, 'doc-endnote': { type: 'listitem', - requiredContext: ['doc-endnotes'], allowedAttrs: [ 'aria-expanded', 'aria-level', @@ -97,9 +95,9 @@ const dpubRoles = { }, 'doc-endnotes': { type: 'landmark', - requiredOwned: ['doc-endnote'], allowedAttrs: ['aria-expanded'], - superclassRole: ['landmark'] + superclassRole: ['landmark'], + deprecated: true }, 'doc-epigraph': { type: 'section', @@ -133,7 +131,6 @@ const dpubRoles = { }, 'doc-glossary': { type: 'landmark', - requiredOwned: ['definition', 'term'], allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 472529b5fe..4d7fbf4e3b 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -48,8 +48,16 @@ const htmlElms = { allowedRoles: true }, area: { + variant: { + href: { + matches: '[href]', + allowedRoles: false + }, + default: { + allowedRoles: ['button', 'link'] + } + }, contentTypes: ['phrasing', 'flow'], - allowedRoles: false, namingMethods: ['altText'] }, article: { @@ -330,11 +338,17 @@ const htmlElms = { img: { variant: { nonEmptyAlt: { - matches: { - attributes: { - alt: '/.+/' + matches: [ + { + // Because foo has no accessible name: + attributes: { + alt: '/.+/' + } + }, + { + hasAccessibleName: true } - }, + ], allowedRoles: [ 'button', 'checkbox', @@ -344,6 +358,7 @@ const htmlElms = { 'menuitemradio', 'option', 'progressbar', + 'radio', 'scrollbar', 'separator', 'slider', @@ -738,6 +753,7 @@ const htmlElms = { 'dialog', 'document', 'feed', + 'group', 'log', 'main', 'marquee', diff --git a/locales/da.json b/locales/da.json index 3e967d5d94..1b2e74e441 100644 --- a/locales/da.json +++ b/locales/da.json @@ -89,6 +89,10 @@ "description": "", "help": "Elementer skal have tilstrækkelig farvekontrast" }, + "color-contrast-enhanced": { + "description": "", + "help": "Elementer skal have tilstrækkelig farvekontrast" + }, "css-orientation-lock": { "description": "", "help": "'CSS Media queries' bør ikke bruges til at låse skærmretningen ('orientation')" @@ -448,6 +452,23 @@ "default": "Kan ikke udregne kontrastforhold" } }, + "color-contrast-enhanced": { + "pass": "Elementet har stor farvekontrast, den er ${data.contrastRatio}", + "fail": "Elementet har ikke nok farvekontrast, den er ${data.contrastRatio} (forgrundsfarve: ${data.fgColor}, baggrundsfarve: ${data.bgColor}, tekststørrelse: ${data.fontSize}, teksttykkelse: ${data.fontWeight}). Forventet kontrastforhold er ${data.expectedContrastRatio}", + "incomplete": { + "bgImage": "Elementets baggrundsfarve kunne ikke detekteres på grund af et baggrundsbillede", + "bgGradient": "Elementets baggrundsfarve kunne ikke detekteres på grund af en baggrundsgradient", + "imgNode": "Elementets baggrundsfarve kunne ikke detekteres, fordi elementet indeholder et billedelement", + "bgOverlap": "Elementets baggrundsfarve kunne ikke detekteres, fordi det er overlappet af et andet element", + "fgAlpha": "Elementets forgrundsfarve kunne ikke detekteres på grund af dets gennemsigtighed", + "elmPartiallyObscured": "Elementets baggrundsfarve kunne ikke detekteres, fordi det er delvist dækket af et andet element", + "elmPartiallyObscuring": "Elementets baggrundsfarve kunne ikke detekteres, fordi det delvist dækker et andet element", + "outsideViewport": "Elementets baggrundsfarve kunne ikke detekteres, fordi det er udenfor sidens 'viewport'", + "equalRatio": "Elementet har et 1:1-kontrastforhold med baggrunden", + "shortTextContent": "Elementets indhold er for kort til at kunne afgøre, om indholdet ren faktisk ER tekst", + "default": "Kan ikke udregne kontrastforhold" + } + }, "link-in-text-block": { "pass": "Links kan adskilles fra den omkringliggende tekst på anden måde end med farve", "fail": "Links bør skille sig ud fra den omkringliggende tekst på anden måde end med farve", diff --git a/locales/de.json b/locales/de.json index 3af18c0877..45304a0a55 100644 --- a/locales/de.json +++ b/locales/de.json @@ -113,6 +113,10 @@ "description": "Stellt sicher, dass der Kontrast zwischen Vorder- und Hintergrundfarbe den in der WCAG 2 als AA ausgewiesenen Kontrastgrenzwerten entspricht.", "help": "Elemente müssen einen ausreichenden Farbkontrast haben." }, + "color-contrast-enhanced": { + "description": "Stellt sicher, dass der Kontrast zwischen Vorder- und Hintergrundfarbe den in der WCAG 2 als AAA ausgewiesenen Kontrastgrenzwerten entspricht.", + "help": "Elemente müssen einen ausreichenden Farbkontrast haben." + }, "css-orientation-lock": { "description": "Stellt sicher, dass der Inhalt nicht nur auf einer sondern auf allen spezifischen Bildschirmausrichtungen angezeigt werden kann.", "help": "CSS Media Queries dürfen nicht genutzt werden um die Bildschirmausrichtung zu sperren." @@ -520,6 +524,25 @@ "pseudoContent": "Die Hintergrundfarbe konnte aufgrund eines pseudo Elementes nicht bestimmt werden." } }, + "color-contrast-enhanced": { + "pass": "Das Element hat einen ausreichenden Kontrast von ${data.contrastRatio}.", + "fail": "Das Element hat einen unzureichenden Kontrast von ${data.contrastRatio} (Vordergrundfarbe: ${data.fgColor}, Hintergrundfarbe: ${data.bgColor}, Schriftgröße: ${data.fontSize}, Schriftstärke: ${data.fontWeight}).", + "incomplete": { + "default": "Das Kontrastverhältnis konnte nicht ermittelt werden.", + "bgImage": "Die Hintergrundfarbe des Elementes konnte aufgrund eines Hintergrundbildes nicht bestimmt werden.", + "bgGradient": "Die Hintergrundfarbe des Elementes konnte aufgrund eines Hintergrundfarbverlaufes nicht bestimmt werden.", + "imgNode": "Die Hintergrundfarbe des Elementes konnte nicht bestimmt werden, da das Element einen Image Node enthält.", + "bgOverlap": "Die Hintergrundfarbe des Elementes konnte nicht bestimmt werden, da es von einem anderen Element überlagert wird.", + "fgAlpha": "Die Vordergrundfarbe des Elementes konnte aufgrund der Alpha-Transparenz nicht ermittelt werden.", + "elmPartiallyObscured": "Die Hintergrundfarbe des Elements konnte nicht bestimmt werden, da es teilweise von anderen Elementen überdeckt wird.", + "elmPartiallyObscuring": "Die Hintergrundfarbe des Elements konnte nicht bestimmt werden, da es teilweise andere Elemente überdeckt.", + "outsideViewport": "Die Hintergrundfarbe des Elements konnte nicht bestimmt werden, da es sich außerhalb des Viewports befindet.", + "equalRatio": "Das Element hat einen 1:1 Kontrast mit der Hintergrundfarbe.", + "shortTextContent": "Der Inhalt des Elements ist zu kurz um zu bestimmen ob es sich wirklich um Textinhalt handelt.", + "nonBmp": "Das Element enthält ausschließlich Nicht-Text Zeichen.", + "pseudoContent": "Die Hintergrundfarbe konnte aufgrund eines pseudo Elementes nicht bestimmt werden." + } + }, "link-in-text-block": { "pass": "Links können vom umgebenenden Text auf unterschiedliche Art und Weise unterschieden werden.", "fail": "Links können (abgesehen von einer farblichen Kennzeichnung) nicht vom umgebenden Text unterschieden werden.", diff --git a/locales/es.json b/locales/es.json index 8a2eb37427..5605020f93 100644 --- a/locales/es.json +++ b/locales/es.json @@ -85,6 +85,10 @@ "description": "Garantiza que el contraste entre colores de primer plano y fondo cumple los límites de la ratio para contraste WCAG 2 AA", "help": "Los elementos deben tener un contraste de colores suficiente" }, + "color-contrast-enhanced": { + "description": "Garantiza que el contraste entre colores de primer plano y fondo cumple los límites de la ratio para contraste WCAG 2 AAA", + "help": "Los elementos deben tener un contraste de colores suficiente" + }, "css-orientation-lock": { "description": "Garantiza que el contenido no está bloqueado en ninguna orientación de pantalla específica, y que el contenido es manejable en cualquier orientación de pantalla", "help": "Las 'CSS Media queries' no se usan para bloquear la orientación de pantalla" @@ -439,6 +443,23 @@ "default": "Imposible determinar la ratio de contraste" } }, + "color-contrast-enhanced": { + "pass": "El elemento tiene un contraste de color suficiente de ${data.contrastRatio}", + "fail": "El elemento tiene un contraste de color insuficiente de ${data.contrastRatio} (color de primer plano: ${data.fgColor}, color de fondo: ${data.bgColor}, tamaño de fuente: ${data.fontSize}, grosor de fuente: ${data.fontWeight}). Ratio de contraste esperado: ${data.expectedContrastRatio}", + "incomplete": { + "bgImage": "El color de fondo del elemento no se pudo determinar debido a una imagen de fondo", + "bgGradient": "El color de fondo del elemento no se pudo determinar debido a un degradado de fondo", + "imgNode": "El color de fondo del elemento no se pudo determinar porque el elemento contiene un nodo de imagen", + "bgOverlap": "El color de fondo no se pudo determinar porque tiene otro elemento superpuesto", + "fgAlpha": "El color de fondo no se pudo determinar debido a una transparencia alfa", + "elmPartiallyObscured": "El color de fondo no se pudo determinar porque está parcialmente oculto por otro elemento", + "elmPartiallyObscuring": "El color de fondo del elemento no se pudo determinar porque se superpone parcialmente a otros elementos", + "outsideViewport": "El color de fondo del elemento no se pudo determinar porque está fuera de la ventana gráfica ('viewport')", + "equalRatio": "El elemento tiene una ratio de contraste 1:1 con el fondo", + "shortTextContent": "El contenido del elemento es demasiado corto para determinar si es contenido de texto propiamente dicho", + "default": "Imposible determinar la ratio de contraste" + } + }, "link-in-text-block": { "pass": "Los enlaces se pueden distinguir respecto al texto adyacente de forma ajena al color", "fail": "Es necesario distinguir los enlaces respecto al texto adyacente de una forma ajena al color", diff --git a/locales/eu.json b/locales/eu.json index 7f7cc729e1..4767e9e93c 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -85,6 +85,10 @@ "description": "Lehen planoko eta hondoko koloreen arteko kontrasteak WCAG 2 AA kontrasterako ratioaren mugak betetzen dituela bermatzen du.", "help": "Elementuek kolore-kontraste nahikoa izan behar dute" }, + "color-contrast-enhanced": { + "description": "Lehen planoko eta hondoko koloreen arteko kontrasteak WCAG 2 AAA kontrasterako ratioaren mugak betetzen dituela bermatzen du.", + "help": "Elementuek kolore-kontraste nahikoa izan behar dute" + }, "css-orientation-lock": { "description": "Bermatzen du edukia ez dagoela blokeatuta pantailako orientazio espezifiko batean, eta edukia maneiagarria dela pantailako edozein orientabidetan.", "help": "'CSS Media query'ak ez dira erabiltzen pantailaren orientazioa blokeatzeko " @@ -439,6 +443,23 @@ "default": "Ezinezkoa da kontraste-ratioa zehaztea" } }, + "color-contrast-enhanced": { + "pass": "Elementuak ${data.contrastRatio}-ko kolore-kontraste nahikoa du", + "fail": "Elementuaren ${data.contrastRatio}-ko kolore-kontrastea ez da nahikoa (ehen planoaren kolorea: ${data.fgColor}, hondoaren kolorea: ${data.bgColor}, letra-iturriaren tamaina: ${data.fontSize}, letra-iturriaren lodiera: ${data.fontWeight}). Espero den kontraste-ratioa: ${data.expectedContrastRatio}", + "incomplete": { + "bgImage": "Elementuaren hondoko kolorea ezin izan da zehaztu, hondoko irudi batengatik", + "bgGradient": "Elementuaren hondoko kolorea ezin izan da zehaztu hondoko degradatu baten ondorioz", + "imgNode": "Elementuaren hondoaren kolorea ezin izan da zehaztu, elementuak irudi-nodo bat duelako.", + "bgOverlap": "Hondoko kolorea ezin izan da zehaztu, gainjarritako beste elementu bat duelako", + "fgAlpha": "Hondoko kolorea ezin izan da zehaztu alfa gardentasun baten ondorioz", + "elmPartiallyObscured": "Hondoaren kolorea ezin izan da zehaztu, beste elementu batek partzialki ezkutatzen duelako", + "elmPartiallyObscuring": "Elementuaren hondoaren kolorea ezin izan da zehaztu, beste elementu batzuei partzialki gainjartzen baitzaie.", + "outsideViewport": "Elementuaren hondoko kolorea ezin izan da zehaztu, leiho grafikotik kanpo dagoelako ('viewport')", + "equalRatio": "Elementuak 1:1 kontraste-ratioa du hondoarekin", + "shortTextContent": "Elementuaren edukia laburregia da testu-edukia bera den zehazteko", + "default": "Ezinezkoa da kontraste-ratioa zehaztea" + } + }, "link-in-text-block": { "pass": "Estekak ondoko testuarekiko bereiz daitezke, koloretik kanpo", "fail": "Alboko testuarekiko loturak koloretik kanpo bereizi behar dira", diff --git a/locales/fr.json b/locales/fr.json index a1cba7785d..a5fb70ea0b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -117,6 +117,10 @@ "description": "Vérifier que les contrastes entre le premier plan et l’arrière-plan rencontrent les seuils de contrastes exigés par les WCAG 2 AA", "help": "Les éléments doivent avoir un contraste de couleurs suffisant" }, + "color-contrast-enhanced": { + "description": "Vérifier que les contrastes entre le premier plan et l’arrière-plan rencontrent les seuils de contrastes exigés par les WCAG 2 AAA", + "help": "Les éléments doivent avoir un contraste de couleurs suffisant" + }, "css-orientation-lock": { "description": "Vérifier que les contenus ne sont pas limités à une orientation spécifique de l’écran, et que le contenu est utilisable sous toutes les orientations de l’écran", "help": "Les CSS Media queries ne sont pas utilisées pour verrouiller l’orientation de l’écran" @@ -555,6 +559,29 @@ "pseudoContent": "La couleur d’arrière plan de l’élément n’a pu être déterminée à cause d’un pseudo-élément" } }, + "color-contrast-enhanced": { + "pass": "L’élément a un contraste de couleurs suffisant de ${data.contrastRatio}", + "fail": { + "default": "L’élément a un contraste de couleurs insuffisant de ${data.contrastRatio} (couleur d’avant plan : ${data.fgColor}, couleur d’arrière plan : ${data.bgColor}, taille de police : ${data.fontSize}, graisse : ${data.fontWeight}). Contraste de couleur attendu : ${data.expectedContrastRatio}", + "fgOnShadowColor": "L’élément a un contraste de couleurs insuffisant de ${data.contrastRatio} entre l’avant plan et la couleur de l’ombre de texte (couleur d’avant plan : ${data.fgColor}, couleur de l’ombre de texte : ${data.shadowColor}, taille de police : ${data.fontSize}, graisse: ${data.fontWeight}). Contraste de couleurs attendu : ${data.expectedContrastRatio}", + "shadowOnBgColor": "L’élément a un contraste de couleurs insuffisant de ${data.contrastRatio} entre la couleur de l’ombre de texte et l’arrière plan (couleur de l’ombre de texte : ${data.shadowColor}, couleur d’arrière plan : ${data.bgColor}, taille de police : ${data.fontSize}, graisse: ${data.fontWeight}). Contraste de couleurs attendu : ${data.expectedContrastRatio}" + }, + "incomplete": { + "default": "Impossible de déterminer le rapport de contraste", + "bgImage": "La couleur d’arrière-plan de l’élément n’a pu être déterminée à cause d’une image d’arrière-plan", + "bgGradient": "La couleur d’arrière-plan de l’élément n’a pu être déterminée à cause d’un dégradé d’arrière-plan", + "imgNode": "La couleur d’arrière-plan de l’élément n’a pu être déterminée, car l’élément contient une balise image", + "bgOverlap": "La couleur d’arrière-plan de l’élément n’a pu être déterminée, car un autre élément le chevauche", + "fgAlpha": "La couleur du texte de l’élément n’a pu être déterminée à cause d’une opacité réduite", + "elmPartiallyObscured": "La couleur d’arrière-plan de l’élément n’a pu être déterminée, car l’élément est partiellement masqué par un autre élément", + "elmPartiallyObscuring": "La couleur d’arrière-plan de l’élément n’a pu être déterminée, car il chevauche partiellement un autre élément", + "outsideViewport": "La couleur d’arrière-plan de l’élément n’a pu être déterminée, car il est à l’extérieur du viewport", + "equalRatio": "L’élément a un rapport de contraste de 1:1 avec son arrière-plan", + "shortTextContent": "Le contenu de l’élément est trop court pour déterminer s’il s’agit réellement d’un contenu textuel", + "nonBmp": "Le contenu de l’élément contient seulement des caractères non textuels", + "pseudoContent": "La couleur d’arrière plan de l’élément n’a pu être déterminée à cause d’un pseudo-élément" + } + }, "link-in-text-block": { "pass": "Les liens peuvent être distingués du texte environnant par un autre moyen que la couleur", "fail": "Les liens doivent se distinguer du texte environnant par un autre moyen que la couleur", diff --git a/locales/ja.json b/locales/ja.json index d9b354a254..a7bcaec24b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -117,6 +117,10 @@ "description": "前景色と背景色のコントラストがWCAG 2のAAコントラスト比のしきい値を満たすことを確認します", "help": "要素には十分な色のコントラストがなければなりません" }, + "color-contrast-enhanced": { + "description": "前景色と背景色のコントラストがWCAG 2のAAAコントラスト比のしきい値を満たすことを確認します", + "help": "要素には十分な色のコントラストがなければなりません" + }, "css-orientation-lock": { "description": "コンテンツが特定のディスプレイの向きに固定されていないこと、およびコンテンツがすべてのディスプレイの向きで操作可能なことを確認します", "help": "ディスプレイの向きを固定するためにCSSメディアクエリーは使用されていません" @@ -541,6 +545,25 @@ "pseudoContent": "擬似要素のため、要素の背景色を判定することができませんでした" } }, + "color-contrast-enhanced": { + "pass": "要素には${data.contrastRatio}の十分なコントラスト比があります", + "fail": "要素のコントラスト比が不十分です ${data.contrastRatio}(前景色: ${data.fgColor}、背景色: ${data.bgColor}、フォントサイズ: ${data.fontSize}、フォントの太さ: ${data.fontWeight})。コントラスト比${data.expectedContrastRatio}を必要とします", + "incomplete": { + "default": "コントラスト比を判定できません", + "bgImage": "背景画像のため、要素の背景色を判定できません", + "bgGradient": "背景グラデーションのため、要素の背景色を判定できません", + "imgNode": "画像ノードが含まれるため、要素の背景色を判定できません", + "bgOverlap": "他の要素と重なっているため、要素の背景色を判定できません", + "fgAlpha": "アルファ透明度により、要素の前景色を判定できません", + "elmPartiallyObscured": "他の要素により部分的に不明瞭なため、要素の背景色を判定できません", + "elmPartiallyObscuring": "他の要素と部分的に重なっているため、要素の背景色を判定できません", + "outsideViewport": "ビューポートの外にあるため、要素の背景色を判定できません", + "equalRatio": "要素のコントラスト比が背景と1:1です", + "shortTextContent": "実際のテキストコンテンツであるかを判断するには要素のコンテンツが短すぎます", + "nonBmp": "要素のコンテンツはテキストではない文字のみを含んでいます", + "pseudoContent": "擬似要素のため、要素の背景色を判定することができませんでした" + } + }, "link-in-text-block": { "pass": "リンクは色以外の方法で周囲のテキストと区別できます", "fail": "リンクは色以外の方法で周囲のテキストと区別させる必要があります", diff --git a/locales/ko.json b/locales/ko.json index 8ea2125d17..034d487c1b 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -2,734 +2,980 @@ "lang": "ko", "rules": { "accesskeys": { - "description": "모든 accesskey 속성 값이 고유한지 확인합니다.", - "help": "accesskey 속성 값은 고유해야 합니다." + "description": "모든 accesskey 어트리뷰트 값이 고유한지 확인하세요.", + "help": "accesskey 어트리뷰트 값은 고유해야 합니다." }, "area-alt": { - "description": "이미지 맵의 요소에 alt 속성이 있는지 확인합니다.", - "help": " 요소의 내용을 기술하는 alt 속성이 필요합니다." + "description": "이미지 맵의 엘리먼트가 대체텍스트를 가지고 있는지 확인하세요.", + "help": "활성 엘리먼트는 반드시 대체텍스트를 가져야 합니다." }, "aria-allowed-attr": { - "description": "요소의 역할(role)에 ARIA 속성이 허용되도록 합니다.", - "help": "요소는 허용 된 ARIA 속성만 사용해야 합니다." + "description": "ARIA 어트리뷰트가 엘리먼트의 역할(role)에 허용되었는지 확인하세요.", + "help": "엘리먼트는 반드시 허용된 ARIA 어트리뷰트만 사용해야 합니다." }, "aria-allowed-role": { - "description": "역할(role) 속성이 요소에 적절한지 확인하세요.", - "help": "요소에 적절한 ARIA 역할(role)을 설정해야 합니다." + "description": "역할(role) 어트리뷰트가 엘리먼트에 적절한 값을 가지고 있는지 확인하세요.", + "help": "ARIA 역할(role)은 엘리먼트에 적절해야 합니다." + }, + "aria-command-name": { + "description": "모든 ARIA 버튼, 링크, 메뉴 아이템이 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA 명령 엘리먼트에는 반드시 접근 가능한 이름이 있어야 합니다." + }, + "aria-dialog-name": { + "description": "모든 ARIA dialog와 alertdialog 노드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA dialog와 alertdialog 노드는 접근 가능한 이름을 가져야 합니다." }, "aria-hidden-body": { - "description": " 요소의 aria-hidden='true' 설정을 확인하세요.", - "help": "aria-hidden='true' 설정이 요소에 설정되어서는 안됩니다." + "description": "문서 body에 aria-hidden='true'가 없게 하세요.", + "help": "aria-hidden='true'는 반드시 문서 body에 없어야 합니다." }, "aria-hidden-focus": { - "description": "aria-hidden 요소에 포커스 가능한 요소가 포함되지 않도록 보장합니다.", - "help": "ARIA 속성을 사용해 숨겨진 요소는 포커스 가능한 요소를 포함해서는 안됩니다." + "description": "aria-hidden 엘리먼트가 초점을 얻을 수 있는(focusable) 엘리먼트를 포함하지 않도록 하세요.", + "help": "ARIA hidden 엘리먼트는 반드시 초점을 얻을 수 있는(focusable) 엘리먼트를 포함하지 않아야 합니다." }, "aria-input-field-name": { - "description": "모든 ARIA 입력 필드(input fields)에 접근 가능한 이름이 있는지 확인하세요.", - "help": "ARIA 입력 필드(input fields)에는 접근 가능한 이름 설정이 필요합니다." + "description": "모든 ARIA 입력 필드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA 입력 필드에는 반드시 접근 가능한 이름이 있어야 합니다." + }, + "aria-meter-name": { + "description": "모든 ARIA meter 노드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA meter 노드에는 반드시 접근 가능한 이름이 있어야 합니다." + }, + "aria-progressbar-name": { + "description": "모든 ARIA progressbar 노드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA progressbar 노드에는 반드시 접근 가능한 이름이 있어야 합니다." }, "aria-required-attr": { - "description": "ARIA 역할(role)을 가진 요소가 모든 필수 ARIA 속성을 갖도록 설정합니다.", - "help": "역할(role)에 필요한 필수 ARIA 속성 설정이 필요합니다." + "description": "ARIA 역할(role)을 가진 엘리먼트가 필수 ARIA 어트리뷰트를 모두 가지고 있는지 확인하세요", + "help": "필수 ARIA 어트리뷰트는 반드시 제공되어야 합니다." }, "aria-required-children": { - "description": "하위 역할(child roles)이 필요한 ARIA 역할을 가진 요소에 해당 요소가 포함되었는지 확인하세요.", - "help": "특정 ARIA 역할에는 특정 자손(chidren)이 포함되어야 합니다." + "description": "하위 역할(child role)이 필요한 ARIA 역할(role)을 가진 엘리먼트가 해당 역할(role)을 포함하고 있는지 확인하세요.", + "help": "일부 ARIA 역할(role)은 반드시 특정한 하위 항목들을 포함해야 합니다." }, "aria-required-parent": { - "description": "부모 역할(parent roles)이 필요한 ARIA 역할을 가진 요소가 포함되었는지 확인하세요.", - "help": "특정 ARIA 역할은 특정 부모 역할을 가진 요소에 포함되어야 합니다." + "description": "상위 역할(parent role)이 필요한 ARIA 역할(role)을 가진 엘리먼트가 해당 역할(role)에 포함되어 있는지 확인하세요.", + "help": "일부 ARIA 역할(role)은 반드시 특정한 상위 항목들에 포함되어야 합니다." + }, + "aria-roledescription": { + "description": "aria-roledescription이 암묵적 혹은 명시적 역할(role)을 가진 엘리먼트에만 사용되었는지 확인하세요.", + "help": "aria-roledescription은 의미론적 역할(role)을 가진 엘리먼트에 사용하세요." }, "aria-roles": { - "description": "역할(role) 속성을 가진 모든 요소가 유효한 값을 사용하도록 합니다.", - "help": "사용된 ARIA 역할(role)은 유효한 값을 준수해야 합니다." + "description": "역할(role) 어트리뷰트를 가진 모든 엘리먼트가 유효한 값을 가지고 있는지 확인하세요.", + "help": "ARIA 역할(role)은 반드시 유효한 값을 준수해야 합니다." + }, + "aria-text": { + "description": "\"role=text\"가 초점을 얻을 수 있는(focusable) 후손을 가지지 않는 엘리먼트에 사용되었는지 확인하세요.", + "help": "\"role=text\"는 초점을 얻을 수 있는(focusable) 후손을 가지지 않아야 합니다." }, "aria-toggle-field-name": { - "description": "모든 ARIA 토글 필드(toggle field)에 접근 가능한 이름이 있는지 확인합니다.", - "help": "ARIA 토글 필드(toggle field)는 접근 가능한 이름이 설정되어야 합니다." + "description": "모든 ARIA toggle 필드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA toggle 필드는 접근 가능한 이름을 가져야 합니다." + }, + "aria-tooltip-name": { + "description": "모든 ARIA tooltip 노드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA tooltip 노드는 반드시 접근 가능한 이름을 가져야 합니다." + }, + "aria-treeitem-name": { + "description": "모든 ARIA treeitem 노드가 접근 가능한 이름을 가지고 있는지 확인하세요.", + "help": "ARIA treeitem 노드는 접근 가능한 이름을 가져야 합니다." }, "aria-valid-attr-value": { - "description": "모든 ARIA 속성이 유효한 값을 갖도록 합니다.", - "help": "ARIA 속성은 유효한 값을 준수해야 합니다." + "description": "모든 ARIA 어트리뷰트가 유효한 값을 가지고 있는지 확인하세요.", + "help": "ARIA 어트리뷰트는 반드시 유효한 값을 준수해야 합니다." }, "aria-valid-attr": { - "description": "aria-로 시작하는 속성이 유효한 ARIA 속성인지 확인합니다.", - "help": "유효하지 않은 ARIA 속성 이름을 사용해서는 안됩니다." + "description": "aria- 로 시작하는 어트리뷰트가 유효한 ARIA 어트리뷰트인지 확인하세요.", + "help": "ARIA 어트리뷰트는 반드시 유효한 이름을 준수해야 합니다." }, "audio-caption": { - "description": "

요소가 올바르게 구성되어 있는지 확인합니다.", - "help": "
요소는 올바르게 정렬 된
,
그룹, diff --git a/test/integration/full/configure-options/configure-options.js b/test/integration/full/configure-options/configure-options.js index 372ce2d7d6..707ee1f404 100644 --- a/test/integration/full/configure-options/configure-options.js +++ b/test/integration/full/configure-options/configure-options.js @@ -206,7 +206,6 @@ describe('Configure Options', function() { iframe.src = '/test/mock/frames/context.html'; iframe.onload = function() { axe.configure(config); - iframe.contentWindow.axe.configure(config); axe.run( '#target', @@ -247,7 +246,6 @@ describe('Configure Options', function() { iframe.src = '/test/mock/frames/noHtml-config.html'; iframe.onload = function() { axe.configure(config); - iframe.contentWindow.axe.configure(config); axe.run('#target', { runOnly: { diff --git a/test/integration/full/contrast-enhanced/simple.html b/test/integration/full/contrast-enhanced/simple.html new file mode 100644 index 0000000000..a38da200f3 --- /dev/null +++ b/test/integration/full/contrast-enhanced/simple.html @@ -0,0 +1,45 @@ + + + + Test Page + + + + + + + + + + +
+
Pass (Regular size text, 21:1)
+
Fail (Regular size text, 6:1)
+
Fail (Regular size text, 6.9:1)
+
Pass (Regular size text, 7:1)
+ +
Fail (Large text, 4.487:1)
+
Pass (Large text, 4.5:1)
+
Fail (Large text, but not quite large enough, 4.5:1)
+ +
Fail (Bold text, 4.487:1)
+
Pass (Bold text, 4.5:1)
+
Fail (Bold text, but not quite large enough, 4.5:1)
+
Fail (Bold text, but not quite bold enough, 4.5:1)
+
+ + + + + + diff --git a/test/integration/full/contrast-enhanced/simple.js b/test/integration/full/contrast-enhanced/simple.js new file mode 100644 index 0000000000..471f35158f --- /dev/null +++ b/test/integration/full/contrast-enhanced/simple.js @@ -0,0 +1,30 @@ +describe('color-contrast shadow dom test', function() { + 'use strict'; + + describe('violations', function() { + it('should find issues in simple tree', function(done) { + axe.run( + '#fixture', + { runOnly: { type: 'rule', values: ['color-contrast-enhanced'] } }, + function(err, results) { + assert.isNull(err); + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.passes[0].nodes, 4); + assert.lengthOf(results.incomplete, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.violations[0].nodes, 7); + assert.equal(results.violations[0].nodes[0].any[0].data.fgColor, '#556666'); + assert.equal(results.violations[0].nodes[1].any[0].data.fgColor, '#556000'); + assert.equal(results.violations[0].nodes[2].any[0].data.fgColor, '#118488'); + assert.equal(results.violations[0].nodes[3].any[0].data.fgColor, '#048488'); + assert.equal(results.violations[0].nodes[4].any[0].data.fgColor, '#118488'); + assert.equal(results.violations[0].nodes[5].any[0].data.fgColor, '#048488'); + assert.equal(results.violations[0].nodes[6].any[0].data.fgColor, '#048488'); + assert.lengthOf(results.incomplete, 0); + done(); + } + ); + }); + }); + +}); diff --git a/test/integration/full/contrast/blending.html b/test/integration/full/contrast/blending.html index 2fbc1ad89e..1af0520c50 100644 --- a/test/integration/full/contrast/blending.html +++ b/test/integration/full/contrast/blending.html @@ -22,18 +22,20 @@ margin: 4rem 2rem; } - #fixture { + .test-group { + position: relative; + z-index: 1; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } - #fixture > div { + .test-group > div { display: flex; flex-direction: row; border: 1px solid white; } - #fixture * { + .test-group * { width: 100px; height: 100px; flex-shrink: 0; @@ -57,128 +59,131 @@ >

-
-
-
+

normal

+
+
+
+
+
+ Test1 +
+
+
+
Test1 result
+
+ +
+
- Test1 + Test2
+
Test2 result
-
Test1 result
-
-
-
-
- Test2 +
+
+
+ Test3 +
+
Test3 result
-
Test2 result
-
-
-
-
- Test3 +
+
+
+
+ Test4 +
+
+
Test4 result
-
Test3 result
-
-
-
-
+
+
- Test4 + Test5
+
Test5 result
-
Test4 result
-
- -
-
-
- Test5 -
-
-
Test5 result
-
-
-
-
- Test6 +
+
+
+ Test6 +
+
Test6 result
-
Test6 result
-
-
-
+
-
-
- Test7 +
+
+
+ Test7 +
+
Test7 result
-
Test7 result
-
-
-
+
-
-
-
-
- Test8 +
+
+
+
+
+ Test8 +
+
Test8 result
-
Test8 result
-
-
-
-
- Test9 +
+
+
+ Test9 +
+
Test9 result
-
Test9 result
diff --git a/test/integration/full/contrast/blending.js b/test/integration/full/contrast/blending.js index 7d1bde4859..6a006afb5e 100644 --- a/test/integration/full/contrast/blending.js +++ b/test/integration/full/contrast/blending.js @@ -1,18 +1,173 @@ +var isIE11 = axe.testUtils.isIE11; + describe('color-contrast blending test', function() { var include = []; var resultElms = []; var expected = [ + // normal 'rgb(223, 112, 96)', 'rgb(255, 128, 128)', 'rgb(191, 223, 191)', 'rgb(125, 38, 54)', 'rgb(179, 38, 0)', 'rgb(179, 0, 77)', - 'rgb(143, 192, 80)', - 'rgb(147, 153, 119)', - 'rgb(221, 221, 221)' + 'rgb(144, 192, 81)', + 'rgb(147, 154, 120)', + 'rgb(221, 221, 221)', + // multiply + 'rgb(191, 112, 96)', + 'rgb(255, 128, 128)', + 'rgb(191, 223, 191)', + 'rgb(125, 0, 54)', + 'rgb(179, 0, 0)', + 'rgb(179, 0, 0)', + 'rgb(144, 171, 81)', + 'rgb(147, 154, 112)', + 'rgb(213, 213, 213)', + // screen + 'rgb(223, 223, 191)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(179, 38, 77)', + 'rgb(255, 38, 0)', + 'rgb(255, 0, 77)', + 'rgb(165, 192, 81)', + 'rgb(150, 157, 120)', + 'rgb(228, 228, 228)', + // overlay + 'rgb(223, 207, 159)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(156, 0, 54)', + 'rgb(255, 0, 0)', + 'rgb(255, 0, 0)', + 'rgb(148, 187, 81)', + 'rgb(147, 154, 114)', + 'rgb(226, 226, 226)', + // darken + 'rgb(191, 112, 96)', + 'rgb(255, 128, 128)', + 'rgb(191, 223, 191)', + 'rgb(125, 0, 54)', + 'rgb(179, 0, 0)', + 'rgb(179, 0, 0)', + 'rgb(144, 171, 81)', + 'rgb(147, 154, 112)', + 'rgb(221, 221, 221)', + // lighten + 'rgb(223, 223, 191)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(179, 38, 77)', + 'rgb(255, 38, 0)', + 'rgb(255, 0, 77)', + 'rgb(165, 192, 81)', + 'rgb(150, 157, 120)', + 'rgb(221, 221, 221)', + // color-dodge + 'rgb(223, 223, 191)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(179, 0, 77)', + 'rgb(255, 0, 0)', + 'rgb(255, 0, 0)', + 'rgb(165, 192, 81)', + 'rgb(150, 157, 120)', + 'rgb(230, 230, 230)', + // color-burn + 'rgb(191, 112, 96)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(125, 0, 54)', + 'rgb(255, 0, 0)', + 'rgb(255, 0, 0)', + 'rgb(144, 171, 81)', + 'rgb(147, 154, 112)', + 'rgb(219, 219, 219)', + // hard-light + 'rgb(223, 112, 96)', + 'rgb(255, 128, 128)', + 'rgb(191, 255, 191)', + 'rgb(125, 0, 54)', + 'rgb(179, 0, 0)', + 'rgb(179, 0, 77)', + 'rgb(144, 192, 81)', + 'rgb(147, 154, 120)', + 'rgb(226, 226, 226)', + // soft-light + 'rgb(206, 209, 167)', + 'rgb(255, 255, 255)', + 'rgb(255, 255, 255)', + 'rgb(163, 0, 61)', + 'rgb(255, 0, 0)', + 'rgb(255, 0, 0)', + 'rgb(155, 180, 81)', + 'rgb(148, 155, 115)', + 'rgb(223, 223, 223)', + // difference + 'rgb(128, 223, 191)', + 'rgb(128, 255, 255)', + 'rgb(255, 223, 255)', + 'rgb(179, 38, 77)', + 'rgb(255, 38, 0)', + 'rgb(255, 0, 77)', + 'rgb(165, 176, 81)', + 'rgb(150, 157, 119)', + 'rgb(183, 183, 183)', + // exclusion + 'rgb(128, 223, 191)', + 'rgb(128, 255, 255)', + 'rgb(255, 223, 255)', + 'rgb(179, 38, 77)', + 'rgb(255, 38, 0)', + 'rgb(255, 0, 77)', + 'rgb(165, 176, 81)', + 'rgb(150, 157, 119)', + 'rgb(198, 198, 198)' ]; - var testElms = Array.from(document.querySelectorAll('#fixture > div')); + + var fixture = document.querySelector('#fixture'); + var testGroup = document.querySelector('.test-group'); + [ + 'multiply', + 'screen', + 'overlay', + 'darken', + 'lighten', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'exclusion' + ].forEach(function(blendMode) { + var nodes = testGroup.cloneNode(true); + var group = testGroup.cloneNode(); + + var heading = document.createElement('h2'); + heading.textContent = blendMode; + fixture.appendChild(heading); + + Array.from(nodes.children).forEach(function(node, index) { + var id = node.id; + var target = node.querySelector('#' + id + '-target'); + var result = node.querySelector('#' + id + '-result'); + var blendModeIndex = blendMode + (index + 1); + + node.id = blendModeIndex; + target.id = blendModeIndex + '-target'; + result.id = blendModeIndex + '-result'; + + target.textContent = blendModeIndex; + result.textContent = blendModeIndex + ' result'; + + target.style.mixBlendMode = blendMode; + group.appendChild(node); + }); + + fixture.appendChild(group); + }); + var testElms = Array.from(document.querySelectorAll('.test-group > div')); testElms.forEach(function(testElm) { var id = testElm.id; var target = testElm.querySelector('#' + id + '-target'); @@ -22,29 +177,38 @@ describe('color-contrast blending test', function() { }); before(function(done) { - axe.run({ include: include }, { runOnly: ['color-contrast'] }, function( - err, - res - ) { - assert.isNull(err); - - // don't care where the result goes as we just want to - // extract the background color for each one - var results = [] - .concat(res.passes) - .concat(res.violations) - .concat(res.incomplete); - results.forEach(function(result) { - result.nodes.forEach(function(node) { - var bgColor = node.any[0].data.bgColor; - var id = node.target[0].split('-')[0]; - var result = document.querySelector(id + '-result'); - result.style.backgroundColor = bgColor; + // mix-blend-mode is not supported on IE11 + // @see https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + if (isIE11) { + this.skip(); + } else { + axe.run({ include: include }, { runOnly: ['color-contrast'] }, function( + err, + res + ) { + assert.isNull(err); + + // don't care where the result goes as we just want to + // extract the background color for each one + var results = [] + .concat(res.passes) + .concat(res.violations) + .concat(res.incomplete); + results.forEach(function(result) { + result.nodes.forEach(function(node) { + var bgColor = node.any[0].data.bgColor; + var id = node.target[0].substring( + 0, + node.target[0].lastIndexOf('-') + ); + var result = document.querySelector(id + '-result'); + result.style.backgroundColor = bgColor; + }); }); - }); - done(); - }); + done(); + }); + } }); resultElms.forEach(function(elm, index) { diff --git a/test/integration/full/get-selector/get-selector.js b/test/integration/full/get-selector/get-selector.js index 9a5133744d..dab3db348e 100644 --- a/test/integration/full/get-selector/get-selector.js +++ b/test/integration/full/get-selector/get-selector.js @@ -1,5 +1,8 @@ describe('axe.utils.getSelector', function() { 'use strict'; + before(function() { + axe.setup(); + }); it('should work on namespaced elements', function() { var fixture = document.querySelector('#fixture'); var node = fixture.firstElementChild; diff --git a/test/integration/full/get-selector/get-selector.xhtml b/test/integration/full/get-selector/get-selector.xhtml index d9139cb2e3..02b23c8473 100644 --- a/test/integration/full/get-selector/get-selector.xhtml +++ b/test/integration/full/get-selector/get-selector.xhtml @@ -1,25 +1,29 @@ - - axe.utils.getSelector test - - - - - - - - -
- -
-
- - - + + axe.utils.getSelector test + + + + + + + + +
+ +
+
+ + + diff --git a/test/integration/full/test-webdriver.js b/test/integration/full/test-webdriver.js index d4b4a9ab79..974db1f717 100644 --- a/test/integration/full/test-webdriver.js +++ b/test/integration/full/test-webdriver.js @@ -188,7 +188,10 @@ function start(options) { options.browser === 'edge' ? 'MicrosoftEdge' : options.browser; var testUrls = globby - .sync(['test/integration/full/**/*.html', '!**/frames/**/*.html']) + .sync([ + 'test/integration/full/**/*.{html,xhtml}', + '!**/frames/**/*.{html,xhtml}' + ]) .map(function(url) { return 'http://localhost:9876/' + url; }); diff --git a/test/integration/rules/aria-allowed-attr/failures.html b/test/integration/rules/aria-allowed-attr/failures.html index 4a5ed02109..c12fd3c7e0 100644 --- a/test/integration/rules/aria-allowed-attr/failures.html +++ b/test/integration/rules/aria-allowed-attr/failures.html @@ -1,25 +1,25 @@ - -
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
-
fail
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -28,12 +28,16 @@ - +> +
+
+
+
diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json index 1d00054e6c..ccedf0b10f 100644 --- a/test/integration/rules/aria-allowed-attr/failures.json +++ b/test/integration/rules/aria-allowed-attr/failures.json @@ -31,6 +31,11 @@ ["#fail27"], ["#fail28"], ["#fail29"], + ["#fail30"], + ["#fail31"], + ["#fail32"], + ["#fail33"], + ["#fail34"], ["#fail35"], ["#fail36"], ["#fail37"], diff --git a/test/integration/rules/aria-allowed-attr/incomplete.html b/test/integration/rules/aria-allowed-attr/incomplete.html index 35793fb4a2..e187dba1c6 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.html +++ b/test/integration/rules/aria-allowed-attr/incomplete.html @@ -1,2 +1,4 @@
Foo
Foo
+Foo +
Foo
diff --git a/test/integration/rules/aria-allowed-attr/incomplete.json b/test/integration/rules/aria-allowed-attr/incomplete.json index 475f884bdd..fa37187123 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.json +++ b/test/integration/rules/aria-allowed-attr/incomplete.json @@ -1,5 +1,10 @@ { "description": "aria-allowed-attr incomplete tests", "rule": "aria-allowed-attr", - "incomplete": [["#incomplete0"], ["#incomplete1"]] + "incomplete": [ + ["#incomplete0"], + ["#incomplete1"], + ["#incomplete2"], + ["#incomplete3"] + ] } diff --git a/test/integration/rules/aria-allowed-attr/passes.html b/test/integration/rules/aria-allowed-attr/passes.html index b02bd1f62b..d27880688a 100644 --- a/test/integration/rules/aria-allowed-attr/passes.html +++ b/test/integration/rules/aria-allowed-attr/passes.html @@ -1941,3 +1941,13 @@ + + diff --git a/test/integration/rules/aria-allowed-attr/passes.json b/test/integration/rules/aria-allowed-attr/passes.json index 454213f927..769cb1f931 100644 --- a/test/integration/rules/aria-allowed-attr/passes.json +++ b/test/integration/rules/aria-allowed-attr/passes.json @@ -93,6 +93,7 @@ ["#pass88"], ["#pass89"], ["#pass90"], - ["#treegrid"] + ["#treegrid"], + ["#pass91"] ] } diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.html b/test/integration/rules/aria-allowed-role/aria-allowed-role.html index ca7aec245f..1ffbd331e5 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -219,6 +219,28 @@

ok
ok
+ + +
hazaar
+ + + + + + +
ok
ok
@@ -226,3 +248,21 @@

ok

ok
ok
+ + +
+
ok
+ + + + + + + + diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.json b/test/integration/rules/aria-allowed-role/aria-allowed-role.json index 33c694ee33..d22ae8b362 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.json +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.json @@ -72,7 +72,15 @@ ["#p-text"], ["#pass-graphics-document"], ["#pass-graphics-object"], - ["#pass-graphics-symbol"] + ["#pass-graphics-symbol"], + ["#pass-header-banner"], + ["#pass-footer-contentinfo"], + ["#pass-img-valid-role-aria-label"], + ["#pass-img-valid-role-title"], + ["#pass-img-valid-role-aria-labelledby"], + ["#pass-img-valid-role-radio"], + ["#pass-imgmap-1"], + ["#pass-imgmap-2"] ], "violations": [ ["#fail-dd-no-role"], @@ -98,7 +106,13 @@ ["#fail-text-1"], ["#fail-text-2"], ["#fail-text-3"], - ["#fail-text-4"] + ["#fail-text-4"], + ["#fail-img-invalid-role-aria-label"], + ["#fail-img-invalid-role-title"], + ["#fail-img-invalid-role-aria-labelledby"], + ["#fail-img-no-accessible-name-present"], + ["#fail-imgmap-1"], + ["#fail-imgmap-2"] ], "incomplete": [["#incomplete1"], ["#incomplete2"]] } 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 45b6028125..1165908ff2 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.html +++ b/test/integration/rules/aria-required-children/aria-required-children.html @@ -23,9 +23,7 @@
-
-
-
+
@@ -56,8 +54,18 @@
+
+ option + option +
+ +
+
option
+ +
+
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 591f7cd180..457c09595e 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.json +++ b/test/integration/rules/aria-required-children/aria-required-children.json @@ -8,7 +8,8 @@ ["#fail4"], ["#fail5"], ["#fail6"], - ["#fail7"] + ["#fail7"], + ["#fail8"] ], "passes": [ ["#pass1"], @@ -18,6 +19,7 @@ ["#pass5"], ["#pass6"], ["#pass7"], + ["#pass8"], ["#pass9"] ], "incomplete": [ @@ -29,8 +31,6 @@ ["#incomplete6"], ["#incomplete7"], ["#incomplete8"], - ["#incomplete9"], - ["#incomplete10"], - ["#incomplete11"] + ["#incomplete9"] ] } diff --git a/test/integration/rules/aria-roles/aria-roles.html b/test/integration/rules/aria-roles/aria-roles.html index b00823f4c0..8eaa284f95 100644 --- a/test/integration/rules/aria-roles/aria-roles.html +++ b/test/integration/rules/aria-roles/aria-roles.html @@ -72,7 +72,6 @@
ok
ok
ok
-
ok
ok
ok
ok
@@ -83,7 +82,6 @@
ok
ok
ok
-
ok
ok
ok
ok
@@ -111,6 +109,9 @@
ok
ok
ok
+ +
ok
+
ok
@@ -131,6 +132,9 @@
fail
+ +
fail
+
fail
diff --git a/test/integration/rules/aria-roles/aria-roles.json b/test/integration/rules/aria-roles/aria-roles.json index 141df46da2..75257c110e 100644 --- a/test/integration/rules/aria-roles/aria-roles.json +++ b/test/integration/rules/aria-roles/aria-roles.json @@ -15,7 +15,9 @@ ["#fail11"], ["#fail12"], ["#fail13"], - ["#fail14"] + ["#fail14"], + ["#fail15"], + ["#fail16"] ], "passes": [ ["#pass1"], @@ -91,7 +93,6 @@ ["#pass72"], ["#pass73"], ["#pass74"], - ["#pass75"], ["#pass76"], ["#pass77"], ["#pass78"], @@ -102,7 +103,6 @@ ["#pass83"], ["#pass84"], ["#pass85"], - ["#pass86"], ["#pass87"], ["#pass88"], ["#pass89"], @@ -129,6 +129,9 @@ ["#pass110"], ["#pass111"], ["#pass112"], - ["#pass113"] + ["#pass113"], + ["#pass114"], + ["#pass115"], + ["#pass116"] ] } diff --git a/test/integration/rules/aria-text/aria-text.html b/test/integration/rules/aria-text/aria-text.html index 28252c0bce..54809b6bd5 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -20,3 +20,11 @@

explicit role.

+

+

Hello

+ + diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index f6c3f1d2d9..9443502a16 100644 --- a/test/integration/rules/aria-text/aria-text.json +++ b/test/integration/rules/aria-text/aria-text.json @@ -1,6 +1,6 @@ { "description": "aria-text tests", "rule": "aria-text", - "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"]], - "passes": [["#pass1"], ["#pass2"], ["#pass3"]] + "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"], ["#pass6"]] } diff --git a/test/integration/rules/autocomplete-valid/autocomplete-valid.html b/test/integration/rules/autocomplete-valid/autocomplete-valid.html index 105d06128b..e38a7798ab 100644 --- a/test/integration/rules/autocomplete-valid/autocomplete-valid.html +++ b/test/integration/rules/autocomplete-valid/autocomplete-valid.html @@ -152,3 +152,12 @@ + + + + + + + + + diff --git a/test/integration/rules/autocomplete-valid/autocomplete-valid.json b/test/integration/rules/autocomplete-valid/autocomplete-valid.json index fafa4cc824..b4eceb2603 100644 --- a/test/integration/rules/autocomplete-valid/autocomplete-valid.json +++ b/test/integration/rules/autocomplete-valid/autocomplete-valid.json @@ -85,6 +85,13 @@ ["#pass80"], ["#pass81"], ["#pass82"], - ["#pass83"] + ["#pass83"], + ["#pass84"], + ["#pass85"], + ["#pass86"], + ["#pass87"], + ["#pass88"], + ["#pass89"], + ["#pass90"] ] } diff --git a/test/integration/rules/color-contrast/color-contrast.html b/test/integration/rules/color-contrast/color-contrast.html index 8d3c1d8ece..a1e2ed0997 100644 --- a/test/integration/rules/color-contrast/color-contrast.html +++ b/test/integration/rules/color-contrast/color-contrast.html @@ -65,7 +65,7 @@
Hello world
diff --git a/test/integration/rules/color-contrast/text-shadows.html b/test/integration/rules/color-contrast/text-shadows.html index 21b5b18210..980538afde 100644 --- a/test/integration/rules/color-contrast/text-shadows.html +++ b/test/integration/rules/color-contrast/text-shadows.html @@ -63,6 +63,10 @@ color: #aaa; background-color: #333; } + .s6 { + color: #444; + background-color: #ccc; + } .f1 { font-size: 12px; @@ -230,6 +234,19 @@ 0 0 0.5em #000, 0 0 0.5em #000, 0 0 0.5em #000; } + .r11 i:nth-child(1) { + text-shadow: 0 0 0.14em #000; + } + .r11 i:nth-child(2) { + text-shadow: 0 0 0.18em #000; + } + .r11 i:nth-child(3) { + text-shadow: 0 0 0.22em #000; + } + .r11 i:nth-child(4) { + text-shadow: 0 0 0.26em #000; + } + .l1 i:nth-child(1) { text-shadow: 0 0 0.03em #fff; } @@ -303,220 +320,433 @@
Sample
- b1-r1-i1 b1-r1-i2 b1-r1-i3 b1-r1-i4 + b1-r1-i1 + b1-r1-i2 + b1-r1-i3 + b1-r1-i4
- b1-r2-i1 b1-r2-i2 b1-r2-i3 b1-r2-i4 + b1-r2-i1 + b1-r2-i2 + b1-r2-i3 + b1-r2-i4
- b1-r3-i1 b1-r3-i2 b1-r3-i3 b1-r3-i4 + b1-r3-i1 + b1-r3-i2 + b1-r3-i3 + b1-r3-i4
- b1-r4-i1 b1-r4-i2 b1-r4-i3 b1-r4-i4 + b1-r4-i1 + b1-r4-i2 + b1-r4-i3 + b1-r4-i4
- b1-r5-i1 b1-r5-i2 b1-r5-i3 b1-r5-i4 + b1-r5-i1 + b1-r5-i2 + b1-r5-i3 + b1-r5-i4
Sample
- b2-r1-i1 b2-r1-i2 b2-r1-i3 b2-r1-i4 + b2-r1-i1 + b2-r1-i2 + b2-r1-i3 + b2-r1-i4
- b2-r2-i1 b2-r2-i2 b2-r2-i3 b2-r2-i4 + b2-r2-i1 + b2-r2-i2 + b2-r2-i3 + b2-r2-i4
- b2-r3-i1 b2-r3-i2 b2-r3-i3 b2-r3-i4 + b2-r3-i1 + b2-r3-i2 + b2-r3-i3 + b2-r3-i4
- b2-r4-i1 b2-r4-i2 b2-r4-i3 b2-r4-i4 + b2-r4-i1 + b2-r4-i2 + b2-r4-i3 + b2-r4-i4
- b2-r5-i1 b2-r5-i2 b2-r5-i3 b2-r5-i4 + b2-r5-i1 + b2-r5-i2 + b2-r5-i3 + b2-r5-i4
- b3-r1-i1 b3-r1-i2 b3-r1-i3 b3-r1-i4 + b3-r1-i1 + b3-r1-i2 + b3-r1-i3 + b3-r1-i4
- b3-r2-i1 b3-r2-i2 b3-r2-i3 b3-r2-i4 + b3-r2-i1 + b3-r2-i2 + b3-r2-i3 + b3-r2-i4
- b3-r3-i1 b3-r3-i2 b3-r3-i3 b3-r3-i4 + b3-r3-i1 + b3-r3-i2 + b3-r3-i3 + b3-r3-i4
- b3-r4-i1 b3-r4-i2 b3-r4-i3 b3-r4-i4 + b3-r4-i1 + b3-r4-i2 + b3-r4-i3 + b3-r4-i4
- b3-r5-i1 b3-r5-i2 b3-r5-i3 b3-r5-i4 + b3-r5-i1 + b3-r5-i2 + b3-r5-i3 + b3-r5-i4
- b4-r1-i1 b4-r1-i2 b4-r1-i3 b4-r1-i4 + b4-r1-i1 + b4-r1-i2 + b4-r1-i3 + b4-r1-i4
- b4-r2-i1 b4-r2-i2 b4-r2-i3 b4-r2-i4 + b4-r2-i1 + b4-r2-i2 + b4-r2-i3 + b4-r2-i4
- b4-r3-i1 b4-r3-i2 b4-r3-i3 b4-r3-i4 + b4-r3-i1 + b4-r3-i2 + b4-r3-i3 + b4-r3-i4
- b4-r4-i1 b4-r4-i2 b4-r4-i3 b4-r4-i4 + b4-r4-i1 + b4-r4-i2 + b4-r4-i3 + b4-r4-i4
- b4-r5-i1 b4-r5-i2 b4-r5-i3 b4-r5-i4 + b4-r5-i1 + b4-r5-i2 + b4-r5-i3 + b4-r5-i4
- b5-r1-i1 b5-r1-i2 b5-r1-i3 b5-r1-i4 + b5-r1-i1 + b5-r1-i2 + b5-r1-i3 + b5-r1-i4
- b5-r2-i1 b5-r2-i2 b5-r2-i3 b5-r2-i4 + b5-r2-i1 + b5-r2-i2 + b5-r2-i3 + b5-r2-i4
- b5-r3-i1 b5-r3-i2 b5-r3-i3 b5-r3-i4 + b5-r3-i1 + b5-r3-i2 + b5-r3-i3 + b5-r3-i4
- b5-r4-i1 b5-r4-i2 b5-r4-i3 b5-r4-i4 + b5-r4-i1 + b5-r4-i2 + b5-r4-i3 + b5-r4-i4
- b5-r5-i1 b5-r5-i2 b5-r5-i3 b5-r5-i4 + b5-r5-i1 + b5-r5-i2 + b5-r5-i3 + b5-r5-i4
- b6-r1-i1 b6-r1-i2 b6-r1-i3 b6-r1-i4 + b6-r1-i1 + b6-r1-i2 + b6-r1-i3 + b6-r1-i4
- b6-r2-i1 b6-r2-i2 b6-r2-i3 b6-r2-i4 + b6-r2-i1 + b6-r2-i2 + b6-r2-i3 + b6-r2-i4
- b6-r3-i1 b6-r3-i2 b6-r3-i3 b6-r3-i4 + b6-r3-i1 + b6-r3-i2 + b6-r3-i3 + b6-r3-i4
- b6-r4-i1 b6-r4-i2 b6-r4-i3 b6-r4-i4 + b6-r4-i1 + b6-r4-i2 + b6-r4-i3 + b6-r4-i4
- b6-r5-i1 b6-r5-i2 b6-r5-i3 b6-r5-i4 + b6-r5-i1 + b6-r5-i2 + b6-r5-i3 + b6-r5-i4
- b7-r1-i1 b7-r1-i2 b7-r1-i3 b7-r1-i4 + b7-r1-i1 + b7-r1-i2 + b7-r1-i3 + b7-r1-i4
- b7-r2-i1 b7-r2-i2 b7-r2-i3 b7-r2-i4 + b7-r2-i1 + b7-r2-i2 + b7-r2-i3 + b7-r2-i4
- b7-r3-i1 b7-r3-i2 b7-r3-i3 b7-r3-i4 + b7-r3-i1 + b7-r3-i2 + b7-r3-i3 + b7-r3-i4
- b7-r4-i1 b7-r4-i2 b7-r4-i3 b7-r4-i4 + b7-r4-i1 + b7-r4-i2 + b7-r4-i3 + b7-r4-i4
- b7-r5-i1 b7-r5-i2 b7-r5-i3 b7-r5-i4 + b7-r5-i1 + b7-r5-i2 + b7-r5-i3 + b7-r5-i4
- b8-r1-i1 b8-r1-i2 b8-r1-i3 b8-r1-i4 + b8-r1-i1 + b8-r1-i2 + b8-r1-i3 + b8-r1-i4
- b8-r2-i1 b8-r2-i2 b8-r2-i3 b8-r2-i4 + b8-r2-i1 + b8-r2-i2 + b8-r2-i3 + b8-r2-i4
- b8-r3-i1 b8-r3-i2 b8-r3-i3 b8-r3-i4 + b8-r3-i1 + b8-r3-i2 + b8-r3-i3 + b8-r3-i4
- b8-r4-i1 b8-r4-i2 b8-r4-i3 b8-r4-i4 + b8-r4-i1 + b8-r4-i2 + b8-r4-i3 + b8-r4-i4
- b8-r5-i1 b8-r5-i2 b8-r5-i3 b8-r5-i4 + b8-r5-i1 + b8-r5-i2 + b8-r5-i3 + b8-r5-i4
- b9-f1-i1 b9-f1-i2 b9-f1-i3 b9-f1-i4 + b9-f1-i1 + b9-f1-i2 + b9-f1-i3 + b9-f1-i4
- b9-f2-i1 b9-f2-i2 b9-f2-i3 b9-f2-i4 + b9-f2-i1 + b9-f2-i2 + b9-f2-i3 + b9-f2-i4
- b9-f3-i1 b9-f3-i2 b9-f3-i3 b9-f3-i4 + b9-f3-i1 + b9-f3-i2 + b9-f3-i3 + b9-f3-i4
- b9-f4-i1 b9-f4-i2 b9-f4-i3 b9-f4-i4 + b9-f4-i1 + b9-f4-i2 + b9-f4-i3 + b9-f4-i4
- b9-f5-i1 b9-f5-i2 b9-f5-i3 b9-f5-i4 + b9-f5-i1 + b9-f5-i2 + b9-f5-i3 + b9-f5-i4
- b10-f1-i1 b10-f1-i2 b10-f1-i3 b10-f1-i4 + b10-f1-i1 + b10-f1-i2 + b10-f1-i3 + b10-f1-i4
- b10-f2-i1 b10-f2-i2 b10-f2-i3 b10-f2-i4 + b10-f2-i1 + b10-f2-i2 + b10-f2-i3 + b10-f2-i4
- b10-f3-i1 b10-f3-i2 b10-f3-i3 b10-f3-i4 + b10-f3-i1 + b10-f3-i2 + b10-f3-i3 + b10-f3-i4
- b10-f4-i1 b10-f4-i2 b10-f4-i3 b10-f4-i4 + b10-f4-i1 + b10-f4-i2 + b10-f4-i3 + b10-f4-i4
- b10-f5-i1 b10-f5-i2 b10-f5-i3 b10-f5-i4 + b10-f5-i1 + b10-f5-i2 + b10-f5-i3 + b10-f5-i4
- b11-f1-i1 b11-f1-i2 b11-f1-i3 b11-f1-i4 + b11-f1-i1 + b11-f1-i2 + b11-f1-i3 + b11-f1-i4
- b11-f2-i1 b11-f2-i2 b11-f2-i3 b11-f2-i4 + b11-f2-i1 + b11-f2-i2 + b11-f2-i3 + b11-f2-i4
- b11-f3-i1 b11-f3-i2 b11-f3-i3 b11-f3-i4 + b11-f3-i1 + b11-f3-i2 + b11-f3-i3 + b11-f3-i4
- b11-f4-i1 b11-f4-i2 b11-f4-i3 b11-f4-i4 + b11-f4-i1 + b11-f4-i2 + b11-f4-i3 + b11-f4-i4
- b11-f5-i1 b11-f5-i2 b11-f5-i3 b11-f5-i4 + b11-f5-i1 + b11-f5-i2 + b11-f5-i3 + b11-f5-i4
- b12-l1-i1 b12-l1-i2 b12-l1-i3 b12-l1-i4 + b12-l1-i1 + b12-l1-i2 + b12-l1-i3 + b12-l1-i4
- b12-l2-i1 b12-l2-i2 b12-l2-i3 b12-l2-i4 + b12-l2-i1 + b12-l2-i2 + b12-l2-i3 + b12-l2-i4
- b12-l3-i1 b12-l3-i2 b12-l3-i3 b12-l3-i4 + b12-l3-i1 + b12-l3-i2 + b12-l3-i3 + b12-l3-i4
- b12-l4-i1 b12-l4-i2 b12-l4-i3 b12-l4-i4 + b12-l4-i1 + b12-l4-i2 + b12-l4-i3 + b12-l4-i4
- b12-l5-i1 b12-l5-i2 b12-l5-i3 b12-l5-i4 + b12-l5-i1 + b12-l5-i2 + b12-l5-i3 + b12-l5-i4 +
+
+ +
+
+ b13-f1-i1 + b13-f1-i2 + b13-f1-i3 + b13-f1-i4 +
+
+ b13-f2-i1 + b13-f2-i2 + b13-f2-i3 + b13-f2-i4 +
+
+ b13-f3-i1 + b13-f3-i2 + b13-f3-i3 + b13-f3-i4 +
+
+ b13-f4-i1 + b13-f4-i2 + b13-f4-i3 + b13-f4-i4 +
+
+ b13-f5-i1 + b13-f5-i2 + b13-f5-i3 + b13-f5-i4
@@ -531,10 +761,6 @@ console.log(res); /* All these test cases are on the edge of pass and fail. If we ever revisit them, these are the ones could be improved on: - - - False positives: - - b5-r3-i1, b5-r3-i2 - - b7-r3-i1, b7-r3-i2 - False negatives: - b3-r1-i1 - b4-r1-i1, b4-r2-i1 diff --git a/test/integration/rules/color-contrast/text-shadows.json b/test/integration/rules/color-contrast/text-shadows.json index 6cc3e775da..81674e45e4 100644 --- a/test/integration/rules/color-contrast/text-shadows.json +++ b/test/integration/rules/color-contrast/text-shadows.json @@ -5,269 +5,291 @@ ["#sample1"], ["#sample2"], - ["#b1 > .r1 > i:nth-child(3)"], - ["#b1 > .r1 > i:nth-child(4)"], - ["#b1 > .r2 > i:nth-child(2)"], - ["#b1 > .r2 > i:nth-child(3)"], - ["#b1 > .r2 > i:nth-child(4)"], - ["#b1 > .r3 > i:nth-child(1)"], - ["#b1 > .r3 > i:nth-child(2)"], - ["#b1 > .r3 > i:nth-child(3)"], - ["#b1 > .r3 > i:nth-child(4)"], + ["#b1-r1-i4"], + ["#b1-r2-i2"], + ["#b1-r2-i3"], + ["#b1-r2-i4"], + ["#b1-r3-i1"], + ["#b1-r3-i2"], + ["#b1-r3-i3"], + ["#b1-r3-i4"], - ["#b2 > .r3 > i:nth-child(1)"], - ["#b2 > .r3 > i:nth-child(2)"], - ["#b2 > .r3 > i:nth-child(3)"], - ["#b2 > .r3 > i:nth-child(4)"], - ["#b2 > .r4 > i:nth-child(4)"], - ["#b2 > .r4 > i:nth-child(1)"], - ["#b2 > .r4 > i:nth-child(2)"], - ["#b2 > .r4 > i:nth-child(3)"], - ["#b2 > .r5 > i:nth-child(3)"], - ["#b2 > .r5 > i:nth-child(4)"], + ["#b2-r3-i1"], + ["#b2-r3-i2"], + ["#b2-r3-i3"], + ["#b2-r3-i4"], + ["#b2-r4-i4"], + ["#b2-r4-i1"], + ["#b2-r4-i2"], + ["#b2-r4-i3"], + ["#b2-r5-i4"], - ["#b3 > .r1 > i:nth-child(3)"], - ["#b3 > .r1 > i:nth-child(4)"], - ["#b3 > .r2 > i:nth-child(1)"], - ["#b3 > .r2 > i:nth-child(2)"], - ["#b3 > .r2 > i:nth-child(3)"], - ["#b3 > .r2 > i:nth-child(4)"], - ["#b3 > .r3 > i:nth-child(1)"], - ["#b3 > .r3 > i:nth-child(2)"], - ["#b3 > .r3 > i:nth-child(3)"], - ["#b3 > .r3 > i:nth-child(4)"], - ["#b3 > .r4 > i:nth-child(3)"], - ["#b3 > .r4 > i:nth-child(4)"], + ["#b3-r1-i3"], + ["#b3-r1-i4"], + ["#b3-r2-i1"], + ["#b3-r2-i2"], + ["#b3-r2-i3"], + ["#b3-r2-i4"], + ["#b3-r3-i1"], + ["#b3-r3-i2"], + ["#b3-r3-i3"], + ["#b3-r3-i4"], + ["#b3-r4-i3"], + ["#b3-r4-i4"], - ["#b4 > .r2 > i:nth-child(2)"], - ["#b4 > .r2 > i:nth-child(3)"], - ["#b4 > .r2 > i:nth-child(4)"], - ["#b4 > .r3 > i:nth-child(1)"], - ["#b4 > .r3 > i:nth-child(2)"], - ["#b4 > .r3 > i:nth-child(3)"], - ["#b4 > .r3 > i:nth-child(4)"], - ["#b4 > .r4 > i:nth-child(1)"], - ["#b4 > .r4 > i:nth-child(2)"], - ["#b4 > .r4 > i:nth-child(3)"], - ["#b4 > .r4 > i:nth-child(4)"], - ["#b4 > .r5 > i:nth-child(1)"], - ["#b4 > .r5 > i:nth-child(2)"], - ["#b4 > .r5 > i:nth-child(3)"], - ["#b4 > .r5 > i:nth-child(4)"], + ["#b4-r2-i2"], + ["#b4-r2-i3"], + ["#b4-r2-i4"], + ["#b4-r3-i1"], + ["#b4-r3-i2"], + ["#b4-r3-i3"], + ["#b4-r3-i4"], + ["#b4-r4-i1"], + ["#b4-r4-i2"], + ["#b4-r4-i3"], + ["#b4-r4-i4"], + ["#b4-r5-i1"], + ["#b4-r5-i2"], + ["#b4-r5-i3"], + ["#b4-r5-i4"], - ["#b5 > .r3 > i:nth-child(1)"], - ["#b5 > .r3 > i:nth-child(2)"], - ["#b5 > .r3 > i:nth-child(3)"], - ["#b5 > .r3 > i:nth-child(4)"], - ["#b5 > .r4 > i:nth-child(3)"], - ["#b5 > .r4 > i:nth-child(4)"], - ["#b3 > .r4 > i:nth-child(1)"], - ["#b3 > .r4 > i:nth-child(2)"], - ["#b5 > .r5 > i:nth-child(3)"], - ["#b5 > .r5 > i:nth-child(4)"], + ["#b5-r3-i4"], + ["#b5-r4-i4"], + ["#b3-r4-i1"], + ["#b3-r4-i2"], + ["#b5-r5-i4"], - ["#b6 > .r8 > i:nth-child(1)"], - ["#b6 > .r8 > i:nth-child(2)"], - ["#b6 > .r8 > i:nth-child(3)"], - ["#b6 > .r9 > i:nth-child(1)"], - ["#b6 > .r9 > i:nth-child(2)"], - ["#b6 > .r9 > i:nth-child(3)"], - ["#b6 > .r9 > i:nth-child(4)"], - ["#b6 > .r10 > i:nth-child(3)"], - ["#b6 > .r10 > i:nth-child(4)"], + ["#b6-r3-i1"], + ["#b6-r3-i2"], + ["#b6-r3-i3"], + ["#b6-r4-i1"], + ["#b6-r4-i2"], + ["#b6-r4-i3"], + ["#b6-r4-i4"], + ["#b6-r5-i4"], - ["#b7 > .r8 > i:nth-child(1)"], - ["#b7 > .r8 > i:nth-child(2)"], - ["#b7 > .r9 > i:nth-child(3)"], - ["#b7 > .r9 > i:nth-child(4)"], - ["#b7 > .r10 > i:nth-child(3)"], - ["#b7 > .r10 > i:nth-child(4)"], + ["#b7-r4-i4"], + ["#b7-r5-i4"], - ["#b8 > .r6 > i:nth-child(3)"], - ["#b8 > .r6 > i:nth-child(4)"], - ["#b8 > .r7 > i:nth-child(1)"], - ["#b8 > .r7 > i:nth-child(2)"], - ["#b8 > .r7 > i:nth-child(3)"], - ["#b8 > .r7 > i:nth-child(4)"], - ["#b8 > .r8 > i:nth-child(1)"], - ["#b8 > .r8 > i:nth-child(2)"], - ["#b8 > .r8 > i:nth-child(3)"], - ["#b8 > .r8 > i:nth-child(4)"], - ["#b8 > .r9 > i:nth-child(1)"], - ["#b8 > .r9 > i:nth-child(2)"], - ["#b8 > .r9 > i:nth-child(3)"], - ["#b8 > .r9 > i:nth-child(4)"], - ["#b8 > .r10 > i:nth-child(3)"], - ["#b8 > .r10 > i:nth-child(4)"], + ["#b8-r1-i4"], + ["#b8-r2-i1"], + ["#b8-r2-i2"], + ["#b8-r2-i3"], + ["#b8-r2-i4"], + ["#b8-r3-i1"], + ["#b8-r3-i2"], + ["#b8-r3-i3"], + ["#b8-r3-i4"], + ["#b8-r4-i1"], + ["#b8-r4-i2"], + ["#b8-r4-i3"], + ["#b8-r4-i4"], + ["#b8-r5-i4"], - ["#b9 > .f1 > i:nth-child(1)"], - ["#b9 > .f1 > i:nth-child(2)"], - ["#b9 > .f1 > i:nth-child(3)"], - ["#b9 > .f1 > i:nth-child(4)"], - ["#b9 > .f2 > i:nth-child(1)"], - ["#b9 > .f2 > i:nth-child(2)"], - ["#b9 > .f2 > i:nth-child(3)"], - ["#b9 > .f2 > i:nth-child(4)"], - ["#b9 > .f3 > i:nth-child(1)"], - ["#b9 > .f3 > i:nth-child(2)"], - ["#b9 > .f3 > i:nth-child(3)"], - ["#b9 > .f3 > i:nth-child(4)"], + ["#b9-f1-i1"], + ["#b9-f1-i2"], + ["#b9-f1-i3"], + ["#b9-f1-i4"], + ["#b9-f2-i1"], + ["#b9-f2-i2"], + ["#b9-f2-i3"], + ["#b9-f2-i4"], + ["#b9-f3-i1"], + ["#b9-f3-i2"], + ["#b9-f3-i3"], + ["#b9-f3-i4"], - ["#b10 > .f1 > i:nth-child(3)"], - ["#b10 > .f1 > i:nth-child(4)"], - ["#b10 > .f2 > i:nth-child(3)"], - ["#b10 > .f2 > i:nth-child(4)"], - ["#b10 > .f3 > i:nth-child(3)"], - ["#b10 > .f3 > i:nth-child(4)"], - ["#b10 > .f4 > i:nth-child(3)"], - ["#b10 > .f4 > i:nth-child(4)"], - ["#b10 > .f5 > i:nth-child(3)"], - ["#b10 > .f5 > i:nth-child(4)"], + ["#b10-f1-i4"], + ["#b10-f2-i4"], + ["#b10-f3-i4"], + ["#b10-f4-i4"], + ["#b10-f5-i4"], - ["#b11 > .f1 > i:nth-child(1)"], - ["#b11 > .f1 > i:nth-child(2)"], - ["#b11 > .f2 > i:nth-child(1)"], - ["#b11 > .f2 > i:nth-child(2)"], - ["#b11 > .f3 > i:nth-child(1)"], - ["#b11 > .f3 > i:nth-child(2)"], + ["#b11-f1-i1"], + ["#b11-f1-i2"], + ["#b11-f1-i3"], + ["#b11-f2-i1"], + ["#b11-f2-i2"], + ["#b11-f2-i3"], + ["#b11-f3-i1"], + ["#b11-f3-i2"], + ["#b11-f3-i3"], - [".l1 > i:nth-child(1)"], - [".l2 > i:nth-child(1)"], - [".l3 > i:nth-child(1)"], - [".l4 > i:nth-child(1)"], - [".l5 > i:nth-child(1)"], - [".l4 > i:nth-child(2)"], - [".l5 > i:nth-child(2)"], - [".l5 > i:nth-child(3)"] + ["#b12-l1-i1"], + ["#b12-l2-i1"], + ["#b12-l3-i1"], + ["#b12-l4-i1"], + ["#b12-l5-i1"], + ["#b12-l4-i2"], + ["#b12-l5-i2"], + ["#b12-l5-i3"], + + ["#b13-f1-i3"], + ["#b13-f1-i4"], + ["#b13-f2-i3"], + ["#b13-f2-i4"], + ["#b13-f3-i3"], + ["#b13-f3-i4"] ], "passes": [ - ["#b1 > .r1 > i:nth-child(1)"], - ["#b1 > .r1 > i:nth-child(2)"], - ["#b1 > .r2 > i:nth-child(1)"], - ["#b1 > .r4 > i:nth-child(1)"], - ["#b1 > .r4 > i:nth-child(2)"], - ["#b1 > .r4 > i:nth-child(3)"], - ["#b1 > .r4 > i:nth-child(4)"], - ["#b1 > .r5 > i:nth-child(1)"], - ["#b1 > .r5 > i:nth-child(2)"], - ["#b1 > .r5 > i:nth-child(3)"], - ["#b1 > .r5 > i:nth-child(4)"], + ["#b1-r1-i1"], + ["#b1-r1-i2"], + ["#b1-r2-i1"], + ["#b1-r4-i1"], + ["#b1-r4-i2"], + ["#b1-r4-i3"], + ["#b1-r4-i4"], + ["#b1-r5-i1"], + ["#b1-r5-i2"], + ["#b1-r5-i3"], + ["#b1-r5-i4"], + + ["#b2-r1-i1"], + ["#b2-r1-i2"], + ["#b2-r1-i3"], + ["#b2-r1-i4"], + ["#b2-r2-i1"], + ["#b2-r2-i2"], + ["#b2-r2-i3"], + ["#b2-r2-i4"], + ["#b2-r5-i1"], + ["#b2-r5-i2"], + ["#b2-r5-i3"], - ["#b2 > .r1 > i:nth-child(1)"], - ["#b2 > .r1 > i:nth-child(2)"], - ["#b2 > .r1 > i:nth-child(3)"], - ["#b2 > .r1 > i:nth-child(4)"], - ["#b2 > .r2 > i:nth-child(1)"], - ["#b2 > .r2 > i:nth-child(2)"], - ["#b2 > .r2 > i:nth-child(3)"], - ["#b2 > .r2 > i:nth-child(4)"], - ["#b2 > .r5 > i:nth-child(1)"], - ["#b2 > .r5 > i:nth-child(2)"], + ["#b3-r1-i1"], + ["#b3-r1-i2"], + ["#b1-r1-i3"], + ["#b3-r5-i1"], + ["#b3-r5-i2"], + ["#b3-r5-i3"], + ["#b3-r5-i4"], - ["#b3 > .r1 > i:nth-child(1)"], - ["#b3 > .r1 > i:nth-child(2)"], - ["#b3 > .r5 > i:nth-child(1)"], - ["#b3 > .r5 > i:nth-child(2)"], - ["#b3 > .r5 > i:nth-child(3)"], - ["#b3 > .r5 > i:nth-child(4)"], + ["#b4-r1-i1"], + ["#b4-r1-i2"], + ["#b4-r1-i3"], + ["#b4-r1-i4"], + ["#b4-r2-i1"], - ["#b4 > .r1 > i:nth-child(1)"], - ["#b4 > .r1 > i:nth-child(2)"], - ["#b4 > .r1 > i:nth-child(3)"], - ["#b4 > .r1 > i:nth-child(4)"], - ["#b4 > .r2 > i:nth-child(1)"], + ["#b5-r1-i1"], + ["#b5-r1-i2"], + ["#b5-r1-i3"], + ["#b5-r1-i4"], + ["#b5-r2-i1"], + ["#b5-r2-i2"], + ["#b5-r2-i3"], + ["#b5-r2-i4"], + ["#b5-r3-i1"], + ["#b5-r3-i2"], + ["#b5-r3-i3"], + ["#b5-r4-i1"], + ["#b5-r4-i2"], + ["#b5-r4-i3"], + ["#b5-r5-i1"], + ["#b5-r5-i2"], + ["#b5-r5-i3"], - ["#b5 > .r1 > i:nth-child(1)"], - ["#b5 > .r1 > i:nth-child(2)"], - ["#b5 > .r1 > i:nth-child(3)"], - ["#b5 > .r1 > i:nth-child(4)"], - ["#b5 > .r2 > i:nth-child(1)"], - ["#b5 > .r2 > i:nth-child(2)"], - ["#b5 > .r2 > i:nth-child(3)"], - ["#b5 > .r2 > i:nth-child(4)"], - ["#b5 > .r4 > i:nth-child(1)"], - ["#b5 > .r4 > i:nth-child(2)"], - ["#b5 > .r5 > i:nth-child(1)"], - ["#b5 > .r5 > i:nth-child(2)"], + ["#b6-r1-i1"], + ["#b6-r1-i2"], + ["#b6-r1-i3"], + ["#b6-r1-i4"], + ["#b6-r2-i1"], + ["#b6-r2-i2"], + ["#b6-r2-i3"], + ["#b6-r2-i4"], + ["#b6-r3-i4"], + ["#b6-r5-i1"], + ["#b6-r5-i2"], + ["#b6-r5-i3"], - ["#b6 > .r6 > i:nth-child(1)"], - ["#b6 > .r6 > i:nth-child(2)"], - ["#b6 > .r6 > i:nth-child(3)"], - ["#b6 > .r6 > i:nth-child(4)"], - ["#b6 > .r7 > i:nth-child(1)"], - ["#b6 > .r7 > i:nth-child(2)"], - ["#b6 > .r7 > i:nth-child(3)"], - ["#b6 > .r7 > i:nth-child(4)"], - ["#b6 > .r8 > i:nth-child(4)"], - ["#b6 > .r10 > i:nth-child(1)"], - ["#b6 > .r10 > i:nth-child(2)"], + ["#b7-r1-i1"], + ["#b7-r1-i2"], + ["#b7-r1-i3"], + ["#b7-r1-i4"], + ["#b7-r2-i1"], + ["#b7-r2-i2"], + ["#b7-r2-i3"], + ["#b7-r2-i4"], + ["#b7-r3-i1"], + ["#b7-r3-i2"], + ["#b7-r3-i3"], + ["#b7-r3-i4"], + ["#b7-r4-i1"], + ["#b7-r4-i2"], + ["#b7-r4-i3"], + ["#b7-r5-i1"], + ["#b7-r5-i2"], + ["#b7-r5-i3"], - ["#b7 > .r6 > i:nth-child(1)"], - ["#b7 > .r6 > i:nth-child(2)"], - ["#b7 > .r6 > i:nth-child(3)"], - ["#b7 > .r6 > i:nth-child(4)"], - ["#b7 > .r7 > i:nth-child(1)"], - ["#b7 > .r7 > i:nth-child(2)"], - ["#b7 > .r7 > i:nth-child(3)"], - ["#b7 > .r7 > i:nth-child(4)"], - ["#b7 > .r8 > i:nth-child(3)"], - ["#b7 > .r8 > i:nth-child(4)"], - ["#b7 > .r9 > i:nth-child(1)"], - ["#b7 > .r9 > i:nth-child(2)"], - ["#b7 > .r10 > i:nth-child(1)"], - ["#b7 > .r10 > i:nth-child(2)"], + ["#b8-r1-i1"], + ["#b8-r1-i2"], + ["#b8-r1-i3"], + ["#b8-r5-i1"], + ["#b8-r5-i2"], + ["#b8-r5-i3"], - ["#b8 > .r6 > i:nth-child(1)"], - ["#b8 > .r6 > i:nth-child(2)"], - ["#b8 > .r10 > i:nth-child(1)"], - ["#b8 > .r10 > i:nth-child(2)"], + ["#b9-f4-i1"], + ["#b9-f4-i2"], + ["#b9-f4-i3"], + ["#b9-f4-i4"], + ["#b9-f5-i1"], + ["#b9-f5-i2"], + ["#b9-f5-i3"], + ["#b9-f5-i4"], - ["#b9 > .f4 > i:nth-child(1)"], - ["#b9 > .f4 > i:nth-child(2)"], - ["#b9 > .f4 > i:nth-child(3)"], - ["#b9 > .f4 > i:nth-child(4)"], - ["#b9 > .f5 > i:nth-child(1)"], - ["#b9 > .f5 > i:nth-child(2)"], - ["#b9 > .f5 > i:nth-child(3)"], - ["#b9 > .f5 > i:nth-child(4)"], + ["#b10-f1-i1"], + ["#b10-f1-i2"], + ["#b10-f1-i3"], + ["#b10-f2-i1"], + ["#b10-f2-i2"], + ["#b10-f2-i3"], + ["#b10-f3-i1"], + ["#b10-f3-i2"], + ["#b10-f3-i3"], + ["#b10-f4-i1"], + ["#b10-f4-i2"], + ["#b10-f4-i3"], + ["#b10-f5-i1"], + ["#b10-f5-i2"], + ["#b10-f5-i3"], - ["#b10 > .f1 > i:nth-child(1)"], - ["#b10 > .f1 > i:nth-child(2)"], - ["#b10 > .f2 > i:nth-child(1)"], - ["#b10 > .f2 > i:nth-child(2)"], - ["#b10 > .f3 > i:nth-child(1)"], - ["#b10 > .f3 > i:nth-child(2)"], - ["#b10 > .f4 > i:nth-child(1)"], - ["#b10 > .f4 > i:nth-child(2)"], - ["#b10 > .f5 > i:nth-child(1)"], - ["#b10 > .f5 > i:nth-child(2)"], + ["#b11-f1-i4"], + ["#b11-f2-i4"], + ["#b11-f3-i4"], + ["#b11-f4-i3"], + ["#b11-f4-i4"], + ["#b11-f4-i1"], + ["#b11-f4-i2"], + ["#b11-f5-i3"], + ["#b11-f5-i4"], + ["#b11-f5-i1"], + ["#b11-f5-i2"], - ["#b11 > .f1 > i:nth-child(3)"], - ["#b11 > .f1 > i:nth-child(4)"], - ["#b11 > .f2 > i:nth-child(3)"], - ["#b11 > .f2 > i:nth-child(4)"], - ["#b11 > .f3 > i:nth-child(3)"], - ["#b11 > .f3 > i:nth-child(4)"], - ["#b11 > .f4 > i:nth-child(3)"], - ["#b11 > .f4 > i:nth-child(4)"], - ["#b11 > .f4 > i:nth-child(1)"], - ["#b11 > .f4 > i:nth-child(2)"], - ["#b11 > .f5 > i:nth-child(3)"], - ["#b11 > .f5 > i:nth-child(4)"], - ["#b11 > .f5 > i:nth-child(1)"], - ["#b11 > .f5 > i:nth-child(2)"], + ["#b12-l1-i2"], + ["#b12-l1-i3"], + ["#b12-l1-i4"], + ["#b12-l2-i2"], + ["#b12-l2-i3"], + ["#b12-l2-i4"], + ["#b12-l3-i2"], + ["#b12-l3-i3"], + ["#b12-l3-i4"], + ["#b12-l4-i3"], + ["#b12-l4-i4"], + ["#b12-l5-i4"], - [".l1 > i:nth-child(2)"], - [".l1 > i:nth-child(3)"], - [".l1 > i:nth-child(4)"], - [".l2 > i:nth-child(2)"], - [".l2 > i:nth-child(3)"], - [".l2 > i:nth-child(4)"], - [".l3 > i:nth-child(2)"], - [".l3 > i:nth-child(3)"], - [".l3 > i:nth-child(4)"], - [".l4 > i:nth-child(3)"], - [".l4 > i:nth-child(4)"], - [".l5 > i:nth-child(4)"] + ["#b13-f1-i1"], + ["#b13-f1-i2"], + ["#b13-f2-i1"], + ["#b13-f2-i2"], + ["#b13-f3-i1"], + ["#b13-f3-i2"], + ["#b13-f4-i1"], + ["#b13-f4-i2"], + ["#b13-f4-i3"], + ["#b13-f4-i4"], + ["#b13-f5-i1"], + ["#b13-f5-i2"], + ["#b13-f5-i3"], + ["#b13-f5-i4"] ] } diff --git a/test/integration/rules/image-alt/image-alt.html b/test/integration/rules/image-alt/image-alt.html index b1226b92ba..a34644a736 100644 --- a/test/integration/rules/image-alt/image-alt.html +++ b/test/integration/rules/image-alt/image-alt.html @@ -23,3 +23,13 @@ + + + + + + + + + + diff --git a/test/integration/rules/image-alt/image-alt.json b/test/integration/rules/image-alt/image-alt.json index d55d112a77..418e9f6137 100644 --- a/test/integration/rules/image-alt/image-alt.json +++ b/test/integration/rules/image-alt/image-alt.json @@ -13,7 +13,11 @@ ["#violation9"], ["#violation10"], ["#violation11"], - ["#violation12"] + ["#violation12"], + ["#violation13"], + ["#violation14"], + ["#violation15"], + ["#violation16"] ], "passes": [ ["#pass1"], @@ -23,6 +27,9 @@ ["#pass5"], ["#pass6"], ["#pass7"], - ["#pass8"] + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] } diff --git a/test/integration/rules/listitem/listitem.html b/test/integration/rules/listitem/listitem.html index 71fbfb6d55..77fea1a212 100644 --- a/test/integration/rules/listitem/listitem.html +++ b/test/integration/rules/listitem/listitem.html @@ -19,3 +19,7 @@
  1. I too do not belong to a list.
+ + + + diff --git a/test/integration/rules/listitem/listitem.json b/test/integration/rules/listitem/listitem.json index 462226fdd1..3d9bd49139 100644 --- a/test/integration/rules/listitem/listitem.json +++ b/test/integration/rules/listitem/listitem.json @@ -2,5 +2,5 @@ "description": "listitem test", "rule": "listitem", "violations": [["#uncontained"], ["#ulrolechanged"], ["#olrolechanged"]], - "passes": [["#contained"], ["#alsocontained"], ["#presentation"], ["#none"]] + "passes": [["#contained"], ["#alsocontained"], ["#presentation"], ["#none"], ["#menuitem"]] } diff --git a/test/integration/rules/nested-interactive/nested-interactive.html b/test/integration/rules/nested-interactive/nested-interactive.html index beb7fbdf37..1af908b261 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -1,14 +1,18 @@ -
pass
- - - + +
pass
+ + + + - -
- - - + +
+ + + + + ignored ignored diff --git a/test/integration/rules/nested-interactive/nested-interactive.json b/test/integration/rules/nested-interactive/nested-interactive.json index 78d3b43176..4db1a12cdb 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.json +++ b/test/integration/rules/nested-interactive/nested-interactive.json @@ -1,13 +1,24 @@ { "description": "nested-interactive tests", "rule": "nested-interactive", - "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]], + "violations": [ + ["#fail1"], + ["#fail2"], + ["#fail3"], + ["#fail4"], + ["#fail5"] + ], "passes": [ ["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"], - ["#pass6"] + ["#pass6"], + ["#pass7"], + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] } diff --git a/test/integration/rules/scrollable-region-focusable/scrollable-region-focusable.html b/test/integration/rules/scrollable-region-focusable/scrollable-region-focusable.html index 7ee3285851..c920d8e570 100644 --- a/test/integration/rules/scrollable-region-focusable/scrollable-region-focusable.html +++ b/test/integration/rules/scrollable-region-focusable/scrollable-region-focusable.html @@ -126,3 +126,9 @@
+ +
+
+

Content

+
+
\ No newline at end of file diff --git a/test/integration/virtual-rules/aria-allowed-attr.js b/test/integration/virtual-rules/aria-allowed-attr.js index ea3082b6ad..b53d188397 100644 --- a/test/integration/virtual-rules/aria-allowed-attr.js +++ b/test/integration/virtual-rules/aria-allowed-attr.js @@ -42,11 +42,11 @@ describe('aria-allowed-attr virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it.skip('should pass for element with no role', function() { + it('should pass for global attributes and element with no role', function() { var results = axe.runVirtualRule('aria-allowed-attr', { nodeName: 'div', attributes: { - 'aria-checked': true + 'aria-busy': true } }); @@ -55,6 +55,19 @@ describe('aria-allowed-attr virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); + it('should fail for non-global attributes and element with no role', function() { + var results = axe.runVirtualRule('aria-allowed-attr', { + nodeName: 'div', + attributes: { + 'aria-checked': true + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + it('should fail for unallowed attributes', function() { var results = axe.runVirtualRule('aria-allowed-attr', { nodeName: 'div', @@ -106,4 +119,17 @@ describe('aria-allowed-attr virtual-rule', function() { assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); + + it('should incomplete for non-global attributes and custom element', function() { + var results = axe.runVirtualRule('aria-allowed-attr', { + nodeName: 'custom-elm1', + attributes: { + 'aria-checked': true + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); }); diff --git a/test/integration/virtual-rules/aria-allowed-role.js b/test/integration/virtual-rules/aria-allowed-role.js index 5f59b2f943..9b579e56e3 100644 --- a/test/integration/virtual-rules/aria-allowed-role.js +++ b/test/integration/virtual-rules/aria-allowed-role.js @@ -17,19 +17,6 @@ describe('aria-allowed-role virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it.skip('should pass for element with no role', function() { - var results = axe.runVirtualRule('aria-allowed-attr', { - nodeName: 'div', - attributes: { - 'aria-checked': true - } - }); - - assert.lengthOf(results.passes, 1); - assert.lengthOf(results.violations, 0); - assert.lengthOf(results.incomplete, 0); - }); - it('should fail for unallowed role', function() { var results = axe.runVirtualRule('aria-allowed-role', { nodeName: 'dd', diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index ba963b99b5..45eebb9c7b 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -38,6 +38,26 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); + it('should pass for element with non-widget content which has negative tabindex', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'button' + }); + var child = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + tabindex: -1 + } + }); + child.children = []; + node.children = [child]; + + var results = axe.runVirtualRule('nested-interactive', node); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + it('should pass for empty element without', function() { var node = new axe.SerialVirtualNode({ nodeName: 'div', @@ -54,7 +74,7 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail for element with focusable content', function() { + it('should pass for element with non-widget content', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); @@ -69,12 +89,12 @@ describe('nested-interactive virtual-rule', function() { var results = axe.runVirtualRule('nested-interactive', node); - assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 0); }); - it('should fail for element with natively focusable content', function() { + it('should fail for element with native widget content', function() { var node = new axe.SerialVirtualNode({ nodeName: 'div', attributes: { @@ -89,7 +109,7 @@ describe('nested-interactive virtual-rule', function() { var results = axe.runVirtualRule('nested-interactive', node); - assert.lengthOf(results.passes, 0); + assert.lengthOf(results.passes, 1); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); diff --git a/test/node/jsdom.js b/test/node/jsdom.js index e88b1393cb..8e03bd5ffd 100644 --- a/test/node/jsdom.js +++ b/test/node/jsdom.js @@ -12,6 +12,8 @@ var domStr = '' + '' + 'Hello' + + 'Main' + + '' + '' + ''; @@ -58,4 +60,104 @@ describe('jsdom axe-core', function() { assert.strictEqual(audit.allowedOrigins.length, 0); }); }); + + describe('isCurrentPageLink', function() { + // because axe only sets the window global when calling axe.run, + // we'll have to create a custom rule that calls + // isCurrentPageLink to gain access to the middle of a run with + // the proper window object + afterEach(function() { + axe.teardown(); + }); + + it('should return true if url starts with #', function() { + var dom = new jsdom.JSDOM(domStr); + var anchor = dom.window.document.getElementById('hash-link'); + + axe.configure({ + checks: [ + { + id: 'check-current-page-link', + evaluate: function() { + return axe.commons.dom.isCurrentPageLink(anchor) === true; + } + } + ], + rules: [ + { + id: 'check-current-page-link', + any: ['check-current-page-link'] + } + ] + }); + + return axe + .run(dom.window.document.documentElement, { + runOnly: ['check-current-page-link'] + }) + .then(function(results) { + assert.strictEqual(results.passes.length, 1); + }); + }); + + it('should return null for absolute link when url is not set', function() { + var dom = new jsdom.JSDOM(domStr); + var anchor = dom.window.document.getElementById('skip'); + + axe.configure({ + checks: [ + { + id: 'check-current-page-link', + evaluate: function() { + return axe.commons.dom.isCurrentPageLink(anchor) === null; + } + } + ], + rules: [ + { + id: 'check-current-page-link', + any: ['check-current-page-link'] + } + ] + }); + + return axe + .run(dom.window.document.documentElement, { + runOnly: ['check-current-page-link'] + }) + .then(function(results) { + assert.strictEqual(results.passes.length, 1); + }); + }); + + it('should return true for absolute link when url is set', function() { + var dom = new jsdom.JSDOM(domStr, { url: 'https://page.com' }); + var anchor = dom.window.document.getElementById('skip'); + + axe.configure({ + checks: [ + { + id: 'check-current-page-link', + evaluate: function() { + return axe.commons.dom.isCurrentPageLink(anchor) === true; + } + } + ], + rules: [ + { + id: 'check-current-page-link', + any: ['check-current-page-link'] + } + ] + }); + + return axe + .run(dom.window.document.documentElement, { + runOnly: ['check-current-page-link'] + }) + .then(function(results) { + assert.strictEqual(results.passes.length, 1); + }); + }); + }); }); diff --git a/test/node/uuid.js b/test/node/uuid.js deleted file mode 100644 index 905a114722..0000000000 --- a/test/node/uuid.js +++ /dev/null @@ -1,19 +0,0 @@ -var assert = require('chai').assert; -var sinon = require('sinon'); -var proxyquire = require('proxyquire'); -var crypto = require('crypto'); // Node package - -// 16 byte array, all 0's -var returnVal = new Array(16).fill(0); -var cryptoStub = sinon.stub(crypto, 'randomBytes').returns(returnVal); - -describe('uuid.v4', function() { - var axe = proxyquire('../../', { crypto: cryptoStub }); - var uuidV4 = axe.utils.uuid.v4; - - it('uses node crypto', function() { - var uuid = uuidV4(); - assert.isTrue(cryptoStub.randomBytes.called); - assert.deepEqual(uuid, '00000000-0000-4000-8000-000000000000'); - }); -}); diff --git a/typings/axe-core/axe-core-tests.ts b/typings/axe-core/axe-core-tests.ts index a572c6c095..32f3e67a72 100644 --- a/typings/axe-core/axe-core-tests.ts +++ b/typings/axe-core/axe-core-tests.ts @@ -1,7 +1,7 @@ import * as axe from '../../axe'; var context: any = document; -var $fixture: any = {}; +var $fixture = [document]; var options = { iframes: false, selectors: false, elementRef: false }; axe.run(context, {}, (error: Error, results: axe.AxeResults) => {