diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b27ebbca4..f2f20d6f9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ unix_nightly_box: &unix_nightly_box orbs: puppeteer: threetreeslight/puppeteer@0.1.2 - browser-tools: circleci/browser-tools@1.4.0 + browser-tools: circleci/browser-tools@1.4.4 set_npm_auth: &set_npm_auth run: npm config set "//registry.npmjs.org/:_authToken" $NPM_AUTH @@ -30,6 +30,13 @@ restore_build: &restore_build keys: - v9-cache-build-<< pipeline.git.revision >> +commands: + browser-tools-job: + steps: + - browser-tools/install-browser-tools: + # TODO: remove when chromedriver downloads are fixed + chrome-version: 116.0.5845.96 + jobs: # Fetch and cache dependencies. dependencies_unix: @@ -47,7 +54,7 @@ jobs: else echo "node_modules does not exist" fi - - browser-tools/install-browser-tools + - browser-tools-job - <<: *set_npm_auth - run: npm ci - run: npx browser-driver-manager install chromedriver --verbose @@ -85,7 +92,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -97,7 +104,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - <<: *restore_build - run: npm run test -- --browsers Firefox @@ -109,7 +116,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -121,7 +128,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -133,7 +140,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -145,7 +152,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -157,7 +164,7 @@ jobs: <<: *unix_box steps: - checkout - - browser-tools/install-browser-tools + - browser-tools-job - <<: *restore_dependency_cache_unix - run: npx browser-driver-manager install chromedriver --verbose - <<: *restore_build @@ -192,7 +199,7 @@ jobs: steps: - checkout - <<: *restore_dependency_cache_unix - - browser-tools/install-browser-tools + - browser-tools-job # install ACT rules # install first as for some reason installing a single package # also re-installs all repo dependencies as well @@ -208,7 +215,7 @@ jobs: steps: - checkout - <<: *restore_dependency_cache_unix - - browser-tools/install-browser-tools + - browser-tools-job # install ARIA practices # install first as for some reason installing a single package # also re-installs all repo dependencies as well diff --git a/.eslintrc.js b/.eslintrc.js index 855c45f454..0dd3a87d43 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,7 @@ module.exports = { 'dot-notation': 2, 'no-new-func': 0, 'no-new-wrappers': 0, + 'no-shadow': 2, 'no-restricted-syntax': [ 'error', { diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b3f81f34..5cecc5de4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ 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.8.0](https://github.com/dequelabs/axe-core/compare/v4.7.2...v4.8.0) (2023-09-06) + +### Features + +- add EN.301.549 tags to rules ([#4063](https://github.com/dequelabs/axe-core/issues/4063)) ([de3da89](https://github.com/dequelabs/axe-core/commit/de3da897e56179d94ef8a0dc1a667b5663c489d1)) +- add rule aria-conditional-attr ([#4094](https://github.com/dequelabs/axe-core/issues/4094)) ([d417630](https://github.com/dequelabs/axe-core/commit/d417630e89a41603426c2bb545b49057f03ed8e5)) +- add rule aria-deprecated-role ([#4074](https://github.com/dequelabs/axe-core/issues/4074)) ([03f2771](https://github.com/dequelabs/axe-core/commit/03f2771ab43bd877b7919c29b4f5e737b5a69544)) +- **aria-input/toggle-field-name:** set impact always to serious ([#4095](https://github.com/dequelabs/axe-core/issues/4095)) ([e031d68](https://github.com/dequelabs/axe-core/commit/e031d68652229a80ba6ff7d02d29a50a846bfa5b)) +- **aria-prohibited-attr:** add rule aria-prohibited-attr ([#4088](https://github.com/dequelabs/axe-core/issues/4088)) ([7b115d3](https://github.com/dequelabs/axe-core/commit/7b115d3a9e7256ae2c0a1d7d0f9ba791a06c8599)) +- **checks:** enable help-same-as-label, but remove from rules ([#4096](https://github.com/dequelabs/axe-core/issues/4096)) ([034038a](https://github.com/dequelabs/axe-core/commit/034038a625b390ed25b30fccc96e3fc1f384dbc1)) +- **d.ts:** improve axe.d.ts types ([#4081](https://github.com/dequelabs/axe-core/issues/4081)) ([7c5f991](https://github.com/dequelabs/axe-core/commit/7c5f99143a1d97e294d21e14917f4963013fc6f8)), closes [#3966](https://github.com/dequelabs/axe-core/issues/3966) +- deprecate & disable duplicate-id / duplicate-id-active ([#4071](https://github.com/dequelabs/axe-core/issues/4071)) ([733c45e](https://github.com/dequelabs/axe-core/commit/733c45e6a40a9f8ff6e75f7db864edff0b404ca2)) +- Deprecate impact on checks, use rules instead ([#4114](https://github.com/dequelabs/axe-core/issues/4114)) ([2cc5547](https://github.com/dequelabs/axe-core/commit/2cc5547634ee783701675631ee3978129707e6f0)) +- **duplicate-id-aria:** set to review on fail and tag as wcag412 ([#4075](https://github.com/dequelabs/axe-core/issues/4075)) ([9f1a3e3](https://github.com/dequelabs/axe-core/commit/9f1a3e3cbffbe09eaf90fa254c6421fd4264cf4a)) +- **impact:** aria-roles / aria-valid-attr-value is always "critical" ([#4112](https://github.com/dequelabs/axe-core/issues/4112)) ([5cc8041](https://github.com/dequelabs/axe-core/commit/5cc8041f74a6f015dcbca36ee7414767528277c2)) +- **impact:** scope-attr-valid is always "moderate" ([#4113](https://github.com/dequelabs/axe-core/issues/4113)) ([131f552](https://github.com/dequelabs/axe-core/commit/131f5524e8c8022ace047ac6d69d779460c85fe6)) +- **new-rule:** aria-braille-equivalent finds incorrect uses of aria-braille attributes ([#4107](https://github.com/dequelabs/axe-core/issues/4107)) ([6260a2f](https://github.com/dequelabs/axe-core/commit/6260a2f25781b465960aec0b1e7781be5496c9bd)) +- **page-no-duplicate-banner/contentinfo:** deprecate options.nativeScopeFilter, take into ancestors with sectioning roles ([#4105](https://github.com/dequelabs/axe-core/issues/4105)) ([c6e07be](https://github.com/dequelabs/axe-core/commit/c6e07bec43ef1935f2afb9429e9f12a937c38f14)) + +### Bug Fixes + +- **access-name:** get name from header elements ([#4097](https://github.com/dequelabs/axe-core/issues/4097)) ([fbe99bf](https://github.com/dequelabs/axe-core/commit/fbe99bf87a3ebd7d6bc4b4eca7a58bbff28a5b23)) +- add <search> element semantics ([#4115](https://github.com/dequelabs/axe-core/issues/4115)) ([637bf6c](https://github.com/dequelabs/axe-core/commit/637bf6c58c3e62877511687d8a6046f8aee63f03)) +- **aria-allowed-attr:** pass aria-expanded on checkbox & switch ([#4110](https://github.com/dequelabs/axe-core/issues/4110)) ([fcf76e0](https://github.com/dequelabs/axe-core/commit/fcf76e04d8534dfed75caf1f2c4a74ef4faa29ae)), closes [#3339](https://github.com/dequelabs/axe-core/issues/3339) +- **aria-allowed-role:** Add doc-glossary to allowed roles for aside element ([#4083](https://github.com/dequelabs/axe-core/issues/4083)) ([6ca38f6](https://github.com/dequelabs/axe-core/commit/6ca38f65c28e9df0c429df1018b519394e22507e)) +- **aria-allowed-role:** add meter to allowed roles for named img ([#4055](https://github.com/dequelabs/axe-core/issues/4055)) ([173f29d](https://github.com/dequelabs/axe-core/commit/173f29da9558a1fd0510609aacc9e4deebdf74b4)), closes [#4054](https://github.com/dequelabs/axe-core/issues/4054) +- **aria-required-childen:** test visibility of grandchildren ([#4091](https://github.com/dequelabs/axe-core/issues/4091)) ([a202b69](https://github.com/dequelabs/axe-core/commit/a202b69b955b45fc10abe06059925013bede07eb)) +- **aria-text:** typo in rule description ([#4131](https://github.com/dequelabs/axe-core/issues/4131)) ([85a0e9c](https://github.com/dequelabs/axe-core/commit/85a0e9c358ae78b4ceb2093dc9891d523eaf25b2)) +- **aria-valid-attr-value:** allow empty value on aria-braille\* & aria-valuetext ([#4109](https://github.com/dequelabs/axe-core/issues/4109)) ([c4c3e65](https://github.com/dequelabs/axe-core/commit/c4c3e658408d89b5ccd747d5fad9031c5d3a0de0)) +- avoid memory issues by doing better cleanup ([#4059](https://github.com/dequelabs/axe-core/issues/4059)) ([16c5cfa](https://github.com/dequelabs/axe-core/commit/16c5cfa66615537b2131a5a381fbed9a5336d853)) +- avoid problems from element IDs that exist on object prototype ([#4060](https://github.com/dequelabs/axe-core/issues/4060)) ([8d135dd](https://github.com/dequelabs/axe-core/commit/8d135dd58ccd72393b981464f66a01e770d9cf95)) +- **color-contrast:** correctly handle flex and position ([#4086](https://github.com/dequelabs/axe-core/issues/4086)) ([9d5f496](https://github.com/dequelabs/axe-core/commit/9d5f496c4ee7e95d113cdceab284fb6ca7be98e3)) +- **color-contrast:** get text stoke from offset shadows ([#4079](https://github.com/dequelabs/axe-core/issues/4079)) ([13acffe](https://github.com/dequelabs/axe-core/commit/13acffe540f834f5321f9c5c124b565cec92ce06)) +- **color-contrast:** ignore format unicode characters ([#4102](https://github.com/dequelabs/axe-core/issues/4102)) ([049522e](https://github.com/dequelabs/axe-core/commit/049522e3ef0676b198763e39e8c8a300c8eeb195)) +- **color-contrast:** ignore zero width characters ([#4103](https://github.com/dequelabs/axe-core/issues/4103)) ([4deb0a0](https://github.com/dequelabs/axe-core/commit/4deb0a0876d574c3d7d586b27ae07d4f5be586db)) +- **color-contrast:** process non-rgb color functions ([#4092](https://github.com/dequelabs/axe-core/issues/4092)) ([9634282](https://github.com/dequelabs/axe-core/commit/963428256d7a119c7b6188868eb9d4a4651a8949)) +- **commons/dom/createGrid:** only add the visible, non-overflow areas of an element to the grid ([#4101](https://github.com/dequelabs/axe-core/issues/4101)) ([d77f47b](https://github.com/dequelabs/axe-core/commit/d77f47b8dd346e205b6cddb4f6ce544ef5f699e4)) +- ensure reporter errors can propagate ([#4111](https://github.com/dequelabs/axe-core/issues/4111)) ([080cc1b](https://github.com/dequelabs/axe-core/commit/080cc1b5f5ed048ab435c312dec291d1b4eb4393)) +- ignore stylesheets fetching style tag in jsdom ([#4138](https://github.com/dequelabs/axe-core/issues/4138)) ([d7c16a4](https://github.com/dequelabs/axe-core/commit/d7c16a481d5a5f68c1e970040e01f125b2025378)) +- **jsdom:** allow axe.setup() without a global window ([#4116](https://github.com/dequelabs/axe-core/issues/4116)) ([33b0314](https://github.com/dequelabs/axe-core/commit/33b0314922762c0e562b613219b5cc96e3ce31f5)) +- **target-size:** correctly calculate bounding box ([#4125](https://github.com/dequelabs/axe-core/issues/4125)) ([1494b4c](https://github.com/dequelabs/axe-core/commit/1494b4c2159fbae2a937cc7c3dc1d269915ef4d4)) +- **target-size:** update to match new spacing requirements ([#4117](https://github.com/dequelabs/axe-core/issues/4117)) ([49eaa0e](https://github.com/dequelabs/axe-core/commit/49eaa0e1663724f70b2571cc7393e306bf0c7321)) +- Use correct copyright year ([#4098](https://github.com/dequelabs/axe-core/issues/4098)) ([cab6a2b](https://github.com/dequelabs/axe-core/commit/cab6a2b2f012f5963d0f4294217578c790508fcc)) +- **utils/clone:** don't try to clone elements from different window context ([#4072](https://github.com/dequelabs/axe-core/issues/4072)) ([55000d0](https://github.com/dequelabs/axe-core/commit/55000d066f018e4c3f2b9ec4eabf23eb1781dfbb)) + ### [4.7.2](https://github.com/dequelabs/axe-core/compare/v4.7.1...v4.7.2) (2023-05-25) ### Bug Fixes diff --git a/axe.d.ts b/axe.d.ts index c174b24c0f..403e504873 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -164,9 +164,9 @@ declare namespace axe { interface NodeResult { html: string; impact?: ImpactValue; - target: string[]; + target: UnlabelledFrameSelector; xpath?: string[]; - ancestry?: string[]; + ancestry?: UnlabelledFrameSelector; any: CheckResult[]; all: CheckResult[]; none: CheckResult[]; @@ -181,8 +181,11 @@ declare namespace axe { relatedNodes?: RelatedNode[]; } interface RelatedNode { - target: string[]; html: string; + target: UnlabelledFrameSelector; + xpath?: string[]; + ancestry?: UnlabelledFrameSelector; + element?: HTMLElement; } interface RuleLocale { [key: string]: { @@ -193,7 +196,7 @@ declare namespace axe { interface CheckMessages { pass: string | { [key: string]: string }; fail: string | { [key: string]: string }; - incomplete: string | { [key: string]: string }; + incomplete?: string | { [key: string]: string }; } interface CheckLocale { [key: string]: CheckMessages; @@ -257,10 +260,31 @@ declare namespace axe { brand?: string; application?: string; } + interface CheckHelper { + async: () => (result: boolean | undefined | Error) => void; + data: (data: unknown) => void; + relatedNodes: (nodes: Element[]) => void; + } + interface AfterResult { + id: string; + data?: unknown; + relatedNodes: SerialDqElement[]; + result: boolean | undefined; + node: SerialDqElement; + } interface Check { id: string; - evaluate?: Function | string; - after?: Function | string; + evaluate?: + | string + | (( + this: CheckHelper, + node: Element, + options: unknown, + virtualNode: VirtualNode + ) => boolean | undefined | void); + after?: + | string + | ((results: AfterResult[], options: unknown) => AfterResult[]); options?: any; matches?: string; enabled?: boolean; @@ -280,9 +304,10 @@ declare namespace axe { all?: string[]; none?: string[]; tags?: string[]; - matches?: string; + matches?: string | ((node: Element, virtualNode: VirtualNode) => boolean); reviewOnFail?: boolean; - metadata?: Omit<RuleMetadata, 'ruleId'>; + actIds?: string[]; + metadata?: Omit<RuleMetadata, 'ruleId' | 'tags' | 'actIds'>; } interface AxePlugin { id: string; @@ -346,7 +371,8 @@ declare namespace axe { type AxeReporter<T = unknown> = ( rawResults: RawResult[], option: RunOptions, - callback: (report: T) => void + resolve: (report: T) => void, + reject: (error: Error) => void ) => void; interface VirtualNode { @@ -367,6 +393,42 @@ declare namespace axe { shadowSelect: (selector: CrossTreeSelector) => Element | null; shadowSelectAll: (selector: CrossTreeSelector) => Element[]; getStandards(): Required<Standards>; + DqElement: new ( + elm: Element, + options?: { absolutePaths?: boolean } + ) => SerialDqElement; + uuid: ( + options?: { random?: Uint8Array | Array<number> }, + buf?: Uint8Array | Array<number>, + offset?: number + ) => string | Uint8Array | Array<number>; + } + + interface Aria { + getRoleType: (role: string | Element | VirtualNode | null) => string | null; + } + + interface Dom { + isFocusable: (node: Element | VirtualNode) => boolean; + isNativelyFocusable: (node: Element | VirtualNode) => boolean; + } + + type AccessibleTextOptions = { + inControlContext?: boolean; + inLabelledByContext?: boolean; + }; + + interface Text { + accessibleText: ( + element: Element, + options?: AccessibleTextOptions + ) => string; + } + + interface Commons { + aria: Aria; + dom: Dom; + text: Text; } interface EnvironmentData { @@ -380,6 +442,7 @@ declare namespace axe { let version: string; let plugins: any; let utils: Utils; + let commons: Commons; /** * Source string to use as an injected script in Selenium diff --git a/bower.json b/bower.json index 09e0dfb056..298c37a578 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "axe-core", - "version": "4.7.2", + "version": "4.8.0", "deprecated": true, "contributors": [ { diff --git a/build/configure.js b/build/configure.js index 79ff1b5880..d4998985e5 100644 --- a/build/configure.js +++ b/build/configure.js @@ -41,7 +41,7 @@ function buildRules(grunt, options, commons, callback) { var axeImpact = Object.freeze(['minor', 'moderate', 'serious', 'critical']); // TODO: require('../axe') does not work if grunt configure is moved after uglify, npm test breaks with undefined. Complicated grunt concurrency issue. var locale = getLocale(grunt, options); options.getFiles = false; - buildManual(grunt, options, commons, function (result) { + buildManual(grunt, options, commons, function (build) { var metadata = { rules: {}, checks: {} @@ -96,8 +96,8 @@ function buildRules(grunt, options, commons, callback) { .join('\n'); var tags = options.tags ? options.tags.split(/\s*,\s*/) : []; - var rules = result.rules; - var checks = result.checks; + var rules = build.rules; + var checks = build.checks; // Translate checks before parsing them so that translations // get applied to the metadata object @@ -113,9 +113,9 @@ function buildRules(grunt, options, commons, callback) { function parseMetaData(source, propType) { var data = source.metadata; - var key = source.id || source.type; - if (key && locale && locale[propType] && propType !== 'checks') { - data = locale[propType][key] || data; + var id = source.id || source.type; + if (id && locale && locale[propType] && propType !== 'checks') { + data = locale[propType][id] || data; } var result = clone(data) || {}; @@ -151,8 +151,8 @@ function buildRules(grunt, options, commons, callback) { } function getIncompleteMsg(summaries) { - var summary = summaries.find(function (summary) { - return typeof summary.incompleteFallbackMessage === 'string'; + var summary = summaries.find(function (element) { + return typeof element.incompleteFallbackMessage === 'string'; }); return summary ? summary.incompleteFallbackMessage : ''; } @@ -184,8 +184,8 @@ function buildRules(grunt, options, commons, callback) { }); } - function findCheck(checks, id) { - return checks.filter(function (check) { + function findCheck(checkCollection, id) { + return checkCollection.filter(function (check) { if (check.id === id) { return true; } @@ -336,21 +336,21 @@ function buildRules(grunt, options, commons, callback) { metadata.rules[rule.id] = parseMetaData(rule, 'rules'); // Translate rules } - var rules; + var result; if (rule.tags.includes('deprecated')) { - rules = descriptions.deprecated.rules; + result = descriptions.deprecated.rules; } else if (rule.tags.includes('experimental')) { - rules = descriptions.experimental.rules; + result = descriptions.experimental.rules; } else if (rule.tags.find(tag => tag.includes('aaa'))) { - rules = descriptions.wcag2aaa.rules; + result = descriptions.wcag2aaa.rules; } else if (rule.tags.includes('best-practice')) { - rules = descriptions.bestPractice.rules; + result = descriptions.bestPractice.rules; } else if (rule.tags.find(tag => tag.startsWith('wcag2a'))) { - rules = descriptions.wcag20.rules; + result = descriptions.wcag20.rules; } else if (rule.tags.find(tag => tag.startsWith('wcag21a'))) { - rules = descriptions.wcag21.rules; + result = descriptions.wcag21.rules; } else { - rules = descriptions.wcag22.rules; + result = descriptions.wcag22.rules; } var issueType = []; @@ -363,7 +363,7 @@ function buildRules(grunt, options, commons, callback) { var actLinks = createActLinksForRule(rule); - rules.push([ + result.push([ `[${rule.id}](https://dequeuniversity.com/rules/axe/${axeVersion}/${rule.id}?application=RuleDescription)`, entities.encode(rule.metadata.description), impact, @@ -408,8 +408,8 @@ ${TOC} ${ruleTables}`; // Translate failureSummaries - metadata.failureSummaries = createFailureSummaryObject(result.misc); - metadata.incompleteFallbackMessage = getIncompleteMsg(result.misc); + metadata.failureSummaries = createFailureSummaryObject(build.misc); + metadata.incompleteFallbackMessage = getIncompleteMsg(build.misc); callback({ auto: replaceFunctions( diff --git a/build/rule-generator.js b/build/rule-generator.js index 208a488f05..ee2c8eca75 100644 --- a/build/rule-generator.js +++ b/build/rule-generator.js @@ -49,7 +49,7 @@ async function run() { const result = await Promise.all( files.map(async meta => { const path = `${meta.dir}/${meta.name}`; - const content = meta.content; + const content = meta.content + '\n'; await createFile(path, content); return path; }) diff --git a/build/rule-generator/get-files-metadata.js b/build/rule-generator/get-files-metadata.js index cc396ad1f3..50abce3091 100644 --- a/build/rule-generator/get-files-metadata.js +++ b/build/rule-generator/get-files-metadata.js @@ -1,4 +1,5 @@ const directories = require('./directories'); +const outdent = require('outdent'); /** * Helper to convert a given string to camel case (split by hyphens if any) @@ -23,8 +24,9 @@ const getRuleSpecFileMeta = (ruleName, ruleHasMatches, ruleChecks) => { name: `${ruleName}.json`, content: JSON.stringify( { - id: `${ruleName}`, - selector: '', + id: ruleName, + impact: '', + selector: '*', ...(ruleHasMatches && { matches: `${ruleName}-matches` }), @@ -70,15 +72,13 @@ const getRuleMatchesFileMeta = ( const fnName = `${camelCase(ruleName)}Matches`; const ruleMatchesJs = { name: `${ruleName}-matches.js`, - content: ` - // TODO: Filter node(s) - - function ${fnName}(node, virtualNode) { - return node - } + content: outdent` + // TODO: Filter node(s) - export default ${fnName} - `, + export default function ${fnName}(node, virtualNode) { + return true; + } + `, dir: directories.rules }; files.push(ruleMatchesJs); @@ -87,12 +87,18 @@ const getRuleMatchesFileMeta = ( if (ruleHasUnitTestAssets) { const ruleMatchesTestJs = { name: `${ruleName}-matches.js`, - content: ` - describe('${ruleName}-matches', function() { - 'use strict'; - // TODO: Write tests - }) - `, + content: outdent` + describe('${ruleName}-matches', () => { + const rule = axe.utils.getRule('${ruleName}'); + const { queryFixture } = axe.testUtils; + + // TODO: Replace with real tests + it('returns false for paragraphs', () => { + const vNode = queryFixture('<p id="target">Hello world</p>'); + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + }); + `, dir: directories.testRuleMatches }; files.push(ruleMatchesTestJs); @@ -116,7 +122,6 @@ const getCheckSpecFileMeta = (name, dir) => { id: `${name}`, evaluate: `${name}-evaluate`, metadata: { - impact: '', messages: { pass: '', fail: '', @@ -142,13 +147,12 @@ const getCheckJsFileMeta = (name, dir) => { const fnName = `${camelCase(name)}Evaluate`; return { name: `${name}-evaluate.js`, - content: ` - // TODO: Logic for check - function ${fnName}(node, options, virtualNode) { - return true - } - export default ${fnName}; - `, + content: outdent` + // TODO: Logic for check + export default function ${fnName}(node, options, virtualNode) { + return true + } + `, dir }; }; @@ -163,12 +167,24 @@ const getCheckJsFileMeta = (name, dir) => { const getCheckTestJsFileMeta = (name, dir) => { return { name: `${name}.js`, - content: ` - describe('${name} tests', function() { - 'use strict'; - // TODO: Write tests - }) - `, + content: outdent` + describe('${name} tests', () => { + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkContext = axe.testUtils.MockCheckContext(); + const checkEvaluate = getCheckEvaluate('${name}'); + + afterEach(() => { + checkContext.reset(); + }); + + // TODO: Replace this with real tests for this check + it('returns false when img has no alt', () => { + const params = checkSetup('<img id="target" />'); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { messageKey: 'missing-alt' }); + }); + }); + `, dir }; }; diff --git a/build/tasks/add-locale.js b/build/tasks/add-locale.js index f3845d5a19..35e15e75df 100644 --- a/build/tasks/add-locale.js +++ b/build/tasks/add-locale.js @@ -45,7 +45,7 @@ module.exports = function (grunt) { var commons = file.src[0]; buildManual(grunt, options, commons, function (result) { - var out = { + var locale = { lang: options.lang, rules: result.rules.reduce(function (out, rule) { out[rule.id] = rule.metadata; @@ -74,10 +74,10 @@ module.exports = function (grunt) { var oldMessages = grunt.file.readJSON(localeFile); // mergeMessages mutates out - mergeMessages(out, oldMessages); + mergeMessages(locale, oldMessages); } - grunt.file.write(file.dest, JSON.stringify(out, null, ' ')); + grunt.file.write(file.dest, JSON.stringify(locale, null, ' ')); console.log('created file at', file.dest); }); }); diff --git a/build/tasks/esbuild.js b/build/tasks/esbuild.js index 6a5479dff9..30dbeb4ef6 100644 --- a/build/tasks/esbuild.js +++ b/build/tasks/esbuild.js @@ -26,7 +26,10 @@ module.exports = function (grunt) { bundle: true }) .then(done) - .catch(done); + .catch(e => { + grunt.fail.fatal(e); + done(); + }); }); }); } diff --git a/build/tasks/validate.js b/build/tasks/validate.js index c36dfddb53..9af5edb28a 100644 --- a/build/tasks/validate.js +++ b/build/tasks/validate.js @@ -112,8 +112,8 @@ function createSchemas() { conform: 'Must have at least two valid messages' } }, + // @deprecated: Use impact on rules instead impact: { - required: true, type: 'string', enum: ['minor', 'moderate', 'serious', 'critical'] } @@ -134,6 +134,7 @@ function createSchemas() { type: 'string' }, impact: { + required: true, type: 'string', enum: ['minor', 'moderate', 'serious', 'critical'] }, @@ -198,12 +199,6 @@ function createSchemas() { type: 'array', items: { type: 'string' - }, - conform: function hasCategoryTag(tags) { - return tags.some(tag => tag.includes('cat.')); - }, - messages: { - conform: 'must include a category tag' } }, actIds: { @@ -307,5 +302,168 @@ function validateRule({ tags, metadata }) { if (help.toLowerCase().includes(prohibitedWord)) { issues.push(`metadata.help can not contain the word '${prohibitedWord}'.`); } + + issues.push(...findTagIssues(tags)); + return issues; +} + +const miscTags = ['ACT', 'experimental', 'review-item', 'deprecated']; + +const categories = [ + 'aria', + 'color', + 'forms', + 'keyboard', + 'language', + 'name-role-value', + 'parsing', + 'semantics', + 'sensory-and-visual-cues', + 'structure', + 'tables', + 'text-alternatives', + 'time-and-media' +]; + +const standardsTags = [ + { + // Has to be first, as others rely on the WCAG level getting picked up first + name: 'WCAG', + standardRegex: /^wcag2(1|2)?a{1,3}(-obsolete)?$/, + criterionRegex: /^wcag\d{3,4}$/ + }, + { + name: 'Section 508', + standardRegex: /^section508$/, + criterionRegex: /^section508\.\d{1,2}\.[a-z]$/, + wcagLevelRegex: /^wcag2aa?$/ + }, + { + name: 'Trusted Tester', + standardRegex: /^TTv5$/, + criterionRegex: /^TT\d{1,3}\.[a-z]$/, + wcagLevelRegex: /^wcag2aa?$/ + }, + { + name: 'EN 301 549', + standardRegex: /^EN-301-549$/, + criterionRegex: /^EN-9\.[1-4]\.[1-9]\.\d{1,2}$/, + wcagLevelRegex: /^wcag21?aa?$/ + } +]; + +function findTagIssues(tags) { + const issues = []; + const catTags = tags.filter(tag => tag.startsWith('cat.')); + const bestPracticeTags = tags.filter(tag => tag === 'best-practice'); + + // Category + if (catTags.length !== 1) { + issues.push(`Must have exactly one cat. tag, got ${catTags.length}`); + } + if (catTags.length && !categories.includes(catTags[0].slice(4))) { + issues.push(`Invalid category tag: ${catTags[0]}`); + } + if (!startsWith(tags, catTags)) { + issues.push(`Tag ${catTags[0]} must be before ${tags[0]}`); + } + tags = removeTags(tags, catTags); + + // Best practice + if (bestPracticeTags.length > 1) { + issues.push( + `Only one best-practice tag is allowed, got ${bestPracticeTags.length}` + ); + } + if (!startsWith(tags, bestPracticeTags)) { + issues.push(`Tag ${bestPracticeTags[0]} must be before ${tags[0]}`); + } + tags = removeTags(tags, bestPracticeTags); + + const standards = {}; + // WCAG, Section 508, Trusted Tester, EN 301 549 + for (const { + name, + standardRegex, + criterionRegex, + wcagLevelRegex + } of standardsTags) { + const standardTags = tags.filter(tag => tag.match(standardRegex)); + const criterionTags = tags.filter(tag => tag.match(criterionRegex)); + if (!standardTags.length && !criterionTags.length) { + continue; + } + + standards[name] = { + name, + standardTag: standardTags[0] ?? null, + criterionTags + }; + if (bestPracticeTags.length !== 0) { + issues.push(`${name} tags cannot be used along side best-practice tag`); + } + if (standardTags.length === 0) { + issues.push(`Expected one ${name} tag, got 0`); + } else if (standardTags.length > 1) { + issues.push(`Expected one ${name} tag, got: ${standardTags.join(', ')}`); + } + if (criterionTags.length === 0) { + issues.push(`Expected at least one ${name} criterion tag, got 0`); + } + + if (wcagLevelRegex) { + const wcagLevel = standards.WCAG.standardTag; + if (!wcagLevel.match(wcagLevelRegex)) { + issues.push(`${name} rules not allowed on ${wcagLevel}`); + } + } + + // Must have the same criteria listed + if (name === 'EN 301 549') { + const wcagCriteria = standards.WCAG.criterionTags.map(tag => + tag.slice(4) + ); + const enCriteria = criterionTags.map(tag => + tag.slice(5).replaceAll('.', '') + ); + if ( + wcagCriteria.length !== enCriteria.length || + !startsWith(wcagCriteria, enCriteria) + ) { + issues.push( + `Expect WCAG and EN criteria numbers to match: ${wcagCriteria.join( + ', ' + )} vs ${enCriteria.join(', ')}}` + ); + } + } + tags = removeTags(tags, [...standardTags, ...criterionTags]); + } + + // Other tags + const usedMiscTags = miscTags.filter(tag => tags.includes(tag)); + const unknownTags = removeTags(tags, usedMiscTags); + if (unknownTags.length) { + issues.push(`Invalid tags: ${unknownTags.join(', ')}`); + } + + // At this point only misc tags are left: + tags = removeTags(tags, unknownTags); + if (!startsWith(tags, usedMiscTags)) { + issues.push( + `Tags [${tags.join(', ')}] should be sorted like [${usedMiscTags.join( + ', ' + )}]` + ); + } + return issues; } + +function startsWith(arr1, arr2) { + return arr2.every((item, i) => item === arr1[i]); +} + +function removeTags(tags, tagsToRemove) { + return tags.filter(tag => !tagsToRemove.includes(tag)); +} diff --git a/doc/API.md b/doc/API.md index 9355005820..5f916713b4 100644 --- a/doc/API.md +++ b/doc/API.md @@ -76,23 +76,26 @@ Each rule in axe-core has a number of tags. These provide metadata about the rul The `experimental`, `ACT`, `TT`, and `section508` tags are only added to some rules. Each rule with a `section508` tag also has a tag to indicate what requirement in old Section 508 the rule is required by. For example `section508.22.a`. -| Tag Name | Accessibility Standard / Purpose | -| ---------------- | ---------------------------------------------------- | -| `wcag2a` | WCAG 2.0 Level A | -| `wcag2aa` | WCAG 2.0 Level AA | -| `wcag2aaa` | WCAG 2.0 Level AAA | -| `wcag21a` | WCAG 2.1 Level A | -| `wcag21aa` | WCAG 2.1 Level AA | -| `wcag22aa` | WCAG 2.2 Level AA | -| `best-practice` | Common accessibility best practices | -| `wcag***` | WCAG success criterion e.g. wcag111 maps to SC 1.1.1 | -| `ACT` | W3C approved Accessibility Conformance Testing rules | -| `section508` | Old Section 508 rules | -| `section508.*.*` | Requirement in old Section 508 | -| `TTv5` | Trusted Tester v5 rules | -| `TT*.*` | Test ID in Trusted Tester | -| `experimental` | Cutting-edge rules, disabled by default | -| `cat.*` | Category mappings used by Deque (see below) | +| Tag Name | Accessibility Standard / Purpose | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `wcag2a` | WCAG 2.0 Level A | +| `wcag2aa` | WCAG 2.0 Level AA | +| `wcag2aaa` | WCAG 2.0 Level AAA | +| `wcag21a` | WCAG 2.1 Level A | +| `wcag21aa` | WCAG 2.1 Level AA | +| `wcag22aa` | WCAG 2.2 Level AA | +| `best-practice` | Common accessibility best practices | +| `wcag2a-obsolete` | WCAG 2.0 Level A, no longer required for conformance | +| `wcag***` | WCAG success criterion e.g. wcag111 maps to SC 1.1.1 | +| `ACT` | W3C approved Accessibility Conformance Testing rules | +| `section508` | Old Section 508 rules | +| `section508.*.*` | Requirement in old Section 508 | +| `TTv5` | Trusted Tester v5 rules | +| `TT*.*` | Test ID in Trusted Tester | +| `EN-301-549` | Rule required under [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf) | +| `EN-9.*` | Section in EN 301 549 listing the requirement | +| `experimental` | Cutting-edge rules, disabled by default | +| `cat.*` | Category mappings used by Deque (see below) | All rules have a `cat.*` tag, which indicates what type of content it is part of. The following `cat.*` tags exist in axe-core: @@ -225,7 +228,7 @@ axe.configure({ - The rules attribute is an Array of rule objects - each rule object can contain the following attributes - `id` - string(required). This uniquely identifies the rule. If the rule already exists, it will be overridden with any of the attributes supplied. The attributes below that are marked required, are only required for new rules. - - `impact` - string(optional). Override the impact defined by checks + - `impact` - string(required). Sets the impact of that rule's results - `reviewOnFail` - boolean(option, default `false`). Override the result of a rule to return "Needs Review" rather than "Violation" if the rule fails. - `selector` - string(optional, default `*`). A [CSS selector](./developer-guide.md#supported-css-selectors) used to identify the elements that are passed into the rule for evaluation. - `excludeHidden` - boolean(optional, default `true`). This indicates whether elements that are hidden from all users are to be passed into the rule for evaluation. @@ -618,6 +621,8 @@ The `assets` attribute expects an array of preload(able) constraints to be fetch The `timeout` attribute in the object configuration is `optional` and has a fallback default value (10000ms). The `timeout` is essential for any network dependent assets that are preloaded, where-in if a given request takes longer than the specified/ default value, the operation is aborted. +Preloading is not applicable to all rules. Even if the `preload` option is enabled, preloading steps may be skipped if no enabled rules require preloading. + ##### Callback Parameter The callback parameter is a function that will be called when the asynchronous `axe.run` function completes. The callback function is passed two parameters. The first parameter will be an error thrown inside of axe if axe.run could not complete. If axe completed correctly the first parameter will be null, and the second parameter will be the results object. diff --git a/doc/developer-guide.md b/doc/developer-guide.md index 1bbd83e4c3..07abffb216 100644 --- a/doc/developer-guide.md +++ b/doc/developer-guide.md @@ -115,12 +115,12 @@ After execution, a Check will return `true`, `false`, or `undefined` depending o Rules are defined by JSON files in the [lib/rules directory](../lib/rules). The JSON object is used to seed the [Rule object](../lib/core/base/rule.js#L30). A valid Rule JSON consists of the following: - `id` - `String` A unique name of the Rule. +- `impact` - `String` (one of `minor`, `moderate`, `serious`, or `critical`). Sets the impact of the results of this rule - `selector` - **optional** `String` which is a [CSS selector](#supported-css-selectors) that specifies the elements of the page on which the Rule runs. axe-core will look inside of the light DOM and _open_ [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) trees for elements matching the provided selector. If omitted, the rule will run against every node. - `excludeHidden` - **optional** `Boolean` Whether the rule should exclude hidden elements. Defaults to `true`. - `enabled` - **optional** `Boolean` Whether the rule is enabled by default. Defaults to `true`. - `pageLevel` - **optional** `Boolean` Whether the rule is page level. Page level rules will only run if given an entire `document` as context. - `matches` - **optional** `String` The ID of the filtering function that will exclude elements that match the `selector` property. See the [`metadata-function-map`](../lib/core/base/metadata-function-map.js) file for all defined IDs. -- `impact` - **optional** `String` (one of `minor`, `moderate`, `serious`, or `critical`). Override the impact defined by checks. - `tags` - **optional** `Array` Strings of the accessibility guidelines of which the Rule applies. - `metadata` - `Object` Consisting of: - `description` - `String` Text string that describes what the rule does. @@ -155,7 +155,7 @@ Similar to Rules, Checks are defined by JSON files in the [lib/checks directory] - `after` - **optional** `String` The ID of the function that gets called for checks that operate on a page-level basis, to process the results from the iframes. - `options` - **optional** `Object` Any information the Check needs that you might need to customize and/or is locale specific. Options can be overridden at runtime (with the options parameter) or config-time. For example, the [valid-lang](../lib/checks/language/valid-lang.json) Check defines what ISO 639-1 language codes it should accept as valid. Options do not need to follow any specific format or type; it is up to the author of a Check to determine the most appropriate format. - `metadata` - `Object` Consisting of: - - `impact` - `String` (one of `minor`, `moderate`, `serious`, or `critical`) + - `impact` - **Deprecated** `String` (one of `minor`, `moderate`, `serious`, or `critical`) - `messages` - `Object` These messages are displayed when the Check passes or fails - `pass` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check passes - `fail` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check fails diff --git a/doc/examples/chrome-debugging-protocol/axe-cdp.js b/doc/examples/chrome-debugging-protocol/axe-cdp.js index bf56073467..5ecae05a75 100644 --- a/doc/examples/chrome-debugging-protocol/axe-cdp.js +++ b/doc/examples/chrome-debugging-protocol/axe-cdp.js @@ -39,7 +39,7 @@ const example = async url => { // its value (`results.result.value` is undefined). By // `JSON.stringify()`ing it, we can `JSON.parse()` it later on // and return a valid results set. - .then(results => JSON.stringify(results)) + .then(res => JSON.stringify(res)) .then(resolve) .catch(reject); }); diff --git a/doc/examples/jsdom/test/a11y.js b/doc/examples/jsdom/test/a11y.js index bd946cef0c..0bc935c2e5 100644 --- a/doc/examples/jsdom/test/a11y.js +++ b/doc/examples/jsdom/test/a11y.js @@ -1,10 +1,11 @@ /* global describe, it */ +const axe = require('axe-core'); const jsdom = require('jsdom'); const { JSDOM } = jsdom; const assert = require('assert'); describe('axe', () => { - const { window } = new JSDOM(`<!DOCTYPE html> + const { document } = new JSDOM(`<!DOCTYPE html> <html lang="en"> <head> <title>JSDOM Example</title> @@ -18,30 +19,30 @@ describe('axe', () => { <p>Not a label</p><input type="text" id="no-label"> </div> </body> - </html>`); - - const axe = require('axe-core'); + </html>`).window; const config = { rules: { 'color-contrast': { enabled: false } } }; - it('should report that good HTML is good', function (done) { - var n = window.document.getElementById('working'); - axe.run(n, config, function (err, result) { - assert.equal(err, null, 'Error is not null'); - assert.equal(result.violations.length, 0, 'Violations is not empty'); - done(); - }); + it('reports that good HTML is good', async () => { + const node = document.getElementById('working'); + const result = await axe.run(node, config); + assert.equal(result.violations.length, 0, 'Violations is not empty'); + }); + + it('reports that bad HTML is bad', async () => { + const node = document.getElementById('broken'); + const results = await axe.run(node, config); + assert.equal(results.violations.length, 1, 'Violations.length is not 1'); }); - it('should report that bad HTML is bad', function (done) { - var n = window.document.getElementById('broken'); - axe.run(n, config, function (err, result) { - assert.equal(err, null, 'Error is not null'); - assert.equal(result.violations.length, 1, 'Violations.length is not 1'); - done(); - }); + it('allows commons after axe.setup() is called', () => { + axe.setup(document); + const input = document.querySelector('input'); + const role = axe.commons.aria.getRole(input); + assert.equal(role, 'textbox'); + axe.teardown(); }); }); diff --git a/doc/examples/puppeteer/axe-puppeteer.js b/doc/examples/puppeteer/axe-puppeteer.js index 2b54d826a1..64f657b33c 100644 --- a/doc/examples/puppeteer/axe-puppeteer.js +++ b/doc/examples/puppeteer/axe-puppeteer.js @@ -9,11 +9,11 @@ const isValidURL = input => { return u.protocol && u.host; }; -// node axe-puppeteer.js <url> -const url = process.argv[2]; -assert(isValidURL(url), 'Invalid URL'); +(async () => { + // node axe-puppeteer.js <url> + const url = process.argv[2]; + assert(isValidURL(url), 'Invalid URL'); -const main = async url => { let browser; let results; try { @@ -34,6 +34,8 @@ const main = async url => { // Get the results from `axe.run()`. results = await handle.jsonValue(); + console.log(results); + // Destroy the handle & return axe results. await handle.dispose(); } catch (err) { @@ -42,19 +44,9 @@ const main = async url => { await browser.close(); } - // Re-throw - throw err; + console.error('Error running axe-core:', err.message); + process.exit(1); } await browser.close(); - return results; -}; - -main(url) - .then(results => { - console.log(results); - }) - .catch(err => { - console.error('Error running axe-core:', err.message); - process.exit(1); - }); +})(); diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 684fa0bfe6..bf03843516 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -12,71 +12,73 @@ ## WCAG 2.0 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :--------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [area-alt](https://dequeuniversity.com/rules/axe/4.7/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, ACT, TTv5, TT6.a | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-attr?application=RuleDescription) | Ensures ARIA attributes are allowed for an element's role | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | -| [aria-command-name](https://dequeuniversity.com/rules/axe/4.7/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT6.a | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | failure | | -| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | -| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.7/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.7/aria-meter-name?application=RuleDescription) | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | -| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.7/aria-progressbar-name?application=RuleDescription) | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | -| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.7/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | -| [aria-required-children](https://dequeuniversity.com/rules/axe/4.7/aria-required-children?application=RuleDescription) | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.7/aria-required-parent?application=RuleDescription) | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | -| [aria-roles](https://dequeuniversity.com/rules/axe/4.7/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Minor, Serious, Critical | cat.aria, wcag2a, wcag412 | failure | [674b10](https://act-rules.github.io/rules/674b10) | -| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.7/aria-toggle-field-name?application=RuleDescription) | Ensures every ARIA toggle field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.7/aria-tooltip-name?application=RuleDescription) | Ensures every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | | -| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, Critical | cat.aria, wcag2a, wcag412 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | -| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | -| [blink](https://dequeuniversity.com/rules/axe/4.7/blink?application=RuleDescription) | Ensures <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b | failure | | -| [button-name](https://dequeuniversity.com/rules/axe/4.7/button-name?application=RuleDescription) | Ensures buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT, TTv5, TT6.a | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1), [m6b1q3](https://act-rules.github.io/rules/m6b1q3) | -| [bypass](https://dequeuniversity.com/rules/axe/4.7/bypass?application=RuleDescription) | Ensures each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | -| [color-contrast](https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, ACT, TTv5, TT13.c | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [definition-list](https://dequeuniversity.com/rules/axe/4.7/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [dlitem](https://dequeuniversity.com/rules/axe/4.7/dlitem?application=RuleDescription) | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [document-title](https://dequeuniversity.com/rules/axe/4.7/document-title?application=RuleDescription) | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, ACT, TTv5, TT12.a | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | -| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.7/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.7/duplicate-id-aria?application=RuleDescription) | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [duplicate-id](https://dequeuniversity.com/rules/axe/4.7/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | -| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.7/form-field-multiple-labels?application=RuleDescription) | Ensures form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c | needs review | | -| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.7/frame-focusable-content?application=RuleDescription) | Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.7/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag412, wcag2a, TTv5, TT12.d | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | -| [frame-title](https://dequeuniversity.com/rules/axe/4.7/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | -| [html-has-lang](https://dequeuniversity.com/rules/axe/4.7/html-has-lang?application=RuleDescription) | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, ACT, TTv5, TT11.a | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | -| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.7/html-lang-valid?application=RuleDescription) | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, ACT, TTv5, TT11.a | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | -| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.7/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, ACT | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | -| [image-alt](https://dequeuniversity.com/rules/axe/4.7/image-alt?application=RuleDescription) | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a, TT7.b | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [input-button-name](https://dequeuniversity.com/rules/axe/4.7/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, ACT, TTv5, TT5.c | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | -| [input-image-alt](https://dequeuniversity.com/rules/axe/4.7/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | -| [label](https://dequeuniversity.com/rules/axe/4.7/label?application=RuleDescription) | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.7/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a | failure, needs review | | -| [link-name](https://dequeuniversity.com/rules/axe/4.7/link-name?application=RuleDescription) | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a, ACT, TTv5, TT6.a | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | -| [list](https://dequeuniversity.com/rules/axe/4.7/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [listitem](https://dequeuniversity.com/rules/axe/4.7/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131 | failure | | -| [marquee](https://dequeuniversity.com/rules/axe/4.7/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b | failure | | -| [meta-refresh](https://dequeuniversity.com/rules/axe/4.7/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | -| [meta-viewport](https://dequeuniversity.com/rules/axe/4.7/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, wcag2aa, wcag144, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | -| [nested-interactive](https://dequeuniversity.com/rules/axe/4.7/nested-interactive?application=RuleDescription) | Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | -| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.7/no-autoplay-audio?application=RuleDescription) | Ensures <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, ACT, TTv5, TT2.a | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | -| [object-alt](https://dequeuniversity.com/rules/axe/4.7/object-alt?application=RuleDescription) | Ensures <object> elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | -| [role-img-alt](https://dequeuniversity.com/rules/axe/4.7/role-img-alt?application=RuleDescription) | Ensures [role='img'] elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | -| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.7/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | -| [select-name](https://dequeuniversity.com/rules/axe/4.7/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, ACT, TTv5, TT5.c | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | -| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.7/server-side-image-map?application=RuleDescription) | Ensures that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a | needs review | | -| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.7/svg-img-alt?application=RuleDescription) | Ensures <svg> elements with an img, graphics-document or graphics-symbol role have an accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, ACT, TTv5, TT7.a | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | -| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.7/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | -| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.7/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, 14.b | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | -| [valid-lang](https://dequeuniversity.com/rules/axe/4.7/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, ACT, TTv5, TT11.b | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | -| [video-caption](https://dequeuniversity.com/rules/axe/4.7/video-caption?application=RuleDescription) | Ensures <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [area-alt](https://dequeuniversity.com/rules/axe/4.8/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.8/aria-allowed-attr?application=RuleDescription) | Ensures an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.8/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | | +| [aria-command-name](https://dequeuniversity.com/rules/axe/4.8/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.8/aria-conditional-attr?application=RuleDescription) | Ensures ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.8/aria-deprecated-role?application=RuleDescription) | Ensures elements do not use deprecated roles | Minor | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.8/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | | +| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.8/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | +| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.8/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-meter-name](https://dequeuniversity.com/rules/axe/4.8/aria-meter-name?application=RuleDescription) | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1 | failure, needs review | | +| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.8/aria-progressbar-name?application=RuleDescription) | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1 | failure, needs review | | +| [aria-prohibited-attr](https://dequeuniversity.com/rules/axe/4.8/aria-prohibited-attr?application=RuleDescription) | Ensures ARIA attributes are not prohibited for an element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.8/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | +| [aria-required-children](https://dequeuniversity.com/rules/axe/4.8/aria-required-children?application=RuleDescription) | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75), [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.8/aria-required-parent?application=RuleDescription) | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | +| [aria-roles](https://dequeuniversity.com/rules/axe/4.8/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [674b10](https://act-rules.github.io/rules/674b10) | +| [aria-toggle-field-name](https://dequeuniversity.com/rules/axe/4.8/aria-toggle-field-name?application=RuleDescription) | Ensures every ARIA toggle field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [aria-tooltip-name](https://dequeuniversity.com/rules/axe/4.8/aria-tooltip-name?application=RuleDescription) | Ensures every ARIA tooltip node has an accessible name | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | | +| [aria-valid-attr-value](https://dequeuniversity.com/rules/axe/4.8/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | [6a7281](https://act-rules.github.io/rules/6a7281) | +| [aria-valid-attr](https://dequeuniversity.com/rules/axe/4.8/aria-valid-attr?application=RuleDescription) | Ensures attributes that begin with aria- are valid ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [5f99a7](https://act-rules.github.io/rules/5f99a7) | +| [blink](https://dequeuniversity.com/rules/axe/4.8/blink?application=RuleDescription) | Ensures <blink> elements are not used | Serious | cat.time-and-media, wcag2a, wcag222, section508, section508.22.j, TTv5, TT2.b, EN-301-549, EN-9.2.2.2 | failure | | +| [button-name](https://dequeuniversity.com/rules/axe/4.8/button-name?application=RuleDescription) | Ensures buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT | 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.8/bypass?application=RuleDescription) | Ensures each page has at least one mechanism for a user to bypass navigation and jump straight to the content | Serious | cat.keyboard, wcag2a, wcag241, section508, section508.22.o, TTv5, TT9.a, EN-301-549, EN-9.2.4.1 | needs review | [cf77f2](https://act-rules.github.io/rules/cf77f2), [047fe0](https://act-rules.github.io/rules/047fe0), [b40fd1](https://act-rules.github.io/rules/b40fd1), [3e12e1](https://act-rules.github.io/rules/3e12e1), [ye5d6e](https://act-rules.github.io/rules/ye5d6e) | +| [color-contrast](https://dequeuniversity.com/rules/axe/4.8/color-contrast?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds | Serious | cat.color, wcag2aa, wcag143, TTv5, TT13.c, EN-301-549, EN-9.1.4.3, ACT | failure, needs review | [afw4f7](https://act-rules.github.io/rules/afw4f7), [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [definition-list](https://dequeuniversity.com/rules/axe/4.8/definition-list?application=RuleDescription) | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure | | +| [dlitem](https://dequeuniversity.com/rules/axe/4.8/dlitem?application=RuleDescription) | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure | | +| [document-title](https://dequeuniversity.com/rules/axe/4.8/document-title?application=RuleDescription) | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242, TTv5, TT12.a, EN-301-549, EN-9.2.4.2, ACT | failure | [2779a5](https://act-rules.github.io/rules/2779a5) | +| [duplicate-id-aria](https://dequeuniversity.com/rules/axe/4.8/duplicate-id-aria?application=RuleDescription) | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | needs review | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [form-field-multiple-labels](https://dequeuniversity.com/rules/axe/4.8/form-field-multiple-labels?application=RuleDescription) | Ensures form field does not have multiple label elements | Moderate | cat.forms, wcag2a, wcag332, TTv5, TT5.c, EN-301-549, EN-9.3.3.2 | needs review | | +| [frame-focusable-content](https://dequeuniversity.com/rules/axe/4.8/frame-focusable-content?application=RuleDescription) | Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1 | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1 | failure, needs review | [akn7bn](https://act-rules.github.io/rules/akn7bn) | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.8/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, wcag2a, wcag412, TTv5, TT12.d, EN-301-549, EN-9.4.1.2 | needs review | [4b1c6c](https://act-rules.github.io/rules/4b1c6c) | +| [frame-title](https://dequeuniversity.com/rules/axe/4.8/frame-title?application=RuleDescription) | Ensures <iframe> and <frame> elements have an accessible name | Serious | cat.text-alternatives, wcag2a, wcag412, section508, section508.22.i, TTv5, TT12.d, EN-301-549, EN-9.4.1.2 | failure, needs review | [cae760](https://act-rules.github.io/rules/cae760) | +| [html-has-lang](https://dequeuniversity.com/rules/axe/4.8/html-has-lang?application=RuleDescription) | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT | failure | [b5c3f8](https://act-rules.github.io/rules/b5c3f8) | +| [html-lang-valid](https://dequeuniversity.com/rules/axe/4.8/html-lang-valid?application=RuleDescription) | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311, TTv5, TT11.a, EN-301-549, EN-9.3.1.1, ACT | failure | [bf051a](https://act-rules.github.io/rules/bf051a) | +| [html-xml-lang-mismatch](https://dequeuniversity.com/rules/axe/4.8/html-xml-lang-mismatch?application=RuleDescription) | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311, EN-301-549, EN-9.3.1.1, ACT | failure | [5b7ae0](https://act-rules.github.io/rules/5b7ae0) | +| [image-alt](https://dequeuniversity.com/rules/axe/4.8/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, TTv5, TT7.a, TT7.b, EN-301-549, EN-9.1.1.1, ACT | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [input-button-name](https://dequeuniversity.com/rules/axe/4.8/input-button-name?application=RuleDescription) | Ensures input buttons have discernible text | Critical | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | +| [input-image-alt](https://dequeuniversity.com/rules/axe/4.8/input-image-alt?application=RuleDescription) | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, wcag412, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, EN-9.4.1.2, ACT | failure, needs review | [59796f](https://act-rules.github.io/rules/59796f) | +| [label](https://dequeuniversity.com/rules/axe/4.8/label?application=RuleDescription) | Ensures every form element has a label | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [link-in-text-block](https://dequeuniversity.com/rules/axe/4.8/link-in-text-block?application=RuleDescription) | Ensure links are distinguished from surrounding text in a way that does not rely on color | Serious | cat.color, wcag2a, wcag141, TTv5, TT13.a, EN-301-549, EN-9.1.4.1 | failure, needs review | | +| [link-name](https://dequeuniversity.com/rules/axe/4.8/link-name?application=RuleDescription) | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | +| [list](https://dequeuniversity.com/rules/axe/4.8/list?application=RuleDescription) | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure | | +| [listitem](https://dequeuniversity.com/rules/axe/4.8/listitem?application=RuleDescription) | Ensures <li> elements are used semantically | Serious | cat.structure, wcag2a, wcag131, EN-301-549, EN-9.1.3.1 | failure | | +| [marquee](https://dequeuniversity.com/rules/axe/4.8/marquee?application=RuleDescription) | Ensures <marquee> elements are not used | Serious | cat.parsing, wcag2a, wcag222, TTv5, TT2.b, EN-301-549, EN-9.2.2.2 | failure | | +| [meta-refresh](https://dequeuniversity.com/rules/axe/4.8/meta-refresh?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Critical | cat.time-and-media, wcag2a, wcag221, TTv5, TT8.a, EN-301-549, EN-9.2.2.1 | failure | [bc659a](https://act-rules.github.io/rules/bc659a), [bisz58](https://act-rules.github.io/rules/bisz58) | +| [meta-viewport](https://dequeuniversity.com/rules/axe/4.8/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, wcag2aa, wcag144, EN-301-549, EN-9.1.4.4, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | +| [nested-interactive](https://dequeuniversity.com/rules/axe/4.8/nested-interactive?application=RuleDescription) | Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies | Serious | cat.keyboard, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | [307n5z](https://act-rules.github.io/rules/307n5z) | +| [no-autoplay-audio](https://dequeuniversity.com/rules/axe/4.8/no-autoplay-audio?application=RuleDescription) | Ensures <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio | Moderate | cat.time-and-media, wcag2a, wcag142, TTv5, TT2.a, EN-301-549, EN-9.1.4.2, ACT | needs review | [80f0bf](https://act-rules.github.io/rules/80f0bf) | +| [object-alt](https://dequeuniversity.com/rules/axe/4.8/object-alt?application=RuleDescription) | Ensures <object> elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, EN-301-549, EN-9.1.1.1 | failure, needs review | [8fc3b6](https://act-rules.github.io/rules/8fc3b6) | +| [role-img-alt](https://dequeuniversity.com/rules/axe/4.8/role-img-alt?application=RuleDescription) | Ensures [role="img"] elements have alternate text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT | failure, needs review | [23a2a8](https://act-rules.github.io/rules/23a2a8) | +| [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.8/scrollable-region-focusable?application=RuleDescription) | Ensure elements that have scrollable content are accessible by keyboard | Serious | cat.keyboard, wcag2a, wcag211, TTv5, TT4.a, EN-301-549, EN-9.2.1.1 | failure | [0ssw9k](https://act-rules.github.io/rules/0ssw9k) | +| [select-name](https://dequeuniversity.com/rules/axe/4.8/select-name?application=RuleDescription) | Ensures select element has an accessible name | Critical | cat.forms, wcag2a, wcag412, section508, section508.22.n, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | +| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.8/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, TTv5, TT4.a, EN-301-549, EN-9.2.1.1 | needs review | | +| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.8/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, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT | failure, needs review | [7d6734](https://act-rules.github.io/rules/7d6734) | +| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.8/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, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure, needs review | [a25f45](https://act-rules.github.io/rules/a25f45) | +| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.8/th-has-data-cells?application=RuleDescription) | Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure, needs review | [d0f69e](https://act-rules.github.io/rules/d0f69e) | +| [valid-lang](https://dequeuniversity.com/rules/axe/4.8/valid-lang?application=RuleDescription) | Ensures lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT | failure | [de46e4](https://act-rules.github.io/rules/de46e4) | +| [video-caption](https://dequeuniversity.com/rules/axe/4.8/video-caption?application=RuleDescription) | Ensures <video> elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2 | needs review | [eac66b](https://act-rules.github.io/rules/eac66b) | ## WCAG 2.1 Level A & AA Rules -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :------ | :------------------------------------- | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.7/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, ACT | failure | [73f2c2](https://act-rules.github.io/rules/73f2c2) | -| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.7/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------- | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [autocomplete-valid](https://dequeuniversity.com/rules/axe/4.8/autocomplete-valid?application=RuleDescription) | Ensure the autocomplete attribute is correct and suitable for the form field | Serious | cat.forms, wcag21aa, wcag135, EN-301-549, EN-9.1.3.5, ACT | failure | [73f2c2](https://act-rules.github.io/rules/73f2c2) | +| [avoid-inline-spacing](https://dequeuniversity.com/rules/axe/4.8/avoid-inline-spacing?application=RuleDescription) | Ensure that text spacing set through style attributes can be adjusted with custom stylesheets | Serious | cat.structure, wcag21aa, wcag1412, EN-301-549, EN-9.1.4.12, ACT | failure | [24afc2](https://act-rules.github.io/rules/24afc2), [9e45ec](https://act-rules.github.io/rules/9e45ec), [78fd32](https://act-rules.github.io/rules/78fd32) | ## WCAG 2.2 Level A & AA Rules @@ -84,42 +86,42 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r | Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | | :----------------------------------------------------------------------------------------------- | :------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :-------- | -| [target-size](https://dequeuniversity.com/rules/axe/4.7/target-size?application=RuleDescription) | Ensure touch target have sufficient size and space | Serious | wcag22aa, wcag258, cat.sensory-and-visual-cues | failure, needs review | | +| [target-size](https://dequeuniversity.com/rules/axe/4.8/target-size?application=RuleDescription) | Ensure touch target have sufficient size and space | Serious | cat.sensory-and-visual-cues, wcag22aa, wcag258 | failure, needs review | | ## Best Practices Rules Rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- | :----------------------------------------- | :------------------------- | :------------------------------------------------- | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.7/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | -| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.7/aria-dialog-name?application=RuleDescription) | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-text](https://dequeuniversity.com/rules/axe/4.7/aria-text?application=RuleDescription) | Ensures "role=text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.7/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.7/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | -| [empty-table-header](https://dequeuniversity.com/rules/axe/4.7/empty-table-header?application=RuleDescription) | Ensures table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.7/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | -| [heading-order](https://dequeuniversity.com/rules/axe/4.7/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.7/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | -| [label-title-only](https://dequeuniversity.com/rules/axe/4.7/label-title-only?application=RuleDescription) | Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | -| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.7/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.7/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.7/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.7/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.7/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | -| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.7/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | -| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.7/presentation-role-conflict?application=RuleDescription) | Elements marked as presentational should not have global ARIA or tabindex to ensure all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | -| [region](https://dequeuniversity.com/rules/axe/4.7/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.7/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | -| [skip-link](https://dequeuniversity.com/rules/axe/4.7/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | -| [tabindex](https://dequeuniversity.com/rules/axe/4.7/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.7/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure, needs review | | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------- | :------------------------- | :------------------------------------------------- | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.8/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.8/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | +| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.8/aria-dialog-name?application=RuleDescription) | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-text](https://dequeuniversity.com/rules/axe/4.8/aria-text?application=RuleDescription) | Ensures role="text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.8/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.8/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | +| [empty-table-header](https://dequeuniversity.com/rules/axe/4.8/empty-table-header?application=RuleDescription) | Ensures table headers have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.8/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, best-practice, review-item | failure, needs review | | +| [heading-order](https://dequeuniversity.com/rules/axe/4.8/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.8/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | +| [label-title-only](https://dequeuniversity.com/rules/axe/4.8/label-title-only?application=RuleDescription) | Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | +| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.8/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.8/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.8/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.8/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.8/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.8/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.8/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.8/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.8/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.8/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | +| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.8/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | +| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.8/presentation-role-conflict?application=RuleDescription) | Elements marked as presentational should not have global ARIA or tabindex to ensure all screen readers ignore them | Minor | cat.aria, best-practice, ACT | failure | [46ca7f](https://act-rules.github.io/rules/46ca7f) | +| [region](https://dequeuniversity.com/rules/axe/4.8/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.8/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate | cat.tables, best-practice | failure | | +| [skip-link](https://dequeuniversity.com/rules/axe/4.8/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | +| [tabindex](https://dequeuniversity.com/rules/axe/4.8/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.8/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure, needs review | | ## WCAG 2.x level AAA rules @@ -127,29 +129,31 @@ Rules that check for conformance to WCAG AAA success criteria that can be fully | Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | | :--------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :------------------------------------------------- | -| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.7/color-contrast-enhanced?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | -| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.7/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | -| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.7/meta-refresh-no-exceptions?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | +| [color-contrast-enhanced](https://dequeuniversity.com/rules/axe/4.8/color-contrast-enhanced?application=RuleDescription) | Ensures the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds | Serious | cat.color, wcag2aaa, wcag146, ACT | failure, needs review | [09o5cg](https://act-rules.github.io/rules/09o5cg) | +| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.8/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249 | needs review | [b20e66](https://act-rules.github.io/rules/b20e66) | +| [meta-refresh-no-exceptions](https://dequeuniversity.com/rules/axe/4.8/meta-refresh-no-exceptions?application=RuleDescription) | Ensures <meta http-equiv="refresh"> is not used for delayed refresh | Minor | cat.time-and-media, wcag2aaa, wcag224, wcag325 | failure | [bisz58](https://act-rules.github.io/rules/bisz58) | ## Experimental Rules Rules we are still testing and developing. They are disabled by default in axe-core, but are enabled for the axe browser extensions. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------- | -| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.7/css-orientation-lock?application=RuleDescription) | Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | -| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.7/focus-order-semantics?application=RuleDescription) | Ensures elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, experimental | failure | | -| [hidden-content](https://dequeuniversity.com/rules/axe/4.7/hidden-content?application=RuleDescription) | Informs users about hidden content. | Minor | cat.structure, experimental, review-item, best-practice | failure, needs review | | -| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.7/label-content-name-mismatch?application=RuleDescription) | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | -| [p-as-heading](https://dequeuniversity.com/rules/axe/4.7/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, experimental | failure, needs review | | -| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.7/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g | failure | | -| [td-has-header](https://dequeuniversity.com/rules/axe/4.7/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b | failure | | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------- | :------------------------- | :------------------------------------------------- | +| [css-orientation-lock](https://dequeuniversity.com/rules/axe/4.8/css-orientation-lock?application=RuleDescription) | Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations | Serious | cat.structure, wcag134, wcag21aa, EN-301-549, EN-9.1.3.4, experimental | failure, needs review | [b33eff](https://act-rules.github.io/rules/b33eff) | +| [focus-order-semantics](https://dequeuniversity.com/rules/axe/4.8/focus-order-semantics?application=RuleDescription) | Ensures elements in the focus order have a role appropriate for interactive content | Minor | cat.keyboard, best-practice, experimental | failure | | +| [hidden-content](https://dequeuniversity.com/rules/axe/4.8/hidden-content?application=RuleDescription) | Informs users about hidden content. | Minor | cat.structure, best-practice, experimental, review-item | failure, needs review | | +| [label-content-name-mismatch](https://dequeuniversity.com/rules/axe/4.8/label-content-name-mismatch?application=RuleDescription) | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | cat.semantics, wcag21a, wcag253, EN-301-549, EN-9.2.5.3, experimental | failure | [2ee8b8](https://act-rules.github.io/rules/2ee8b8) | +| [p-as-heading](https://dequeuniversity.com/rules/axe/4.8/p-as-heading?application=RuleDescription) | Ensure bold, italic text and font-size is not used to style <p> elements as a heading | Serious | cat.semantics, wcag2a, wcag131, EN-301-549, EN-9.1.3.1, experimental | failure, needs review | | +| [table-fake-caption](https://dequeuniversity.com/rules/axe/4.8/table-fake-caption?application=RuleDescription) | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, EN-301-549, EN-9.1.3.1 | failure | | +| [td-has-header](https://dequeuniversity.com/rules/axe/4.8/td-has-header?application=RuleDescription) | Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers | Critical | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure | | ## Deprecated Rules Deprecated rules are disabled by default and will be removed in the next major release. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :----------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | -| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.7/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, deprecated | failure, needs review | | -| [audio-caption](https://dequeuniversity.com/rules/axe/4.7/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :----------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | +| [aria-roledescription](https://dequeuniversity.com/rules/axe/4.8/aria-roledescription?application=RuleDescription) | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2, deprecated | failure, needs review | | +| [audio-caption](https://dequeuniversity.com/rules/axe/4.8/audio-caption?application=RuleDescription) | Ensures <audio> elements have captions | Critical | cat.time-and-media, wcag2a, wcag121, EN-301-549, EN-9.1.2.1, section508, section508.22.a, deprecated | needs review | [2eb176](https://act-rules.github.io/rules/2eb176), [afb423](https://act-rules.github.io/rules/afb423) | +| [duplicate-id-active](https://dequeuniversity.com/rules/axe/4.8/duplicate-id-active?application=RuleDescription) | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| [duplicate-id](https://dequeuniversity.com/rules/axe/4.8/duplicate-id?application=RuleDescription) | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | diff --git a/doc/run-partial.md b/doc/run-partial.md index ae7114d8ea..a2570d2a92 100644 --- a/doc/run-partial.md +++ b/doc/run-partial.md @@ -13,6 +13,8 @@ const axeResults = await axe.finishRun(partialResults, options); **note**: The code in this page uses native DOM methods. This will only work on frames with the same origin. Scripts do not have access to `contentWindow` of cross-origin frames. Use of `runPartial` and `finishRun` in browser drivers like Selenium and Puppeteer works in the same way. +**note**: Because `axe.runPartial()` is designed to be serialized, it will not return element references even if the `elementRef` option is set. + ## axe.runPartial(context, options): Promise<PartialResult> When using `axe.runPartial()` it is important to keep in mind that the `context` may be different for different frames. For example, `context` can be done in such a way that in frame A, `main` is excluded, and in frame B `footer` is. The `axe.utils.getFrameContexts` method will provide a list of frames that must be tested, and what context to test it with. diff --git a/lib/checks/aria/aria-errormessage-evaluate.js b/lib/checks/aria/aria-errormessage-evaluate.js index ed10261f64..b7b971d4e2 100644 --- a/lib/checks/aria/aria-errormessage-evaluate.js +++ b/lib/checks/aria/aria-errormessage-evaluate.js @@ -24,10 +24,10 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; * @memberof checks * @return {Mixed} True if aria-errormessage references an existing element that uses a supported technique. Undefined if it does not reference an existing element. False otherwise. */ -function ariaErrormessageEvaluate(node, options, virtualNode) { +export default function ariaErrormessageEvaluate(node, options, virtualNode) { options = Array.isArray(options) ? options : []; - const attr = virtualNode.attr('aria-errormessage'); + const errorMessageAttr = virtualNode.attr('aria-errormessage'); const hasAttr = virtualNode.hasAttr('aria-errormessage'); const invaid = virtualNode.attr('aria-invalid'); const hasInvallid = virtualNode.hasAttr('aria-invalid'); @@ -74,12 +74,10 @@ function ariaErrormessageEvaluate(node, options, virtualNode) { } // limit results to elements that actually have this attribute - if (options.indexOf(attr) === -1 && hasAttr) { - this.data(tokenList(attr)); - return validateAttrValue.call(this, attr); + if (options.indexOf(errorMessageAttr) === -1 && hasAttr) { + this.data(tokenList(errorMessageAttr)); + return validateAttrValue.call(this, errorMessageAttr); } return true; } - -export default ariaErrormessageEvaluate; diff --git a/lib/checks/aria/aria-required-children-evaluate.js b/lib/checks/aria/aria-required-children-evaluate.js index beef5289af..226bd54703 100644 --- a/lib/checks/aria/aria-required-children-evaluate.js +++ b/lib/checks/aria/aria-required-children-evaluate.js @@ -28,43 +28,37 @@ export default function ariaRequiredChildrenEvaluate( ) { const reviewEmpty = options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : []; - const role = getExplicitRole(virtualNode, { dpub: true }); - const required = requiredOwned(role); + const explicitRole = getExplicitRole(virtualNode, { dpub: true }); + const required = requiredOwned(explicitRole); if (required === null) { return true; } - const { ownedRoles, ownedElements } = getOwnedRoles(virtualNode, required); - const unallowed = ownedRoles.filter(({ role }) => !required.includes(role)); + const ownedRoles = getOwnedRoles(virtualNode, required); + const unallowed = ownedRoles.filter( + ({ role, vNode }) => vNode.props.nodeType === 1 && !required.includes(role) + ); if (unallowed.length) { - this.relatedNodes(unallowed.map(({ ownedElement }) => ownedElement)); + this.relatedNodes(unallowed.map(({ vNode }) => vNode)); this.data({ messageKey: 'unallowed', values: unallowed - .map(({ ownedElement, attr }) => - getUnallowedSelector(ownedElement, attr) - ) + .map(({ vNode, attr }) => getUnallowedSelector(vNode, attr)) .filter((selector, index, array) => array.indexOf(selector) === index) .join(', ') }); return false; } - const missing = missingRequiredChildren( - virtualNode, - role, - required, - ownedRoles - ); - if (!missing) { + if (hasRequiredChildren(required, ownedRoles)) { return true; } - this.data(missing); + this.data(required); // Only review empty nodes when a node is both empty and does not have an aria-owns relationship - if (reviewEmpty.includes(role) && !ownedElements.some(isContent)) { + if (reviewEmpty.includes(explicitRole) && !ownedRoles.some(isContent)) { return undefined; } @@ -75,22 +69,20 @@ export default function ariaRequiredChildrenEvaluate( * Get all owned roles of an element */ function getOwnedRoles(virtualNode, required) { + let vNode; const ownedRoles = []; - const ownedElements = getOwnedVirtual(virtualNode).filter(vNode => { - return vNode.props.nodeType !== 1 || isVisibleToScreenReaders(vNode); - }); - - for (let i = 0; i < ownedElements.length; i++) { - const ownedElement = ownedElements[i]; - if (ownedElement.props.nodeType !== 1) { + const ownedVirtual = getOwnedVirtual(virtualNode); + while ((vNode = ownedVirtual.shift())) { + if (vNode.props.nodeType === 3) { + ownedRoles.push({ vNode, role: null }); + } + if (vNode.props.nodeType !== 1 || !isVisibleToScreenReaders(vNode)) { continue; } - const role = getRole(ownedElement, { noPresentational: true }); - - const globalAriaAttr = getGlobalAriaAttr(ownedElement); - const hasGlobalAriaOrFocusable = - !!globalAriaAttr || isFocusable(ownedElement); + const role = getRole(vNode, { noPresentational: true }); + const globalAriaAttr = getGlobalAriaAttr(vNode); + const hasGlobalAriaOrFocusable = !!globalAriaAttr || isFocusable(vNode); // if owned node has no role or is presentational, or if role // allows group or rowgroup, we keep parsing the descendant tree. @@ -101,37 +93,21 @@ function getOwnedRoles(virtualNode, required) { (['group', 'rowgroup'].includes(role) && required.some(requiredRole => requiredRole === role)) ) { - ownedElements.push(...ownedElement.children); + ownedVirtual.push(...vNode.children); } else if (role || hasGlobalAriaOrFocusable) { - ownedRoles.push({ - role, - attr: globalAriaAttr || 'tabindex', - ownedElement - }); + const attr = globalAriaAttr || 'tabindex'; + ownedRoles.push({ role, attr, vNode }); } } - return { ownedRoles, ownedElements }; + return ownedRoles; } /** - * Get missing children roles + * See if any required roles are in the ownedRoles array */ -function missingRequiredChildren(virtualNode, role, required, ownedRoles) { - for (let i = 0; i < ownedRoles.length; i++) { - const { role } = ownedRoles[i]; - - if (required.includes(role)) { - required = required.filter(requiredRole => requiredRole !== role); - return null; - } - } - - if (required.length) { - return required; - } - - return null; +function hasRequiredChildren(required, ownedRoles) { + return ownedRoles.some(({ role }) => role && required.includes(role)); } /** @@ -151,7 +127,6 @@ function getGlobalAriaAttr(vNode) { */ function getUnallowedSelector(vNode, attr) { const { nodeName, nodeType } = vNode.props; - if (nodeType === 3) { return `#text`; } @@ -160,20 +135,19 @@ function getUnallowedSelector(vNode, attr) { if (role) { return `[role=${role}]`; } - if (attr) { return nodeName + `[${attr}]`; } - return nodeName; } /** * Check if the node has content, or is itself content - * @param {VirtualNode} vNode + * @Object {Object} OwnedRole + * @property {VirtualNode} vNode * @returns {Boolean} */ -function isContent(vNode) { +function isContent({ vNode }) { if (vNode.props.nodeType === 3) { return vNode.props.nodeValue.trim().length > 0; } diff --git a/lib/checks/aria/braille-label-equivalent-evaluate.js b/lib/checks/aria/braille-label-equivalent-evaluate.js new file mode 100644 index 0000000000..ce301f684b --- /dev/null +++ b/lib/checks/aria/braille-label-equivalent-evaluate.js @@ -0,0 +1,22 @@ +import { sanitize, accessibleTextVirtual } from '../../commons/text'; + +/** + * Check that if aria-braillelabel is not empty, the element has an accessible text + * @memberof checks + * @return {Boolean} + */ +export default function brailleLabelEquivalentEvaluate( + node, + options, + virtualNode +) { + const brailleLabel = virtualNode.attr('aria-braillelabel') ?? ''; + if (!brailleLabel.trim()) { + return true; + } + try { + return sanitize(accessibleTextVirtual(virtualNode)) !== ''; + } catch { + return undefined; + } +} diff --git a/lib/checks/aria/braille-label-equivalent.json b/lib/checks/aria/braille-label-equivalent.json new file mode 100644 index 0000000000..a4d4dd0c1d --- /dev/null +++ b/lib/checks/aria/braille-label-equivalent.json @@ -0,0 +1,12 @@ +{ + "id": "braille-label-equivalent", + "evaluate": "braille-label-equivalent-evaluate", + "metadata": { + "impact": "serious", + "messages": { + "pass": "aria-braillelabel is used on an element with accessible text", + "fail": "aria-braillelabel is used on an element with no accessible text", + "incomplete": "Unable to compute accessible text" + } + } +} diff --git a/lib/checks/aria/braille-roledescription-equivalent-evaluate.js b/lib/checks/aria/braille-roledescription-equivalent-evaluate.js new file mode 100644 index 0000000000..5e03af7fac --- /dev/null +++ b/lib/checks/aria/braille-roledescription-equivalent-evaluate.js @@ -0,0 +1,29 @@ +import { sanitize } from '../../commons/text'; + +/** + * Check that if aria-brailleroledescription is not empty, + * the element has a non-empty aria-roledescription + * @memberof checks + * @return {Boolean} + */ +export default function brailleRoleDescriptionEquivalentEvaluate( + node, + options, + virtualNode +) { + const brailleRoleDesc = virtualNode.attr('aria-brailleroledescription') ?? ''; + if (sanitize(brailleRoleDesc) === '') { + return true; + } + const roleDesc = virtualNode.attr('aria-roledescription'); + if (typeof roleDesc !== 'string') { + this.data({ messageKey: 'noRoleDescription' }); + return false; + } + + if (sanitize(roleDesc) === '') { + this.data({ messageKey: 'emptyRoleDescription' }); + return false; + } + return true; +} diff --git a/lib/checks/aria/braille-roledescription-equivalent.json b/lib/checks/aria/braille-roledescription-equivalent.json new file mode 100644 index 0000000000..4005868c9c --- /dev/null +++ b/lib/checks/aria/braille-roledescription-equivalent.json @@ -0,0 +1,14 @@ +{ + "id": "braille-roledescription-equivalent", + "evaluate": "braille-roledescription-equivalent-evaluate", + "metadata": { + "impact": "serious", + "messages": { + "pass": "aria-brailleroledescription is used on an element with aria-roledescription", + "fail": { + "noRoleDescription": "aria-brailleroledescription is used on an element with no aria-roledescription", + "emptyRoleDescription": "aria-brailleroledescription is used on an element with an empty aria-roledescription" + } + } + } +} diff --git a/lib/checks/aria/no-implicit-explicit-label.json b/lib/checks/aria/no-implicit-explicit-label.json index b269bb642e..d5ed5a0b22 100644 --- a/lib/checks/aria/no-implicit-explicit-label.json +++ b/lib/checks/aria/no-implicit-explicit-label.json @@ -2,7 +2,7 @@ "id": "no-implicit-explicit-label", "evaluate": "no-implicit-explicit-label-evaluate", "metadata": { - "impact": "moderate", + "impact": "serious", "messages": { "pass": "There is no mismatch between a <label> and accessible name", "incomplete": "Check that the <label> does not need be part of the ARIA ${data} field's name" diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index b960f70185..3268156e62 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -71,14 +71,19 @@ export default function colorContrastEvaluate(node, options, virtualNode) { return undefined; } - const bgNodes = []; - const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); - const fgColor = getForegroundColor(node, false, bgColor, options); // Thin shadows only. Thicker shadows are included in the background instead const shadowColors = getTextShadowColors(node, { minRatio: 0.001, maxRatio: shadowOutlineEmMax }); + if (shadowColors === null) { + this.data({ messageKey: 'complexTextShadows' }); + return undefined; + } + + const bgNodes = []; + const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); + const fgColor = getForegroundColor(node, false, bgColor, options); let contrast = null; let contrastContributor = null; diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index 855caa1284..135eeae4e7 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -38,6 +38,7 @@ "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", + "complexTextShadows": "Element's contrast could not be determined because it uses complex text shadows", "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", diff --git a/lib/checks/generic/page-no-duplicate-evaluate.js b/lib/checks/generic/page-no-duplicate-evaluate.js index 3521a44952..1ce7130035 100644 --- a/lib/checks/generic/page-no-duplicate-evaluate.js +++ b/lib/checks/generic/page-no-duplicate-evaluate.js @@ -1,6 +1,7 @@ import cache from '../../core/base/cache'; import { querySelectorAllFilter } from '../../core/utils'; import { isVisibleToScreenReaders, findUpVirtual } from '../../commons/dom'; +import { getRole } from '../../commons/aria'; function pageNoDuplicateEvaluate(node, options, virtualNode) { if (!options || !options.selector || typeof options.selector !== 'string') { @@ -21,6 +22,7 @@ function pageNoDuplicateEvaluate(node, options, virtualNode) { isVisibleToScreenReaders(elm) ); + // @deprecated options.nativeScopeFilter // Filter elements that, within certain contexts, don't map their role. // e.g. a <footer> inside a <main> is not a banner, but in the <body> context it is if (typeof options.nativeScopeFilter === 'string') { @@ -32,6 +34,10 @@ function pageNoDuplicateEvaluate(node, options, virtualNode) { }); } + if (typeof options.role === 'string') { + elms = elms.filter(elm => getRole(elm) === options.role); + } + this.relatedNodes( elms.filter(elm => elm !== virtualNode).map(elm => elm.actualNode) ); diff --git a/lib/checks/keyboard/focusable-not-tabbable.json b/lib/checks/keyboard/focusable-not-tabbable.json index eb75b1891a..1a5ddc9857 100644 --- a/lib/checks/keyboard/focusable-not-tabbable.json +++ b/lib/checks/keyboard/focusable-not-tabbable.json @@ -6,7 +6,7 @@ "messages": { "pass": "No focusable elements contained within element", "incomplete": "Check if the focusable elements immediately move the focus indicator", - "fail": "Focusable content should have tabindex='-1' or be removed from the DOM" + "fail": "Focusable content should have tabindex=\"-1\" or be removed from the DOM" } } } diff --git a/lib/checks/keyboard/no-focusable-content.json b/lib/checks/keyboard/no-focusable-content.json index 9608c74389..b23469c1a7 100644 --- a/lib/checks/keyboard/no-focusable-content.json +++ b/lib/checks/keyboard/no-focusable-content.json @@ -7,7 +7,7 @@ "pass": "Element does not have 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')" + "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/keyboard/page-no-duplicate-banner.json b/lib/checks/keyboard/page-no-duplicate-banner.json index e0c8121aa3..f6646097e5 100644 --- a/lib/checks/keyboard/page-no-duplicate-banner.json +++ b/lib/checks/keyboard/page-no-duplicate-banner.json @@ -4,7 +4,7 @@ "after": "page-no-duplicate-after", "options": { "selector": "header:not([role]), [role=banner]", - "nativeScopeFilter": "article, aside, main, nav, section" + "role": "banner" }, "metadata": { "impact": "moderate", diff --git a/lib/checks/keyboard/page-no-duplicate-contentinfo.json b/lib/checks/keyboard/page-no-duplicate-contentinfo.json index d62619d525..20ef0008db 100644 --- a/lib/checks/keyboard/page-no-duplicate-contentinfo.json +++ b/lib/checks/keyboard/page-no-duplicate-contentinfo.json @@ -4,7 +4,7 @@ "after": "page-no-duplicate-after", "options": { "selector": "footer:not([role]), [role=contentinfo]", - "nativeScopeFilter": "article, aside, main, nav, section" + "role": "contentinfo" }, "metadata": { "impact": "moderate", diff --git a/lib/checks/label/help-same-as-label.json b/lib/checks/label/help-same-as-label.json index 318c13767e..f9e1eafec4 100644 --- a/lib/checks/label/help-same-as-label.json +++ b/lib/checks/label/help-same-as-label.json @@ -1,7 +1,6 @@ { "id": "help-same-as-label", "evaluate": "help-same-as-label-evaluate", - "enabled": false, "metadata": { "impact": "minor", "messages": { diff --git a/lib/checks/lists/only-dlitems-evaluate.js b/lib/checks/lists/only-dlitems-evaluate.js index 266d42a560..c3e40fda34 100644 --- a/lib/checks/lists/only-dlitems-evaluate.js +++ b/lib/checks/lists/only-dlitems-evaluate.js @@ -4,22 +4,22 @@ import { getRole, getExplicitRole } from '../../commons/aria'; /** * @deprecated */ -function onlyDlitemsEvaluate(node, options, virtualNode) { +export default function onlyDlitemsEvaluate(node, options, virtualNode) { const ALLOWED_ROLES = ['definition', 'term', 'list']; const base = { badNodes: [], hasNonEmptyTextNode: false }; - const content = virtualNode.children.reduce((content, child) => { + const content = virtualNode.children.reduce((vNodes, child) => { const { actualNode } = child; if ( actualNode.nodeName.toUpperCase() === 'DIV' && getRole(actualNode) === null ) { - return content.concat(child.children); + return vNodes.concat(child.children); } - return content.concat(child); + return vNodes.concat(child); }, []); const result = content.reduce((out, childNode) => { @@ -50,5 +50,3 @@ function onlyDlitemsEvaluate(node, options, virtualNode) { return !!result.badNodes.length || result.hasNonEmptyTextNode; } - -export default onlyDlitemsEvaluate; diff --git a/lib/checks/mobile/target-offset-evaluate.js b/lib/checks/mobile/target-offset-evaluate.js index d9314b3fc4..e799cb68f1 100644 --- a/lib/checks/mobile/target-offset-evaluate.js +++ b/lib/checks/mobile/target-offset-evaluate.js @@ -12,7 +12,9 @@ export default function targetOffsetEvaluate(node, options, vNode) { if (getRoleType(vNeighbor) !== 'widget' || !isFocusable(vNeighbor)) { continue; } - const offset = roundToSingleDecimal(getOffset(vNode, vNeighbor)); + // the offset code works off radius but we want our messaging to reflect diameter + const offset = + roundToSingleDecimal(getOffset(vNode, vNeighbor, minOffset / 2)) * 2; if (offset + roundingMargin >= minOffset) { continue; } diff --git a/lib/checks/mobile/target-offset.json b/lib/checks/mobile/target-offset.json index 1954d1e970..45513ac9a8 100644 --- a/lib/checks/mobile/target-offset.json +++ b/lib/checks/mobile/target-offset.json @@ -7,11 +7,11 @@ "metadata": { "impact": "serious", "messages": { - "pass": "Target has sufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", - "fail": "Target has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", + "pass": "Target has sufficient space from its closest neighbors (${data.closestOffset}px should be at least ${data.minOffset}px)", + "fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px)", "incomplete": { - "default": "Element with negative tabindex has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px). Is this a target?", - "nonTabbableNeighbor": "Target has insufficient offset from a neighbor with negative tabindex (${data.closestOffset}px should be at least ${data.minOffset}px). Is the neighbor a target?" + "default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is this a target?", + "nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is the neighbor a target?" } } } diff --git a/lib/checks/mobile/target-size-evaluate.js b/lib/checks/mobile/target-size-evaluate.js index 4f18351adf..a0224c8ad2 100644 --- a/lib/checks/mobile/target-size-evaluate.js +++ b/lib/checks/mobile/target-size-evaluate.js @@ -1,8 +1,10 @@ import { findNearbyElms, isFocusable, isInTabOrder } from '../../commons/dom'; import { getRoleType } from '../../commons/aria'; -import { splitRects, hasVisualOverlap } from '../../commons/math'; - -const roundingMargin = 0.05; +import { + splitRects, + rectHasMinimumSize, + hasVisualOverlap +} from '../../commons/math'; /** * Determine if an element has a minimum size, taking into account @@ -165,12 +167,6 @@ function isDescendantNotInTabOrder(vAncestor, vNode) { ); } -function rectHasMinimumSize(minSize, { width, height }) { - return ( - width + roundingMargin >= minSize && height + roundingMargin >= minSize - ); -} - function mapActualNodes(vNodes) { return vNodes.map(({ actualNode }) => actualNode); } diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js index 0fed9ea4b3..0e14b8a168 100644 --- a/lib/checks/navigation/region-evaluate.js +++ b/lib/checks/navigation/region-evaluate.js @@ -4,7 +4,6 @@ import * as standards from '../../commons/standards'; import matches from '../../commons/matches'; import cache from '../../core/base/cache'; -const landmarkRoles = standards.getAriaRolesByType('landmark'); const implicitAriaLiveRoles = ['alert', 'log', 'status']; export default function regionEvaluate(node, options, virtualNode) { @@ -92,6 +91,7 @@ function isRegion(virtualNode, options) { const node = virtualNode.actualNode; const role = getRole(virtualNode); const ariaLive = (node.getAttribute('aria-live') || '').toLowerCase().trim(); + const landmarkRoles = standards.getAriaRolesByType('landmark'); // Ignore content inside of aria-live if ( diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index e3dac584ad..4275eb47cf 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -1,7 +1,7 @@ import { tokenList } from '../../core/utils'; import { isVisibleToScreenReaders } from '../../commons/dom'; -function tdHeadersAttrEvaluate(node) { +export default function tdHeadersAttrEvaluate(node) { const cells = []; const reviewCells = []; const badCells = []; @@ -14,12 +14,9 @@ function tdHeadersAttrEvaluate(node) { } } - const ids = cells.reduce((ids, cell) => { - if (cell.getAttribute('id')) { - ids.push(cell.getAttribute('id')); - } - return ids; - }, []); + const ids = cells + .filter(cell => cell.getAttribute('id')) + .map(cell => cell.getAttribute('id')); cells.forEach(cell => { let isSelf = false; @@ -64,5 +61,3 @@ function tdHeadersAttrEvaluate(node) { return true; } - -export default tdHeadersAttrEvaluate; diff --git a/lib/commons/aria/get-accessible-refs.js b/lib/commons/aria/get-accessible-refs.js index 1f0ed55d57..9ad5627f03 100644 --- a/lib/commons/aria/get-accessible-refs.js +++ b/lib/commons/aria/get-accessible-refs.js @@ -13,22 +13,26 @@ function cacheIdRefs(node, idRefs, refAttrs) { if (node.hasAttribute) { if (node.nodeName.toUpperCase() === 'LABEL' && node.hasAttribute('for')) { const id = node.getAttribute('for'); - idRefs[id] = idRefs[id] || []; - idRefs[id].push(node); + if (!idRefs.has(id)) { + idRefs.set(id, [node]); + } else { + idRefs.get(id).push(node); + } } for (let i = 0; i < refAttrs.length; ++i) { const attr = refAttrs[i]; const attrValue = sanitize(node.getAttribute(attr) || ''); - if (!attrValue) { continue; } - const tokens = tokenList(attrValue); - for (let k = 0; k < tokens.length; ++k) { - idRefs[tokens[k]] = idRefs[tokens[k]] || []; - idRefs[tokens[k]].push(node); + for (const token of tokenList(attrValue)) { + if (!idRefs.has(token)) { + idRefs.set(token, [node]); + } else { + idRefs.get(token).push(node); + } } } } @@ -50,22 +54,21 @@ function getAccessibleRefs(node) { let root = getRootNode(node); root = root.documentElement || root; // account for shadow roots - const idRefsByRoot = cache.get('idRefsByRoot', () => new WeakMap()); + const idRefsByRoot = cache.get('idRefsByRoot', () => new Map()); let idRefs = idRefsByRoot.get(root); if (!idRefs) { - idRefs = {}; + idRefs = new Map(); idRefsByRoot.set(root, idRefs); const refAttrs = Object.keys(standards.ariaAttrs).filter(attr => { const { type } = standards.ariaAttrs[attr]; return idRefsRegex.test(type); }); - cacheIdRefs(root, idRefs, refAttrs); } - return idRefs[node.id] || []; + return idRefs.get(node.id) ?? []; } export default getAccessibleRefs; diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index 262f90fe1b..69eb1352a6 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -54,7 +54,11 @@ function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) { } const textRects = getVisibleChildTextRects(elm); - let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); + let bgColors = + getTextShadowColors(elm, { + minRatio: shadowOutlineEmMax, + ignoreEdgeCount: true + }) ?? []; // Empty object shouldn't be necessary. Just being safe. if (bgColors.length) { bgColors = [{ color: bgColors.reduce(flattenShadowColors) }]; } diff --git a/lib/commons/color/get-stroke-colors-from-shadows.js b/lib/commons/color/get-stroke-colors-from-shadows.js new file mode 100644 index 0000000000..c3f60142e7 --- /dev/null +++ b/lib/commons/color/get-stroke-colors-from-shadows.js @@ -0,0 +1,97 @@ +import Color from './color'; + +/** Magic numbers **/ +// Alpha value to use when text shadows are offset between .5px and 1.5px +const SHADOW_STROKE_ALPHA = 0.54; +// Shadows offset by less than this are not visible enough no matter how much you stack them +const VISIBLE_SHADOW_MIN_PX = 0.5; +// Shadows offset by more than this have full opacity +const OPAQUE_STROKE_OFFSET_MIN_PX = 1.5; + +const edges = ['top', 'right', 'bottom', 'left']; + +/** + * Work out which color(s) of an array of text shadows form a stroke around the text. + * @param {Array[]} testShadows Parsed test shadows (see color.parseTestShadow()) + * @param {Object} options (optional) + * @property {Bool} ignoreEdgeCount Do not return null when if shadows cover 2 or 3 edges, ignore those instead + * @returns {Array|null} Array of colors or null if text-shadow was too complex to measure + */ +export default function getStrokeColorsFromShadows( + parsedShadows, + { ignoreEdgeCount = false } = {} +) { + const shadowMap = getShadowColorsMap(parsedShadows); + const shadowsByColor = Object.entries(shadowMap).map(([colorStr, sides]) => { + const edgeCount = edges.filter(side => sides[side].length !== 0).length; + return { colorStr, sides, edgeCount }; + }); + + // Bail immediately if any shadow group covers too much of the text to be ignored, but not enough to be tested + if ( + !ignoreEdgeCount && + shadowsByColor.some(({ edgeCount }) => edgeCount > 1 && edgeCount < 4) + ) { + return null; + } + + return shadowsByColor + .map(shadowGroupToColor) + .filter(shadow => shadow !== null); +} + +/** + * Create a map of colors to the sides they are on + */ +function getShadowColorsMap(parsedShadows) { + const colorMap = {}; + for (const { colorStr, pixels } of parsedShadows) { + colorMap[colorStr] ??= { top: [], right: [], bottom: [], left: [] }; + const borders = colorMap[colorStr]; + const [offsetX, offsetY] = pixels; + + if (offsetX > VISIBLE_SHADOW_MIN_PX) { + borders.right.push(offsetX); + } else if (-offsetX > VISIBLE_SHADOW_MIN_PX) { + borders.left.push(-offsetX); + } + if (offsetY > VISIBLE_SHADOW_MIN_PX) { + borders.bottom.push(offsetY); + } else if (-offsetY > VISIBLE_SHADOW_MIN_PX) { + borders.top.push(-offsetY); + } + } + return colorMap; +} + +/** + * Using colorStr and thickness of sides, create a color object + */ +function shadowGroupToColor({ colorStr, sides, edgeCount }) { + if (edgeCount !== 4) { + return null; // ignore thin shadows and shadows on one side of the text + } + const strokeColor = new Color(); + strokeColor.parseString(colorStr); + + // Detect whether any sides' shadows are thin enough to be considered + // translucent, and if so, calculate an alpha value to apply on top of + // the parsed color. + let density = 0; + let isSolid = true; + edges.forEach(edge => { + // Decimal values are ignored. a .6px shadow is treated as 1px + // because it is not rendered evenly around the text. + // I.e. .6 ends up as 70% alpha on one side and 16% on the other. + density += sides[edge].length / 4; + isSolid &&= sides[edge].every( + offset => offset > OPAQUE_STROKE_OFFSET_MIN_PX + ); + }); + + if (!isSolid) { + // As more shadows surround the text, the opacity increases + strokeColor.alpha = 1 - Math.pow(SHADOW_STROKE_ALPHA, density); + } + return strokeColor; +} diff --git a/lib/commons/color/get-text-shadow-colors.js b/lib/commons/color/get-text-shadow-colors.js index 756ad93d19..a089b8c885 100644 --- a/lib/commons/color/get-text-shadow-colors.js +++ b/lib/commons/color/get-text-shadow-colors.js @@ -1,18 +1,28 @@ import Color from './color'; import assert from '../../core/utils/assert'; +import getStrokeColorsFromShadows from './get-stroke-colors-from-shadows'; +import parseTextShadows from './parse-text-shadows'; /** - * Get text-shadow colors that can impact the color contrast of the text + * Get text-shadow colors that can impact the color contrast of the text, including from: + * - Shadows which are individually thick enough (minRatio <= thickness <= maxRatio) to distinguish as characters + * - Groups of "thin" shadows (thickness < minRatio) that collectively act as a pseudo-text-stroke (see #4064) * @param {Element} node DOM Element * @param {Object} options (optional) - * @property {Bool} minRatio Ignore shadows smaller than this, ratio shadow size divided by font size + * @property {Bool} minRatio Treat shadows smaller than this as "thin", ratio shadow size divided by font size * @property {Bool} maxRatio Ignore shadows equal or larger than this, ratio shadow size divided by font size + * @property {Bool} ignoreEdgeCount Do not return null when if shadows cover 2 or 3 edges, ignore those instead + * @returns {Array|null} Array of colors or null if text-shadow was too complex to measure */ -function getTextShadowColors(node, { minRatio, maxRatio } = {}) { +export default function getTextShadowColors( + node, + { minRatio, maxRatio, ignoreEdgeCount } = {} +) { + const shadowColors = []; const style = window.getComputedStyle(node); const textShadow = style.getPropertyValue('text-shadow'); if (textShadow === 'none') { - return []; + return shadowColors; } const fontSizeStr = style.getPropertyValue('font-size'); @@ -22,80 +32,53 @@ function getTextShadowColors(node, { minRatio, maxRatio } = {}) { `Unable to determine font-size value ${fontSizeStr}` ); - const shadowColors = []; + const thinShadows = []; const shadows = parseTextShadows(textShadow); - shadows.forEach(({ colorStr, pixels }) => { + for (const shadow of shadows) { // Defaults only necessary for IE - colorStr = colorStr || style.getPropertyValue('color'); - const [offsetY, offsetX, blurRadius = 0] = pixels; - if ( - (!minRatio || blurRadius >= fontSize * minRatio) && - (!maxRatio || blurRadius < fontSize * maxRatio) - ) { - const color = textShadowColor({ - colorStr, - offsetY, - offsetX, - blurRadius, - fontSize + const colorStr = shadow.colorStr || style.getPropertyValue('color'); + const [offsetX, offsetY, blurRadius = 0] = shadow.pixels; + if (maxRatio && blurRadius >= fontSize * maxRatio) { + continue; + } + if (minRatio && blurRadius < fontSize * minRatio) { + thinShadows.push({ colorStr, pixels: shadow.pixels }); + continue; + } + if (thinShadows.length > 0) { + // Inset any stroke colors before this shadow + const strokeColors = getStrokeColorsFromShadows(thinShadows, { + ignoreEdgeCount }); - shadowColors.push(color); + if (strokeColors === null) { + return null; // Exit early if text-shadow is too complex + } + shadowColors.push(...strokeColors); + thinShadows.splice(0, thinShadows.length); // empty } - }); - return shadowColors; -} -/** - * Parse text-shadow property value. Required for IE, which can return the color - * either at the start or the end, and either in rgb(a) or as a named color - */ -function parseTextShadows(textShadow) { - let current = { pixels: [] }; - let str = textShadow.trim(); - const shadows = [current]; - if (!str) { - return []; + const color = textShadowColor({ + colorStr, + offsetX, + offsetY, + blurRadius, + fontSize + }); + shadowColors.push(color); } - while (str) { - const colorMatch = - str.match(/^rgba?\([0-9,.\s]+\)/i) || - str.match(/^[a-z]+/i) || - str.match(/^#[0-9a-f]+/i); - const pixelMatch = str.match(/^([0-9.-]+)px/i) || str.match(/^(0)/); - - if (colorMatch) { - assert( - !current.colorStr, - `Multiple colors identified in text-shadow: ${textShadow}` - ); - str = str.replace(colorMatch[0], '').trim(); - current.colorStr = colorMatch[0]; - } else if (pixelMatch) { - assert( - current.pixels.length < 3, - `Too many pixel units in text-shadow: ${textShadow}` - ); - str = str.replace(pixelMatch[0], '').trim(); - const pixelUnit = parseFloat( - (pixelMatch[1][0] === '.' ? '0' : '') + pixelMatch[1] - ); - current.pixels.push(pixelUnit); - } else if (str[0] === ',') { - // multiple text-shadows in a single string (e.g. `text-shadow: 1px 1px 1px #000, 3px 3px 5px blue;` - assert( - current.pixels.length >= 2, - `Missing pixel value in text-shadow: ${textShadow}` - ); - current = { pixels: [] }; - shadows.push(current); - str = str.substr(1).trim(); - } else { - throw new Error(`Unable to process text-shadows: ${textShadow}`); + if (thinShadows.length > 0) { + // Append any remaining stroke colors + const strokeColors = getStrokeColorsFromShadows(thinShadows, { + ignoreEdgeCount + }); + if (strokeColors === null) { + return null; // Exit early if text-shadow is too complex } + shadowColors.push(...strokeColors); } - return shadows; + return shadowColors; } function textShadowColor({ colorStr, offsetX, offsetY, blurRadius, fontSize }) { @@ -121,5 +104,3 @@ function blurRadiusToAlpha(blurRadius, fontSize) { const relativeBlur = blurRadius / fontSize; return 0.185 / (relativeBlur + 0.4); } - -export default getTextShadowColors; diff --git a/lib/commons/color/incomplete-data.js b/lib/commons/color/incomplete-data.js index 0ece568087..c9b7194f32 100644 --- a/lib/commons/color/incomplete-data.js +++ b/lib/commons/color/incomplete-data.js @@ -1,9 +1,12 @@ +import cache from '../../core/base/cache'; + +const cacheKey = 'color.incompleteData'; + /** * API for handling incomplete color data * @namespace axe.commons.color.incompleteData * @inner */ -let data = {}; const incompleteData = { /** * Store incomplete data by key with a string value @@ -17,6 +20,7 @@ const incompleteData = { if (typeof key !== 'string') { throw new Error('Incomplete data: key must be a string'); } + const data = cache.get(cacheKey, () => ({})); if (reason) { data[key] = reason; } @@ -31,7 +35,8 @@ const incompleteData = { * @return {String} String for reason we couldn't tell */ get: function (key) { - return data[key]; + const data = cache.get(cacheKey); + return data?.[key]; }, /** * Clear incomplete data on demand @@ -40,7 +45,7 @@ const incompleteData = { * @instance */ clear: function () { - data = {}; + cache.set(cacheKey, {}); } }; diff --git a/lib/commons/color/index.js b/lib/commons/color/index.js index 78cc26c68b..edd341d5ec 100644 --- a/lib/commons/color/index.js +++ b/lib/commons/color/index.js @@ -16,7 +16,9 @@ export { default as getContrast } from './get-contrast'; export { default as getForegroundColor } from './get-foreground-color'; export { default as getOwnBackgroundColor } from './get-own-background-color'; export { default as getRectStack } from './get-rect-stack'; +export { default as getStrokeColorsFromShadows } from './get-stroke-colors-from-shadows'; +export { default as getTextShadowColors } from './get-text-shadow-colors'; export { default as hasValidContrastRatio } from './has-valid-contrast-ratio'; export { default as incompleteData } from './incomplete-data'; -export { default as getTextShadowColors } from './get-text-shadow-colors'; +export { default as parseTextShadows } from './parse-text-shadows'; export { getStackingContext, stackingContextToColor } from './stacking-context'; diff --git a/lib/commons/color/parse-text-shadows.js b/lib/commons/color/parse-text-shadows.js new file mode 100644 index 0000000000..386c4b028b --- /dev/null +++ b/lib/commons/color/parse-text-shadows.js @@ -0,0 +1,61 @@ +import assert from '../../core/utils/assert'; + +/** + * Parse text-shadow property value. Required for IE, which can return the color + * either at the start or the end, and either in rgb(a) or as a named color + * @param {String} textShadow + * @returns {Array} Array of objects with `pixels` and `colorStr` properties + */ +export default function parseTextShadows(textShadow) { + let current = { pixels: [] }; + let str = textShadow.trim(); + const shadows = [current]; + if (!str) { + return []; + } + + while (str) { + const colorMatch = + // match a color name or function (e.g. `oklch(39.2% 0.4 0 / 0.5)`) or a hex value + str.match(/^[a-z]+(\([^)]+\))?/i) || str.match(/^#[0-9a-f]+/i); + const pixelMatch = str.match(/^([0-9.-]+)px/i) || str.match(/^(0)/); + + if (colorMatch) { + assert( + !current.colorStr, + `Multiple colors identified in text-shadow: ${textShadow}` + ); + str = str.replace(colorMatch[0], '').trim(); + current.colorStr = colorMatch[0]; + } else if (pixelMatch) { + assert( + current.pixels.length < 3, + `Too many pixel units in text-shadow: ${textShadow}` + ); + str = str.replace(pixelMatch[0], '').trim(); + const pixelUnit = parseFloat( + (pixelMatch[1][0] === '.' ? '0' : '') + pixelMatch[1] + ); + current.pixels.push(pixelUnit); + } else if (str[0] === ',') { + // multiple text-shadows in a single string (e.g. `text-shadow: 1px 1px 1px #000, 3px 3px 5px blue;` + assert( + current.pixels.length >= 2, + `Missing pixel value in text-shadow: ${textShadow}` + ); + current = { pixels: [] }; + shadows.push(current); + str = str.substr(1).trim(); + } else { + throw new Error(`Unable to process text-shadows: ${str}`); + } + } + + shadows.forEach(({ pixels }) => { + if (pixels.length === 2) { + pixels.push(0); // Append default blur + } + }); + + return shadows; +} diff --git a/lib/commons/color/stacking-context.js b/lib/commons/color/stacking-context.js index 05c49e5745..db19340848 100644 --- a/lib/commons/color/stacking-context.js +++ b/lib/commons/color/stacking-context.js @@ -60,9 +60,9 @@ import flattenColors from './flatten-colors'; * @return {Object} */ export function getStackingContext(elm, elmStack) { - const vNode = getNodeFromTree(elm); - if (vNode._stackingContext) { - return vNode._stackingContext; + const virtualNode = getNodeFromTree(elm); + if (virtualNode._stackingContext) { + return virtualNode._stackingContext; } const stackingContext = []; @@ -108,7 +108,7 @@ export function getStackingContext(elm, elmStack) { context.bgColor = bgColor; }); - vNode._stackingContext = stackingContext; + virtualNode._stackingContext = stackingContext; return stackingContext; } diff --git a/lib/commons/dom/create-grid.js b/lib/commons/dom/create-grid.js index 465acdc0f2..612e885bbc 100644 --- a/lib/commons/dom/create-grid.js +++ b/lib/commons/dom/create-grid.js @@ -6,11 +6,13 @@ import { getNodeFromTree, getScroll, isShadowRoot } from '../../core/utils'; import constants from '../../core/constants'; import cache from '../../core/base/cache'; import assert from '../../core/utils/assert'; +import getOverflowHiddenAncestors from './get-overflow-hidden-ancestors'; +import { getIntersectionRect } from '../math'; -const ROOT_ORDER = 0; -const DEFAULT_ORDER = 0.1; -const FLOAT_ORDER = 0.2; -const POSITION_STATIC_ORDER = 0.3; +const ROOT_LEVEL = 0; +const DEFAULT_LEVEL = 0.1; +const FLOAT_LEVEL = 0.2; +const POSITION_LEVEL = 0.3; let nodeIndex = 0; /** @@ -39,7 +41,9 @@ export default function createGrid( } nodeIndex = 0; - vNode._stackingOrder = [createContext(ROOT_ORDER, null)]; + vNode._stackingOrder = [ + createStackingContext(ROOT_LEVEL, nodeIndex++, null) + ]; rootGrid ??= new Grid(); addNodeToGrid(rootGrid, vNode); @@ -251,72 +255,94 @@ function isFlexOrGridContainer(vNode) { /** * Determine the stacking order of an element. The stacking order is an array of - * zIndex values for each stacking context parent. + * stacking contexts in ancestor order. * @param {VirtualNode} vNode * @param {VirtualNode} parentVNode - * @param {Number} nodeIndex + * @param {Number} treeOrder * @return {Number[]} */ -function createStackingOrder(vNode, parentVNode, nodeIndex) { +function createStackingOrder(vNode, parentVNode, treeOrder) { const stackingOrder = parentVNode._stackingOrder.slice(); - // if a positioned element has z-index: auto or 0 (step #8), or if - // a non-positioned floating element (step #5), treat it as its - // own stacking context - // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - if (!isStackingContext(vNode, parentVNode)) { - if (vNode.getComputedStylePropertyValue('position') !== 'static') { - // Put positioned elements above floated elements - stackingOrder.push(createContext(POSITION_STATIC_ORDER, vNode)); - } else if (vNode.getComputedStylePropertyValue('float') !== 'none') { - // Put floated elements above z-index: 0 - // (step #5 floating get sorted below step #8 positioned) - stackingOrder.push(createContext(FLOAT_ORDER, vNode)); - } - return stackingOrder; - } - // if an element creates a stacking context, find the first // true stack (not a "fake" stack created from positioned or // floated elements without a z-index) and create a new stack at // that point (step #5 and step #8) // @see https://www.w3.org/Style/css2-updates/css2/zindex.html - const index = stackingOrder.findIndex(({ value }) => - [ROOT_ORDER, FLOAT_ORDER, POSITION_STATIC_ORDER].includes(value) - ); - if (index !== -1) { - stackingOrder.splice(index, stackingOrder.length - index); + if (isStackingContext(vNode, parentVNode)) { + const index = stackingOrder.findIndex(({ stackLevel }) => + [ROOT_LEVEL, FLOAT_LEVEL, POSITION_LEVEL].includes(stackLevel) + ); + if (index !== -1) { + stackingOrder.splice(index, stackingOrder.length - index); + } } - const zIndex = getRealZIndex(vNode, parentVNode); - if (!['auto', '0'].includes(zIndex)) { - stackingOrder.push(createContext(parseInt(zIndex), vNode)); - return stackingOrder; - } - // since many things can create a new stacking context without position or - // z-index, we need to know the order in the dom to sort them by. Use the - // nodeIndex property to create a number less than the "fake" stacks from - // positioned or floated elements but still larger than 0 - // 10 pad gives us the ability to sort up to 1B nodes (padStart does not - // exist in ie11) - let float = nodeIndex.toString(); - while (float.length < 10) { - float = '0' + float; - } - stackingOrder.push( - createContext(parseFloat(`${DEFAULT_ORDER}${float}`), vNode) - ); - + const stackLevel = getStackLevel(vNode, parentVNode); + if (stackLevel !== null) { + stackingOrder.push(createStackingContext(stackLevel, treeOrder, vNode)); + } return stackingOrder; } -function createContext(value, vNode) { +/** + * Create a stacking context, keeping track of the stack level, tree order, and virtual + * node container. + * @see https://www.w3.org/Style/css2-updates/css2/zindex.html + * @see https://www.w3.org/Style/css2-updates/css2/visuren.html#layers + * @param {Number} stackLevel - The stack level of the stacking context + * @param {Number} treeOrder - The elements depth-first traversal order + * @param {VirtualNode} vNode - The virtual node that is the container for the stacking context + */ +function createStackingContext(stackLevel, treeOrder, vNode) { return { - value, + stackLevel, + treeOrder, vNode }; } +/** + * Calculate the level of the stacking context. + * @param {VirtualNode} vNode - The virtual node container of the stacking context + * @param {VirtualNode} parentVNode - The parent virtual node of the vNode + * @return {Number|null} + */ +function getStackLevel(vNode, parentVNode) { + const zIndex = getRealZIndex(vNode, parentVNode); + if (!['auto', '0'].includes(zIndex)) { + return parseInt(zIndex); + } + + // if a positioned element has z-index: auto or 0 (step #8), or if + // a non-positioned floating element (step #5), treat it as its + // own stacking context + // @see https://www.w3.org/Style/css2-updates/css2/zindex.html + + // Put positioned elements above floated elements + if (vNode.getComputedStylePropertyValue('position') !== 'static') { + return POSITION_LEVEL; + } + + // Put floated elements above z-index: 0 + // (step #5 floating get sorted below step #8 positioned) + if (vNode.getComputedStylePropertyValue('float') !== 'none') { + return FLOAT_LEVEL; + } + + if (isStackingContext(vNode, parentVNode)) { + return DEFAULT_LEVEL; + } + + return null; +} + +/** + * Calculate the z-index value of a node taking into account when doesn't apply. + * @param {VirtualNode} vNode - The virtual node to get z-index of + * @param {VirtualNode} parentVNode - The parent virtual node of the vNode + * @return {Number|'auto'} + */ function getRealZIndex(vNode, parentVNode) { const position = vNode.getComputedStylePropertyValue('position'); if (position === 'static' && !isFlexOrGridContainer(parentVNode)) { @@ -357,7 +383,7 @@ function findScrollRegionParent(vNode, parentVNode) { // cache result of parent scroll region so we don't have to look up the entire // tree again for a child node checkedNodes.forEach( - vNode => (vNode._scrollRegionParent = scrollRegionParent) + virtualNode => (virtualNode._scrollRegionParent = scrollRegionParent) ); return scrollRegionParent; } @@ -368,11 +394,22 @@ function findScrollRegionParent(vNode, parentVNode) { * @param {VirtualNode} */ function addNodeToGrid(grid, vNode) { - vNode.clientRects.forEach(rect => { + const overflowHiddenNodes = getOverflowHiddenAncestors(vNode); + + vNode.clientRects.forEach(clientRect => { + // ignore any rects that are outside the bounds of overflow hidden ancestors + const visibleRect = overflowHiddenNodes.reduce((rect, overflowNode) => { + return rect && getIntersectionRect(rect, overflowNode.boundingClientRect); + }, clientRect); + + if (!visibleRect) { + return; + } + // save a reference to where this element is in the grid so we // can find it even if it's in a subgrid vNode._grid ??= grid; - const gridRect = grid.getGridPositionOfRect(rect); + const gridRect = grid.getGridPositionOfRect(visibleRect); grid.loopGridPosition(gridRect, gridCell => { if (!gridCell.includes(vNode)) { gridCell.push(vNode); diff --git a/lib/commons/dom/get-target-rects.js b/lib/commons/dom/get-target-rects.js new file mode 100644 index 0000000000..fc5b2aabe7 --- /dev/null +++ b/lib/commons/dom/get-target-rects.js @@ -0,0 +1,38 @@ +import findNearbyElms from './find-nearby-elms'; +import isInTabOrder from './is-in-tab-order'; +import { splitRects, hasVisualOverlap } from '../math'; +import memoize from '../../core/utils/memoize'; + +export default memoize(getTargetRects); + +/** + * Return all unobscured rects of a target. + * @see https://www.w3.org/TR/WCAG22/#dfn-bounding-boxes + * @param {VitualNode} vNode + * @return {DOMRect[]} + */ +function getTargetRects(vNode) { + const nodeRect = vNode.boundingClientRect; + const overlappingVNodes = findNearbyElms(vNode).filter(vNeighbor => { + return ( + hasVisualOverlap(vNode, vNeighbor) && + vNeighbor.getComputedStylePropertyValue('pointer-events') !== 'none' && + !isDescendantNotInTabOrder(vNode, vNeighbor) + ); + }); + + if (!overlappingVNodes.length) { + return [nodeRect]; + } + + const obscuringRects = overlappingVNodes.map( + ({ boundingClientRect: rect }) => rect + ); + return splitRects(nodeRect, obscuringRects); +} + +function isDescendantNotInTabOrder(vAncestor, vNode) { + return ( + vAncestor.actualNode.contains(vNode.actualNode) && !isInTabOrder(vNode) + ); +} diff --git a/lib/commons/dom/get-target-size.js b/lib/commons/dom/get-target-size.js new file mode 100644 index 0000000000..926bbc0d0d --- /dev/null +++ b/lib/commons/dom/get-target-size.js @@ -0,0 +1,29 @@ +import getTargetRects from './get-target-rects'; +import { rectHasMinimumSize } from '../math'; +import memoize from '../../core/utils/memoize'; + +export default memoize(getTargetSize); + +/** + * Compute the target size of an element. + * @see https://www.w3.org/TR/WCAG22/#dfn-targets + */ +function getTargetSize(vNode, minSize) { + const rects = getTargetRects(vNode); + return getLargestRect(rects, minSize); +} + +// Find the largest rectangle in the array, prioritize ones that meet a minimum size +function getLargestRect(rects, minSize) { + return rects.reduce((rectA, rectB) => { + const rectAisMinimum = rectHasMinimumSize(minSize, rectA); + const rectBisMinimum = rectHasMinimumSize(minSize, rectB); + // Prioritize rects that pass the minimum + if (rectAisMinimum !== rectBisMinimum) { + return rectAisMinimum ? rectA : rectB; + } + const areaA = rectA.width * rectA.height; + const areaB = rectB.width * rectB.height; + return areaA > areaB ? rectA : rectB; + }); +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 9e5f16a741..57ae0756c9 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -17,6 +17,8 @@ export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-anc export { default as getRootNode } from './get-root-node'; export { default as getScrollOffset } from './get-scroll-offset'; export { default as getTabbableElements } from './get-tabbable-elements'; +export { default as getTargetRects } from './get-target-rects'; +export { default as getTargetSize } from './get-target-size'; export { default as getTextElementStack } from './get-text-element-stack'; export { default as getViewportSize } from './get-viewport-size'; export { default as getVisibleChildTextRects } from './get-visible-child-text-rects'; diff --git a/lib/commons/dom/visually-sort.js b/lib/commons/dom/visually-sort.js index 1ceaf8245e..ee5a0c905f 100644 --- a/lib/commons/dom/visually-sort.js +++ b/lib/commons/dom/visually-sort.js @@ -1,8 +1,10 @@ import createGrid from './create-grid'; + /** * Visually sort nodes based on their stack order * References: * https://www.w3.org/Style/css2-updates/css2/zindex.html + * https://www.w3.org/Style/css2-updates/css2/visuren.html#layers * @param {VirtualNode} * @param {VirtualNode} */ @@ -19,14 +21,19 @@ export default function visuallySort(a, b) { } // 7. the child stacking contexts with positive stack levels (least positive first). - if (b._stackingOrder[i].value > a._stackingOrder[i].value) { + if (b._stackingOrder[i].stackLevel > a._stackingOrder[i].stackLevel) { return 1; } // 2. the child stacking contexts with negative stack levels (most negative first). - if (b._stackingOrder[i].value < a._stackingOrder[i].value) { + if (b._stackingOrder[i].stackLevel < a._stackingOrder[i].stackLevel) { return -1; } + + // stacks share the same stack level so compare document order + if (b._stackingOrder[i].treeOrder !== a._stackingOrder[i].treeOrder) { + return b._stackingOrder[i].treeOrder - a._stackingOrder[i].treeOrder; + } } // nodes are the same stacking order diff --git a/lib/commons/matches/condition.js b/lib/commons/matches/condition.js index 2086efd9b2..88ad371c6e 100644 --- a/lib/commons/matches/condition.js +++ b/lib/commons/matches/condition.js @@ -10,11 +10,9 @@ * ``` * * @param {any} argument - * @param {Function|Null|undefined} condition + * @param {Function|Null|undefined} matcher * @returns {Boolean} */ -function condition(arg, condition) { - return !!condition(arg); +export default function condition(arg, matcher) { + return !!matcher(arg); } - -export default condition; diff --git a/lib/commons/math/get-offset.js b/lib/commons/math/get-offset.js index 408a3e7cef..664871fda0 100644 --- a/lib/commons/math/get-offset.js +++ b/lib/commons/math/get-offset.js @@ -1,91 +1,76 @@ +import { getTargetRects, getTargetSize } from '../dom'; +import { getBoundingRect } from './get-bounding-rect'; +import { isPointInRect } from './is-point-in-rect'; +import { getRectCenter } from './get-rect-center'; +import rectHasMinimumSize from './rect-has-minimum-size'; + /** - * Get the offset between node A and node B + * Get the offset between target A and neighbor B. This assumes that the target is undersized and needs to check the spacing exception. * @method getOffset * @memberof axe.commons.math - * @param {VirtualNode} vNodeA - * @param {VirtualNode} vNodeB + * @param {VirtualNode} vTarget + * @param {VirtualNode} vNeighbor + * @param {Number} radius * @returns {number} */ -export default function getOffset(vNodeA, vNodeB) { - const rectA = vNodeA.boundingClientRect; - const rectB = vNodeB.boundingClientRect; - const pointA = getFarthestPoint(rectA, rectB); - const pointB = getClosestPoint(pointA, rectA, rectB); - return pointDistance(pointA, pointB); -} +export default function getOffset(vTarget, vNeighbor, minRadiusNeighbour = 12) { + const targetRects = getTargetRects(vTarget); + const neighborRects = getTargetRects(vNeighbor); -/** - * Get a point on rectA that is farthest away from rectB - * @param {Rect} rectA - * @param {Rect} rectB - * @returns {Point} - */ -function getFarthestPoint(rectA, rectB) { - const dimensionProps = [ - ['x', 'left', 'right', 'width'], - ['y', 'top', 'bottom', 'height'] - ]; - const farthestPoint = {}; - dimensionProps.forEach(([axis, start, end, diameter]) => { - if (rectB[start] < rectA[start] && rectB[end] > rectA[end]) { - farthestPoint[axis] = rectA[start] + rectA[diameter] / 2; // center | middle - return; - } - // Work out which edge of A is farthest away from the center of B - const centerB = rectB[start] + rectB[diameter] / 2; - const startDistance = Math.abs(centerB - rectA[start]); - const endDistance = Math.abs(centerB - rectA[end]); - if (startDistance >= endDistance) { - farthestPoint[axis] = rectA[start]; // left | top - } else { - farthestPoint[axis] = rectA[end]; // right | bottom - } - }); - return farthestPoint; -} + if (!targetRects.length || !neighborRects.length) { + return 0; + } -/** - * Get a point on the adjacentRect, that is as close the point given from ownRect - * @param {Point} ownRectPoint - * @param {Rect} ownRect - * @param {Rect} adjacentRect - * @returns {Point} - */ -function getClosestPoint({ x, y }, ownRect, adjacentRect) { - if (pointInRect({ x, y }, adjacentRect)) { - // Check if there is an opposite corner inside the adjacent rectangle - const closestPoint = getCornerInAdjacentRect( - { x, y }, - ownRect, - adjacentRect - ); - if (closestPoint !== null) { - return closestPoint; + const targetBoundingBox = targetRects.reduce(getBoundingRect); + const targetCenter = getRectCenter(targetBoundingBox); + + // calculate distance to the closest edge of each neighbor rect + let minDistance = Infinity; + for (const rect of neighborRects) { + if (isPointInRect(targetCenter, rect)) { + return 0; } - adjacentRect = ownRect; + + const closestPoint = getClosestPoint(targetCenter, rect); + const distance = pointDistance(targetCenter, closestPoint); + minDistance = Math.min(minDistance, distance); + } + + const neighborTargetSize = getTargetSize(vNeighbor); + if (rectHasMinimumSize(minRadiusNeighbour * 2, neighborTargetSize)) { + return minDistance; } - const { top, right, bottom, left } = adjacentRect; - // Is the adjacent rect horizontally or vertically aligned - const xAligned = x >= left && x <= right; - const yAligned = y >= top && y <= bottom; - // Find the closest edge of the adjacent rect - const closestX = Math.abs(left - x) < Math.abs(right - x) ? left : right; - const closestY = Math.abs(top - y) < Math.abs(bottom - y) ? top : bottom; + const neighborBoundingBox = neighborRects.reduce(getBoundingRect); + const neighborCenter = getRectCenter(neighborBoundingBox); + // subtract the radius of the circle from the distance to center to get distance to edge of the neighbor circle + const centerDistance = + pointDistance(targetCenter, neighborCenter) - minRadiusNeighbour; - if (!xAligned && yAligned) { - return { x: closestX, y }; // Closest horizontal point - } else if (xAligned && !yAligned) { - return { x, y: closestY }; // Closest vertical point - } else if (!xAligned && !yAligned) { - return { x: closestX, y: closestY }; // Closest diagonal corner + return Math.max(0, Math.min(minDistance, centerDistance)); +} + +function getClosestPoint(point, rect) { + let x; + let y; + + if (point.x < rect.left) { + x = rect.left; + } else if (point.x > rect.right) { + x = rect.right; + } else { + x = point.x; } - // ownRect (partially) obscures adjacentRect - if (Math.abs(x - closestX) < Math.abs(y - closestY)) { - return { x: closestX, y }; // Inside, closest edge is horizontal + + if (point.y < rect.top) { + y = rect.top; + } else if (point.y > rect.bottom) { + y = rect.bottom; } else { - return { x, y: closestY }; // Inside, closest edge is vertical + y = point.y; } + + return { x, y }; } /** @@ -95,55 +80,5 @@ function getClosestPoint({ x, y }, ownRect, adjacentRect) { * @returns {number} */ function pointDistance(pointA, pointB) { - const xDistance = Math.abs(pointA.x - pointB.x); - const yDistance = Math.abs(pointA.y - pointB.y); - if (!xDistance || !yDistance) { - return xDistance || yDistance; // If either is 0, return the other - } - return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2)); -} - -/** - * Return if a point is within a rect - * @param {Point} point - * @param {Rect} rect - * @returns {boolean} - */ -function pointInRect({ x, y }, rect) { - return y >= rect.top && x <= rect.right && y <= rect.bottom && x >= rect.left; -} - -/** - * - * @param {Point} ownRectPoint - * @param {Rect} ownRect - * @param {Rect} adjacentRect - * @returns {Point | null} With x and y - */ -function getCornerInAdjacentRect({ x, y }, ownRect, adjacentRect) { - let closestX, closestY; - // Find the opposite corner, if it is inside the adjacent rect; - if (x === ownRect.left && ownRect.right < adjacentRect.right) { - closestX = ownRect.right; - } else if (x === ownRect.right && ownRect.left > adjacentRect.left) { - closestX = ownRect.left; - } - if (y === ownRect.top && ownRect.bottom < adjacentRect.bottom) { - closestY = ownRect.bottom; - } else if (y === ownRect.bottom && ownRect.top > adjacentRect.top) { - closestY = ownRect.top; - } - - if (!closestX && !closestY) { - return null; // opposite corners are outside the rect, or {x,y} was a center point - } else if (!closestY) { - return { x: closestX, y }; - } else if (!closestX) { - return { x, y: closestY }; - } - if (Math.abs(x - closestX) < Math.abs(y - closestY)) { - return { x: closestX, y }; - } else { - return { x, y: closestY }; - } + return Math.hypot(pointA.x - pointB.x, pointA.y - pointB.y); } diff --git a/lib/commons/math/index.js b/lib/commons/math/index.js index b6edc3d4aa..88ac3a9cdc 100644 --- a/lib/commons/math/index.js +++ b/lib/commons/math/index.js @@ -4,5 +4,6 @@ export { default as getOffset } from './get-offset'; export { getRectCenter } from './get-rect-center'; export { default as hasVisualOverlap } from './has-visual-overlap'; export { isPointInRect } from './is-point-in-rect'; +export { default as rectHasMinimumSize } from './rect-has-minimum-size'; export { default as rectsOverlap } from './rects-overlap'; export { default as splitRects } from './split-rects'; diff --git a/lib/commons/math/rect-has-minimum-size.js b/lib/commons/math/rect-has-minimum-size.js new file mode 100644 index 0000000000..014a888e35 --- /dev/null +++ b/lib/commons/math/rect-has-minimum-size.js @@ -0,0 +1,7 @@ +const roundingMargin = 0.05; + +export default function rectHasMinimumSize(minSize, { width, height }) { + return ( + width + roundingMargin >= minSize && height + roundingMargin >= minSize + ); +} diff --git a/lib/commons/math/split-rects.js b/lib/commons/math/split-rects.js index 4816299eee..c7d45bafda 100644 --- a/lib/commons/math/split-rects.js +++ b/lib/commons/math/split-rects.js @@ -5,13 +5,13 @@ * @memberof axe.commons.math * @param {DOMRect} outerRect * @param {DOMRect[]} overlapRects - * @returns {Rect[]} Unique array of rects + * @returns {DOMRect[]} Unique array of rects */ export default function splitRects(outerRect, overlapRects) { let uniqueRects = [outerRect]; for (const overlapRect of overlapRects) { - uniqueRects = uniqueRects.reduce((uniqueRects, inputRect) => { - return uniqueRects.concat(splitRect(inputRect, overlapRect)); + uniqueRects = uniqueRects.reduce((rects, inputRect) => { + return rects.concat(splitRect(inputRect, overlapRect)); }, []); } return uniqueRects; @@ -37,19 +37,33 @@ function splitRect(inputRect, clipRect) { rects.push({ top, left, bottom, right: clipRect.left }); } if (rects.length === 0) { + // Fully overlapping + if (isEnclosedRect(inputRect, clipRect)) { + return []; + } + rects.push(inputRect); // No intersection } + return rects.map(computeRect); // add x / y / width / height } const between = (num, min, max) => num > min && num < max; function computeRect(baseRect) { - return { - ...baseRect, - x: baseRect.left, - y: baseRect.top, - height: baseRect.bottom - baseRect.top, - width: baseRect.right - baseRect.left - }; + return new window.DOMRect( + baseRect.left, + baseRect.top, + baseRect.right - baseRect.left, + baseRect.bottom - baseRect.top + ); +} + +function isEnclosedRect(rectA, rectB) { + return ( + rectA.top >= rectB.top && + rectA.left >= rectB.left && + rectA.bottom <= rectB.bottom && + rectA.right <= rectB.right + ); } diff --git a/lib/commons/standards/implicit-html-roles.js b/lib/commons/standards/implicit-html-roles.js index 0a7b2da142..64455b1cb2 100644 --- a/lib/commons/standards/implicit-html-roles.js +++ b/lib/commons/standards/implicit-html-roles.js @@ -10,13 +10,19 @@ import isRowHeader from '../table/is-row-header'; import sanitize from '../text/sanitize'; import isFocusable from '../dom/is-focusable'; import { closest } from '../../core/utils'; +import cache from '../../core/base/cache'; import getExplicitRole from '../aria/get-explicit-role'; -const sectioningElementSelector = - getElementsByContentType('sectioning') - .map(nodeName => `${nodeName}:not([role])`) - .join(', ') + - ' , main:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]'; +const getSectioningElementSelector = () => { + return cache.get('sectioningElementSelector', () => { + return ( + getElementsByContentType('sectioning') + .map(nodeName => `${nodeName}:not([role])`) + .join(', ') + + ' , main:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]' + ); + }); +}; // sectioning elements only have an accessible name if the // aria-label, aria-labelledby, or title attribute has valid @@ -64,7 +70,7 @@ const implicitHtmlRoles = { fieldset: 'group', figure: 'figure', footer: vNode => { - const sectioningElement = closest(vNode, sectioningElementSelector); + const sectioningElement = closest(vNode, getSectioningElementSelector()); return !sectioningElement ? 'contentinfo' : null; }, @@ -78,7 +84,7 @@ const implicitHtmlRoles = { h5: 'heading', h6: 'heading', header: vNode => { - const sectioningElement = closest(vNode, sectioningElementSelector); + const sectioningElement = closest(vNode, getSectioningElementSelector()); return !sectioningElement ? 'banner' : null; }, @@ -151,6 +157,7 @@ const implicitHtmlRoles = { option: 'option', output: 'status', progress: 'progressbar', + search: 'search', section: vNode => { return hasAccessibleName(vNode) ? 'region' : null; }, diff --git a/lib/commons/table/get-scope.js b/lib/commons/table/get-scope.js index 6e91461112..110eac5a2c 100644 --- a/lib/commons/table/get-scope.js +++ b/lib/commons/table/get-scope.js @@ -11,7 +11,7 @@ import { nodeLookup } from '../../core/utils'; * @param {HTMLTableCellElement|AbstractVirtualNode} cell The table cell to test * @return {Boolean|String} Returns `false` if not a column header, or the scope of the column header element */ -function getScope(el) { +export default function getScope(el) { const { vNode, domNode: cell } = nodeLookup(el); const scope = vNode.attr('scope'); @@ -32,29 +32,25 @@ function getScope(el) { } else if (!vNode.actualNode) { return 'auto'; } - var tableGrid = toGrid(findUp(cell, 'table')); - var pos = getCellPosition(cell, tableGrid); + const tableGrid = toGrid(findUp(cell, 'table')); + const pos = getCellPosition(cell, tableGrid); // The element is in a row with all th elements, that makes it a column header - var headerRow = tableGrid[pos.y].reduce((headerRow, cell) => { - return headerRow && cell.nodeName.toUpperCase() === 'TH'; - }, true); + const headerRow = tableGrid[pos.y].every( + node => node.nodeName.toUpperCase() === 'TH' + ); if (headerRow) { return 'col'; } // The element is in a column with all th elements, that makes it a row header - var headerCol = tableGrid + const headerCol = tableGrid .map(col => col[pos.x]) - .reduce((headerCol, cell) => { - return headerCol && cell && cell.nodeName.toUpperCase() === 'TH'; - }, true); + .every(node => node && node.nodeName.toUpperCase() === 'TH'); if (headerCol) { return 'row'; } return 'auto'; } - -export default getScope; diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js index e47c259134..c1ce96f69e 100644 --- a/lib/commons/text/accessible-text-virtual.js +++ b/lib/commons/text/accessible-text-virtual.js @@ -18,7 +18,7 @@ import isIconLigature from '../text/is-icon-ligature'; * @property {Bool} inLabelledByContext * @return {string} */ -function accessibleTextVirtual(virtualNode, context = {}) { +export default function accessibleTextVirtual(virtualNode, context = {}) { context = prepareContext(virtualNode, context); // Step 2A, check visibility @@ -42,7 +42,7 @@ function accessibleTextVirtual(virtualNode, context = {}) { ]; // Find the first step that returns a non-empty string - const accName = computationSteps.reduce((accName, step) => { + const accessibleName = computationSteps.reduce((accName, step) => { if (context.startNode === virtualNode) { accName = sanitize(accName); } @@ -55,9 +55,9 @@ function accessibleTextVirtual(virtualNode, context = {}) { }, ''); if (context.debug) { - axe.log(accName || '{empty-value}', virtualNode.actualNode, context); + axe.log(accessibleName || '{empty-value}', virtualNode.actualNode, context); } - return accName; + return accessibleName; } /** @@ -165,5 +165,3 @@ accessibleTextVirtual.alreadyProcessed = function alreadyProcessed( context.processed.push(virtualnode); return false; }; - -export default accessibleTextVirtual; diff --git a/lib/commons/text/form-control-value.js b/lib/commons/text/form-control-value.js index e892781aa5..b4ef057cc0 100644 --- a/lib/commons/text/form-control-value.js +++ b/lib/commons/text/form-control-value.js @@ -13,7 +13,7 @@ import isHiddenForEveryone from '../dom/is-hidden-for-everyone'; import { nodeLookup, querySelectorAll } from '../../core/utils'; import log from '../../core/log'; -const controlValueRoles = [ +export const controlValueRoles = [ 'textbox', 'progressbar', 'scrollbar', diff --git a/lib/commons/text/has-unicode.js b/lib/commons/text/has-unicode.js index 9d10ad6834..a7ac371906 100644 --- a/lib/commons/text/has-unicode.js +++ b/lib/commons/text/has-unicode.js @@ -1,7 +1,8 @@ import { getUnicodeNonBmpRegExp, getSupplementaryPrivateUseRegExp, - getPunctuationRegExp + getPunctuationRegExp, + getCategoryFormatRegExp } from './unicode'; import emojiRegexText from 'emoji-regex'; @@ -20,19 +21,22 @@ import emojiRegexText from 'emoji-regex'; */ function hasUnicode(str, options) { const { emoji, nonBmp, punctuations } = options; + let value = false; + if (emoji) { - return emojiRegexText().test(str); + value ||= emojiRegexText().test(str); } if (nonBmp) { - return ( + value ||= getUnicodeNonBmpRegExp().test(str) || - getSupplementaryPrivateUseRegExp().test(str) - ); + getSupplementaryPrivateUseRegExp().test(str) || + getCategoryFormatRegExp().test(str); } if (punctuations) { - return getPunctuationRegExp().test(str); + value ||= getPunctuationRegExp().test(str); } - return false; + + return value; } export default hasUnicode; diff --git a/lib/commons/text/is-icon-ligature.js b/lib/commons/text/is-icon-ligature.js index 5e17c55984..428117f0de 100644 --- a/lib/commons/text/is-icon-ligature.js +++ b/lib/commons/text/is-icon-ligature.js @@ -13,7 +13,7 @@ import cache from '../../core/base/cache'; * @param {Number} differenceThreshold Percent of differences in pixel data or pixel width needed to determine if a font is a ligature font * @return {Boolean} */ -function isIconLigature( +export default function isIconLigature( textVNode, differenceThreshold = 0.15, occurrenceThreshold = 3 @@ -93,11 +93,7 @@ function isIconLigature( // keep track of each font encountered and the number of times it shows up // as a ligature. - if (!cache.get('fonts')) { - cache.set('fonts', {}); - } - const fonts = cache.get('fonts'); - + const fonts = cache.get('fonts', () => ({})); const style = window.getComputedStyle(textVNode.parent.actualNode); const fontFamily = style.getPropertyValue('font-family'); @@ -109,7 +105,7 @@ function isIconLigature( } const font = fonts[fontFamily]; - // improve the performance by only comparing the image data of a fon a certain number of times + // improve the performance by only comparing the image data of a font a certain number of times // NOTE: This MIGHT cause an issue if someone uses an icon font to render actual text. // We're leaving this as-is, unless someone reports a false positive over it. if (font.occurrences >= occurrenceThreshold) { @@ -143,6 +139,14 @@ function isIconLigature( const firstChar = nodeValue.charAt(0); let width = canvasContext.measureText(firstChar).width; + // we already checked for typical zero-width unicode formatting characters further up, + // so we assume that any remaining zero-width characters are part of an icon ligature + // @see https://github.com/dequelabs/axe-core/issues/3918 + if (width === 0) { + font.numLigatures++; + return true; + } + // ensure font meets the 30px width requirement (30px font-size doesn't // necessarily mean its 30px wide when drawn) if (width < 30) { @@ -191,8 +195,8 @@ function isIconLigature( // calculate the difference between the width of each character and the // combined with of all characters - const expectedWidth = nodeValue.split('').reduce((width, char) => { - return width + canvasContext.measureText(char).width; + const expectedWidth = nodeValue.split('').reduce((totalWidth, char) => { + return totalWidth + canvasContext.measureText(char).width; }, 0); const actualWidth = canvasContext.measureText(nodeValue).width; @@ -209,5 +213,3 @@ function isIconLigature( return false; } - -export default isIconLigature; diff --git a/lib/commons/text/native-text-alternative.js b/lib/commons/text/native-text-alternative.js index ff2b12ad08..14beed82ed 100644 --- a/lib/commons/text/native-text-alternative.js +++ b/lib/commons/text/native-text-alternative.js @@ -9,7 +9,7 @@ import nativeTextMethods from './native-text-methods'; * @property {Bool} debug Enable logging for formControlValue * @return {String} Accessible text */ -function nativeTextAlternative(virtualNode, context = {}) { +export default function nativeTextAlternative(virtualNode, context = {}) { const { actualNode } = virtualNode; if ( virtualNode.props.nodeType !== 1 || @@ -20,14 +20,14 @@ function nativeTextAlternative(virtualNode, context = {}) { const textMethods = findTextMethods(virtualNode); // Find the first step that returns a non-empty string - const accName = textMethods.reduce((accName, step) => { + const accessibleName = textMethods.reduce((accName, step) => { return accName || step(virtualNode, context); }, ''); if (context.debug) { - axe.log(accName || '{empty-value}', actualNode, context); + axe.log(accessibleName || '{empty-value}', actualNode, context); } - return accName; + return accessibleName; } /** @@ -42,5 +42,3 @@ function findTextMethods(virtualNode) { return methods.map(methodName => nativeTextMethods[methodName]); } - -export default nativeTextAlternative; diff --git a/lib/commons/text/remove-unicode.js b/lib/commons/text/remove-unicode.js index ddf10ae7ed..4527cfc871 100644 --- a/lib/commons/text/remove-unicode.js +++ b/lib/commons/text/remove-unicode.js @@ -1,7 +1,8 @@ import { getUnicodeNonBmpRegExp, getSupplementaryPrivateUseRegExp, - getPunctuationRegExp + getPunctuationRegExp, + getCategoryFormatRegExp } from './unicode.js'; import emojiRegexText from 'emoji-regex'; @@ -25,8 +26,10 @@ function removeUnicode(str, options) { str = str.replace(emojiRegexText(), ''); } if (nonBmp) { - str = str.replace(getUnicodeNonBmpRegExp(), ''); - str = str.replace(getSupplementaryPrivateUseRegExp(), ''); + str = str + .replace(getUnicodeNonBmpRegExp(), '') + .replace(getSupplementaryPrivateUseRegExp(), '') + .replace(getCategoryFormatRegExp(), ''); } if (punctuations) { str = str.replace(getPunctuationRegExp(), ''); diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js index a3d669f342..09fbe6a581 100644 --- a/lib/commons/text/subtree-text.js +++ b/lib/commons/text/subtree-text.js @@ -1,8 +1,10 @@ import accessibleTextVirtual from './accessible-text-virtual'; import namedFromContents from '../aria/named-from-contents'; import getOwnedVirtual from '../aria/get-owned-virtual'; +import getRole from '../aria/get-role'; import getElementsByContentType from '../standards/get-elements-by-content-type'; import getElementSpec from '../standards/get-element-spec'; +import { controlValueRoles } from './form-control-value'; /** * Get the accessible text for an element that can get its name from content @@ -16,20 +18,23 @@ function subtreeText(virtualNode, context = {}) { const { alreadyProcessed } = accessibleTextVirtual; context.startNode = context.startNode || virtualNode; const { strict, inControlContext, inLabelledByContext } = context; + const role = getRole(virtualNode); const { contentTypes } = getElementSpec(virtualNode, { noMatchAccessibleName: true }); if ( alreadyProcessed(virtualNode, context) || virtualNode.props.nodeType !== 1 || - contentTypes?.includes('embedded') // canvas, video, etc + contentTypes?.includes('embedded') || // canvas, video, etc + controlValueRoles.includes(role) ) { return ''; } if ( - !namedFromContents(virtualNode, { strict }) && - !context.subtreeDescendant + !context.subtreeDescendant && + !context.inLabelledByContext && + !namedFromContents(virtualNode, { strict }) ) { return ''; } @@ -40,6 +45,7 @@ function subtreeText(virtualNode, context = {}) { * chosen to ignore this, but only for direct content, not for labels / aria-labelledby. * That way in `a[href] > article > #text` the text is used for the accessible name, * See: https://github.com/dequelabs/axe-core/issues/1461 + * See: https://github.com/w3c/accname/issues/120 */ if (!strict) { const subtreeDescendant = !inControlContext && !inLabelledByContext; diff --git a/lib/commons/text/unicode.js b/lib/commons/text/unicode.js index 9a8fd11901..f76d354cba 100644 --- a/lib/commons/text/unicode.js +++ b/lib/commons/text/unicode.js @@ -83,3 +83,15 @@ export function getSupplementaryPrivateUseRegExp() { // ┏━━━━━━┻━━━━━━┓┏━━━━━━┻━━━━━━┓ return /[\uDB80-\uDBBF][\uDC00-\uDFFF]/g; } + +/** + * Get regular expression for unicode format category. + * When we drop IE11 we can instead use unicode character escape `/p{Cf}/gu` + * Reference: + * - https://www.compart.com/en/unicode/category/Cf + * + * @returns {RegExp} + */ +export function getCategoryFormatRegExp() { + return /[\xAD\u0600-\u0605\u061C\u06DD\u070F\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC38]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/g; +} diff --git a/lib/commons/text/unsupported.js b/lib/commons/text/unsupported.js index 04d63f1836..197b41600f 100644 --- a/lib/commons/text/unsupported.js +++ b/lib/commons/text/unsupported.js @@ -1,5 +1,7 @@ -const unsupported = { - accessibleNameFromFieldValue: ['combobox', 'listbox', 'progressbar'] +export default { + // Element's who's value is not consistently picked up in the accessible name + // Supported in Chrome 114, Firefox 115, but not Safari 16.5: + // <input aria-labelledby="lbl"> + // <div id="lbl" role="progressbar" aria-valuenow="23"></div> + accessibleNameFromFieldValue: ['progressbar'] }; - -export default unsupported; diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index b8aee7d4e1..47b57f66d9 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -4,6 +4,7 @@ import standards from '../../standards'; import RuleResult from './rule-result'; import { clone, + DqElement, queue, preload, findBy, @@ -15,162 +16,10 @@ import constants from '../constants'; const dotRegex = /\{\{.+?\}\}/g; -function getDefaultOrigin() { - // @see https://html.spec.whatwg.org/multipage/webappapis.html#dom-origin-dev - // window.origin does not exist in ie11 - // prevent origin default "null" string on CDP `Page.setDocumentContent` https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-setDocumentContent - if (window.origin && window.origin !== 'null') { - return window.origin; - } - // window.location does not exist in node when we run the build - if ( - window.location && - window.location.origin && - window.location.origin !== 'null' - ) { - return window.location.origin; - } -} - -/*eslint no-unused-vars: 0*/ -function getDefaultConfiguration(audit) { - var config; - if (audit) { - config = clone(audit); - // Commons are configured into axe like everything else, - // however because things go funky if we have multiple commons objects - // we're not using the copy of that. - config.commons = audit.commons; - } else { - config = {}; - } - - config.reporter = config.reporter || null; - config.noHtml = config.noHtml || false; - - if (!config.allowedOrigins) { - const defaultOrigin = getDefaultOrigin(); - config.allowedOrigins = defaultOrigin ? [defaultOrigin] : []; - } - - config.rules = config.rules || []; - config.checks = config.checks || []; - config.data = { checks: {}, rules: {}, ...config.data }; - return config; -} - -function unpackToObject(collection, audit, method) { - var i, l; - for (i = 0, l = collection.length; i < l; i++) { - audit[method](collection[i]); - } -} - -/** - * Merge two check locales (a, b), favoring `b`. - * - * Both locale `a` and the returned shape resemble: - * - * { - * impact: string, - * messages: { - * pass: string | function, - * fail: string | function, - * incomplete: string | { - * [key: string]: string | function - * } - * } - * } - * - * Locale `b` follows the `axe.CheckLocale` shape and resembles: - * - * { - * pass: string, - * fail: string, - * incomplete: string | { [key: string]: string } - * } - */ - -const mergeCheckLocale = (a, b) => { - let { pass, fail } = b; - // If the message(s) are Strings, they have not yet been run - // thru doT (which will return a Function). - if (typeof pass === 'string' && dotRegex.test(pass)) { - pass = doT.compile(pass); - } - if (typeof fail === 'string' && dotRegex.test(fail)) { - fail = doT.compile(fail); - } - return { - ...a, - messages: { - pass: pass || a.messages.pass, - fail: fail || a.messages.fail, - incomplete: - typeof a.messages.incomplete === 'object' - ? // TODO: for compleness-sake, we should be running - // incomplete messages thru doT as well. This was - // out-of-scope for runtime localization, but should - // eventually be addressed. - { ...a.messages.incomplete, ...b.incomplete } - : b.incomplete - } - }; -}; - -/** - * Merge two rule locales (a, b), favoring `b`. - */ - -const mergeRuleLocale = (a, b) => { - let { help, description } = b; - // If the message(s) are Strings, they have not yet been run - // thru doT (which will return a Function). - if (typeof help === 'string' && dotRegex.test(help)) { - help = doT.compile(help); - } - if (typeof description === 'string' && dotRegex.test(description)) { - description = doT.compile(description); - } - return { - ...a, - help: help || a.help, - description: description || a.description - }; -}; - -/** - * Merge two failure messages (a, b), favoring `b`. - */ - -const mergeFailureMessage = (a, b) => { - let { failureMessage } = b; - // If the message(s) are Strings, they have not yet been run - // thru doT (which will return a Function). - if (typeof failureMessage === 'string' && dotRegex.test(failureMessage)) { - failureMessage = doT.compile(failureMessage); - } - return { - ...a, - failureMessage: failureMessage || a.failureMessage - }; -}; - -/** - * Merge two incomplete fallback messages (a, b), favoring `b`. - */ - -const mergeFallbackMessage = (a, b) => { - if (typeof b === 'string' && dotRegex.test(b)) { - b = doT.compile(b); - } - return b || a; -}; - /** * Constructor which holds configured rules and information about the document under test */ -class Audit { +export default class Audit { constructor(audit) { // defaults this.lang = 'en'; @@ -422,6 +271,7 @@ class Audit { */ run(context, options, resolve, reject) { this.normalizeOptions(options); + DqElement.setRunOptions(options); // TODO: es-modules_selectCache axe._selectCache = []; @@ -439,11 +289,11 @@ class Audit { const preloaderQueue = queue(); // defer preload if preload dependent rules exist if (runLaterRules.length) { - preloaderQueue.defer(resolve => { + preloaderQueue.defer(res => { // handle both success and fail of preload // and resolve, to allow to run all checks preload(options) - .then(assets => resolve(assets)) + .then(assets => res(assets)) .catch(err => { /** * Note: @@ -451,7 +301,7 @@ class Audit { * -> instead we resolve as `undefined` */ console.warn(`Couldn't load preload assets: `, err); - resolve(undefined); + res(undefined); }); }); } @@ -672,6 +522,157 @@ class Audit { } } +function getDefaultOrigin() { + // @see https://html.spec.whatwg.org/multipage/webappapis.html#dom-origin-dev + // window.origin does not exist in ie11 + // prevent origin default "null" string on CDP `Page.setDocumentContent` https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-setDocumentContent + if (window.origin && window.origin !== 'null') { + return window.origin; + } + // window.location does not exist in node when we run the build + if ( + window.location && + window.location.origin && + window.location.origin !== 'null' + ) { + return window.location.origin; + } +} + +function getDefaultConfiguration(audit) { + let config; + if (audit) { + config = clone(audit); + // Commons are configured into axe like everything else, + // however because things go funky if we have multiple commons objects + // we're not using the copy of that. + config.commons = audit.commons; + } else { + config = {}; + } + + config.reporter = config.reporter || null; + config.noHtml = config.noHtml || false; + + if (!config.allowedOrigins) { + const defaultOrigin = getDefaultOrigin(); + config.allowedOrigins = defaultOrigin ? [defaultOrigin] : []; + } + + config.rules = config.rules || []; + config.checks = config.checks || []; + config.data = { checks: {}, rules: {}, ...config.data }; + return config; +} + +function unpackToObject(collection, audit, method) { + let i, l; + for (i = 0, l = collection.length; i < l; i++) { + audit[method](collection[i]); + } +} + +/** + * Merge two check locales (a, b), favoring `b`. + * + * Both locale `a` and the returned shape resemble: + * + * { + * impact: string, + * messages: { + * pass: string | function, + * fail: string | function, + * incomplete: string | { + * [key: string]: string | function + * } + * } + * } + * + * Locale `b` follows the `axe.CheckLocale` shape and resembles: + * + * { + * pass: string, + * fail: string, + * incomplete: string | { [key: string]: string } + * } + */ + +const mergeCheckLocale = (a, b) => { + let { pass, fail } = b; + // If the message(s) are Strings, they have not yet been run + // thru doT (which will return a Function). + if (typeof pass === 'string' && dotRegex.test(pass)) { + pass = doT.compile(pass); + } + if (typeof fail === 'string' && dotRegex.test(fail)) { + fail = doT.compile(fail); + } + return { + ...a, + messages: { + pass: pass || a.messages.pass, + fail: fail || a.messages.fail, + incomplete: + typeof a.messages.incomplete === 'object' + ? // TODO: for compleness-sake, we should be running + // incomplete messages thru doT as well. This was + // out-of-scope for runtime localization, but should + // eventually be addressed. + { ...a.messages.incomplete, ...b.incomplete } + : b.incomplete + } + }; +}; + +/** + * Merge two rule locales (a, b), favoring `b`. + */ + +const mergeRuleLocale = (a, b) => { + let { help, description } = b; + // If the message(s) are Strings, they have not yet been run + // thru doT (which will return a Function). + if (typeof help === 'string' && dotRegex.test(help)) { + help = doT.compile(help); + } + if (typeof description === 'string' && dotRegex.test(description)) { + description = doT.compile(description); + } + return { + ...a, + help: help || a.help, + description: description || a.description + }; +}; + +/** + * Merge two failure messages (a, b), favoring `b`. + */ + +const mergeFailureMessage = (a, b) => { + let { failureMessage } = b; + // If the message(s) are Strings, they have not yet been run + // thru doT (which will return a Function). + if (typeof failureMessage === 'string' && dotRegex.test(failureMessage)) { + failureMessage = doT.compile(failureMessage); + } + return { + ...a, + failureMessage: failureMessage || a.failureMessage + }; +}; + +/** + * Merge two incomplete fallback messages (a, b), favoring `b`. + */ + +const mergeFallbackMessage = (a, b) => { + if (typeof b === 'string' && dotRegex.test(b)) { + b = doT.compile(b); + } + return b || a; +}; + /** * Splits a given array of rules to two, with rules that can be run immediately and one's that are dependent on preloadedAssets * @method getRulesToRun @@ -777,5 +778,3 @@ function getHelpUrl({ brand, application, lang }, ruleId, version) { (lang && lang !== 'en' ? '&lang=' + encodeURIComponent(lang) : '') ); } - -export default Audit; diff --git a/lib/core/base/check.js b/lib/core/base/check.js index 593c9b521e..961188bf02 100644 --- a/lib/core/base/check.js +++ b/lib/core/base/check.js @@ -1,6 +1,6 @@ import metadataFunctionMap from './metadata-function-map'; import CheckResult from './check-result'; -import { DqElement, checkHelper, deepMerge } from '../utils'; +import { nodeSerializer, checkHelper, deepMerge } from '../utils'; export function createExecutionContext(spec) { /*eslint no-eval:0 */ @@ -108,7 +108,7 @@ Check.prototype.run = function run(node, options, context, resolve, reject) { // possible reference error. if (node && node.actualNode) { // Save a reference to the node we errored on for futher debugging. - e.errorNode = new DqElement(node).toJSON(); + e.errorNode = nodeSerializer.toSpec(node); } reject(e); return; @@ -162,7 +162,7 @@ Check.prototype.runSync = function runSync(node, options, context) { // possible reference error. if (node && node.actualNode) { // Save a reference to the node we errored on for futher debugging. - e.errorNode = new DqElement(node).toJSON(); + e.errorNode = nodeSerializer.toSpec(node); } throw e; } diff --git a/lib/core/base/context/normalize-context.js b/lib/core/base/context/normalize-context.js index ac1dc6265d..8187b3d64e 100644 --- a/lib/core/base/context/normalize-context.js +++ b/lib/core/base/context/normalize-context.js @@ -120,7 +120,7 @@ function assertLabelledFrameSelector(selector) { ); assert( selector.fromFrames.every( - selector => !objectHasOwn(selector, 'fromFrames') + fromFrameSelector => !objectHasOwn(fromFrameSelector, 'fromFrames') ), 'Invalid context; fromFrames selector must be appended, rather than nested' ); @@ -137,13 +137,15 @@ function assertLabelledShadowDomSelector(selector) { ); assert( selector.fromShadowDom.every( - selector => !objectHasOwn(selector, 'fromFrames') + fromShadowDomSelector => + !objectHasOwn(fromShadowDomSelector, 'fromFrames') ), 'shadow selector must be inside fromFrame instead' ); assert( selector.fromShadowDom.every( - selector => !objectHasOwn(selector, 'fromShadowDom') + fromShadowDomSelector => + !objectHasOwn(fromShadowDomSelector, 'fromShadowDom') ), 'fromShadowDom selector must be appended, rather than nested' ); diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index 96123ddbb8..a5995adfe4 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -14,7 +14,7 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; import constants from '../constants'; import log from '../log'; -function Rule(spec, parentAudit) { +export default function Rule(spec, parentAudit) { this._audit = parentAudit; /** @@ -258,7 +258,7 @@ Rule.prototype.run = function run(context, options = {}, resolve, reject) { .then(results => { const result = getResult(results); if (result) { - result.node = new DqElement(node, options); + result.node = new DqElement(node); ruleResult.nodes.push(result); // mark rule as incomplete rather than failure for rules with reviewOnFail @@ -286,7 +286,7 @@ Rule.prototype.run = function run(context, options = {}, resolve, reject) { // Defer the rule's execution to prevent "unresponsive script" warnings. // See https://github.com/dequelabs/axe-core/pull/1172 for discussion and details. - q.defer(resolve => setTimeout(resolve, 0)); + q.defer(res => setTimeout(res, 0)); if (options.performanceTimer) { this._logRulePerformance(); @@ -327,7 +327,7 @@ Rule.prototype.runSync = function runSync(context, options = {}) { const result = getResult(results); if (result) { - result.node = node.actualNode ? new DqElement(node, options) : null; + result.node = node.actualNode ? new DqElement(node) : null; ruleResult.nodes.push(result); // mark rule as incomplete rather than failure for rules with reviewOnFail @@ -407,7 +407,7 @@ function getResult(results) { let hasResults = false; const result = {}; results.forEach(r => { - const res = r.results.filter(result => result); + const res = r.results.filter(_result => _result); result[r.type] = res; if (res.length) { hasResults = true; @@ -537,9 +537,9 @@ Rule.prototype.after = function after(result, options) { var ruleID = this.id; afterChecks.forEach(check => { var beforeResults = findCheckResults(result.nodes, check.id); - var option = getCheckOption(check, ruleID, options); + var checkOption = getCheckOption(check, ruleID, options); - var afterResults = check.after(beforeResults, option); + var afterResults = check.after(beforeResults, checkOption.options); if (this.reviewOnFail) { afterResults.forEach(checkResult => { @@ -633,5 +633,3 @@ Rule.prototype.configure = function configure(spec) { this.impact = spec.impact; } }; - -export default Rule; diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index e752e07040..589b61237f 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -3,7 +3,6 @@ import { isXHTML, validInputTypes } from '../../utils'; import { isFocusable, getTabbableElements } from '../../../commons/dom'; import cache from '../cache'; -let isXHTMLGlobal; let nodeIndex = 0; class VirtualNode extends AbstractVirtualNode { @@ -27,11 +26,7 @@ class VirtualNode extends AbstractVirtualNode { this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; - - if (typeof isXHTMLGlobal === 'undefined') { - isXHTMLGlobal = isXHTML(node.ownerDocument); - } - this._isXHTML = isXHTMLGlobal; + this._isXHTML = isXHTML(node.ownerDocument); // we will normalize the type prop for inputs by looking strictly // at the attribute and not what the browser resolves the type diff --git a/lib/core/public/finish-run.js b/lib/core/public/finish-run.js index bc3a194f1c..31e6417c2a 100644 --- a/lib/core/public/finish-run.js +++ b/lib/core/public/finish-run.js @@ -3,7 +3,7 @@ import { mergeResults, publishMetaData, finalizeRuleResult, - DqElement, + nodeSerializer, clone } from '../utils'; @@ -47,13 +47,13 @@ function getMergedFrameSpecs({ } // Include the selector/ancestry/... from the parent frames return childFrameSpecs.map(childFrameSpec => { - return DqElement.mergeSpecs(childFrameSpec, parentFrameSpec); + return nodeSerializer.mergeSpecs(childFrameSpec, parentFrameSpec); }); } function createReport(results, options) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { const reporter = getReporter(options.reporter); - reporter(results, options, resolve); + reporter(results, options, resolve, reject); }); } diff --git a/lib/core/public/load.js b/lib/core/public/load.js index 02fce5e0cf..124c849b12 100644 --- a/lib/core/public/load.js +++ b/lib/core/public/load.js @@ -2,6 +2,16 @@ import Audit from '../base/audit'; import cleanup from './cleanup'; import runRules from './run-rules'; import respondable from '../utils/respondable'; +import nodeSerializer from '../utils/node-serializer'; + +/** + * Sets up Rules, Messages and default options for Checks, must be invoked before attempting analysis + * @param {Object} audit The "audit specification" object + * @private + */ +export default function load(audit) { + axe._audit = new Audit(audit); +} function runCommand(data, keepalive, callback) { var resolve = callback; @@ -23,10 +33,12 @@ function runCommand(data, keepalive, callback) { return runRules( context, options, - (results, cleanup) => { + (results, cleanupFn) => { + // Serialize all DqElements + results = nodeSerializer.mapRawResults(results); resolve(results); // Cleanup AFTER resolve so that selectors can be generated - cleanup(); + cleanupFn(); }, reject ); @@ -52,14 +64,3 @@ if (window.top !== window) { }); }); } - -/** - * Sets up Rules, Messages and default options for Checks, must be invoked before attempting analysis - * @param {Object} audit The "audit specification" object - * @private - */ -function load(audit) { - axe._audit = new Audit(audit); -} - -export default load; diff --git a/lib/core/public/run-partial.js b/lib/core/public/run-partial.js index 5ded49a5a6..50ab6465da 100644 --- a/lib/core/public/run-partial.js +++ b/lib/core/public/run-partial.js @@ -1,7 +1,7 @@ import Context from '../base/context'; import teardown from './teardown'; import { - DqElement, + nodeSerializer, getSelectorData, assert, getEnvironmentData @@ -22,17 +22,17 @@ export default function runPartial(...args) { axe._selectorData = getSelectorData(contextObj.flatTree); axe._running = true; + // Even in the top frame, we don't support this with runPartial + options.elementRef = false; + return ( new Promise((res, rej) => { axe._audit.run(contextObj, options, res, rej); }) .then(results => { - results = results.map(({ nodes, ...result }) => ({ - nodes: nodes.map(serializeNode), - ...result - })); + results = nodeSerializer.mapRawResults(results); const frames = contextObj.frames.map(({ node }) => { - return new DqElement(node, options).toJSON(); + return nodeSerializer.toSpec(node); }); let environmentData; if (contextObj.initiator) { @@ -50,16 +50,3 @@ export default function runPartial(...args) { }) ); } - -function serializeNode({ node, ...nodeResult }) { - nodeResult.node = node.toJSON(); - for (const type of ['any', 'all', 'none']) { - nodeResult[type] = nodeResult[type].map( - ({ relatedNodes, ...checkResult }) => ({ - ...checkResult, - relatedNodes: relatedNodes.map(node => node.toJSON()) - }) - ); - } - return nodeResult; -} diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 79bd5405cf..5eb26d7c9f 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -19,7 +19,7 @@ import log from '../log'; * @param {Function} resolve Called when done running rules, receives ([results : Object], teardown : Function) * @param {Function} reject Called when execution failed, receives (err : Error) */ -function runRules(context, options, resolve, reject) { +export default function runRules(context, options, resolve, reject) { try { context = new Context(context); axe._tree = context.flatTree; @@ -52,8 +52,8 @@ function runRules(context, options, resolve, reject) { // Add wrapper object so that we may use the same "merge" function for results from inside and outside frames var results = mergeResults( - data.map(results => { - return { results }; + data.map(res => { + return { results: res }; }) ); @@ -79,5 +79,3 @@ function runRules(context, options, resolve, reject) { reject(e); }); } - -export default runRules; diff --git a/lib/core/public/run.js b/lib/core/public/run.js index b5bdbd40e9..74983c7e86 100644 --- a/lib/core/public/run.js +++ b/lib/core/public/run.js @@ -1,6 +1,6 @@ import { getReporter } from './reporter'; import normalizeRunParams from './run/normalize-run-params'; -import { setupGlobals, resetGlobals } from './run/globals-setup'; +import { setupGlobals } from './run/globals-setup'; import { assert } from '../utils'; const noop = () => {}; @@ -36,28 +36,34 @@ export default function run(...args) { axe.utils.performanceTimer.start(); } - function handleRunRules(rawResults, cleanup) { + function handleRunRules(rawResults, teardown) { const respond = results => { axe._running = false; - cleanup(); + teardown(); try { - callback(null, results); + resolve(results); } catch (e) { axe.log(e); } - resolve(results); }; + const wrappedReject = err => { + axe._running = false; + teardown(); + try { + reject(err); + } catch (e) { + axe.log(e); + } + }; + if (options.performanceTimer) { axe.utils.performanceTimer.end(); } try { - createReport(rawResults, options, respond); + createReport(rawResults, options, respond, wrappedReject); } catch (err) { - axe._running = false; - cleanup(); - callback(err); - reject(err); + wrappedReject(err); } } @@ -66,7 +72,6 @@ export default function run(...args) { axe.utils.performanceTimer.end(); } axe._running = false; - resetGlobals(); callback(err); reject(err); } @@ -83,21 +88,21 @@ function getPromiseHandlers(callback) { resolve = _resolve; }); } else { - resolve = reject = noop; + resolve = result => callback(null, result); + reject = err => callback(err); } return { thenable, reject, resolve }; } -function createReport(rawResults, options, respond) { +function createReport(rawResults, options, respond, reject) { const reporter = getReporter(options.reporter); - const results = reporter(rawResults, options, respond); + const results = reporter(rawResults, options, respond, reject); if (results !== undefined) { respond(results); } } function handleError(err, callback) { - resetGlobals(); if (typeof callback === 'function' && callback !== noop) { callback(err.message); return; diff --git a/lib/core/public/setup.js b/lib/core/public/setup.js index 3584a205e5..bcb1258f5d 100644 --- a/lib/core/public/setup.js +++ b/lib/core/public/setup.js @@ -1,4 +1,5 @@ import { getFlattenedTree, getSelectorData } from '../utils'; +import { setupGlobals } from './run/globals-setup'; /** * Setup axe-core so axe.common functions can work properly. @@ -10,7 +11,16 @@ function setup(node) { 'Axe is already setup. Call `axe.teardown()` before calling `axe.setup` again.' ); } + // Normalize document + if ( + node && + typeof node.documentElement === 'object' && + typeof node.defaultView === 'object' + ) { + node = node.documentElement; + } + setupGlobals(node); axe._tree = getFlattenedTree(node); axe._selectorData = getSelectorData(axe._tree); diff --git a/lib/core/reporters/helpers/incomplete-fallback-msg.js b/lib/core/reporters/helpers/incomplete-fallback-msg.js index 7a14e025a6..05e9c389a9 100644 --- a/lib/core/reporters/helpers/incomplete-fallback-msg.js +++ b/lib/core/reporters/helpers/incomplete-fallback-msg.js @@ -4,12 +4,12 @@ * @return {String} */ export default function incompleteFallbackMessage() { - let { incompleteFallbackMessage } = axe._audit.data; - if (typeof incompleteFallbackMessage === 'function') { - incompleteFallbackMessage = incompleteFallbackMessage(); + let { incompleteFallbackMessage: message } = axe._audit.data; + if (typeof message === 'function') { + message = message(); } - if (typeof incompleteFallbackMessage !== 'string') { + if (typeof message !== 'string') { return ''; } - return incompleteFallbackMessage; + return message; } diff --git a/lib/core/reporters/helpers/process-aggregate.js b/lib/core/reporters/helpers/process-aggregate.js index 1eddfbb687..dfd1436144 100644 --- a/lib/core/reporters/helpers/process-aggregate.js +++ b/lib/core/reporters/helpers/process-aggregate.js @@ -1,36 +1,7 @@ import constants from '../../constants'; +import { nodeSerializer } from '../../utils'; -function normalizeRelatedNodes(node, options) { - ['any', 'all', 'none'].forEach(type => { - if (!Array.isArray(node[type])) { - return; - } - node[type] - .filter(checkRes => Array.isArray(checkRes.relatedNodes)) - .forEach(checkRes => { - checkRes.relatedNodes = checkRes.relatedNodes.map(relatedNode => { - var res = { - html: relatedNode?.source ?? 'Undefined' - }; - if (options.elementRef && !relatedNode?.fromFrame) { - res.element = relatedNode?.element ?? null; - } - if (options.selectors !== false || relatedNode?.fromFrame) { - res.target = relatedNode?.selector ?? [':root']; - } - if (options.ancestry) { - res.ancestry = relatedNode?.ancestry ?? [':root']; - } - if (options.xpath) { - res.xpath = relatedNode?.xpath ?? ['/']; - } - return res; - }); - }); - }); -} - -var resultKeys = constants.resultGroups; +const resultKeys = constants.resultGroups; /** * Configures the processing of axe results. @@ -55,8 +26,8 @@ var resultKeys = constants.resultGroups; * @return {Object} * */ -function processAggregate(results, options) { - var resultObject = axe.utils.aggregateResult(results); +export default function processAggregate(results, options) { + const resultObject = axe.utils.aggregateResult(results); resultKeys.forEach(key => { if (options.resultTypes && !options.resultTypes.includes(key)) { @@ -73,19 +44,8 @@ function processAggregate(results, options) { if (Array.isArray(ruleResult.nodes) && ruleResult.nodes.length > 0) { ruleResult.nodes = ruleResult.nodes.map(subResult => { if (typeof subResult.node === 'object') { - subResult.html = subResult.node.source; - if (options.elementRef && !subResult.node.fromFrame) { - subResult.element = subResult.node.element; - } - if (options.selectors !== false || subResult.node.fromFrame) { - subResult.target = subResult.node.selector; - } - if (options.ancestry) { - subResult.ancestry = subResult.node.ancestry; - } - if (options.xpath) { - subResult.xpath = subResult.node.xpath; - } + const serialElm = trimElementSpec(subResult.node, options); + Object.assign(subResult, serialElm); } delete subResult.result; delete subResult.node; @@ -96,7 +56,7 @@ function processAggregate(results, options) { }); } - resultKeys.forEach(key => delete ruleResult[key]); + resultKeys.forEach(resultKey => delete ruleResult[resultKey]); delete ruleResult.pageLevel; delete ruleResult.result; @@ -107,4 +67,41 @@ function processAggregate(results, options) { return resultObject; } -export default processAggregate; +function normalizeRelatedNodes(node, options) { + ['any', 'all', 'none'].forEach(type => { + if (!Array.isArray(node[type])) { + return; + } + node[type] + .filter(checkRes => Array.isArray(checkRes.relatedNodes)) + .forEach(checkRes => { + checkRes.relatedNodes = checkRes.relatedNodes.map(relatedNode => { + return trimElementSpec(relatedNode, options); + }); + }); + }); +} + +function trimElementSpec(elmSpec = {}, runOptions) { + // Pass options to limit which properties are calculated + elmSpec = nodeSerializer.dqElmToSpec(elmSpec, runOptions); + const serialElm = {}; + if (axe._audit.noHtml) { + serialElm.html = null; + } else { + serialElm.html = elmSpec.source ?? 'Undefined'; + } + if (runOptions.elementRef && !elmSpec.fromFrame) { + serialElm.element = elmSpec.element ?? null; + } + if (runOptions.selectors !== false || elmSpec.fromFrame) { + serialElm.target = elmSpec.selector ?? [':root']; + } + if (runOptions.ancestry) { + serialElm.ancestry = elmSpec.ancestry ?? [':root']; + } + if (runOptions.xpath) { + serialElm.xpath = elmSpec.xpath ?? ['/']; + } + return serialElm; +} diff --git a/lib/core/reporters/raw.js b/lib/core/reporters/raw.js index b9b78fa041..eae5e5f381 100644 --- a/lib/core/reporters/raw.js +++ b/lib/core/reporters/raw.js @@ -1,3 +1,5 @@ +import { nodeSerializer } from '../utils'; + const rawReporter = (results, options, callback) => { if (typeof options === 'function') { callback = options; @@ -13,16 +15,9 @@ const rawReporter = (results, options, callback) => { const transformedResult = { ...result }; const types = ['passes', 'violations', 'incomplete', 'inapplicable']; for (const type of types) { - // Some tests don't include all of the types, so we have to guard against that here. - // TODO: ensure tests always use "proper" results to avoid having these hacks in production code paths. - if (transformedResult[type] && Array.isArray(transformedResult[type])) { - transformedResult[type] = transformedResult[type].map( - ({ node, ...typeResult }) => { - node = typeof node?.toJSON === 'function' ? node.toJSON() : node; - return { node, ...typeResult }; - } - ); - } + transformedResult[type] = nodeSerializer.mapRawNodeResults( + transformedResult[type] + ); } return transformedResult; diff --git a/lib/core/utils/aggregate-result.js b/lib/core/utils/aggregate-result.js index a0e039a9f5..d60122a060 100644 --- a/lib/core/utils/aggregate-result.js +++ b/lib/core/utils/aggregate-result.js @@ -3,8 +3,8 @@ import constants from '../constants'; function copyToGroup(resultObject, subResult, group) { var resultCopy = Object.assign({}, subResult); resultCopy.nodes = (resultCopy[group] || []).concat(); - constants.resultGroups.forEach(group => { - delete resultCopy[group]; + constants.resultGroups.forEach(resultGroup => { + delete resultCopy[resultGroup]; }); resultObject[group].push(resultCopy); } diff --git a/lib/core/utils/check-helper.js b/lib/core/utils/check-helper.js index 0156e60e5a..fbedf3645c 100644 --- a/lib/core/utils/check-helper.js +++ b/lib/core/utils/check-helper.js @@ -43,7 +43,7 @@ function checkHelper(checkResult, options, resolve, reject) { node = node.actualNode; } if (node instanceof window.Node) { - const dqElm = new DqElement(node, options); + const dqElm = new DqElement(node); checkResult.relatedNodes.push(dqElm); } }); diff --git a/lib/core/utils/clone.js b/lib/core/utils/clone.js index 45d0b17e03..ee4802fc6c 100644 --- a/lib/core/utils/clone.js +++ b/lib/core/utils/clone.js @@ -1,35 +1,47 @@ /** - * Deeply clones an object or array + * Deeply clones an object or array. DOM nodes or collections of DOM nodes are not deeply cloned and are instead returned as is. * @param {Mixed} obj The object/array to clone - * @return {Mixed} A clone of the initial object or array + * @return {Mixed} A clone of the initial object or array */ -function clone(obj) { - /* eslint guard-for-in: 0*/ - var index, - length, - out = obj; - // DOM nodes cannot be cloned. +export default function clone(obj) { + return cloneRecused(obj, new Map()); +} + +// internal function to hide non-user facing parameters +function cloneRecused(obj, seen) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + // don't clone DOM nodes. since we can pass nodes from different window contexts + // we'll also use duck typing to determine what is a DOM node if ( (window?.Node && obj instanceof window.Node) || - (window?.HTMLCollection && obj instanceof window.HTMLCollection) + (window?.HTMLCollection && obj instanceof window.HTMLCollection) || + ('nodeName' in obj && 'nodeType' in obj && 'ownerDocument' in obj) ) { return obj; } - if (obj !== null && typeof obj === 'object') { - if (Array.isArray(obj)) { - out = []; - for (index = 0, length = obj.length; index < length; index++) { - out[index] = clone(obj[index]); - } - } else { - out = {}; - for (index in obj) { - out[index] = clone(obj[index]); - } - } + // handle circular references by caching the cloned object and returning it + if (seen.has(obj)) { + return seen.get(obj); + } + + if (Array.isArray(obj)) { + const out = []; + seen.set(obj, out); + obj.forEach(value => { + out.push(cloneRecused(value, seen)); + }); + return out; + } + + const out = {}; + seen.set(obj, out); + // eslint-disable-next-line guard-for-in + for (const key in obj) { + out[key] = cloneRecused(obj[key], seen); } return out; } - -export default clone; diff --git a/lib/core/utils/collect-results-from-frames.js b/lib/core/utils/collect-results-from-frames.js index 6d37f57b42..be17f8ccfd 100644 --- a/lib/core/utils/collect-results-from-frames.js +++ b/lib/core/utils/collect-results-from-frames.js @@ -19,6 +19,9 @@ export default function collectResultsFromFrames( resolve, reject ) { + // elementRefs can't be passed across frame boundaries + options = { ...options, elementRef: false }; + var q = queue(); var frames = parentContent.frames; diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index db1c5f65bb..3cf790fe1a 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -3,6 +3,9 @@ import getAncestry from './get-ancestry'; import getXpath from './get-xpath'; import getNodeFromTree from './get-node-from-tree'; import AbstractVirtualNode from '../base/virtual-node/abstract-virtual-node'; +import cache from '../base/cache'; + +const CACHE_KEY = 'DqElm.RunOptions'; function truncate(str, maxLength) { maxLength = maxLength || 300; @@ -30,9 +33,14 @@ function getSource(element) { * "Serialized" `HTMLElement`. It will calculate the CSS selector, * grab the source (outerHTML) and offer an array for storing frame paths * @param {HTMLElement} element The element to serialize + * @param {Object} options Propagated from axe.run/etc * @param {Object} spec Properties to use in place of the element when instantiated on Elements from other frames */ -function DqElement(elm, options = {}, spec = {}) { +function DqElement(elm, options = null, spec = {}) { + if (!options) { + options = cache.get(CACHE_KEY) ?? {}; + } + this.spec = spec; if (elm instanceof AbstractVirtualNode) { this._virtualNode = elm; @@ -48,6 +56,8 @@ function DqElement(elm, options = {}, spec = {}) { */ this.fromFrame = this.spec.selector?.length > 1; + this._includeElementInJson = options.elementRef; + if (options.absolutePaths) { this._options = { toRoot: true }; } @@ -106,14 +116,24 @@ DqElement.prototype = { return this._element; }, + /** + * Converts to a "spec", a form suitable for use with JSON.stringify + * (*not* to pre-stringified JSON) + * @returns {Object} + */ toJSON() { - return { + const spec = { selector: this.selector, source: this.source, xpath: this.xpath, ancestry: this.ancestry, - nodeIndexes: this.nodeIndexes + nodeIndexes: this.nodeIndexes, + fromFrame: this.fromFrame }; + if (this._includeElementInJson) { + spec.element = this._element; + } + return spec; } }; @@ -122,14 +142,29 @@ DqElement.fromFrame = function fromFrame(node, options, frame) { return new DqElement(frame.element, options, spec); }; -DqElement.mergeSpecs = function mergeSpec(node, frame) { +DqElement.mergeSpecs = function mergeSpecs(child, parentFrame) { + // Parameter order reversed for backcompat return { - ...node, - selector: [...frame.selector, ...node.selector], - ancestry: [...frame.ancestry, ...node.ancestry], - xpath: [...frame.xpath, ...node.xpath], - nodeIndexes: [...frame.nodeIndexes, ...node.nodeIndexes] + ...child, + selector: [...parentFrame.selector, ...child.selector], + ancestry: [...parentFrame.ancestry, ...child.ancestry], + xpath: [...parentFrame.xpath, ...child.xpath], + nodeIndexes: [...parentFrame.nodeIndexes, ...child.nodeIndexes], + fromFrame: true }; }; +/** + * Set the default options to be used + * @param {Object} RunOptions Options passed to axe.run() + * @property {boolean} elementRef Add element when toJSON is called + * @property {boolean} absolutePaths Use absolute path fro selectors + */ +DqElement.setRunOptions = function setRunOptions({ + elementRef, + absolutePaths +}) { + cache.set(CACHE_KEY, { elementRef, absolutePaths }); +}; + export default DqElement; diff --git a/lib/core/utils/finalize-result.js b/lib/core/utils/finalize-result.js index 53b97ebdba..94f3134e96 100644 --- a/lib/core/utils/finalize-result.js +++ b/lib/core/utils/finalize-result.js @@ -5,10 +5,10 @@ import aggregateNodeResults from './aggregate-node-results'; * @param ruleResult {object} * @return {object} */ -function finalizeRuleResult(ruleResult) { +export default function finalizeRuleResult(ruleResult) { // we don't use getRule so that this code does not throw but returns // the results - const rule = axe._audit.rules.find(rule => rule.id === ruleResult.id); + const rule = axe._audit.rules.find(({ id }) => id === ruleResult.id); if (rule && rule.impact) { ruleResult.nodes.forEach(node => { ['any', 'all', 'none'].forEach(checkType => { @@ -24,5 +24,3 @@ function finalizeRuleResult(ruleResult) { return ruleResult; } - -export default finalizeRuleResult; diff --git a/lib/core/utils/find-by.js b/lib/core/utils/find-by.js index ee84ecdb35..252a0c1903 100644 --- a/lib/core/utils/find-by.js +++ b/lib/core/utils/find-by.js @@ -9,7 +9,13 @@ */ function findBy(array, key, value) { if (Array.isArray(array)) { - return array.find(obj => typeof obj === 'object' && obj[key] === value); + return array.find( + obj => + obj !== null && + typeof obj === 'object' && + Object.hasOwn(obj, key) && + obj[key] === value + ); } } diff --git a/lib/core/utils/frame-messenger/message-id.js b/lib/core/utils/frame-messenger/message-id.js index 0766e1b36d..d01533ca7e 100644 --- a/lib/core/utils/frame-messenger/message-id.js +++ b/lib/core/utils/frame-messenger/message-id.js @@ -1,5 +1,6 @@ import { v4 as createUuid } from '../uuid'; +// No cache, so that this can persist across axe.run calls const messageIds = []; export function createMessageId() { diff --git a/lib/core/utils/frame-messenger/post-message.js b/lib/core/utils/frame-messenger/post-message.js index 5f90b76ca3..a682bee5d1 100644 --- a/lib/core/utils/frame-messenger/post-message.js +++ b/lib/core/utils/frame-messenger/post-message.js @@ -15,10 +15,6 @@ import { createMessageId } from './message-id'; * @return {Boolean} true if the message was sent */ export function postMessage(win, data, sendToParent, replyHandler) { - if (typeof replyHandler === 'function') { - storeReplyHandler(data.channelId, replyHandler, sendToParent); - } - // Prevent messaging to an inappropriate window sendToParent ? assertIsParentWindow(win) : assertIsFrameWindow(win); if (data.message instanceof Error && !sendToParent) { @@ -37,6 +33,9 @@ export function postMessage(win, data, sendToParent, replyHandler) { return false; } + if (typeof replyHandler === 'function') { + storeReplyHandler(data.channelId, replyHandler, sendToParent); + } // There is no way to know the origin of `win`, so we'll try them all. allowedOrigins.forEach(origin => { try { diff --git a/lib/core/utils/get-flattened-tree.js b/lib/core/utils/get-flattened-tree.js index 84fb41b18c..2fb869e78a 100644 --- a/lib/core/utils/get-flattened-tree.js +++ b/lib/core/utils/get-flattened-tree.js @@ -23,6 +23,36 @@ import { cacheNodeSelectors } from './selector-cache'; let hasShadowRoot; +/** + * Recursvely returns an array of the virtual DOM nodes at this level + * excluding comment nodes and the shadow DOM nodes <content> and <slot> + * + * @param {Node} [node=document.documentElement] optional node. NOTE: passing in anything other than body or the documentElement may result in incomplete results. + * @param {String} [shadowId] optional ID of the shadow DOM that is the closest shadow + * ancestor of the node + */ +export default function getFlattenedTree( + node = document.documentElement, + shadowId +) { + hasShadowRoot = false; + const selectorMap = {}; + cache.set('nodeMap', new WeakMap()); + cache.set('selectorMap', selectorMap); + + // specifically pass `null` to the parent to designate the top + // node of the tree. if parent === undefined then we know + // we are in a disconnected tree + const tree = flattenTree(node, shadowId, null); + tree[0]._selectorMap = selectorMap; + + // allow rules and checks to know if there is a shadow root attached + // to the current tree + tree[0]._hasShadowRoot = hasShadowRoot; + + return tree; +} + /** * find all the fallback content for a <slot> and return these as an array * this array will also include any #text nodes @@ -67,8 +97,8 @@ function createNode(node, parent, shadowId) { function flattenTree(node, shadowId, parent) { // using a closure here and therefore cannot easily refactor toreduce the statements var retVal, realArray, nodeName; - function reduceShadowDOM(res, child, parent) { - var replacements = flattenTree(child, shadowId, parent); + function reduceShadowDOM(res, child, parentVNode) { + const replacements = flattenTree(child, shadowId, parentVNode); if (replacements) { res = res.concat(replacements); } @@ -145,32 +175,3 @@ function flattenTree(node, shadowId, parent) { } } } - -/** - * Recursvely returns an array of the virtual DOM nodes at this level - * excluding comment nodes and the shadow DOM nodes <content> and <slot> - * - * @param {Node} [node=document.documentElement] optional node. NOTE: passing in anything other than body or the documentElement may result in incomplete results. - * @param {String} [shadowId] optional ID of the shadow DOM that is the closest shadow - * ancestor of the node - */ -function getFlattenedTree(node = document.documentElement, shadowId) { - hasShadowRoot = false; - const selectorMap = {}; - cache.set('nodeMap', new WeakMap()); - cache.set('selectorMap', selectorMap); - - // specifically pass `null` to the parent to designate the top - // node of the tree. if parent === undefined then we know - // we are in a disconnected tree - const tree = flattenTree(node, shadowId, null); - tree[0]._selectorMap = selectorMap; - - // allow rules and checks to know if there is a shadow root attached - // to the current tree - tree[0]._hasShadowRoot = hasShadowRoot; - - return tree; -} - -export default getFlattenedTree; diff --git a/lib/core/utils/get-rule.js b/lib/core/utils/get-rule.js index 21d18f88ac..14b42d9f19 100644 --- a/lib/core/utils/get-rule.js +++ b/lib/core/utils/get-rule.js @@ -3,9 +3,9 @@ * @param {String} ruelId the rule id * @return {Rule} */ -function getRule(ruleId) { +export default function getRule(ruleId) { // TODO: es-modules_audit - var rule = axe._audit.rules.find(rule => rule.id === ruleId); + const rule = axe._audit.rules.find(({ id }) => id === ruleId); if (!rule) { throw new Error(`Cannot find rule by id: ${ruleId}`); @@ -13,5 +13,3 @@ function getRule(ruleId) { return rule; } - -export default getRule; diff --git a/lib/core/utils/get-scroll.js b/lib/core/utils/get-scroll.js index 306237270e..7270104bee 100644 --- a/lib/core/utils/get-scroll.js +++ b/lib/core/utils/get-scroll.js @@ -1,12 +1,14 @@ +import memoize from './memoize'; + /** * Get the scroll position of given element * @method getScroll * @memberof axe.utils - * @param {Element} node + * @param {Element} elm * @param {buffer} (Optional) allowed negligence in overflow * @returns {Object | undefined} */ -export default function getScroll(elm, buffer = 0) { +function getScroll(elm, buffer = 0) { const overflowX = elm.scrollWidth > elm.clientWidth + buffer; const overflowY = elm.scrollHeight > elm.clientHeight + buffer; @@ -38,3 +40,5 @@ function isScrollable(style, prop) { const overflowProp = style.getPropertyValue(prop); return ['scroll', 'auto'].includes(overflowProp); } + +export default memoize(getScroll); diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index 02c192f8ec..28af5bf6af 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -5,7 +5,6 @@ import matchesSelector from './element-matches'; import isXHTML from './is-xhtml'; import getShadowSelector from './get-shadow-selector'; -let xhtml; const ignoredAttributes = [ 'class', 'style', @@ -238,9 +237,7 @@ function getElmId(elm) { * @return {String|Array<String>} Base CSS selector for the node */ function getBaseSelector(elm) { - if (typeof xhtml === 'undefined') { - xhtml = isXHTML(document); - } + const xhtml = isXHTML(document); return escapeSelector(xhtml ? elm.localName : elm.nodeName.toLowerCase()); } diff --git a/lib/core/utils/get-shadow-selector.js b/lib/core/utils/get-shadow-selector.js index 22ecbd0c4a..9a60f4f2a3 100644 --- a/lib/core/utils/get-shadow-selector.js +++ b/lib/core/utils/get-shadow-selector.js @@ -4,7 +4,7 @@ * @param {Object} optional options * @returns {String|Array<String>} Unique CSS selector for the node */ -function getShadowSelector(generateSelector, elm, options = {}) { +export default function getShadowSelector(generateSelector, elm, options = {}) { if (!elm) { return ''; } @@ -25,7 +25,5 @@ function getShadowSelector(generateSelector, elm, options = {}) { } stack.unshift({ elm, doc }); - return stack.map(({ elm, doc }) => generateSelector(elm, options, doc)); + return stack.map(item => generateSelector(item.elm, options, item.doc)); } - -export default getShadowSelector; diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index ad166f1326..6e18e7f99c 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -56,6 +56,7 @@ export { export { default as matchAncestry } from './match-ancestry'; export { default as memoize } from './memoize'; export { default as mergeResults } from './merge-results'; +export { default as nodeSerializer } from './node-serializer'; export { default as nodeSorter } from './node-sorter'; export { default as nodeLookup } from './node-lookup'; export { default as parseCrossOriginStylesheet } from './parse-crossorigin-stylesheet'; diff --git a/lib/core/utils/is-xhtml.js b/lib/core/utils/is-xhtml.js index 09851a2153..3ed4c51776 100644 --- a/lib/core/utils/is-xhtml.js +++ b/lib/core/utils/is-xhtml.js @@ -1,3 +1,5 @@ +import memoize from './memoize'; + /** * Determines if a document node is XHTML * @method isXHTML @@ -5,11 +7,11 @@ * @param {Node} doc a document node * @return {Boolean} */ -function isXHTML(doc) { - if (!doc.createElement) { +const isXHTML = memoize(doc => { + if (!doc?.createElement) { return false; } return doc.createElement('A').localName === 'A'; -} +}); export default isXHTML; diff --git a/lib/core/utils/match-ancestry.js b/lib/core/utils/match-ancestry.js index e9d7185e58..00b5f481cf 100644 --- a/lib/core/utils/match-ancestry.js +++ b/lib/core/utils/match-ancestry.js @@ -1,20 +1,20 @@ /** * Check if two ancestries are identical */ -function matchAncestry(ancestryA, ancestryB) { +export default function matchAncestry(ancestryA, ancestryB) { if (ancestryA.length !== ancestryB.length) { return false; } - return ancestryA.every((selectorA, index) => { - const selectorB = ancestryB[index]; + return ancestryA.every((selectorA, ancestorIndex) => { + const selectorB = ancestryB[ancestorIndex]; if (!Array.isArray(selectorA)) { return selectorA === selectorB; } if (selectorA.length !== selectorB.length) { return false; } - return selectorA.every((str, index) => selectorB[index] === str); + return selectorA.every( + (str, selectorIndex) => selectorB[selectorIndex] === str + ); }); } - -export default matchAncestry; diff --git a/lib/core/utils/matches.js b/lib/core/utils/matches.js index 5a20573edf..f49bace8f3 100644 --- a/lib/core/utils/matches.js +++ b/lib/core/utils/matches.js @@ -1,5 +1,19 @@ import cssParser from './css-parser'; +/** + * matches implementation that operates on a VirtualNode + * + * @method matches + * @memberof axe.utils + * @param {VirtualNode} vNode VirtualNode to match + * @param {String} selector CSS selector string + * @return {Boolean} + */ +export default function matches(vNode, selector) { + const expressions = convertSelector(selector); + return expressions.some(expression => matchesExpression(vNode, expression)); +} + function matchesTag(vNode, exp) { return ( vNode.props.nodeType === 1 && @@ -232,11 +246,11 @@ function optimizedMatchesExpression(vNode, expressions, index, matchAnyParent) { const isArray = Array.isArray(expressions); const expression = isArray ? expressions[index] : expressions; - let matches = matchExpression(vNode, expression); + let machedExpression = matchExpression(vNode, expression); - while (!matches && matchAnyParent && vNode.parent) { + while (!machedExpression && matchAnyParent && vNode.parent) { vNode = vNode.parent; - matches = matchExpression(vNode, expression); + machedExpression = matchExpression(vNode, expression); } if (index > 0) { @@ -247,8 +261,8 @@ function optimizedMatchesExpression(vNode, expressions, index, matchAnyParent) { ); } - matches = - matches && + machedExpression = + machedExpression && optimizedMatchesExpression( vNode.parent, expressions, @@ -257,7 +271,7 @@ function optimizedMatchesExpression(vNode, expressions, index, matchAnyParent) { ); } - return matches; + return machedExpression; } /** @@ -278,19 +292,3 @@ export function matchesExpression(vNode, expressions, matchAnyParent) { matchAnyParent ); } - -/** - * matches implementation that operates on a VirtualNode - * - * @method matches - * @memberof axe.utils - * @param {VirtualNode} vNode VirtualNode to match - * @param {String} selector CSS selector string - * @return {Boolean} - */ -function matches(vNode, selector) { - const expressions = convertSelector(selector); - return expressions.some(expression => matchesExpression(vNode, expression)); -} - -export default matches; diff --git a/lib/core/utils/merge-results.js b/lib/core/utils/merge-results.js index 8d1f2af6dc..e5c9547adf 100644 --- a/lib/core/utils/merge-results.js +++ b/lib/core/utils/merge-results.js @@ -1,4 +1,4 @@ -import DqElement from './dq-element'; +import nodeSerializer from './node-serializer'; import getAllChecks from './get-all-checks'; import findBy from './find-by'; @@ -6,17 +6,17 @@ import findBy from './find-by'; * Adds the owning frame's CSS selector onto each instance of DqElement * @private * @param {Array} resultSet `nodes` array on a `RuleResult` - * @param {HTMLElement} frameElement The frame element - * @param {String} frameSelector Unique CSS selector for the frame + * @param {Object} options Propagated from axe.run/etc + * @param {Object} frameSpec The spec describing the owning frame (see nodeSerializer.toSpec) */ function pushFrame(resultSet, options, frameSpec) { resultSet.forEach(res => { - res.node = DqElement.fromFrame(res.node, options, frameSpec); + res.node = nodeSerializer.mergeSpecs(res.node, frameSpec); const checks = getAllChecks(res); checks.forEach(check => { check.relatedNodes = check.relatedNodes.map(node => - DqElement.fromFrame(node, options, frameSpec) + nodeSerializer.mergeSpecs(node, frameSpec) ); }); }); @@ -68,8 +68,10 @@ function normalizeResult(result) { /** * Merges one or more RuleResults (possibly from different frames) into one RuleResult * @private - * @param {Array} frameResults Array of objects including the RuleResults as `results` and frame as `frame` - * @return {Array} The merged RuleResults; should only have one result per rule + * @param {Array} frameResults Array of objects including the RuleResults as `results` and + * owning frame as either an Element `frameElement` or a spec `frameSpec` (see nodeSerializer.toSpec) + * @param {Object} options Propagated from axe.run/etc + * @return {Array} The merged RuleResults; should only have one result per rule */ function mergeResults(frameResults, options) { const mergedResult = []; @@ -79,7 +81,7 @@ function mergeResults(frameResults, options) { return; } - const frameSpec = getFrameSpec(frameResult, options); + const frameSpec = getFrameSpec(frameResult); results.forEach(ruleResult => { if (ruleResult.nodes && frameSpec) { pushFrame(ruleResult.nodes, options, frameSpec); @@ -128,9 +130,9 @@ function nodeIndexSort(nodeIndexesA = [], nodeIndexesB = []) { export default mergeResults; -function getFrameSpec(frameResult, options) { +function getFrameSpec(frameResult) { if (frameResult.frameElement) { - return new DqElement(frameResult.frameElement, options); + return nodeSerializer.toSpec(frameResult.frameElement); } else if (frameResult.frameSpec) { return frameResult.frameSpec; } diff --git a/lib/core/utils/node-serializer.js b/lib/core/utils/node-serializer.js new file mode 100644 index 0000000000..8a3c805f0d --- /dev/null +++ b/lib/core/utils/node-serializer.js @@ -0,0 +1,133 @@ +import assert from './assert'; +import DqElement from './dq-element'; + +let customSerializer = null; + +const nodeSerializer = { + /** + * @param {Object} newSerializer + * @property {Function} toSpec (Optional) Converts a DqElement to a "spec", a form + * suitable for JSON.stringify to consume. Output must include all properties + * that DqElement.toJSON() would have. Will always be invoked from the + * input element's original page context. + * @property {Function} mergeSpecs (Optional) Merges two specs (produced by toSpec) which + * represent element's parent frame and an element, respectively. Will + * *not* necessarily be invoked from *either* node's original page context. + * This operation must be associative, that is, these two expressions must + * produce the same result: + * - mergeSpecs(a, mergeSpecs(b, c)) + * - mergeSpecs(mergeSpecs(a, b), c) + */ + update(serializer) { + assert(typeof serializer === 'object', 'serializer must be an object'); + customSerializer = serializer; + }, + + /** + * Converts an Element or VirtualNode to something that can be serialized. + * @param {Element|VirtualNode} node + * @return {Object} A "spec", a form suitable for JSON.stringify to consume. + */ + toSpec(node) { + return nodeSerializer.dqElmToSpec(new DqElement(node)); + }, + + /** + * Converts an DqElement to a serializable object. Optionally provide runOptions + * to limit which properties are included. + * @param {DqElement|SpecObject} dqElm + * @param {Object} runOptions (Optional) Set of options passed into rules or checks + * @param {Boolean} runOptions.selectors (Optional) Include selector in output + * @param {Boolean} runOptions.ancestry (Optional) Include ancestry in output + * @param {Boolean} runOptions.xpath (Optional) Include xpath in output + * @return {SpecObject} A "spec", a form suitable for JSON.stringify to consume. + */ + dqElmToSpec(dqElm, runOptions) { + if (dqElm instanceof DqElement === false) { + return dqElm; + } + // Optionally remove selector, ancestry, xpath + // to prevent unnecessary calculations + if (runOptions) { + dqElm = cloneLimitedDqElement(dqElm, runOptions); + } + + if (typeof customSerializer?.toSpec === 'function') { + return customSerializer.toSpec(dqElm); + } + return dqElm.toJSON(); + }, + + /** + * Merges two specs (produced by toSpec) which represent + * element's parent frame and an element, + * @param {Object} nodeSpec + * @param {Object} parentFrameSpec + * @returns {Object} The merged spec + */ + mergeSpecs(nodeSpec, parentFrameSpec) { + if (typeof customSerializer?.mergeSpecs === 'function') { + return customSerializer.mergeSpecs(nodeSpec, parentFrameSpec); + } + return DqElement.mergeSpecs(nodeSpec, parentFrameSpec); + }, + + /** + * Convert DqElements in RawResults to serialized nodes + * @param {undefined|RawNodeResult[]} rawResults + * @returns {undefined|RawNodeResult[]} + */ + mapRawResults(rawResults) { + return rawResults.map(rawResult => ({ + ...rawResult, + nodes: nodeSerializer.mapRawNodeResults(rawResult.nodes) + })); + }, + + /** + * Convert DqElements in RawNodeResults to serialized nodes + * @param {undefined|RawNodeResult[]} rawResults + * @returns {undefined|RawNodeResult[]} + */ + mapRawNodeResults(nodeResults) { + return nodeResults?.map(({ node, ...nodeResult }) => { + nodeResult.node = nodeSerializer.dqElmToSpec(node); + + for (const type of ['any', 'all', 'none']) { + nodeResult[type] = nodeResult[type].map( + ({ relatedNodes, ...checkResult }) => { + checkResult.relatedNodes = relatedNodes.map( + nodeSerializer.dqElmToSpec + ); + return checkResult; + } + ); + } + return nodeResult; + }); + } +}; + +export default nodeSerializer; + +/** + * Create a new DqElement with only the properties we actually want serialized + * This prevents nodeSerializer from generating selectors / xpath / ancestry + * when it's not needed. The rest is dummy data to prevent possible errors. + */ +function cloneLimitedDqElement(dqElm, runOptions) { + const fromFrame = dqElm.fromFrame; + const { ancestry: hasAncestry, xpath: hasXpath } = runOptions; + const hasSelectors = runOptions.selectors !== false || fromFrame; + + dqElm = new DqElement(dqElm.element, runOptions, { + source: dqElm.source, + nodeIndexes: dqElm.nodeIndexes, + selector: hasSelectors ? dqElm.selector : [':root'], + ancestry: hasAncestry ? dqElm.ancestry : [':root'], + xpath: hasXpath ? dqElm.xpath : '/' + }); + + dqElm.fromFrame = fromFrame; + return dqElm; +} diff --git a/lib/core/utils/preload-cssom.js b/lib/core/utils/preload-cssom.js index c919d023c4..3b86ddf031 100644 --- a/lib/core/utils/preload-cssom.js +++ b/lib/core/utils/preload-cssom.js @@ -173,7 +173,11 @@ function getStylesheetsFromDocumentFragment(rootNode, convertDataToStylesheet) { isLink, root: rootNode }); - out.push(stylesheet.sheet); + // prevent error in jsdom with style elements not having a `sheet` property + // @see https://github.com/jsdom/jsdom/issues/3179 + if (stylesheet.sheet) { + out.push(stylesheet.sheet); + } return out; }, []) ); diff --git a/lib/core/utils/preload.js b/lib/core/utils/preload.js index 82da84ade3..f7338dc41e 100644 --- a/lib/core/utils/preload.js +++ b/lib/core/utils/preload.js @@ -3,14 +3,83 @@ import preloadMedia from './preload-media'; import uniqueArray from './unique-array'; import constants from '../constants'; +/** + * Returns a Promise with results of all requested preload(able) assets. eg: ['cssom']. + * + * @param {Object} options run configuration options (or defaults) passed via axe.run + * @return {Object} Promise + */ +export default function preload(options) { + const preloadFunctionsMap = { + cssom: preloadCssom, + media: preloadMedia + }; + + if (!shouldPreload(options)) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const { assets, timeout } = getPreloadConfig(options); + + /** + * Start `timeout` timer for preloading assets + * -> reject if allowed time expires. + */ + const preloadTimeout = setTimeout( + () => reject(new Error(`Preload assets timed out.`)), + timeout + ); + + /** + * Fetch requested `assets` + */ + Promise.all( + assets.map(asset => + preloadFunctionsMap[asset](options).then(results => { + return { + [asset]: results + }; + }) + ) + ) + .then(results => { + /** + * Combine array of results into an object map + * + * From -> + * [{cssom: [...], aom: [...]}] + * To -> + * { + * cssom: [...] + * aom: [...] + * } + */ + const preloadAssets = results.reduce((out, result) => { + return { + ...out, + ...result + }; + }, {}); + + clearTimeout(preloadTimeout); + resolve(preloadAssets); + }) + .catch(err => { + clearTimeout(preloadTimeout); + reject(err); + }); + }); +} + /** * Validated the preload object - * @param {Object | boolean} preload configuration object or boolean passed via the options parameter to axe.run + * @param {Object | boolean} preloadObj configuration object or boolean passed via the options parameter to axe.run * @return {boolean} * @private */ -function isValidPreloadObject(preload) { - return typeof preload === 'object' && Array.isArray(preload.assets); +function isValidPreloadObject(preloadObj) { + return typeof preloadObj === 'object' && Array.isArray(preloadObj.assets); } /** @@ -77,74 +146,3 @@ export function getPreloadConfig(options) { } return config; } - -/** - * Returns a Promise with results of all requested preload(able) assets. eg: ['cssom']. - * - * @param {Object} options run configuration options (or defaults) passed via axe.run - * @return {Object} Promise - */ -function preload(options) { - const preloadFunctionsMap = { - cssom: preloadCssom, - media: preloadMedia - }; - - if (!shouldPreload(options)) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const { assets, timeout } = getPreloadConfig(options); - - /** - * Start `timeout` timer for preloading assets - * -> reject if allowed time expires. - */ - const preloadTimeout = setTimeout( - () => reject(new Error(`Preload assets timed out.`)), - timeout - ); - - /** - * Fetch requested `assets` - */ - Promise.all( - assets.map(asset => - preloadFunctionsMap[asset](options).then(results => { - return { - [asset]: results - }; - }) - ) - ) - .then(results => { - /** - * Combine array of results into an object map - * - * From -> - * [{cssom: [...], aom: [...]}] - * To -> - * { - * cssom: [...] - * aom: [...] - * } - */ - const preloadAssets = results.reduce((out, result) => { - return { - ...out, - ...result - }; - }, {}); - - clearTimeout(preloadTimeout); - resolve(preloadAssets); - }) - .catch(err => { - clearTimeout(preloadTimeout); - reject(err); - }); - }); -} - -export default preload; diff --git a/lib/core/utils/publish-metadata.js b/lib/core/utils/publish-metadata.js index ff3b66b3e0..8354ceab01 100644 --- a/lib/core/utils/publish-metadata.js +++ b/lib/core/utils/publish-metadata.js @@ -4,6 +4,29 @@ import findBy from './find-by'; import extendMetaData from './extend-meta-data'; import incompleteFallbackMessage from '../reporters/helpers/incomplete-fallback-msg'; +/** + * Publish metadata from axe._audit.data + * @param {RuleResult} result Result to publish to + * @private + */ +export default function publishMetaData(ruleResult) { + // TODO: es-modules_audit + const checksData = axe._audit.data.checks || {}; + const rulesData = axe._audit.data.rules || {}; + const rule = findBy(axe._audit.rules, 'id', ruleResult.id) || {}; + + ruleResult.tags = clone(rule.tags || []); + + const shouldBeTrue = extender(checksData, true, rule); + const shouldBeFalse = extender(checksData, false, rule); + ruleResult.nodes.forEach(detail => { + detail.any.forEach(shouldBeTrue); + detail.all.forEach(shouldBeTrue); + detail.none.forEach(shouldBeFalse); + }); + extendMetaData(ruleResult, clone(rulesData[ruleResult.id] || {})); +} + /** * Construct incomplete message from check.data * @param {Object} checkData Check result with reason specified @@ -12,10 +35,10 @@ import incompleteFallbackMessage from '../reporters/helpers/incomplete-fallback- * @private */ function getIncompleteReason(checkData, messages) { - function getDefaultMsg(messages) { - if (messages.incomplete && messages.incomplete.default) { + function getDefaultMsg(message) { + if (message.incomplete && message.incomplete.default) { // fall back to the default message if no reason specified - return messages.incomplete.default; + return message.incomplete.default; } else { return incompleteFallbackMessage(); } @@ -82,28 +105,3 @@ function extender(checksData, shouldBeTrue, rule) { extendMetaData(check, data); }; } - -/** - * Publish metadata from axe._audit.data - * @param {RuleResult} result Result to publish to - * @private - */ -function publishMetaData(ruleResult) { - // TODO: es-modules_audit - const checksData = axe._audit.data.checks || {}; - const rulesData = axe._audit.data.rules || {}; - const rule = findBy(axe._audit.rules, 'id', ruleResult.id) || {}; - - ruleResult.tags = clone(rule.tags || []); - - const shouldBeTrue = extender(checksData, true, rule); - const shouldBeFalse = extender(checksData, false, rule); - ruleResult.nodes.forEach(detail => { - detail.any.forEach(shouldBeTrue); - detail.all.forEach(shouldBeTrue); - detail.none.forEach(shouldBeFalse); - }); - extendMetaData(ruleResult, clone(rulesData[ruleResult.id] || {})); -} - -export default publishMetaData; diff --git a/lib/core/utils/query-selector-all-filter.js b/lib/core/utils/query-selector-all-filter.js index 770e3651c1..d1d92461b3 100644 --- a/lib/core/utils/query-selector-all-filter.js +++ b/lib/core/utils/query-selector-all-filter.js @@ -1,3 +1,4 @@ +import cache from '../base/cache'; import { matchesExpression, convertSelector } from './matches'; import { getNodesMatchingExpression } from './selector-cache'; @@ -19,17 +20,20 @@ function createLocalVariables( return retVal; } -/** - * Allocating new objects in createLocalVariables is quite expensive given - * that matchExpressions is in the hot path. - * - * Keep track of previously allocated objects to avoid useless allocations - * and garbage collection. This is intentionally shared between calls of - * matchExpressions. - */ -const recycledLocalVariables = []; - function matchExpressions(domTree, expressions, filter) { + /** + * Allocating new objects in createLocalVariables is quite expensive given + * that matchExpressions is in the hot path. + * + * Keep track of previously allocated objects to avoid useless allocations + * and garbage collection. This is intentionally shared between calls of + * matchExpressions. + */ + const recycledLocalVariables = cache.get( + 'qsa.recycledLocalVariables', + () => [] + ); + const stack = []; const vNodes = Array.isArray(domTree) ? domTree : [domTree]; let currentLevel = createLocalVariables( diff --git a/lib/core/utils/selector-cache.js b/lib/core/utils/selector-cache.js index 5f81700678..1771c89c76 100644 --- a/lib/core/utils/selector-cache.js +++ b/lib/core/utils/selector-cache.js @@ -91,9 +91,12 @@ function findMatchingNodes(expression, selectorMap, shadowId) { nodes = selectorMap['*']; } else { if (exp.id) { - // a selector must match all parts, otherwise we can just exit - // early - if (!selectorMap[idsKey] || !selectorMap[idsKey][exp.id]?.length) { + // a selector must match all parts, otherwise we can just exit early + if ( + !selectorMap[idsKey] || + !Object.hasOwn(selectorMap[idsKey], exp.id) || + !selectorMap[idsKey][exp.id]?.length + ) { return; } @@ -176,7 +179,9 @@ function getSharedValues(a, b) { * @param {Object} map */ function cacheSelector(key, vNode, map) { - map[key] = map[key] || []; + if (!Object.hasOwn(map, key)) { + map[key] = []; + } map[key].push(vNode); } diff --git a/lib/intro.stub b/lib/intro.stub index 6d76fb1e4c..77aa61cf83 100644 --- a/lib/intro.stub +++ b/lib/intro.stub @@ -1,5 +1,5 @@ /*! axe v<%= pkg.version %> - * Copyright (c) <%= grunt.template.today("yyyy") %> Deque Systems, Inc. + * Copyright (c) 2015 - <%= grunt.template.today("yyyy") %> Deque Systems, Inc. * * Your use of this Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/lib/rules/accesskeys.json b/lib/rules/accesskeys.json index a2064bdfe0..d5eb681cd7 100644 --- a/lib/rules/accesskeys.json +++ b/lib/rules/accesskeys.json @@ -1,5 +1,6 @@ { "id": "accesskeys", + "impact": "serious", "selector": "[accesskey]", "excludeHidden": false, "tags": ["cat.keyboard", "best-practice"], diff --git a/lib/rules/area-alt.json b/lib/rules/area-alt.json index afe0ddbe3c..06218e354b 100644 --- a/lib/rules/area-alt.json +++ b/lib/rules/area-alt.json @@ -1,5 +1,6 @@ { "id": "area-alt", + "impact": "critical", "selector": "map area[href]", "excludeHidden": false, "tags": [ @@ -9,9 +10,12 @@ "wcag412", "section508", "section508.22.a", - "ACT", "TTv5", - "TT6.a" + "TT6.a", + "EN-301-549", + "EN-9.2.4.4", + "EN-9.4.1.2", + "ACT" ], "actIds": ["c487ae"], "metadata": { diff --git a/lib/rules/aria-allowed-attr.json b/lib/rules/aria-allowed-attr.json index 90cf8f3a19..b7073c38cf 100644 --- a/lib/rules/aria-allowed-attr.json +++ b/lib/rules/aria-allowed-attr.json @@ -1,13 +1,14 @@ { "id": "aria-allowed-attr", + "impact": "critical", "matches": "aria-allowed-attr-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "actIds": ["5c01ea"], "metadata": { - "description": "Ensures ARIA attributes are allowed for an element's role", - "help": "Elements must only use allowed ARIA attributes" + "description": "Ensures an element's role supports its ARIA attributes", + "help": "Elements must only use supported ARIA attributes" }, - "all": ["aria-allowed-attr", "aria-conditional-attr"], + "all": ["aria-allowed-attr"], "any": [], - "none": ["aria-unsupported-attr", "aria-prohibited-attr"] + "none": ["aria-unsupported-attr"] } diff --git a/lib/rules/aria-allowed-role.json b/lib/rules/aria-allowed-role.json index c4750796af..43035835ca 100644 --- a/lib/rules/aria-allowed-role.json +++ b/lib/rules/aria-allowed-role.json @@ -1,5 +1,6 @@ { "id": "aria-allowed-role", + "impact": "minor", "excludeHidden": false, "selector": "[role]", "matches": "aria-allowed-role-matches", diff --git a/lib/rules/aria-braille-equivalent.json b/lib/rules/aria-braille-equivalent.json new file mode 100644 index 0000000000..a406e59fb1 --- /dev/null +++ b/lib/rules/aria-braille-equivalent.json @@ -0,0 +1,14 @@ +{ + "id": "aria-braille-equivalent", + "reviewOnFail": true, + "impact": "serious", + "selector": "[aria-brailleroledescription], [aria-braillelabel]", + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "metadata": { + "description": "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent", + "help": "aria-braille attributes must have a non-braille equivalent" + }, + "all": ["braille-roledescription-equivalent", "braille-label-equivalent"], + "any": [], + "none": [] +} diff --git a/lib/rules/aria-command-name.json b/lib/rules/aria-command-name.json index 636da2914f..bd7803d14d 100644 --- a/lib/rules/aria-command-name.json +++ b/lib/rules/aria-command-name.json @@ -1,8 +1,18 @@ { "id": "aria-command-name", + "impact": "serious", "selector": "[role=\"link\"], [role=\"button\"], [role=\"menuitem\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT6.a"], + "tags": [ + "cat.aria", + "wcag2a", + "wcag412", + "TTv5", + "TT6.a", + "EN-301-549", + "EN-9.4.1.2", + "ACT" + ], "actIds": ["97a4e1"], "metadata": { "description": "Ensures every ARIA button, link and menuitem has an accessible name", diff --git a/lib/rules/aria-conditional-attr.json b/lib/rules/aria-conditional-attr.json new file mode 100644 index 0000000000..ac1f57ae4b --- /dev/null +++ b/lib/rules/aria-conditional-attr.json @@ -0,0 +1,14 @@ +{ + "id": "aria-conditional-attr", + "impact": "serious", + "matches": "aria-allowed-attr-matches", + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "actIds": ["5c01ea"], + "metadata": { + "description": "Ensures ARIA attributes are used as described in the specification of the element's role", + "help": "ARIA attributes must be used as specified for the element's role" + }, + "all": ["aria-conditional-attr"], + "any": [], + "none": [] +} diff --git a/lib/rules/aria-deprecated-role.json b/lib/rules/aria-deprecated-role.json new file mode 100644 index 0000000000..3e181f366a --- /dev/null +++ b/lib/rules/aria-deprecated-role.json @@ -0,0 +1,15 @@ +{ + "id": "aria-deprecated-role", + "impact": "minor", + "selector": "[role]", + "matches": "no-empty-role-matches", + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "actIds": ["674b10"], + "metadata": { + "description": "Ensures elements do not use deprecated roles", + "help": "Deprecated ARIA roles must not be used" + }, + "all": [], + "any": [], + "none": ["deprecatedrole"] +} diff --git a/lib/rules/aria-dialog-name.json b/lib/rules/aria-dialog-name.json index 810d15d826..fe30cd09d7 100644 --- a/lib/rules/aria-dialog-name.json +++ b/lib/rules/aria-dialog-name.json @@ -1,5 +1,6 @@ { "id": "aria-dialog-name", + "impact": "serious", "selector": "[role=\"dialog\"], [role=\"alertdialog\"]", "matches": "no-naming-method-matches", "tags": ["cat.aria", "best-practice"], diff --git a/lib/rules/aria-hidden-body.json b/lib/rules/aria-hidden-body.json index f44a1922ab..20697f7997 100644 --- a/lib/rules/aria-hidden-body.json +++ b/lib/rules/aria-hidden-body.json @@ -1,12 +1,13 @@ { "id": "aria-hidden-body", + "impact": "critical", "selector": "body", "excludeHidden": false, "matches": "is-initiator-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "metadata": { - "description": "Ensures aria-hidden='true' is not present on the document body.", - "help": "aria-hidden='true' must not be present on the document body" + "description": "Ensures aria-hidden=\"true\" is not present on the document body.", + "help": "aria-hidden=\"true\" must not be present on the document body" }, "all": [], "any": ["aria-hidden-body"], diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index 0115f19496..b49c75ae86 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -1,9 +1,18 @@ { "id": "aria-hidden-focus", + "impact": "serious", "selector": "[aria-hidden=\"true\"]", "matches": "aria-hidden-focus-matches", "excludeHidden": false, - "tags": ["cat.name-role-value", "wcag2a", "wcag412", "TTv5", "TT6.a"], + "tags": [ + "cat.name-role-value", + "wcag2a", + "wcag412", + "TTv5", + "TT6.a", + "EN-301-549", + "EN-9.4.1.2" + ], "actIds": ["6cfa84"], "metadata": { "description": "Ensures aria-hidden elements are not focusable nor contain focusable elements", diff --git a/lib/rules/aria-input-field-name.json b/lib/rules/aria-input-field-name.json index 042d5d77f9..9aff44648d 100644 --- a/lib/rules/aria-input-field-name.json +++ b/lib/rules/aria-input-field-name.json @@ -1,8 +1,18 @@ { "id": "aria-input-field-name", + "impact": "serious", "selector": "[role=\"combobox\"], [role=\"listbox\"], [role=\"searchbox\"], [role=\"slider\"], [role=\"spinbutton\"], [role=\"textbox\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT5.c"], + "tags": [ + "cat.aria", + "wcag2a", + "wcag412", + "TTv5", + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT" + ], "actIds": ["e086e5"], "metadata": { "description": "Ensures every ARIA input field has an accessible name", diff --git a/lib/rules/aria-meter-name.json b/lib/rules/aria-meter-name.json index 5408e0d659..f62d5c975e 100644 --- a/lib/rules/aria-meter-name.json +++ b/lib/rules/aria-meter-name.json @@ -1,8 +1,9 @@ { "id": "aria-meter-name", + "impact": "serious", "selector": "[role=\"meter\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag111"], + "tags": ["cat.aria", "wcag2a", "wcag111", "EN-301-549", "EN-9.1.1.1"], "metadata": { "description": "Ensures every ARIA meter node has an accessible name", "help": "ARIA meter nodes must have an accessible name" diff --git a/lib/rules/aria-progressbar-name.json b/lib/rules/aria-progressbar-name.json index 0cad72bad3..10650ff1e6 100644 --- a/lib/rules/aria-progressbar-name.json +++ b/lib/rules/aria-progressbar-name.json @@ -1,8 +1,9 @@ { "id": "aria-progressbar-name", + "impact": "serious", "selector": "[role=\"progressbar\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag111"], + "tags": ["cat.aria", "wcag2a", "wcag111", "EN-301-549", "EN-9.1.1.1"], "metadata": { "description": "Ensures every ARIA progressbar node has an accessible name", "help": "ARIA progressbar nodes must have an accessible name" diff --git a/lib/rules/aria-prohibited-attr.json b/lib/rules/aria-prohibited-attr.json new file mode 100644 index 0000000000..f97a04774b --- /dev/null +++ b/lib/rules/aria-prohibited-attr.json @@ -0,0 +1,14 @@ +{ + "id": "aria-prohibited-attr", + "impact": "serious", + "matches": "aria-allowed-attr-matches", + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "actIds": ["5c01ea"], + "metadata": { + "description": "Ensures ARIA attributes are not prohibited for an element's role", + "help": "Elements must only use permitted ARIA attributes" + }, + "all": [], + "any": [], + "none": ["aria-prohibited-attr"] +} diff --git a/lib/rules/aria-required-attr.json b/lib/rules/aria-required-attr.json index 50f02d52ab..cfe0323bc8 100644 --- a/lib/rules/aria-required-attr.json +++ b/lib/rules/aria-required-attr.json @@ -1,7 +1,8 @@ { "id": "aria-required-attr", + "impact": "critical", "selector": "[role]", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "actIds": ["4e8ab6"], "metadata": { "description": "Ensures elements with ARIA roles have all required ARIA attributes", diff --git a/lib/rules/aria-required-children.json b/lib/rules/aria-required-children.json index d29c911d8e..f81242a613 100644 --- a/lib/rules/aria-required-children.json +++ b/lib/rules/aria-required-children.json @@ -1,8 +1,9 @@ { "id": "aria-required-children", + "impact": "critical", "selector": "[role]", "matches": "aria-required-children-matches", - "tags": ["cat.aria", "wcag2a", "wcag131"], + "tags": ["cat.aria", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "actIds": ["bc4a75", "ff89c9"], "metadata": { "description": "Ensures elements with an ARIA role that require child roles contain them", diff --git a/lib/rules/aria-required-parent.json b/lib/rules/aria-required-parent.json index 4ae13fd4a1..370a0ed674 100644 --- a/lib/rules/aria-required-parent.json +++ b/lib/rules/aria-required-parent.json @@ -1,8 +1,9 @@ { "id": "aria-required-parent", + "impact": "critical", "selector": "[role]", "matches": "aria-required-parent-matches", - "tags": ["cat.aria", "wcag2a", "wcag131"], + "tags": ["cat.aria", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "actIds": ["ff89c9"], "metadata": { "description": "Ensures elements with an ARIA role that require parent roles are contained by them", diff --git a/lib/rules/aria-roledescription.json b/lib/rules/aria-roledescription.json index 73013d9e98..0fc3b6624a 100644 --- a/lib/rules/aria-roledescription.json +++ b/lib/rules/aria-roledescription.json @@ -1,7 +1,15 @@ { "id": "aria-roledescription", + "impact": "serious", "selector": "[aria-roledescription]", - "tags": ["cat.aria", "wcag2a", "wcag412", "deprecated"], + "tags": [ + "cat.aria", + "wcag2a", + "wcag412", + "EN-301-549", + "EN-9.4.1.2", + "deprecated" + ], "enabled": false, "metadata": { "description": "Ensure aria-roledescription is only used on elements with an implicit or explicit role", diff --git a/lib/rules/aria-roles.json b/lib/rules/aria-roles.json index 497baa41c0..3cc591cb3a 100644 --- a/lib/rules/aria-roles.json +++ b/lib/rules/aria-roles.json @@ -1,8 +1,9 @@ { "id": "aria-roles", + "impact": "critical", "selector": "[role]", "matches": "no-empty-role-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "actIds": ["674b10"], "metadata": { "description": "Ensures all elements with a role attribute use a valid value", @@ -10,5 +11,5 @@ }, "all": [], "any": [], - "none": ["invalidrole", "abstractrole", "unsupportedrole", "deprecatedrole"] + "none": ["invalidrole", "abstractrole", "unsupportedrole"] } diff --git a/lib/rules/aria-text.json b/lib/rules/aria-text.json index aa284a5867..30db550134 100644 --- a/lib/rules/aria-text.json +++ b/lib/rules/aria-text.json @@ -1,9 +1,10 @@ { "id": "aria-text", + "impact": "serious", "selector": "[role=text]", "tags": ["cat.aria", "best-practice"], "metadata": { - "description": "Ensures \"role=text\" is used on elements with no focusable descendants", + "description": "Ensures role=\"text\" is used on elements with no focusable descendants", "help": "\"role=text\" should have no focusable descendants" }, "all": [], diff --git a/lib/rules/aria-toggle-field-name.json b/lib/rules/aria-toggle-field-name.json index b50cf20869..e673facfbb 100644 --- a/lib/rules/aria-toggle-field-name.json +++ b/lib/rules/aria-toggle-field-name.json @@ -1,8 +1,18 @@ { "id": "aria-toggle-field-name", + "impact": "serious", "selector": "[role=\"checkbox\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"], [role=\"radio\"], [role=\"switch\"], [role=\"option\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "TTv5", "TT5.c"], + "tags": [ + "cat.aria", + "wcag2a", + "wcag412", + "TTv5", + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT" + ], "actIds": ["e086e5"], "metadata": { "description": "Ensures every ARIA toggle field has an accessible name", diff --git a/lib/rules/aria-tooltip-name.json b/lib/rules/aria-tooltip-name.json index 3095ba3e23..c90b7ffde8 100644 --- a/lib/rules/aria-tooltip-name.json +++ b/lib/rules/aria-tooltip-name.json @@ -1,8 +1,9 @@ { "id": "aria-tooltip-name", + "impact": "serious", "selector": "[role=\"tooltip\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "metadata": { "description": "Ensures every ARIA tooltip node has an accessible name", "help": "ARIA tooltip nodes must have an accessible name" diff --git a/lib/rules/aria-treeitem-name.json b/lib/rules/aria-treeitem-name.json index 4b8fa87d09..091cd7ee21 100644 --- a/lib/rules/aria-treeitem-name.json +++ b/lib/rules/aria-treeitem-name.json @@ -1,5 +1,6 @@ { "id": "aria-treeitem-name", + "impact": "serious", "selector": "[role=\"treeitem\"]", "matches": "no-naming-method-matches", "tags": ["cat.aria", "best-practice"], diff --git a/lib/rules/aria-valid-attr-value.json b/lib/rules/aria-valid-attr-value.json index 0a6b03d0b6..924380388a 100644 --- a/lib/rules/aria-valid-attr-value.json +++ b/lib/rules/aria-valid-attr-value.json @@ -1,7 +1,8 @@ { "id": "aria-valid-attr-value", + "impact": "critical", "matches": "aria-has-attr-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "actIds": ["6a7281"], "metadata": { "description": "Ensures all ARIA attributes have valid values", diff --git a/lib/rules/aria-valid-attr.json b/lib/rules/aria-valid-attr.json index bd52c57929..22476a16c3 100644 --- a/lib/rules/aria-valid-attr.json +++ b/lib/rules/aria-valid-attr.json @@ -1,7 +1,8 @@ { "id": "aria-valid-attr", + "impact": "critical", "matches": "aria-has-attr-matches", - "tags": ["cat.aria", "wcag2a", "wcag412"], + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], "actIds": ["5f99a7"], "metadata": { "description": "Ensures attributes that begin with aria- are valid ARIA attributes", diff --git a/lib/rules/audio-caption.json b/lib/rules/audio-caption.json index cbcf650dfe..3974aade12 100644 --- a/lib/rules/audio-caption.json +++ b/lib/rules/audio-caption.json @@ -1,5 +1,6 @@ { "id": "audio-caption", + "impact": "critical", "selector": "audio", "enabled": false, "excludeHidden": false, @@ -7,6 +8,8 @@ "cat.time-and-media", "wcag2a", "wcag121", + "EN-301-549", + "EN-9.1.2.1", "section508", "section508.22.a", "deprecated" diff --git a/lib/rules/autocomplete-valid.json b/lib/rules/autocomplete-valid.json index fd838dd86e..f1a1e01905 100644 --- a/lib/rules/autocomplete-valid.json +++ b/lib/rules/autocomplete-valid.json @@ -1,7 +1,15 @@ { "id": "autocomplete-valid", + "impact": "serious", "matches": "autocomplete-matches", - "tags": ["cat.forms", "wcag21aa", "wcag135", "ACT"], + "tags": [ + "cat.forms", + "wcag21aa", + "wcag135", + "EN-301-549", + "EN-9.1.3.5", + "ACT" + ], "actIds": ["73f2c2"], "metadata": { "description": "Ensure the autocomplete attribute is correct and suitable for the form field", diff --git a/lib/rules/avoid-inline-spacing.json b/lib/rules/avoid-inline-spacing.json index 3cbff24cbd..352f2a8d2c 100644 --- a/lib/rules/avoid-inline-spacing.json +++ b/lib/rules/avoid-inline-spacing.json @@ -1,8 +1,16 @@ { "id": "avoid-inline-spacing", + "impact": "serious", "selector": "[style]", "matches": "is-visible-on-screen-matches", - "tags": ["cat.structure", "wcag21aa", "wcag1412", "ACT"], + "tags": [ + "cat.structure", + "wcag21aa", + "wcag1412", + "EN-301-549", + "EN-9.1.4.12", + "ACT" + ], "actIds": ["24afc2", "9e45ec", "78fd32"], "metadata": { "description": "Ensure that text spacing set through style attributes can be adjusted with custom stylesheets", diff --git a/lib/rules/blink.json b/lib/rules/blink.json index bca2f0405c..de3b7b16b6 100644 --- a/lib/rules/blink.json +++ b/lib/rules/blink.json @@ -1,5 +1,6 @@ { "id": "blink", + "impact": "serious", "selector": "blink", "excludeHidden": false, "tags": [ @@ -9,7 +10,9 @@ "section508", "section508.22.j", "TTv5", - "TT2.b" + "TT2.b", + "EN-301-549", + "EN-9.2.2.2" ], "metadata": { "description": "Ensures <blink> elements are not used", diff --git a/lib/rules/button-name.json b/lib/rules/button-name.json index 6ea4703975..50ca03ba42 100644 --- a/lib/rules/button-name.json +++ b/lib/rules/button-name.json @@ -1,5 +1,6 @@ { "id": "button-name", + "impact": "critical", "selector": "button", "matches": "no-explicit-name-required-matches", "tags": [ @@ -8,9 +9,11 @@ "wcag412", "section508", "section508.22.a", - "ACT", "TTv5", - "TT6.a" + "TT6.a", + "EN-301-549", + "EN-9.4.1.2", + "ACT" ], "actIds": ["97a4e1", "m6b1q3"], "metadata": { diff --git a/lib/rules/bypass.json b/lib/rules/bypass.json index 62c3626393..cc7752bf45 100644 --- a/lib/rules/bypass.json +++ b/lib/rules/bypass.json @@ -1,5 +1,6 @@ { "id": "bypass", + "impact": "serious", "selector": "html", "pageLevel": true, "matches": "bypass-matches", @@ -11,7 +12,9 @@ "section508", "section508.22.o", "TTv5", - "TT9.a" + "TT9.a", + "EN-301-549", + "EN-9.2.4.1" ], "actIds": ["cf77f2", "047fe0", "b40fd1", "3e12e1", "ye5d6e"], "metadata": { diff --git a/lib/rules/color-contrast-enhanced.json b/lib/rules/color-contrast-enhanced.json index 0cc2751582..60187a8c08 100644 --- a/lib/rules/color-contrast-enhanced.json +++ b/lib/rules/color-contrast-enhanced.json @@ -1,5 +1,6 @@ { "id": "color-contrast-enhanced", + "impact": "serious", "matches": "color-contrast-matches", "excludeHidden": false, "enabled": false, diff --git a/lib/rules/color-contrast.json b/lib/rules/color-contrast.json index 5abd09598d..27ed7b19ce 100644 --- a/lib/rules/color-contrast.json +++ b/lib/rules/color-contrast.json @@ -1,8 +1,18 @@ { "id": "color-contrast", + "impact": "serious", "matches": "color-contrast-matches", "excludeHidden": false, - "tags": ["cat.color", "wcag2aa", "wcag143", "ACT", "TTv5", "TT13.c"], + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT" + ], "actIds": ["afw4f7", "09o5cg"], "metadata": { "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", diff --git a/lib/rules/css-orientation-lock.json b/lib/rules/css-orientation-lock.json index c37fda6cde..ddbf7857d2 100644 --- a/lib/rules/css-orientation-lock.json +++ b/lib/rules/css-orientation-lock.json @@ -1,7 +1,15 @@ { "id": "css-orientation-lock", + "impact": "serious", "selector": "html", - "tags": ["cat.structure", "wcag134", "wcag21aa", "experimental"], + "tags": [ + "cat.structure", + "wcag134", + "wcag21aa", + "EN-301-549", + "EN-9.1.3.4", + "experimental" + ], "actIds": ["b33eff"], "metadata": { "description": "Ensures content is not locked to any specific display orientation, and the content is operable in all display orientations", diff --git a/lib/rules/definition-list.json b/lib/rules/definition-list.json index b6180a4a0f..c88a1a48ec 100644 --- a/lib/rules/definition-list.json +++ b/lib/rules/definition-list.json @@ -1,8 +1,9 @@ { "id": "definition-list", + "impact": "serious", "selector": "dl", "matches": "no-role-matches", - "tags": ["cat.structure", "wcag2a", "wcag131"], + "tags": ["cat.structure", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "metadata": { "description": "Ensures <dl> elements are structured correctly", "help": "<dl> elements must only directly contain properly-ordered <dt> and <dd> groups, <script>, <template> or <div> elements" diff --git a/lib/rules/dlitem.json b/lib/rules/dlitem.json index 1d1de21528..8aca260d3a 100644 --- a/lib/rules/dlitem.json +++ b/lib/rules/dlitem.json @@ -1,8 +1,9 @@ { "id": "dlitem", + "impact": "serious", "selector": "dd, dt", "matches": "no-role-matches", - "tags": ["cat.structure", "wcag2a", "wcag131"], + "tags": ["cat.structure", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "metadata": { "description": "Ensures <dt> and <dd> elements are contained by a <dl>", "help": "<dt> and <dd> elements must be contained by a <dl>" diff --git a/lib/rules/document-title.json b/lib/rules/document-title.json index 4bd5992cbb..9ee06b5998 100644 --- a/lib/rules/document-title.json +++ b/lib/rules/document-title.json @@ -1,14 +1,17 @@ { "id": "document-title", + "impact": "serious", "selector": "html", "matches": "is-initiator-matches", "tags": [ "cat.text-alternatives", "wcag2a", "wcag242", - "ACT", "TTv5", - "TT12.a" + "TT12.a", + "EN-301-549", + "EN-9.2.4.2", + "ACT" ], "actIds": ["2779a5"], "metadata": { diff --git a/lib/rules/duplicate-id-active.json b/lib/rules/duplicate-id-active.json index 6edfbc5277..ef8a8687b3 100644 --- a/lib/rules/duplicate-id-active.json +++ b/lib/rules/duplicate-id-active.json @@ -1,9 +1,11 @@ { "id": "duplicate-id-active", + "impact": "serious", "selector": "[id]", "matches": "duplicate-id-active-matches", "excludeHidden": false, - "tags": ["cat.parsing", "wcag2a", "wcag411"], + "tags": ["cat.parsing", "wcag2a-obsolete", "wcag411", "deprecated"], + "enabled": false, "actIds": ["3ea0c8"], "metadata": { "description": "Ensures every id attribute value of active elements is unique", diff --git a/lib/rules/duplicate-id-aria.json b/lib/rules/duplicate-id-aria.json index e2e9caf1fe..8b42aae686 100644 --- a/lib/rules/duplicate-id-aria.json +++ b/lib/rules/duplicate-id-aria.json @@ -1,9 +1,11 @@ { "id": "duplicate-id-aria", + "impact": "critical", "selector": "[id]", "matches": "duplicate-id-aria-matches", "excludeHidden": false, - "tags": ["cat.parsing", "wcag2a", "wcag411"], + "tags": ["cat.parsing", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "reviewOnFail": true, "actIds": ["3ea0c8"], "metadata": { "description": "Ensures every id attribute value used in ARIA and in labels is unique", diff --git a/lib/rules/duplicate-id.json b/lib/rules/duplicate-id.json index 5068a41a0f..2189f8e54e 100644 --- a/lib/rules/duplicate-id.json +++ b/lib/rules/duplicate-id.json @@ -1,9 +1,11 @@ { "id": "duplicate-id", + "impact": "minor", "selector": "[id]", "matches": "duplicate-id-misc-matches", "excludeHidden": false, - "tags": ["cat.parsing", "wcag2a", "wcag411"], + "tags": ["cat.parsing", "wcag2a-obsolete", "wcag411", "deprecated"], + "enabled": false, "actIds": ["3ea0c8"], "metadata": { "description": "Ensures every id attribute value is unique", diff --git a/lib/rules/empty-heading.json b/lib/rules/empty-heading.json index a6bf008fb0..1b510a4233 100644 --- a/lib/rules/empty-heading.json +++ b/lib/rules/empty-heading.json @@ -1,10 +1,10 @@ { "id": "empty-heading", + "impact": "minor", "selector": "h1, h2, h3, h4, h5, h6, [role=\"heading\"]", "matches": "heading-matches", "tags": ["cat.name-role-value", "best-practice"], "actIds": ["ffd0e9"], - "impact": "minor", "metadata": { "description": "Ensures headings have discernible text", "help": "Headings should not be empty" diff --git a/lib/rules/empty-table-header.json b/lib/rules/empty-table-header.json index 49ff72909e..dd81a18824 100644 --- a/lib/rules/empty-table-header.json +++ b/lib/rules/empty-table-header.json @@ -1,5 +1,6 @@ { "id": "empty-table-header", + "impact": "minor", "selector": "th:not([role]), [role=\"rowheader\"], [role=\"columnheader\"]", "tags": ["cat.name-role-value", "best-practice"], "metadata": { diff --git a/lib/rules/focus-order-semantics.json b/lib/rules/focus-order-semantics.json index 02cd707e45..8c73c2b9f8 100644 --- a/lib/rules/focus-order-semantics.json +++ b/lib/rules/focus-order-semantics.json @@ -1,5 +1,6 @@ { "id": "focus-order-semantics", + "impact": "minor", "selector": "div, h1, h2, h3, h4, h5, h6, [role=heading], p, span", "matches": "inserted-into-focus-order-matches", "tags": ["cat.keyboard", "best-practice", "experimental"], diff --git a/lib/rules/form-field-multiple-labels.json b/lib/rules/form-field-multiple-labels.json index 3d4d95a1f7..24a6d38829 100644 --- a/lib/rules/form-field-multiple-labels.json +++ b/lib/rules/form-field-multiple-labels.json @@ -1,8 +1,17 @@ { "id": "form-field-multiple-labels", + "impact": "moderate", "selector": "input, select, textarea", "matches": "label-matches", - "tags": ["cat.forms", "wcag2a", "wcag332", "TTv5", "TT5.c"], + "tags": [ + "cat.forms", + "wcag2a", + "wcag332", + "TTv5", + "TT5.c", + "EN-301-549", + "EN-9.3.3.2" + ], "metadata": { "description": "Ensures form field does not have multiple label elements", "help": "Form field must not have multiple label elements" diff --git a/lib/rules/frame-focusable-content.json b/lib/rules/frame-focusable-content.json index 903a1361f1..b8f8b8538c 100644 --- a/lib/rules/frame-focusable-content.json +++ b/lib/rules/frame-focusable-content.json @@ -1,8 +1,17 @@ { "id": "frame-focusable-content", + "impact": "serious", "selector": "html", "matches": "frame-focusable-content-matches", - "tags": ["cat.keyboard", "wcag2a", "wcag211", "TTv5", "TT4.a"], + "tags": [ + "cat.keyboard", + "wcag2a", + "wcag211", + "TTv5", + "TT4.a", + "EN-301-549", + "EN-9.2.1.1" + ], "actIds": ["akn7bn"], "metadata": { "description": "Ensures <frame> and <iframe> elements with focusable content do not have tabindex=-1", diff --git a/lib/rules/frame-tested.json b/lib/rules/frame-tested.json index e718a1bc21..b8037ecdab 100644 --- a/lib/rules/frame-tested.json +++ b/lib/rules/frame-tested.json @@ -1,7 +1,8 @@ { "id": "frame-tested", + "impact": "critical", "selector": "html, frame, iframe", - "tags": ["cat.structure", "review-item", "best-practice"], + "tags": ["cat.structure", "best-practice", "review-item"], "metadata": { "description": "Ensures <iframe> and <frame> elements contain the axe-core script", "help": "Frames should be tested with axe-core" diff --git a/lib/rules/frame-title-unique.json b/lib/rules/frame-title-unique.json index fe0f73d0da..5a7e321861 100644 --- a/lib/rules/frame-title-unique.json +++ b/lib/rules/frame-title-unique.json @@ -1,8 +1,17 @@ { "id": "frame-title-unique", + "impact": "serious", "selector": "frame[title], iframe[title]", "matches": "frame-title-has-text-matches", - "tags": ["cat.text-alternatives", "wcag412", "wcag2a", "TTv5", "TT12.d"], + "tags": [ + "cat.text-alternatives", + "wcag2a", + "wcag412", + "TTv5", + "TT12.d", + "EN-301-549", + "EN-9.4.1.2" + ], "actIds": ["4b1c6c"], "metadata": { "description": "Ensures <iframe> and <frame> elements contain a unique title attribute", diff --git a/lib/rules/frame-title.json b/lib/rules/frame-title.json index 6751a35ecf..e6046a33a1 100644 --- a/lib/rules/frame-title.json +++ b/lib/rules/frame-title.json @@ -1,5 +1,6 @@ { "id": "frame-title", + "impact": "serious", "selector": "frame, iframe", "matches": "no-negative-tabindex-matches", "tags": [ @@ -9,7 +10,9 @@ "section508", "section508.22.i", "TTv5", - "TT12.d" + "TT12.d", + "EN-301-549", + "EN-9.4.1.2" ], "actIds": ["cae760"], "metadata": { diff --git a/lib/rules/heading-order.json b/lib/rules/heading-order.json index 8ecf8ec5f8..843a291ec2 100644 --- a/lib/rules/heading-order.json +++ b/lib/rules/heading-order.json @@ -1,5 +1,6 @@ { "id": "heading-order", + "impact": "moderate", "selector": "h1, h2, h3, h4, h5, h6, [role=heading]", "matches": "heading-matches", "tags": ["cat.semantics", "best-practice"], diff --git a/lib/rules/hidden-content.json b/lib/rules/hidden-content.json index b4728a2234..05323aca13 100644 --- a/lib/rules/hidden-content.json +++ b/lib/rules/hidden-content.json @@ -1,8 +1,9 @@ { "id": "hidden-content", + "impact": "minor", "selector": "*", "excludeHidden": false, - "tags": ["cat.structure", "experimental", "review-item", "best-practice"], + "tags": ["cat.structure", "best-practice", "experimental", "review-item"], "metadata": { "description": "Informs users about hidden content.", "help": "Hidden content on the page should be analyzed" diff --git a/lib/rules/html-has-lang.json b/lib/rules/html-has-lang.json index b27ea7a787..933d96e4e0 100644 --- a/lib/rules/html-has-lang.json +++ b/lib/rules/html-has-lang.json @@ -1,8 +1,18 @@ { "id": "html-has-lang", + "impact": "serious", "selector": "html", "matches": "is-initiator-matches", - "tags": ["cat.language", "wcag2a", "wcag311", "ACT", "TTv5", "TT11.a"], + "tags": [ + "cat.language", + "wcag2a", + "wcag311", + "TTv5", + "TT11.a", + "EN-301-549", + "EN-9.3.1.1", + "ACT" + ], "actIds": ["b5c3f8"], "metadata": { "description": "Ensures every HTML document has a lang attribute", diff --git a/lib/rules/html-lang-valid.json b/lib/rules/html-lang-valid.json index b761f400f0..34c65cbfab 100644 --- a/lib/rules/html-lang-valid.json +++ b/lib/rules/html-lang-valid.json @@ -1,7 +1,17 @@ { "id": "html-lang-valid", + "impact": "serious", "selector": "html[lang]:not([lang=\"\"]), html[xml\\:lang]:not([xml\\:lang=\"\"])", - "tags": ["cat.language", "wcag2a", "wcag311", "ACT", "TTv5", "TT11.a"], + "tags": [ + "cat.language", + "wcag2a", + "wcag311", + "TTv5", + "TT11.a", + "EN-301-549", + "EN-9.3.1.1", + "ACT" + ], "actIds": ["bf051a"], "metadata": { "description": "Ensures the lang attribute of the <html> element has a valid value", diff --git a/lib/rules/html-xml-lang-mismatch.json b/lib/rules/html-xml-lang-mismatch.json index 43c4d6cdd7..8a5ebc4f75 100644 --- a/lib/rules/html-xml-lang-mismatch.json +++ b/lib/rules/html-xml-lang-mismatch.json @@ -1,8 +1,16 @@ { "id": "html-xml-lang-mismatch", + "impact": "moderate", "selector": "html[lang][xml\\:lang]", "matches": "xml-lang-mismatch-matches", - "tags": ["cat.language", "wcag2a", "wcag311", "ACT"], + "tags": [ + "cat.language", + "wcag2a", + "wcag311", + "EN-301-549", + "EN-9.3.1.1", + "ACT" + ], "actIds": ["5b7ae0"], "metadata": { "description": "Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page", diff --git a/lib/rules/identical-links-same-purpose.json b/lib/rules/identical-links-same-purpose.json index d655277fce..6f8b580e06 100644 --- a/lib/rules/identical-links-same-purpose.json +++ b/lib/rules/identical-links-same-purpose.json @@ -1,5 +1,6 @@ { "id": "identical-links-same-purpose", + "impact": "minor", "selector": "a[href], area[href], [role=\"link\"]", "excludeHidden": false, "enabled": false, diff --git a/lib/rules/image-alt.json b/lib/rules/image-alt.json index 84408c6103..1fe2262273 100644 --- a/lib/rules/image-alt.json +++ b/lib/rules/image-alt.json @@ -1,5 +1,6 @@ { "id": "image-alt", + "impact": "critical", "selector": "img", "matches": "no-explicit-name-required-matches", "tags": [ @@ -8,10 +9,12 @@ "wcag111", "section508", "section508.22.a", - "ACT", "TTv5", "TT7.a", - "TT7.b" + "TT7.b", + "EN-301-549", + "EN-9.1.1.1", + "ACT" ], "actIds": ["23a2a8"], "metadata": { diff --git a/lib/rules/img-redundant-alt.json b/lib/rules/img-redundant-alt.json index 1e7192b08a..a84660a516 100644 --- a/lib/rules/img-redundant-alt.json +++ b/lib/rules/img-redundant-alt.json @@ -1,5 +1,6 @@ { "id": "image-redundant-alt", + "impact": "minor", "selector": "img", "tags": ["cat.text-alternatives", "best-practice"], "metadata": { diff --git a/lib/rules/input-button-name.json b/lib/rules/input-button-name.json index c7fee8c4de..60d116bc8f 100644 --- a/lib/rules/input-button-name.json +++ b/lib/rules/input-button-name.json @@ -1,5 +1,6 @@ { "id": "input-button-name", + "impact": "critical", "selector": "input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]", "matches": "no-explicit-name-required-matches", "tags": [ @@ -8,9 +9,11 @@ "wcag412", "section508", "section508.22.a", - "ACT", "TTv5", - "TT5.c" + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT" ], "actIds": ["97a4e1"], "metadata": { diff --git a/lib/rules/input-image-alt.json b/lib/rules/input-image-alt.json index 7525bb4532..4e6f4ebca3 100644 --- a/lib/rules/input-image-alt.json +++ b/lib/rules/input-image-alt.json @@ -1,5 +1,6 @@ { "id": "input-image-alt", + "impact": "critical", "selector": "input[type=\"image\"]", "matches": "no-explicit-name-required-matches", "tags": [ @@ -9,9 +10,12 @@ "wcag412", "section508", "section508.22.a", - "ACT", "TTv5", - "TT7.a" + "TT7.a", + "EN-301-549", + "EN-9.1.1.1", + "EN-9.4.1.2", + "ACT" ], "actIds": ["59796f"], "metadata": { diff --git a/lib/rules/label-content-name-mismatch.json b/lib/rules/label-content-name-mismatch.json index bf754f59c6..c14e30f1f5 100644 --- a/lib/rules/label-content-name-mismatch.json +++ b/lib/rules/label-content-name-mismatch.json @@ -1,7 +1,15 @@ { "id": "label-content-name-mismatch", + "impact": "serious", "matches": "label-content-name-mismatch-matches", - "tags": ["cat.semantics", "wcag21a", "wcag253", "experimental"], + "tags": [ + "cat.semantics", + "wcag21a", + "wcag253", + "EN-301-549", + "EN-9.2.5.3", + "experimental" + ], "actIds": ["2ee8b8"], "metadata": { "description": "Ensures that elements labelled through their content must have their visible text as part of their accessible name", diff --git a/lib/rules/label-title-only.json b/lib/rules/label-title-only.json index 79813f2183..8f6190b128 100644 --- a/lib/rules/label-title-only.json +++ b/lib/rules/label-title-only.json @@ -1,5 +1,6 @@ { "id": "label-title-only", + "impact": "serious", "selector": "input, select, textarea", "matches": "label-matches", "tags": ["cat.forms", "best-practice"], diff --git a/lib/rules/label.json b/lib/rules/label.json index 0ecd18324a..e781f23116 100644 --- a/lib/rules/label.json +++ b/lib/rules/label.json @@ -1,5 +1,6 @@ { "id": "label", + "impact": "critical", "selector": "input, textarea", "matches": "label-matches", "tags": [ @@ -8,9 +9,11 @@ "wcag412", "section508", "section508.22.n", - "ACT", "TTv5", - "TT5.c" + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT" ], "actIds": ["e086e5"], "metadata": { @@ -27,5 +30,5 @@ "non-empty-placeholder", "presentational-role" ], - "none": ["help-same-as-label", "hidden-explicit-label"] + "none": ["hidden-explicit-label"] } diff --git a/lib/rules/landmark-banner-is-top-level.json b/lib/rules/landmark-banner-is-top-level.json index 01408253e6..8eeff69e21 100644 --- a/lib/rules/landmark-banner-is-top-level.json +++ b/lib/rules/landmark-banner-is-top-level.json @@ -1,5 +1,6 @@ { "id": "landmark-banner-is-top-level", + "impact": "moderate", "selector": "header:not([role]), [role=banner]", "matches": "landmark-has-body-context-matches", "tags": ["cat.semantics", "best-practice"], diff --git a/lib/rules/landmark-complementary-is-top-level.json b/lib/rules/landmark-complementary-is-top-level.json index a9ab778875..950b5c9f6b 100644 --- a/lib/rules/landmark-complementary-is-top-level.json +++ b/lib/rules/landmark-complementary-is-top-level.json @@ -1,5 +1,6 @@ { "id": "landmark-complementary-is-top-level", + "impact": "moderate", "selector": "aside:not([role]), [role=complementary]", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-contentinfo-is-top-level.json b/lib/rules/landmark-contentinfo-is-top-level.json index 51b5319272..489ef333cc 100644 --- a/lib/rules/landmark-contentinfo-is-top-level.json +++ b/lib/rules/landmark-contentinfo-is-top-level.json @@ -1,5 +1,6 @@ { "id": "landmark-contentinfo-is-top-level", + "impact": "moderate", "selector": "footer:not([role]), [role=contentinfo]", "matches": "landmark-has-body-context-matches", "tags": ["cat.semantics", "best-practice"], diff --git a/lib/rules/landmark-main-is-top-level.json b/lib/rules/landmark-main-is-top-level.json index 4ed904907d..3b510ca34a 100644 --- a/lib/rules/landmark-main-is-top-level.json +++ b/lib/rules/landmark-main-is-top-level.json @@ -1,5 +1,6 @@ { "id": "landmark-main-is-top-level", + "impact": "moderate", "selector": "main:not([role]), [role=main]", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-no-duplicate-banner.json b/lib/rules/landmark-no-duplicate-banner.json index 9746ae1931..ee26e4a1ab 100644 --- a/lib/rules/landmark-no-duplicate-banner.json +++ b/lib/rules/landmark-no-duplicate-banner.json @@ -1,5 +1,6 @@ { "id": "landmark-no-duplicate-banner", + "impact": "moderate", "selector": "header:not([role]), [role=banner]", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-no-duplicate-contentinfo.json b/lib/rules/landmark-no-duplicate-contentinfo.json index 9006a47971..ddf4e96cbf 100644 --- a/lib/rules/landmark-no-duplicate-contentinfo.json +++ b/lib/rules/landmark-no-duplicate-contentinfo.json @@ -1,5 +1,6 @@ { "id": "landmark-no-duplicate-contentinfo", + "impact": "moderate", "selector": "footer:not([role]), [role=contentinfo]", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-no-duplicate-main.json b/lib/rules/landmark-no-duplicate-main.json index f09d5d432d..3f4bbc870e 100644 --- a/lib/rules/landmark-no-duplicate-main.json +++ b/lib/rules/landmark-no-duplicate-main.json @@ -1,5 +1,6 @@ { "id": "landmark-no-duplicate-main", + "impact": "moderate", "selector": "main:not([role]), [role=main]", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-one-main.json b/lib/rules/landmark-one-main.json index 5e7330dc27..97a03588fa 100644 --- a/lib/rules/landmark-one-main.json +++ b/lib/rules/landmark-one-main.json @@ -1,5 +1,6 @@ { "id": "landmark-one-main", + "impact": "moderate", "selector": "html", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/landmark-unique-matches.js b/lib/rules/landmark-unique-matches.js index 1f7963b479..b1384e719a 100644 --- a/lib/rules/landmark-unique-matches.js +++ b/lib/rules/landmark-unique-matches.js @@ -4,48 +4,45 @@ import { getRole } from '../commons/aria'; import { getAriaRolesByType } from '../commons/standards'; import { accessibleTextVirtual } from '../commons/text'; -function landmarkUniqueMatches(node, virtualNode) { - /* - * Since this is a best-practice rule, we are filtering elements as dictated by ARIA 1.1 Practices regardless of treatment by browser/AT combinations. - * - * Info: https://www.w3.org/TR/wai-aria-practices-1.1/#aria_landmark - */ - var excludedParentsForHeaderFooterLandmarks = [ - 'article', - 'aside', - 'main', - 'nav', - 'section' - ].join(','); - function isHeaderFooterLandmark(headerFooterElement) { - return !closest( - headerFooterElement, - excludedParentsForHeaderFooterLandmarks - ); - } +/* + * Since this is a best-practice rule, we are filtering elements as dictated by ARIA 1.1 Practices regardless of treatment by browser/AT combinations. + * + * Info: https://www.w3.org/TR/wai-aria-practices-1.1/#aria_landmark + */ +const excludedParentsForHeaderFooterLandmarks = [ + 'article', + 'aside', + 'main', + 'nav', + 'section' +].join(','); - function isLandmarkVirtual(virtualNode) { - var { actualNode } = virtualNode; - var landmarkRoles = getAriaRolesByType('landmark'); - var role = getRole(actualNode); - if (!role) { - return false; - } +export default function landmarkUniqueMatches(node, virtualNode) { + return ( + isLandmarkVirtual(virtualNode) && isVisibleToScreenReaders(virtualNode) + ); +} - var nodeName = actualNode.nodeName.toUpperCase(); - if (nodeName === 'HEADER' || nodeName === 'FOOTER') { - return isHeaderFooterLandmark(virtualNode); - } +function isLandmarkVirtual(vNode) { + const landmarkRoles = getAriaRolesByType('landmark'); + const role = getRole(vNode); + if (!role) { + return false; + } - if (nodeName === 'SECTION' || nodeName === 'FORM') { - var accessibleText = accessibleTextVirtual(virtualNode); - return !!accessibleText; - } + const { nodeName } = vNode.props; + if (nodeName === 'header' || nodeName === 'footer') { + return isHeaderFooterLandmark(vNode); + } - return landmarkRoles.indexOf(role) >= 0 || role === 'region'; + if (nodeName === 'section' || nodeName === 'form') { + const accessibleText = accessibleTextVirtual(vNode); + return !!accessibleText; } - return isLandmarkVirtual(virtualNode) && isVisibleToScreenReaders(node); + return landmarkRoles.indexOf(role) >= 0 || role === 'region'; } -export default landmarkUniqueMatches; +function isHeaderFooterLandmark(headerFooterElement) { + return !closest(headerFooterElement, excludedParentsForHeaderFooterLandmarks); +} diff --git a/lib/rules/landmark-unique.json b/lib/rules/landmark-unique.json index 88ada5150a..dff8639ad5 100644 --- a/lib/rules/landmark-unique.json +++ b/lib/rules/landmark-unique.json @@ -1,5 +1,6 @@ { "id": "landmark-unique", + "impact": "moderate", "selector": "[role=banner], [role=complementary], [role=contentinfo], [role=main], [role=navigation], [role=region], [role=search], [role=form], form, footer, header, aside, main, nav, section", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/link-in-text-block.json b/lib/rules/link-in-text-block.json index c047fcaa4e..86818cf85b 100644 --- a/lib/rules/link-in-text-block.json +++ b/lib/rules/link-in-text-block.json @@ -1,9 +1,18 @@ { "id": "link-in-text-block", + "impact": "serious", "selector": "a[href], [role=link]", "matches": "link-in-text-block-matches", "excludeHidden": false, - "tags": ["cat.color", "wcag2a", "wcag141", "TTv5", "TT13.a"], + "tags": [ + "cat.color", + "wcag2a", + "wcag141", + "TTv5", + "TT13.a", + "EN-301-549", + "EN-9.1.4.1" + ], "metadata": { "description": "Ensure links are distinguished from surrounding text in a way that does not rely on color", "help": "Links must be distinguishable without relying on color" diff --git a/lib/rules/link-name.json b/lib/rules/link-name.json index 5b8a8a3d42..4a1c371cde 100644 --- a/lib/rules/link-name.json +++ b/lib/rules/link-name.json @@ -1,16 +1,20 @@ { "id": "link-name", + "impact": "serious", "selector": "a[href]", "tags": [ "cat.name-role-value", "wcag2a", - "wcag412", "wcag244", + "wcag412", "section508", "section508.22.a", - "ACT", "TTv5", - "TT6.a" + "TT6.a", + "EN-301-549", + "EN-9.2.4.4", + "EN-9.4.1.2", + "ACT" ], "actIds": ["c487ae"], "metadata": { diff --git a/lib/rules/list.json b/lib/rules/list.json index 872c9530a5..07e3205f00 100644 --- a/lib/rules/list.json +++ b/lib/rules/list.json @@ -1,8 +1,9 @@ { "id": "list", + "impact": "serious", "selector": "ul, ol", "matches": "no-role-matches", - "tags": ["cat.structure", "wcag2a", "wcag131"], + "tags": ["cat.structure", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "metadata": { "description": "Ensures that lists are structured correctly", "help": "<ul> and <ol> must only directly contain <li>, <script> or <template> elements" diff --git a/lib/rules/listitem.json b/lib/rules/listitem.json index 8f186010b2..a7f8600c2b 100644 --- a/lib/rules/listitem.json +++ b/lib/rules/listitem.json @@ -1,8 +1,9 @@ { "id": "listitem", + "impact": "serious", "selector": "li", "matches": "no-role-matches", - "tags": ["cat.structure", "wcag2a", "wcag131"], + "tags": ["cat.structure", "wcag2a", "wcag131", "EN-301-549", "EN-9.1.3.1"], "metadata": { "description": "Ensures <li> elements are used semantically", "help": "<li> elements must be contained in a <ul> or <ol>" diff --git a/lib/rules/marquee.json b/lib/rules/marquee.json index 995719cc71..fe46a86aa2 100644 --- a/lib/rules/marquee.json +++ b/lib/rules/marquee.json @@ -1,8 +1,17 @@ { "id": "marquee", + "impact": "serious", "selector": "marquee", "excludeHidden": false, - "tags": ["cat.parsing", "wcag2a", "wcag222", "TTv5", "TT2.b"], + "tags": [ + "cat.parsing", + "wcag2a", + "wcag222", + "TTv5", + "TT2.b", + "EN-301-549", + "EN-9.2.2.2" + ], "metadata": { "description": "Ensures <marquee> elements are not used", "help": "<marquee> elements are deprecated and must not be used" diff --git a/lib/rules/meta-refresh-no-exceptions.json b/lib/rules/meta-refresh-no-exceptions.json index 1d6c0c4d6e..2837e0397f 100644 --- a/lib/rules/meta-refresh-no-exceptions.json +++ b/lib/rules/meta-refresh-no-exceptions.json @@ -1,5 +1,6 @@ { "id": "meta-refresh-no-exceptions", + "impact": "minor", "selector": "meta[http-equiv=\"refresh\"][content]", "excludeHidden": false, "enabled": false, diff --git a/lib/rules/meta-refresh.json b/lib/rules/meta-refresh.json index 7514642140..ab27dca06f 100644 --- a/lib/rules/meta-refresh.json +++ b/lib/rules/meta-refresh.json @@ -1,8 +1,17 @@ { "id": "meta-refresh", + "impact": "critical", "selector": "meta[http-equiv=\"refresh\"][content]", "excludeHidden": false, - "tags": ["cat.time-and-media", "wcag2a", "wcag221", "TTv5", "TT8.a"], + "tags": [ + "cat.time-and-media", + "wcag2a", + "wcag221", + "TTv5", + "TT8.a", + "EN-301-549", + "EN-9.2.2.1" + ], "actIds": ["bc659a", "bisz58"], "metadata": { "description": "Ensures <meta http-equiv=\"refresh\"> is not used for delayed refresh", diff --git a/lib/rules/meta-viewport-large.json b/lib/rules/meta-viewport-large.json index fe42730ef8..ee7b8f8668 100644 --- a/lib/rules/meta-viewport-large.json +++ b/lib/rules/meta-viewport-large.json @@ -1,5 +1,6 @@ { "id": "meta-viewport-large", + "impact": "minor", "selector": "meta[name=\"viewport\"]", "matches": "is-initiator-matches", "excludeHidden": false, diff --git a/lib/rules/meta-viewport.json b/lib/rules/meta-viewport.json index 88c187634e..083662fa24 100644 --- a/lib/rules/meta-viewport.json +++ b/lib/rules/meta-viewport.json @@ -1,9 +1,17 @@ { "id": "meta-viewport", + "impact": "critical", "selector": "meta[name=\"viewport\"]", "matches": "is-initiator-matches", "excludeHidden": false, - "tags": ["cat.sensory-and-visual-cues", "wcag2aa", "wcag144", "ACT"], + "tags": [ + "cat.sensory-and-visual-cues", + "wcag2aa", + "wcag144", + "EN-301-549", + "EN-9.1.4.4", + "ACT" + ], "actIds": ["b4f0c3"], "metadata": { "description": "Ensures <meta name=\"viewport\"> does not disable text scaling and zooming", diff --git a/lib/rules/nested-interactive.json b/lib/rules/nested-interactive.json index ab8867b33b..89f8c588e4 100644 --- a/lib/rules/nested-interactive.json +++ b/lib/rules/nested-interactive.json @@ -1,7 +1,16 @@ { "id": "nested-interactive", + "impact": "serious", "matches": "nested-interactive-matches", - "tags": ["cat.keyboard", "wcag2a", "wcag412", "TTv5", "TT6.a"], + "tags": [ + "cat.keyboard", + "wcag2a", + "wcag412", + "TTv5", + "TT6.a", + "EN-301-549", + "EN-9.4.1.2" + ], "actIds": ["307n5z"], "metadata": { "description": "Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies", diff --git a/lib/rules/no-autoplay-audio.json b/lib/rules/no-autoplay-audio.json index 25522e1be2..7f2f37b308 100644 --- a/lib/rules/no-autoplay-audio.json +++ b/lib/rules/no-autoplay-audio.json @@ -1,10 +1,20 @@ { "id": "no-autoplay-audio", + "impact": "moderate", "excludeHidden": false, "selector": "audio[autoplay], video[autoplay]", "matches": "no-autoplay-audio-matches", "reviewOnFail": true, - "tags": ["cat.time-and-media", "wcag2a", "wcag142", "ACT", "TTv5", "TT2.a"], + "tags": [ + "cat.time-and-media", + "wcag2a", + "wcag142", + "TTv5", + "TT2.a", + "EN-301-549", + "EN-9.1.4.2", + "ACT" + ], "actIds": ["80f0bf"], "metadata": { "description": "Ensures <video> or <audio> elements do not autoplay audio for more than 3 seconds without a control mechanism to stop or mute the audio", diff --git a/lib/rules/object-alt.json b/lib/rules/object-alt.json index 191cd41c52..687f16db69 100644 --- a/lib/rules/object-alt.json +++ b/lib/rules/object-alt.json @@ -1,5 +1,6 @@ { "id": "object-alt", + "impact": "serious", "selector": "object[data]", "matches": "object-is-loaded-matches", "tags": [ @@ -7,7 +8,9 @@ "wcag2a", "wcag111", "section508", - "section508.22.a" + "section508.22.a", + "EN-301-549", + "EN-9.1.1.1" ], "actIds": ["8fc3b6"], "metadata": { diff --git a/lib/rules/p-as-heading.json b/lib/rules/p-as-heading.json index 0cc747a472..68e6e195b5 100644 --- a/lib/rules/p-as-heading.json +++ b/lib/rules/p-as-heading.json @@ -1,8 +1,16 @@ { "id": "p-as-heading", + "impact": "serious", "selector": "p", "matches": "p-as-heading-matches", - "tags": ["cat.semantics", "wcag2a", "wcag131", "experimental"], + "tags": [ + "cat.semantics", + "wcag2a", + "wcag131", + "EN-301-549", + "EN-9.1.3.1", + "experimental" + ], "metadata": { "description": "Ensure bold, italic text and font-size is not used to style <p> elements as a heading", "help": "Styled <p> elements must not be used as headings" diff --git a/lib/rules/page-has-heading-one.json b/lib/rules/page-has-heading-one.json index 42b183326a..7cc81f75b1 100644 --- a/lib/rules/page-has-heading-one.json +++ b/lib/rules/page-has-heading-one.json @@ -1,5 +1,6 @@ { "id": "page-has-heading-one", + "impact": "moderate", "selector": "html", "tags": ["cat.semantics", "best-practice"], "metadata": { diff --git a/lib/rules/presentation-role-conflict.json b/lib/rules/presentation-role-conflict.json index 93491d8142..48af7c2ceb 100644 --- a/lib/rules/presentation-role-conflict.json +++ b/lib/rules/presentation-role-conflict.json @@ -1,5 +1,6 @@ { "id": "presentation-role-conflict", + "impact": "minor", "selector": "img[alt=''], [role=\"none\"], [role=\"presentation\"]", "matches": "has-implicit-chromium-role-matches", "tags": ["cat.aria", "best-practice", "ACT"], diff --git a/lib/rules/region.json b/lib/rules/region.json index 6187df928f..2ab0b837dd 100644 --- a/lib/rules/region.json +++ b/lib/rules/region.json @@ -1,5 +1,6 @@ { "id": "region", + "impact": "moderate", "selector": "body *", "tags": ["cat.keyboard", "best-practice"], "metadata": { diff --git a/lib/rules/role-img-alt.json b/lib/rules/role-img-alt.json index 2e55b1c631..8f3f8f9b85 100644 --- a/lib/rules/role-img-alt.json +++ b/lib/rules/role-img-alt.json @@ -1,5 +1,6 @@ { "id": "role-img-alt", + "impact": "serious", "selector": "[role='img']:not(img, area, input, object)", "matches": "html-namespace-matches", "tags": [ @@ -8,14 +9,16 @@ "wcag111", "section508", "section508.22.a", - "ACT", "TTv5", - "TT7.a" + "TT7.a", + "EN-301-549", + "EN-9.1.1.1", + "ACT" ], "actIds": ["23a2a8"], "metadata": { - "description": "Ensures [role='img'] elements have alternate text", - "help": "[role='img'] elements must have an alternative text" + "description": "Ensures [role=\"img\"] elements have alternate text", + "help": "[role=\"img\"] elements must have an alternative text" }, "all": [], "any": ["aria-label", "aria-labelledby", "non-empty-title"], diff --git a/lib/rules/scope-attr-valid.json b/lib/rules/scope-attr-valid.json index d846e44369..096a983e4b 100644 --- a/lib/rules/scope-attr-valid.json +++ b/lib/rules/scope-attr-valid.json @@ -1,5 +1,6 @@ { "id": "scope-attr-valid", + "impact": "moderate", "selector": "td[scope], th[scope]", "tags": ["cat.tables", "best-practice"], "metadata": { diff --git a/lib/rules/scrollable-region-focusable.json b/lib/rules/scrollable-region-focusable.json index 952c997590..eb7073ce73 100644 --- a/lib/rules/scrollable-region-focusable.json +++ b/lib/rules/scrollable-region-focusable.json @@ -1,8 +1,17 @@ { "id": "scrollable-region-focusable", + "impact": "serious", "selector": "*:not(select,textarea)", "matches": "scrollable-region-focusable-matches", - "tags": ["cat.keyboard", "wcag2a", "wcag211", "TTv5", "TT4.a"], + "tags": [ + "cat.keyboard", + "wcag2a", + "wcag211", + "TTv5", + "TT4.a", + "EN-301-549", + "EN-9.2.1.1" + ], "actIds": ["0ssw9k"], "metadata": { "description": "Ensure elements that have scrollable content are accessible by keyboard", diff --git a/lib/rules/select-name.json b/lib/rules/select-name.json index 38d0657eeb..3a77da8e3e 100644 --- a/lib/rules/select-name.json +++ b/lib/rules/select-name.json @@ -1,5 +1,6 @@ { "id": "select-name", + "impact": "critical", "selector": "select", "tags": [ "cat.forms", @@ -7,9 +8,11 @@ "wcag412", "section508", "section508.22.n", - "ACT", "TTv5", - "TT5.c" + "TT5.c", + "EN-301-549", + "EN-9.4.1.2", + "ACT" ], "actIds": ["e086e5"], "metadata": { @@ -25,5 +28,5 @@ "non-empty-title", "presentational-role" ], - "none": ["help-same-as-label", "hidden-explicit-label"] + "none": ["hidden-explicit-label"] } diff --git a/lib/rules/server-side-image-map.json b/lib/rules/server-side-image-map.json index 4f94e9d61f..88456fb738 100644 --- a/lib/rules/server-side-image-map.json +++ b/lib/rules/server-side-image-map.json @@ -1,5 +1,6 @@ { "id": "server-side-image-map", + "impact": "minor", "selector": "img[ismap]", "tags": [ "cat.text-alternatives", @@ -8,7 +9,9 @@ "section508", "section508.22.f", "TTv5", - "TT4.a" + "TT4.a", + "EN-301-549", + "EN-9.2.1.1" ], "metadata": { "description": "Ensures that server-side image maps are not used", diff --git a/lib/rules/skip-link.json b/lib/rules/skip-link.json index b2401ae1ab..12686f08b1 100644 --- a/lib/rules/skip-link.json +++ b/lib/rules/skip-link.json @@ -1,5 +1,6 @@ { "id": "skip-link", + "impact": "moderate", "selector": "a[href^=\"#\"], a[href^=\"/#\"]", "matches": "skip-link-matches", "tags": ["cat.keyboard", "best-practice"], diff --git a/lib/rules/svg-img-alt.json b/lib/rules/svg-img-alt.json index 0920dfb584..fbbe9b4d03 100644 --- a/lib/rules/svg-img-alt.json +++ b/lib/rules/svg-img-alt.json @@ -1,5 +1,6 @@ { "id": "svg-img-alt", + "impact": "serious", "selector": "[role=\"img\"], [role=\"graphics-symbol\"], svg[role=\"graphics-document\"]", "matches": "svg-namespace-matches", "tags": [ @@ -8,9 +9,11 @@ "wcag111", "section508", "section508.22.a", - "ACT", "TTv5", - "TT7.a" + "TT7.a", + "EN-301-549", + "EN-9.1.1.1", + "ACT" ], "actIds": ["7d6734"], "metadata": { diff --git a/lib/rules/tabindex.json b/lib/rules/tabindex.json index a1663c9c88..c479c863b0 100644 --- a/lib/rules/tabindex.json +++ b/lib/rules/tabindex.json @@ -1,5 +1,6 @@ { "id": "tabindex", + "impact": "serious", "selector": "[tabindex]", "tags": ["cat.keyboard", "best-practice"], "metadata": { diff --git a/lib/rules/table-duplicate-name.json b/lib/rules/table-duplicate-name.json index 3bb9a48c55..da3a076c00 100644 --- a/lib/rules/table-duplicate-name.json +++ b/lib/rules/table-duplicate-name.json @@ -1,5 +1,6 @@ { "id": "table-duplicate-name", + "impact": "minor", "selector": "table", "tags": ["cat.tables", "best-practice"], "metadata": { diff --git a/lib/rules/table-fake-caption.json b/lib/rules/table-fake-caption.json index 646cc1937c..4561ebc506 100644 --- a/lib/rules/table-fake-caption.json +++ b/lib/rules/table-fake-caption.json @@ -1,5 +1,6 @@ { "id": "table-fake-caption", + "impact": "serious", "selector": "table", "matches": "data-table-matches", "tags": [ @@ -8,7 +9,9 @@ "wcag2a", "wcag131", "section508", - "section508.22.g" + "section508.22.g", + "EN-301-549", + "EN-9.1.3.1" ], "metadata": { "description": "Ensure that tables with a caption use the <caption> element.", diff --git a/lib/rules/target-size.json b/lib/rules/target-size.json index 7891718522..7a03bd0309 100644 --- a/lib/rules/target-size.json +++ b/lib/rules/target-size.json @@ -1,9 +1,10 @@ { "id": "target-size", + "impact": "serious", "selector": "*", "enabled": false, "matches": "widget-not-inline-matches", - "tags": ["wcag22aa", "wcag258", "cat.sensory-and-visual-cues"], + "tags": ["cat.sensory-and-visual-cues", "wcag22aa", "wcag258"], "metadata": { "description": "Ensure touch target have sufficient size and space", "help": "All touch targets must be 24px large, or leave sufficient space" diff --git a/lib/rules/td-has-header.json b/lib/rules/td-has-header.json index ac7a232dc9..1e1880f272 100644 --- a/lib/rules/td-has-header.json +++ b/lib/rules/td-has-header.json @@ -1,5 +1,6 @@ { "id": "td-has-header", + "impact": "critical", "selector": "table", "matches": "data-table-large-matches", "tags": [ @@ -10,7 +11,9 @@ "section508", "section508.22.g", "TTv5", - "TT14.b" + "TT14.b", + "EN-301-549", + "EN-9.1.3.1" ], "metadata": { "description": "Ensure that each non-empty data cell in a <table> larger than 3 by 3 has one or more table headers", diff --git a/lib/rules/td-headers-attr.json b/lib/rules/td-headers-attr.json index 084844db90..b729c6acd0 100644 --- a/lib/rules/td-headers-attr.json +++ b/lib/rules/td-headers-attr.json @@ -1,5 +1,6 @@ { "id": "td-headers-attr", + "impact": "serious", "selector": "table", "matches": "table-or-grid-role-matches", "tags": [ @@ -9,7 +10,9 @@ "section508", "section508.22.g", "TTv5", - "TT14.b" + "TT14.b", + "EN-301-549", + "EN-9.1.3.1" ], "actIds": ["a25f45"], "metadata": { diff --git a/lib/rules/th-has-data-cells.json b/lib/rules/th-has-data-cells.json index 023efc871c..90bee0a2d8 100644 --- a/lib/rules/th-has-data-cells.json +++ b/lib/rules/th-has-data-cells.json @@ -1,5 +1,6 @@ { "id": "th-has-data-cells", + "impact": "serious", "selector": "table", "matches": "data-table-matches", "tags": [ @@ -9,7 +10,9 @@ "section508", "section508.22.g", "TTv5", - "14.b" + "TT14.b", + "EN-301-549", + "EN-9.1.3.1" ], "actIds": ["d0f69e"], "metadata": { diff --git a/lib/rules/valid-lang.json b/lib/rules/valid-lang.json index a6e50b80b6..688fb8bd60 100644 --- a/lib/rules/valid-lang.json +++ b/lib/rules/valid-lang.json @@ -1,7 +1,17 @@ { "id": "valid-lang", + "impact": "serious", "selector": "[lang]:not(html), [xml\\:lang]:not(html)", - "tags": ["cat.language", "wcag2aa", "wcag312", "ACT", "TTv5", "TT11.b"], + "tags": [ + "cat.language", + "wcag2aa", + "wcag312", + "TTv5", + "TT11.b", + "EN-301-549", + "EN-9.3.1.2", + "ACT" + ], "actIds": ["de46e4"], "metadata": { "description": "Ensures lang attributes have valid values", diff --git a/lib/rules/video-caption.json b/lib/rules/video-caption.json index 27cea4010c..f2ff1b7210 100644 --- a/lib/rules/video-caption.json +++ b/lib/rules/video-caption.json @@ -1,5 +1,6 @@ { "id": "video-caption", + "impact": "critical", "selector": "video", "tags": [ "cat.text-alternatives", @@ -8,7 +9,9 @@ "section508", "section508.22.a", "TTv5", - "TT17.a" + "TT17.a", + "EN-301-549", + "EN-9.1.2.2" ], "actIds": ["eac66b"], "metadata": { diff --git a/lib/standards/aria-attrs.js b/lib/standards/aria-attrs.js index 1143ca1fc7..d52b346363 100644 --- a/lib/standards/aria-attrs.js +++ b/lib/standards/aria-attrs.js @@ -14,10 +14,12 @@ const ariaAttrs = { }, 'aria-braillelabel': { type: 'string', + allowEmpty: true, global: true }, 'aria-brailleroledescription': { type: 'string', + allowEmpty: true, global: true }, 'aria-busy': { @@ -214,7 +216,8 @@ const ariaAttrs = { type: 'decimal' }, 'aria-valuetext': { - type: 'string' + type: 'string', + allowEmpty: true } }; diff --git a/lib/standards/aria-roles.js b/lib/standards/aria-roles.js index 7a39a1486d..340fdb9b14 100644 --- a/lib/standards/aria-roles.js +++ b/lib/standards/aria-roles.js @@ -18,11 +18,13 @@ const ariaRoles = { alert: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, alertdialog: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded', 'aria-modal'], superclassRole: ['alert', 'dialog'], accessibleNameRequired: true @@ -38,11 +40,13 @@ const ariaRoles = { }, article: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-posinset', 'aria-setsize', 'aria-expanded'], superclassRole: ['document'] }, banner: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -67,6 +71,7 @@ const ariaRoles = { cell: { type: 'structure', requiredContext: ['row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-colindex', 'aria-colspan', @@ -82,7 +87,7 @@ const ariaRoles = { // Note: aria-required is not in the 1.1 spec but is // consistently supported in ATs and was added in 1.2 requiredAttrs: ['aria-checked'], - allowedAttrs: ['aria-readonly', 'aria-required'], + allowedAttrs: ['aria-readonly', 'aria-expanded', 'aria-required'], superclassRole: ['input'], accessibleNameRequired: true, nameFromContent: true, @@ -96,6 +101,7 @@ const ariaRoles = { columnheader: { type: 'structure', requiredContext: ['row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-sort', 'aria-colindex', @@ -132,6 +138,7 @@ const ariaRoles = { }, complementary: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -141,6 +148,7 @@ const ariaRoles = { }, contentinfo: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -151,6 +159,7 @@ const ariaRoles = { }, definition: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, @@ -161,6 +170,7 @@ const ariaRoles = { }, dialog: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded', 'aria-modal'], superclassRole: ['window'], accessibleNameRequired: true @@ -168,6 +178,7 @@ const ariaRoles = { directory: { type: 'structure', deprecated: true, + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['list'], // Note: spec difference @@ -175,6 +186,7 @@ const ariaRoles = { }, document: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['structure'] }, @@ -186,11 +198,13 @@ const ariaRoles = { feed: { type: 'structure', requiredOwned: ['article'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['list'] }, figure: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], // Note: spec difference @@ -198,12 +212,14 @@ const ariaRoles = { }, form: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, grid: { type: 'composite', requiredOwned: ['rowgroup', 'row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-level', 'aria-multiselectable', @@ -235,12 +251,14 @@ const ariaRoles = { }, group: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-activedescendant', 'aria-expanded'], superclassRole: ['section'] }, heading: { type: 'structure', requiredAttrs: ['aria-level'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['sectionhead'], // Note: spec difference @@ -249,6 +267,7 @@ const ariaRoles = { }, img: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], accessibleNameRequired: true, @@ -277,6 +296,7 @@ const ariaRoles = { list: { type: 'structure', requiredOwned: ['listitem'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, @@ -309,21 +329,25 @@ const ariaRoles = { }, log: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, main: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, marquee: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, math: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], childrenPresentational: true @@ -339,6 +363,7 @@ const ariaRoles = { 'menu', 'separator' ], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-activedescendant', 'aria-expanded', @@ -357,6 +382,7 @@ const ariaRoles = { 'menu', 'separator' ], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-activedescendant', 'aria-expanded', @@ -419,6 +445,7 @@ const ariaRoles = { }, navigation: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -429,6 +456,7 @@ const ariaRoles = { }, note: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, @@ -461,6 +489,7 @@ const ariaRoles = { }, progressbar: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-expanded', 'aria-valuemax', @@ -486,6 +515,7 @@ const ariaRoles = { radiogroup: { type: 'composite', // Note: spec difference (no required owned) + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-readonly', 'aria-required', @@ -503,6 +533,7 @@ const ariaRoles = { }, region: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'], // Note: spec difference @@ -539,6 +570,7 @@ const ariaRoles = { rowheader: { type: 'structure', requiredContext: ['row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-sort', 'aria-colindex', @@ -577,6 +609,7 @@ const ariaRoles = { }, search: { type: 'landmark', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['landmark'] }, @@ -664,6 +697,7 @@ const ariaRoles = { }, status: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'] }, @@ -689,7 +723,7 @@ const ariaRoles = { switch: { type: 'widget', requiredAttrs: ['aria-checked'], - allowedAttrs: ['aria-readonly', 'aria-required'], + allowedAttrs: ['aria-expanded', 'aria-readonly', 'aria-required'], superclassRole: ['checkbox'], accessibleNameRequired: true, nameFromContent: true, @@ -704,6 +738,7 @@ const ariaRoles = { tab: { type: 'widget', requiredContext: ['tablist'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-posinset', 'aria-selected', @@ -717,6 +752,7 @@ const ariaRoles = { table: { type: 'structure', requiredOwned: ['rowgroup', 'row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-colcount', 'aria-rowcount', 'aria-expanded'], // NOTE: although the spec says this is not named from contents, // the accessible text acceptance tests (#139 and #140) require @@ -743,6 +779,7 @@ const ariaRoles = { }, tabpanel: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], // Note: spec difference @@ -750,6 +787,7 @@ const ariaRoles = { }, term: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], // Note: spec difference @@ -779,11 +817,13 @@ const ariaRoles = { }, timer: { type: 'widget', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['status'] }, toolbar: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-orientation', 'aria-activedescendant', @@ -794,6 +834,7 @@ const ariaRoles = { }, tooltip: { type: 'structure', + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: ['aria-expanded'], superclassRole: ['section'], nameFromContent: true @@ -801,6 +842,7 @@ const ariaRoles = { tree: { type: 'composite', requiredOwned: ['group', 'treeitem'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-multiselectable', 'aria-required', @@ -815,6 +857,7 @@ const ariaRoles = { treegrid: { type: 'composite', requiredOwned: ['rowgroup', 'row'], + // Spec difference: Aria-expanded removed in 1.2 allowedAttrs: [ 'aria-activedescendant', 'aria-colcount', diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 7e99be92a9..ce773e111c 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -85,6 +85,7 @@ const htmlElms = { 'doc-dedication', 'doc-example', 'doc-footnote', + 'doc-glossary', 'doc-pullquote', 'doc-tip' ] @@ -358,6 +359,7 @@ const htmlElms = { 'menuitem', 'menuitemcheckbox', 'menuitemradio', + 'meter', 'option', 'progressbar', 'radio', @@ -747,6 +749,10 @@ const htmlElms = { allowedRoles: false, noAriaAttrs: true }, + search: { + contentTypes: ['flow'], + allowedRoles: ['form', 'group', 'none', 'presentation', 'region', 'search'] + }, section: { contentTypes: ['sectioning', 'flow'], allowedRoles: [ diff --git a/locales/_template.json b/locales/_template.json index 3cb970b002..925bb3a0dc 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -10,24 +10,36 @@ "help": "Active <area> elements must have alternate text" }, "aria-allowed-attr": { - "description": "Ensures ARIA attributes are allowed for an element's role", - "help": "Elements must only use allowed ARIA attributes" + "description": "Ensures an element's role supports its ARIA attributes", + "help": "Elements must only use supported ARIA attributes" }, "aria-allowed-role": { "description": "Ensures role attribute has an appropriate value for the element", "help": "ARIA role should be appropriate for the element" }, + "aria-braille-equivalent": { + "description": "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent", + "help": "aria-braille attributes must have a non-braille equivalent" + }, "aria-command-name": { "description": "Ensures every ARIA button, link and menuitem has an accessible name", "help": "ARIA commands must have an accessible name" }, + "aria-conditional-attr": { + "description": "Ensures ARIA attributes are used as described in the specification of the element's role", + "help": "ARIA attributes must be used as specified for the element's role" + }, + "aria-deprecated-role": { + "description": "Ensures elements do not use deprecated roles", + "help": "Deprecated ARIA roles must not be used" + }, "aria-dialog-name": { "description": "Ensures every ARIA dialog and alertdialog node has an accessible name", "help": "ARIA dialog and alertdialog nodes should have an accessible name" }, "aria-hidden-body": { - "description": "Ensures aria-hidden='true' is not present on the document body.", - "help": "aria-hidden='true' must not be present on the document body" + "description": "Ensures aria-hidden=\"true\" is not present on the document body.", + "help": "aria-hidden=\"true\" must not be present on the document body" }, "aria-hidden-focus": { "description": "Ensures aria-hidden elements are not focusable nor contain focusable elements", @@ -45,6 +57,10 @@ "description": "Ensures every ARIA progressbar node has an accessible name", "help": "ARIA progressbar nodes must have an accessible name" }, + "aria-prohibited-attr": { + "description": "Ensures ARIA attributes are not prohibited for an element's role", + "help": "Elements must only use permitted ARIA attributes" + }, "aria-required-attr": { "description": "Ensures elements with ARIA roles have all required ARIA attributes", "help": "Required ARIA attributes must be provided" @@ -66,7 +82,7 @@ "help": "ARIA roles used must conform to valid values" }, "aria-text": { - "description": "Ensures \"role=text\" is used on elements with no focusable descendants", + "description": "Ensures role=\"text\" is used on elements with no focusable descendants", "help": "\"role=text\" should have no focusable descendants" }, "aria-toggle-field-name": { @@ -334,8 +350,8 @@ "help": "All page content should be contained by landmarks" }, "role-img-alt": { - "description": "Ensures [role='img'] elements have alternate text", - "help": "[role='img'] elements must have an alternative text" + "description": "Ensures [role=\"img\"] elements have alternate text", + "help": "[role=\"img\"] elements must have an alternative text" }, "scope-attr-valid": { "description": "Ensures the scope attribute is used correctly on tables", @@ -529,6 +545,18 @@ "plural": "Invalid ARIA attribute names: ${data.values}" } }, + "braille-label-equivalent": { + "pass": "aria-braillelabel is used on an element with accessible text", + "fail": "aria-braillelabel is used on an element with no accessible text", + "incomplete": "Unable to compute accessible text" + }, + "braille-roledescription-equivalent": { + "pass": "aria-brailleroledescription is used on an element with aria-roledescription", + "fail": { + "noRoleDescription": "aria-brailleroledescription is used on an element with no aria-roledescription", + "emptyRoleDescription": "aria-brailleroledescription is used on an element with an empty aria-roledescription" + } + }, "deprecatedrole": { "pass": "ARIA role is not deprecated", "fail": "The role used is deprecated: ${data}" @@ -611,6 +639,7 @@ "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", + "complexTextShadows": "Element's contrast could not be determined because it uses complex text shadows", "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", @@ -681,7 +710,7 @@ "focusable-not-tabbable": { "pass": "No focusable elements contained within element", "incomplete": "Check if the focusable elements immediately move the focus indicator", - "fail": "Focusable content should have tabindex='-1' or be removed from the DOM" + "fail": "Focusable content should have tabindex=\"-1\" or be removed from the DOM" }, "frame-focusable-content": { "pass": "Element does not have focusable descendants", @@ -696,7 +725,7 @@ "pass": "Element does not have 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')" + "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" }, @@ -833,11 +862,11 @@ "fail": "${data} on <meta> tag disables zooming on mobile devices" }, "target-offset": { - "pass": "Target has sufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", - "fail": "Target has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", + "pass": "Target has sufficient space from its closest neighbors (${data.closestOffset}px should be at least ${data.minOffset}px)", + "fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px)", "incomplete": { - "default": "Element with negative tabindex has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px). Is this a target?", - "nonTabbableNeighbor": "Target has insufficient offset from a neighbor with negative tabindex (${data.closestOffset}px should be at least ${data.minOffset}px). Is the neighbor a target?" + "default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is this a target?", + "nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is the neighbor a target?" } }, "target-size": { diff --git a/locales/fr.json b/locales/fr.json index c20c0d4d9e..2cd83e97c4 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -113,14 +113,14 @@ "description": "Vérifier que chaque page dispose au minimum d’un mécanisme de contournement de la navigation pour accéder directement au contenu", "help": "Chaque page doit fournir des moyens de contourner les contenus répétés" }, - "color-contrast": { - "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" }, + "color-contrast": { + "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" + }, "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" @@ -289,9 +289,13 @@ "description": "Vérifier que l’élément <marquee> n’est pas utilisé", "help": "L’élément <marquee> est déprécié et ne doit pas être utilisé" }, + "meta-refresh-no-exceptions": { + "description": "Vérifier que <meta http-equiv=\"refresh\"> n’est pas utilisé pour une actualisation différée", + "help": "L'actualisation différée ne doit pas être utilisée" + }, "meta-refresh": { - "description": "Vérifier que <meta http-equiv=\"refresh\"> n’est pas utilisé", - "help": "La page HTML ne doit pas être actualisée automatiquement" + "description": "Vérifier que <meta http-equiv=\"refresh\"> n’est pas utilisé pour une actualisation différée", + "help": "L'actualisation différée en dessous de 20 heures ne doit pas être utilisée" }, "meta-viewport-large": { "description": "Vérifier que <meta name=\"viewport\"> permet un agrandissement significatif", @@ -369,6 +373,10 @@ "description": "Vérifier que les tableaux avec une légende utilisent l’élément <caption>", "help": "Les données ou les cellules d’entête ne devraient pas être utilisées pour légender un tableau de données" }, + "target-size": { + "description": "Vérifier que la cible tactile a une taille et un espace suffisants", + "help": "Toutes les cibles tactiles doivent faire 24px de large, ou être suffisamment grandes" + }, "td-has-header": { "description": "Vérifier que chaque cellule de données non vide dans un tableau de données a une ou plusieurs cellules d’entête", "help": "Chaque élément td non vide dans un tableau plus grand que 3 × 3 doit avoir une cellule d’entête associée" diff --git a/locales/ja.json b/locales/ja.json index 3efa3adccb..8ee94527f8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -2,8 +2,8 @@ "lang": "ja", "rules": { "accesskeys": { - "description": "すべてのaccesskey属性値が一意であることを確認します", - "help": "accesskey属性値は一意でなければなりません" + "description": "すべてのaccesskey属性の値が一意であることを確認します", + "help": "accesskey属性の値は一意でなければなりません" }, "area-alt": { "description": "イメージマップの<area>要素に代替テキストが存在することを確認します", @@ -17,25 +17,37 @@ "description": "role属性の値が要素に対して適切であることを確認します", "help": "ARIAロールは要素に対して適切でなければなりません" }, + "aria-braille-equivalent": { + "description": "aria-braillelabelとaria-brailleroledescriptionには、点字以外の同等のものが存在することを確認します", + "help": "aria-braille属性には、点じ以外の同等のものがなければなりません " + }, "aria-command-name": { "description": "すべてのARIA button、link、menuitemにアクセシブルな名前があることを確認します", "help": "ARIAコマンドにはアクセシブルな名前がなければなりません" }, + "aria-conditional-attr": { + "description": "ARIA属性が要素のロールの仕様に従って使用されていることを確認します", + "help": "ARIA属性は要素のロールの仕様に従って使用しなければなりません" + }, + "aria-deprecated-role": { + "description": "要素に非推奨のロールが使用されていないことを確認します", + "help": "非推奨のARIAロールを使用してはなりません" + }, "aria-dialog-name": { "description": "すべてのARIA dialog、alertdialogノードにアクセシブルな名前があることを確認します", "help": "ARIA dialogとalertdialogノードにはアクセシブルな名前がなければなりません" }, "aria-hidden-body": { - "description": "ドキュメント本体にaria-hidden='true'が存在していないことを確認します", + "description": "ドキュメント本体にaria-hidden='true'が存在しないことを確認します", "help": "ドキュメント本体にaria-hidden='true'が存在してはなりません" }, "aria-hidden-focus": { - "description": "aria-hidden要素にフォーカス可能な要素が含まれていないことを確認します", - "help": "ARIA hidden要素にフォーカス可能な要素を含んではなりません" + "description": "aria-hiddenが指定されている要素にフォーカスできないこと、その要素にフォーカス可能な要素が含まれていないことを確認します", + "help": "aria-hiddenが指定されている要素は、フォーカス可能であったり、フォーカス可能な要素を含んでいたりしてはなりません" }, "aria-input-field-name": { "description": "すべてのARIA入力欄にアクセシブルな名前があることを確認します", - "help": "ARIA入力欄にアクセシブルな名前があります" + "help": "ARIA入力欄にはアクセシブルな名前がなければなりません" }, "aria-meter-name": { "description": "すべてのARIA meterノードにアクセシブルな名前があることを確認します", @@ -45,6 +57,10 @@ "description": "すべてのARIA progressbarノードにアクセシブルな名前があることを確認します", "help": "ARIA progressbarノードにはアクセシブルな名前がなければなりません" }, + "aria-prohibited-attr": { + "description": "要素のロールでARIA属性が禁止されていないことを確認します", + "help": "要素には禁止されているARIA属性を使用してはなりません" + }, "aria-required-attr": { "description": "ARIAロールのある要素にすべての必須ARIA属性が存在することを確認します", "help": "必須のARIA属性が提供されていなければなりません" @@ -59,10 +75,10 @@ }, "aria-roledescription": { "description": "aria-roledescriptionが暗黙的もしくは明示的なロールを持った要素に使用されていることを確認します", - "help": "aria-roledescriptionはセマンティックなロールを持った要素に使用します" + "help": "aria-roledescriptionはセマンティックなロールを持った要素に使用しなければなりません" }, "aria-roles": { - "description": "すべてのロール属性が指定された要素で、有効な値が使用されていることを確認します", + "description": "すべてのrole属性が指定された要素で、有効な値が使用されていることを確認します", "help": "使用されているARIAロールは有効な値に一致しなければなりません" }, "aria-text": { @@ -71,14 +87,14 @@ }, "aria-toggle-field-name": { "description": "すべてのARIAトグル欄にアクセシブルな名前があることを確認します", - "help": "ARIAトグル欄にアクセシブルな名前があります" + "help": "ARIAトグル欄にはアクセシブルな名前がなければなりません" }, "aria-tooltip-name": { - "description": "すべてのARIA tooltipノードにはアクセシブルな名前があることを確認します", + "description": "すべてのARIA tooltipノードにアクセシブルな名前があることを確認します", "help": "ARIA tooltipノードにはアクセシブルな名前がなければなりません" }, "aria-treeitem-name": { - "description": "すべてのARIA treeitemノードにはアクセシブルな名前があることを確認します", + "description": "すべてのARIA treeitemノードにアクセシブルな名前があることを確認します", "help": "ARIA treeitemノードにはアクセシブルな名前がなければなりません" }, "aria-valid-attr-value": { @@ -103,7 +119,7 @@ }, "blink": { "description": "<blink>要素が使用されていないことを確認します", - "help": "<blink>要素は廃止されており、使用するべきではありません" + "help": "<blink>要素の使用は非推奨で、使用するべきではありません" }, "button-name": { "description": "ボタンに認識可能なテキストが存在することを確認します", @@ -114,20 +130,20 @@ "help": "ページには繰り返されるブロックをスキップする手段が存在しなければなりません" }, "color-contrast-enhanced": { - "description": "前景色と背景色のコントラストがWCAG 2のAAAコントラスト比のしきい値を満たすことを確認します", - "help": "要素には十分な色のコントラストがなければなりません" + "description": "前景色と背景色のコントラストがWCAG 2のAAAコントラスト比(高度)のしきい値を満たすことを確認します", + "help": "要素は色のコントラスト比(高度)の閾値を満たしていなければなりません" }, "color-contrast": { - "description": "前景色と背景色のコントラストがWCAG 2のAAコントラスト比のしきい値を満たすことを確認します", - "help": "要素には十分な色のコントラストがなければなりません" + "description": "前景色と背景色のコントラストがWCAG 2のAAコントラスト比(最低限)のしきい値を満たすことを確認します", + "help": "要素は色のコントラスト比(最低限)の閾値を満たしていなければなりません" }, "css-orientation-lock": { "description": "コンテンツが特定のディスプレイの向きに固定されていないこと、およびコンテンツがすべてのディスプレイの向きで操作可能なことを確認します", - "help": "ディスプレイの向きを固定するためにCSSメディアクエリーは使用されていません" + "help": "CSSメディアクエリーはディスプレイの向きを固定するために使用してはなりません" }, "definition-list": { "description": "<dl>要素の構造が正しいことを確認します", - "help": "<dl>要素は、適切な順序で並べられた<dt>および<dd>グループ、<script>要素または<template>要素のみを直接含んでいなければなりません" + "help": "<dl>要素は、適切な順序で並べられた<dt>および<dd>のグループ、<script>要素、<template>要素またはdiv要素のみを直接含んでいなければなりません" }, "dlitem": { "description": "<dt>および<dd>要素が<dl>に含まれていることを確認します", @@ -138,16 +154,16 @@ "help": "ドキュメントにはナビゲーションを補助するために<title>要素がなければなりません" }, "duplicate-id-active": { - "description": "活性要素のid属性値が一意であることを確認します", - "help": "活性要素のIDは一意でなければなりません" + "description": "アクティブな要素のid属性の値が一意であることを確認します", + "help": "アクティブな要素のIDは一意でなければなりません" }, "duplicate-id-aria": { - "description": "ARIAおよびラベルに使用されているすべてのid属性値が一意であることを確認します", + "description": "ARIAおよびラベルに使用されているすべてのid属性の値が一意であることを確認します", "help": "ARIAおよびラベルに使用されているIDは一意でなければなりません" }, "duplicate-id": { - "description": "すべてのid属性値が一意であることを確認します", - "help": "id属性値は一意でなければなりません" + "description": "すべてのid属性の値が一意であることを確認します", + "help": "id属性の値は一意でなければなりません" }, "empty-heading": { "description": "見出しに認識可能なテキストが存在することを確認します", @@ -158,28 +174,28 @@ "help": "テーブルのヘッダーは空にしてはなりません" }, "focus-order-semantics": { - "description": "フォーカス順序に含まれる要素に適切なロールがあることを確認します", - "help": "フォーカス順序に含まれる要素には、インタラクティブコンテンツに適したロールが必要です" + "description": "フォーカス順序に含まれる要素にインタラクティブコンテンツに適したロールがあることを確認します", + "help": "フォーカス順序に含まれる要素には、適切なロールがなければなりません" }, "form-field-multiple-labels": { "description": "フォームフィールドに複数のlabel要素が存在しないことを確認します", - "help": "複数のlabel要素をフォームフィールドに付与するべきではありません" + "help": "フォームフィールドに複数のlabel要素を付与してはなりりません" }, "frame-focusable-content": { - "description": "tabindex=-1が指定されている<frame>と<iframe>要素が、フォーカス可能なコンテンツを含まないことを確認します", - "help": "tabindex=-1が指定されているフレームには、フォーカス可能なコンテンツが含まれていてはなりません" + "description": "フォーカス可能な<frame>と<iframe>要素に、tabindex=-1が指定されていないことを確認します", + "help": "フォーカス可能なコンテンツを含むフレームには、tabindex=-1が指定されていてはなりません" }, "frame-tested": { "description": "<iframe>および<frame>要素にaxe-coreスクリプトが含まれていることを確認します", - "help": "フレームはaxe-coreでテストする必要があります" + "help": "フレームはaxe-coreでテストしなければなりません" }, "frame-title-unique": { "description": "<iframe>および<frame>要素に一意のtitle属性が含まれていることを確認します", "help": "フレームには一意のtitle属性がなければなりません" }, "frame-title": { - "description": "<iframe>および<frame>要素に空ではないtitle属性が存在することを確認します", - "help": "フレームにはtitle属性がなければなりません" + "description": "<iframe>および<frame>要素にアクセシブルな名前が存在することを確認します", + "help": "フレームにはアクセシブルな名前がなければなりません" }, "heading-order": { "description": "見出しの順序が意味的に正しいことを確認します", @@ -187,7 +203,7 @@ }, "hidden-content": { "description": "隠れているコンテンツについてユーザーに通知します", - "help": "ページ上の隠れているコンテンツは分析できません" + "help": "ページ上の隠れているコンテンツは分析されなければなりません" }, "html-has-lang": { "description": "すべてのHTMLドキュメントにlang属性が存在することを確認します", @@ -202,8 +218,8 @@ "help": "HTML要素に指定されたlangおよびxml:lang属性は同じ基本言語を持たなければなりません" }, "identical-links-same-purpose": { - "description": "同じアクセシブルな名前を持つ複数のリンクが同様の目的を果たすことを確認します", - "help": "同じ名前を持つ複数のリンクは同様の目的を持っています" + "description": "同じアクセシブルな名前を持つ複数のリンクが類似した目的を果たすことを確認します", + "help": "同じ名前を持つ複数のリンクは類似した目的を持っていなければなりません" }, "image-alt": { "description": "<img>要素に代替テキストが存在する、またはnoneまたはpresentationのロールが存在することを確認します", @@ -226,7 +242,7 @@ "help": "要素の視認できるテキストはそれらのアクセシブルな名前の一部でなければなりません" }, "label-title-only": { - "description": "すべてのフォーム要素がtitleまたはaria-describedby属性を使用して単独でラベル付けされていないことを確認します", + "description": "すべてのフォーム要素に視認できるラベルがあり、非表示のラベル、titleまたはaria-describedby属性のみを使用してラベル付けされていないことを確認します", "help": "フォーム要素には視認できるラベルがなければなりません" }, "label": { @@ -239,7 +255,7 @@ }, "landmark-complementary-is-top-level": { "description": "complementaryランドマークあるいはasideがトップレベルにあることを確認します", - "help": "他の要素にasideを含んではなりません" + "help": "asideは他の要素に含まれるべきではありません" }, "landmark-contentinfo-is-top-level": { "description": "contentinfoランドマークがトップレベルにあることを確認します", @@ -262,16 +278,16 @@ "help": "ドキュメントに複数のmainランドマークが存在してはなりません" }, "landmark-one-main": { - "description": "ドキュメントのランドマークが1つのみであること、およびページ内の各iframeのランドマークが最大で1つであることを確認します", + "description": "ドキュメントにmainランドマークが含まれていることを確認します", "help": "ドキュメントにはmainランドマークが1つ含まれていなければなりません" }, "landmark-unique": { "help": "ランドマークが一意であることを確認します", - "description": "ランドマークは一意のロール又はロール/ラベル/タイトル (例: アクセシブルな名前) の組み合わせがなければなりません" + "description": "ランドマークには一意のロール又はロール/ラベル/タイトル (すなわちアクセシブルな名前) の組み合わせがなければなりません" }, "link-in-text-block": { - "description": "色に依存することなくリンクを区別できます", - "help": "リンクは色に依存しない方法で周囲のテキストと区別できなければなりません" + "description": "リンクが色に依存しない形で周囲のテキストと区別できることを確認します", + "help": "リンクは色に依存しない形で区別できなければなりません" }, "link-name": { "description": "リンクに認識可能なテキストが存在することを確認します", @@ -290,60 +306,60 @@ "help": "<marquee>要素は非推奨のため、使用してはなりません" }, "meta-refresh-no-exceptions": { - "description": "<meta http-equiv=\"refresh\">が使用されていないことを確認します", - "help": "制限時間のある更新が存在してはなりません" + "description": "一定時間経過後のページの自動リロードのために<meta http-equiv=\"refresh\">が使用されていないことを確認します", + "help": "一定時間経過後のページの自動リロードを使用してはなりません" }, "meta-refresh": { - "description": "<meta http-equiv=\"refresh\">が使用されていないことを確認します", - "help": "制限時間のある更新が存在してはなりません" + "description": "一定時間経過後のページの自動リロードのために<meta http-equiv=\"refresh\">が使用されていないことを確認します", + "help": "20時間より短い時間経過後のページの自動リロードを使用してはなりません" }, "meta-viewport-large": { "description": "<meta name=\"viewport\">で大幅に拡大縮小できることを確認します", - "help": "ユーザーがズームをしてテキストを最大500%まで拡大できるようにするべきです" + "help": "ユーザーがズームをしてテキストを最大500%まで拡大できなければなりません" }, "meta-viewport": { - "description": "<meta name=\"viewport\">がテキストの拡大縮小およびズーミングを無効化しないことを確認します", - "help": "ズーミングや拡大縮小は無効にしてはなりません" + "description": "<meta name=\"viewport\">がテキストのサイズ変更やズームを無効化しないことを確認します", + "help": "ズーム機能やテキストのサイズ変更は無効にしてはなりません" }, "nested-interactive": { - "description": "ネストされた対話的なコントロールはスクリーン・リーダーで読み上げられません", - "help": "対話的なコントロールがネストされていないことを確認します" + "description": "スクリーン・リーダーで必ずしもよみあげられなかったり支援技術のフォーカスに関する問題を引き起こす可能性があったりするため、対話的なコントロールがネストされていないことを確認します", + "help": "対話的なコントロールはネストされていてはなりません" }, "no-autoplay-audio": { "description": "<video> または <audio> 要素が音声を停止またはミュートするコントロールなしに音声を3秒より長く自動再生しないことを確認します", - "help": "<video> または <audio> 要素は音声を自動再生しません" + "help": "<video> または <audio> 要素は音声を自動再生してはなりません" }, "object-alt": { "description": "<object>要素に代替テキストが存在することを確認します", "help": "<object>要素には代替テキストがなければなりません" }, "p-as-heading": { - "description": "見出しのスタイル調整のためにp要素が使用されていないことを確認します", - "help": "p要素を見出しとしてスタイル付けするために太字、イタリック体、およびフォントサイズを使用しません" + "description": "<p>要素を見出しとしてスタイル付けするために太字、イタリック体、およびフォントサイズが使用されていないことを確認します", + "help": "スタイル付けした<p>要素を見出しとして使用してはなりません" }, "page-has-heading-one": { - "description": "ページ、またはそのフレームの少なくとも1つにはレベル1の見出しが含まれていることを確認します", + "description": "ページ、またはそのページ中のフレームの少なくとも1つにはレベル1の見出しが含まれていることを確認します", "help": "ページにはレベル1の見出しが含まれていなければなりません" }, "presentation-role-conflict": { - "description": "roleがnoneまたはpresentationで、roleの競合の解決が必要な要素をマークします", - "help": "roleがnoneまたはpresentationの要素をマークしなければなりません" + "description": "すべてのスクリーン・リーダーに確実に無視させるために、プレゼンテーション目的とされている要素にはグローバルなARIAまたはtabindexが指定されていてはなりません", + "help": "プレゼンテーション目的とされている要素が一貫して無視されることを確認します" }, "region": { "description": "ページのすべてのコンテンツがlandmarkに含まれていることを確認します", "help": "ページのすべてのコンテンツはlandmarkに含まれていなければなりません" }, "role-img-alt": { - "description": "[role='img'] 要素に代替テキストが存在することを確認します", - "help": "[role='img'] 要素に代替テキストが必要です" + "description": "[role='img'] の要素に代替テキストが存在することを確認します", + "help": "[role='img'] の要素には代替テキストがなければなりません" }, "scope-attr-valid": { "description": "scope属性がテーブルで正しく使用されていることを確認します", "help": "scope属性は正しく使用されなければなりません" }, "scrollable-region-focusable": { - "description": "スクロール可能なコンテンツを持つ要素はキーボードでアクセスできるようにするべきです", - "help": "スクロール可能な領域にキーボードでアクセスできるようにします" + "description": "スクロール可能なコンテンツを持つ要素がキーボードでアクセスできることを確認します", + "help": "スクロール可能な領域はキーボードでアクセスできなければなりません" }, "select-name": { "description": "select要素にはアクセシブルな名前があることを確認します", @@ -358,36 +374,36 @@ "help": "スキップリンクのターゲットが存在し、フォーカス可能でなければなりません" }, "svg-img-alt": { - "description": "img、graphics-document または graphics-symbol ロールを持つ svg 要素にアクセシブルなテキストがあることを確認します", - "help": "img ロールを持つ svg 要素に代替テキストが存在します" + "description": "img、graphics-documentまたはgraphics-symbolロールを持つsvg要素にアクセシブルなテキストがあることを確認します", + "help": "imgロールを持つ<svg>要素には代替テキストが存在しなければなりません" }, "tabindex": { "description": "tabindex属性値が0より大きくないことを確認します", - "help": "要素に0より大きいtabindex属性を指定するべきではありません" + "help": "要素に指定するtabindexは0より大きい値であってはなりません" }, "table-duplicate-name": { - "description": "テーブルのサマリーとキャプションが同一ではないことを確認します", - "help": "<caption>要素にsummary属性と同じテキストを含んではなりません" + "description": "<caption>要素の内用がsummary属性のテキストと同一ではないことを確認します", + "help": "テーブルのキャプションとサマリーは同一であってはなりません" }, "table-fake-caption": { "description": "キャプション付きのテーブルが<caption>要素を用いていることを確認します", - "help": "データテーブルにキャプションをつけるためにデータまたはヘッダーセルを用いるべきではありません" + "help": "データテーブルにキャプションをつけるためにデータまたはヘッダーセルを用いてはなりません" }, "target-size": { "description": "タッチターゲットのサイズとスペースが十分にあることを確認します", - "help": "すべてのタッチターゲットは24pxの大きさか、十分なスペースが必要です" + "help": "すべてのタッチターゲットは24pxの大きさか、十分なスペースがなければなりません" }, "td-has-header": { - "description": "大きなテーブルの空ではないデータセルに1つかそれ以上のテーブルヘッダーが存在することを確認します", - "help": "3×3より大きいテーブルの空ではないtd要素はテーブルヘッダーと関連づいていなければなりません" + "description": "3×3より大きい<table>の空ではないデータセルにはそれぞれ1つ以上のテーブルヘッダーが存在することを確認します", + "help": "大きい<table>の空ではない<td>要素は対応するテーブルヘッダーと関連づけられていなければなりません" }, "td-headers-attr": { - "description": "ヘッダーを使用しているテーブル内の各セルが、そのテーブル内の他のセルを参照していることを確認します", - "help": "table要素内のheaders属性を使用するすべてのセルは同じ表内の他のセルのみを参照しなければなりません" + "description": "テーブルでheaders属性を使用している各セルの参照先が同じテーブル内の他のセルであることを確認します", + "help": "テーブルのheaders属性を使用するすべてのセルは同じ表内の他のセルのみを参照しなければなりません" }, "th-has-data-cells": { - "description": "データテーブルのテーブルヘッダーがデータセルを参照していることを確認します", - "help": "すべてのth要素およびrole=columnheader/rowheaderを持つ要素にはそれらが説明するデータセルがなければなりません" + "description": "すべての<th>要素およびrole=columnheader/rowheaderを持つ要素にはそれらが説明するデータセルがあることを確認します", + "help": "データテーブルのテーブルヘッダーはデータセルを参照していなければなりません" }, "valid-lang": { "description": "lang属性に有効な値が存在することを確認します", @@ -412,72 +428,80 @@ "singular": "ARIA属性は許可されていません: ${data.values}", "plural": "ARIA属性は許可されていません: ${data.values}" }, - "incomplete": "次の要素のARIA属性が無視されても問題ないことを確認します: ${data.values}" + "incomplete": "この要素のARIA属性が無視されても問題ないことを確認しましょう: ${data.values}" }, "aria-allowed-role": { "pass": "ARIAロールは指定された要素に対して許可されています", "fail": { - "singular": "ARIA ロール ${data.values} は指定された要素に許可されていません", - "plural": "ARIA ロール ${data.values} は指定された要素に許可されていません" + "singular": "ARIAロール${data.values}は指定された要素では許可されていません", + "plural": "ARIAロール${data.values}は指定された要素では許可されていません" }, "incomplete": { - "singular": "ARIA ロール ${data.values} この要素に許可されていないため、要素が表示されたときはARIAロールを削除する必要があります", - "plural": "ARIA ロール ${data.values} この要素に許可されていないため、要素が表示されたときはARIAロールを削除する必要があります" + "singular": "ARIAロール${data.values}はこの要素では許可されていないため、要素が表示されたときはARIAロールを削除する必要があります", + "plural": "ARIAロール${data.values}はこの要素では許可されていないため、要素が表示されたときはARIAロールを削除する必要があります" } }, "aria-busy": { - "pass": "要素にはaria-busy属性が存在しています", - "fail": "要素に aria-busy=\"true\" が設定されていません" + "pass": "要素にはaria-busy属性が存在します", + "fail": "ローダーを表示中に、要素aria-busy=\"true\"が指定されています" + }, + "aria-conditional-attr": { + "pass": "ARIA 属性が許可されています", + "fail": { + "checkbox": "aria-checkedを削除するか\"${data.checkState}\"を設定して、チェックボックスの実際の状態と合うようにしましょう", + "rowSingular": "treegridの行でサポートされている属性ですが、${data.ownerRole}: ${data.invalidAttrs}の場合はこの限りではありません", + "rowPlural": "treegridの行でサポートされている属性ですが、${data.ownerRole}: ${data.invalidAttrs}の場合はこの限りではありません" + } }, "aria-errormessage": { - "pass": "サポートされているaria-errormessageの技術を使用しています", + "pass": "aria-errormessageが存在して、サポートされているaria-errormessageの手法を用いているスクリーン・リーダーが認識できる要素を参照しています", "fail": { - "singular": "aria-errormessage の値 `${data.values}` はメッセージを通知する方法を使用しなければなりません (例えば、aria-live、aria-describedby、role=alert等)", - "plural": "aria-errormessage の値 `${data.values}` はメッセージを通知する方法を使用しなければなりません (例えば、aria-live、aria-describedby、role=alert等)", - "hidden": "aria-errormessage の値 `${data.values}` は隠れている要素を参照することはできません" + "singular": "aria-errormessageの値`${data.values}`はメッセージを通知する方法を使用しなければなりません (例えば、aria-live、aria-describedby、role=alert等)", + "plural": "aria-errormessageの値`${data.values}`はメッセージを通知する方法を使用しなければなりません (例えば、aria-live、aria-describedby、role=alert等)", + "hidden": "aria-errormessageの値 `${data.values}`は非表示の要素を参照することはできません" }, "incomplete": { - "singular": "aria-errormessageの値 `${data.values}` は存在する要素を参照していることを確認しましょう", - "plural": "aria-errormessageの値 `${data.values}` は存在する要素を参照していることを確認しましょう", - "idrefs": "aria-errormessage 要素がページ上に存在するかどうか判定できません: ${data.values}" + "singular": "aria-errormessageの値 `${data.values}`は存在する要素を参照していることを確認しましょう", + "plural": "aria-errormessageの値 `${data.values}`は存在する要素を参照していることを確認しましょう", + "idrefs": "aria-errormessage要素がページ上に存在するかどうか判定できません: ${data.values}" } }, "aria-hidden-body": { - "pass": "ドキュメント本体にaria-hidden属性は存在していません", + "pass": "ドキュメント本体にaria-hidden属性は存在しません", "fail": "aria-hidden=trueはドキュメント本体に存在してはなりません" }, "aria-level": { - "pass": "aria-level の値は有効です", - "incomplete": "6より大きい aria-level の値は、どのスクリーンリーダーとブラウザーの組み合わせでもサポートされていません" + "pass": "aria-levelの値は有効です", + "incomplete": "6より大きいaria-levelの値は、どのスクリーンリーダーとブラウザーの組み合わせでもサポートされていません" }, "aria-prohibited-attr": { "pass": "ARIA属性は使用できます", "fail": { - "hasRolePlural": "${data.prohibited} 属性は \"${data.role}\" ロールでは使用できません", - "hasRoleSingular": "${data.prohibited} 属性は \"${data.role}\" ロールでは使用できません", - "noRolePlural": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では使用できません", - "noRoleSingular": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では使用できません" + "hasRolePlural": "${data.prohibited}属性は\"${data.role}\"ロールでは使用できません", + "hasRoleSingular": "${data.prohibited}属性は\"${data.role}\"ロールでは使用できません", + "noRolePlural": "${data.prohibited}属性は、有効なrole属性のない${data.nodeName}では使用できません", + "noRoleSingular": "${data.prohibited}属性は、有効なrole属性のない${data.nodeName}では使用できません" }, "incomplete": { - "hasRoleSingular": "${data.prohibited} 属性はロール \"${data.role}\" では十分にサポートされていません", - "hasRolePlural": "${data.prohibited} 属性はロール \"${data.role}\" では十分にサポートされていません", - "noRoleSingular": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では十分にサポートされていません", - "noRolePlural": "${data.prohibited} 属性は、有効なロール属性のない ${data.nodeName} では十分にサポートされていません" + "hasRoleSingular": "${data.prohibited}属性はロール\"${data.role}\"では十分にサポートされていません", + "hasRolePlural": "${data.prohibited}属性はロール\"${data.role}\"では十分にサポートされていません", + "noRoleSingular": "${data.prohibited}属性は、有効なrole属性のない${data.nodeName}では十分にサポートされていません", + "noRolePlural": "${data.prohibited}属性は、有効なrole属性のない${data.nodeName}では十分にサポートされていません" } }, "aria-required-attr": { - "pass": "すべての必須のARIA属性が存在しています", + "pass": "すべての必須のARIA属性が存在します", "fail": { "singular": "必須のARIA属性が提供されていません: ${data.values}", "plural": "必須のARIA属性が提供されていません: ${data.values}" } }, "aria-required-children": { - "pass": "必須のARIA子ロールが存在しています", + "pass": "必須のARIA子ロールが存在します", "fail": { "singular": "必須のARIA子ロールが提供されていません: ${data.values}", "plural": "必須のARIA子ロールが提供されていません: ${data.values}", - "unallowed": "要素には許可されていないARIA子ロールがあります (関連ノードを参照)" + "unallowed": "要素には許可されていないARIA子ロールがあります: ${data.values}" }, "incomplete": { "singular": "ARIAの子ロールを追加することが求められます: ${data.values}", @@ -485,7 +509,7 @@ } }, "aria-required-parent": { - "pass": "必須のARIA親ロールが存在しています", + "pass": "必須のARIA親ロールが存在します", "fail": { "singular": "必須のARIA親ロールが提供されていません: ${data.values}", "plural": "必須のARIA親ロールが提供されていません: ${data.values}" @@ -509,7 +533,7 @@ "incomplete": { "noId": "ARIA属性で指定されている要素のIDがページ上に存在しません: ${data.needsReview}", "noIdShadow": "ARIA属性で指定されている要素のIDがページ上に存在しないか、別のshadow DOMツリーの小要素です: ${data.needsReview}", - "ariaCurrent": "ARIA属性値が無効であるため、\"aria-current=true\" として扱われます: ${data.needsReview}", + "ariaCurrent": "ARIA属性値が無効であるため、\"aria-current=true\"として扱われます: ${data.needsReview}", "idrefs": "ARIA属性で指定されている要素のIDがページ上に存在するかどうか判定できません: ${data.needsReview}", "empty": "ARIA属性値が空のときは無視されます: ${data.needsReview}" } @@ -521,25 +545,37 @@ "plural": "無効なARIA属性名です: ${data.values}" } }, + "braille-label-equivalent": { + "pass": "aria-braillelabelはアクセシブルなテキストがある要素に対して使用されています", + "fail": "aria-braillelabelがアクセシブルなテキストがない要素に対して使用されています", + "incomplete": "アクセシブルなテキストを算出することができません" + }, + "braille-roledescription-equivalent": { + "pass": "aria-brailleroledescriptionはaria-roledescriptionが指定されている要素に対して使用されています", + "fail": { + "noRoleDescription": "aria-brailleroledescriptionがaria-roledescriptionが指定されていない要素に対して使用されています", + "emptyRoleDescription": "aria-brailleroledescriptionが空のaria-roledescriptionが指定されている要素に対して使用されています" + } + }, "deprecatedrole": { - "pass": "推奨されていないARIAロールではありません", + "pass": "非推奨のARIAロールではありません", "fail": "非推奨のロールが使用されています: ${data}" }, "fallbackrole": { "pass": "1つのロール値のみ使用されています", "fail": "古いブラウザーではフォールバックロールがサポートされていないため、ロール値は1つのみ使用します", - "incomplete": "'presentation' と 'none' のロールは同義なので、どちらか一方のみを使用します。" + "incomplete": "'presentation'と'none'のロールは同義なので、どちらか一方のみを使用します。" }, "has-global-aria-attribute": { "pass": { - "singular": "要素にはARIAのグローバル属性が指定されています: ${data.values}", - "plural": "要素にはARIAのグローバル属性が指定されています: ${data.values}" + "singular": "要素にはグローバルなARIA属性が指定されています: ${data.values}", + "plural": "要素にはグローバルなARIA属性が指定されています: ${data.values}" }, - "fail": "要素にはARIAのグローバル属性が指定されていません" + "fail": "要素にはグローバルなARIA属性が指定されていません" }, "has-widget-role": { - "pass": "要素にはwidgetロールが存在しています", - "fail": "要素にはwidgetロールが存在していません" + "pass": "要素にはwidgetロールが存在します", + "fail": "要素にはwidgetロールが存在しません" }, "invalidrole": { "pass": "ARIAロールが有効です", @@ -554,7 +590,7 @@ }, "no-implicit-explicit-label": { "pass": "<label> とアクセシブルな名前の間に不一致はありません", - "incomplete": "<label> が ARIA ${data} 欄の名前の一部である必要がないことを確認しましょう" + "incomplete": "<label>がARIA ${data}欄の名前の一部である必要がないことを確認しましょう" }, "unsupportedrole": { "pass": "ARIAロールはサポートされています", @@ -584,7 +620,7 @@ "equalRatio": "要素のコントラスト比が背景と1:1です", "shortTextContent": "実際のテキストコンテンツであるかを判断するには要素のコンテンツが短すぎます", "nonBmp": "要素のコンテンツはテキストではない文字のみを含んでいます", - "pseudoContent": "擬似要素のため、要素の背景色を判定することができませんでした" + "pseudoContent": "擬似要素のため、要素の背景色を判定することができません" } }, "color-contrast": { @@ -603,6 +639,7 @@ "bgGradient": "背景グラデーションのため、要素の背景色を判定できません", "imgNode": "画像ノードが含まれるため、要素の背景色を判定できません", "bgOverlap": "他の要素と重なっているため、要素の背景色を判定できません", + "complexTextShadows": "複雑なtext-shadowが用いられているため、要素のコントラスト比を判定できません", "fgAlpha": "アルファ透明度により、要素の前景色を判定できません", "elmPartiallyObscured": "他の要素により部分的に不明瞭なため、要素の背景色を判定できません", "elmPartiallyObscuring": "他の要素と部分的に重なっているため、要素の背景色を判定できません", @@ -610,22 +647,26 @@ "equalRatio": "要素のコントラスト比が背景と1:1です", "shortTextContent": "実際のテキストコンテンツであるかを判断するには要素のコンテンツが短すぎます", "nonBmp": "要素のコンテンツはテキストではない文字のみを含んでいます", - "pseudoContent": "擬似要素のため、要素の背景色を判定することができませんでした" + "pseudoContent": "擬似要素のため、要素の背景色を判定することができません" } }, "link-in-text-block-style": { - "pass": "リンクは視覚的なスタイル設定によって周囲のテキストと区別できます", + "pass": "リンクは視覚的なスタイル付けによって周囲のテキストと区別できます", + "incomplete": { + "default": "リンクを近接したテキストと区別するために、スタイル付けが必要か確認しましょう。", + "pseudoContent": "リンクの擬似スタイルが、周囲のテキストと区別するのに充分か確認しましょう。" + }, "fail": "リンクには、周囲のテキストと区別するためのスタイル (下線など) がありません" }, "link-in-text-block": { "pass": "リンクは色以外の方法で周囲のテキストと区別できます", "fail": { - "fgContrast": "このリンクは、周囲のテキストとの${data.contrastRatio}:1の色のコントラストが不十分です。(最小コントラストは ${data.requiredContrastRatio}:1、リンクテキスト: ${data.nodeColor}、周囲のテキスト: ${data.parentColor})", - "bgContrast": "リンクの背景の色コントラストが ${data.contrastRatio} で十分ではありません (最小コントラストは ${data.requiredContrastRatio}:1、リンク背景色: ${data.nodeBackgroundColor}、周囲の背景色: ${data.parentBackgroundColor})" + "fgContrast": "このリンクは、周囲のテキストとの${data.contrastRatio}:1の色のコントラストが不十分です。(最小コントラストは${data.requiredContrastRatio}:1、リンクテキスト: ${data.nodeColor}、周囲のテキスト: ${data.parentColor})", + "bgContrast": "リンクの背景の色コントラストが${data.contrastRatio}で十分ではありません (最小コントラストは${data.requiredContrastRatio}:1、リンク背景色: ${data.nodeBackgroundColor}、周囲の背景色: ${data.parentBackgroundColor})" }, "incomplete": { - "default": "コントラスト比を判定できません", - "bgContrast": "要素のコントラスト比を判定できません。明確なホバー/フォーカススタイルを確認します", + "default": "要素の前景コントラスト比を判定できません", + "bgContrast": "要素の背景コントラスト比を判定できません", "bgImage": "背景画像のため、要素のコントラスト比を判定できません", "bgGradient": "背景グラデーションのため、要素のコントラスト比を判定できません", "imgNode": "画像ノードが含まれるため、要素のコントラスト比を判定できません", @@ -641,16 +682,16 @@ "fail": "autocomplete属性は誤ってフォーマットされています" }, "accesskeys": { - "pass": "アクセスキー属性値は一意です", - "fail": "ドキュメントに同じアクセスキーを持った複数の要素が存在しています" + "pass": "accesskey属性の値は一意です", + "fail": "ドキュメントに同じaccesskeyを持った複数の要素が存在します" }, "focusable-content": { "pass": "要素内にフォーカス可能な要素が含まれています", "fail": "要素にフォーカス可能なコンテンツが存在するべきです" }, "focusable-disabled": { - "pass": "要素内にフォーカス不可能な要素は含まれていません", - "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認します", + "pass": "要素内にフォーカス可能な要素は含まれていません", + "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認しましょう", "fail": "フォーカス可能なコンテンツは無効にするか、DOMから削除するべきです" }, "focusable-element": { @@ -658,18 +699,18 @@ "fail": "要素はフォーカス可能であるべきです" }, "focusable-modal-open": { - "pass": "モーダル開いている時、フォーカス可能な要素がありません", - "incomplete": "現在の状態で、フォーカス可能な要素がタブ移動可能になっていないことを確認します" + "pass": "モーダルが開いている時に、フォーカス可能な要素はありません", + "incomplete": "現在の状態で、フォーカス可能な要素にタブ移動可能になっていないことを確認しましょう" }, "focusable-no-name": { - "pass": "要素がタブ順序にない、またはアクセシブルなテキストが存在しています", - "fail": "要素がタブ順序にあり、アクセシブルなテキストが存在していません", + "pass": "要素がタブ順序にない、またはアクセシブルなテキストが存在します", + "fail": "要素がタブ順序にあり、アクセシブルなテキストが存在しません", "incomplete": "要素にアクセシブルな名前があるか判定できません" }, "focusable-not-tabbable": { "pass": "要素内にフォーカス不可能な要素は含まれていません", - "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認します", - "fail": "フォーカス可能なコンテンツは tabindex='-1' を指定するか、DOMから削除するべきです" + "incomplete": "フォーカス可能な要素がすぐにフォーカスインジケータを動かすかどうか確認しましょう", + "fail": "フォーカス可能なコンテンツはtabindex='-1'を指定するか、DOMから削除するべきです" }, "frame-focusable-content": { "pass": "要素の子孫にフォーカス可能なものはありません", @@ -677,14 +718,14 @@ "incomplete": "要素に子孫があるか判定できませんでした" }, "landmark-is-top-level": { - "pass": "${data.role} ランドマークはトップレベルにあります", - "fail": "${data.role} ランドマークが他のランドマークに含まれています" + "pass": "${data.role}ランドマークはトップレベルにあります", + "fail": "${data.role}ランドマークは他のランドマークに含まれています" }, "no-focusable-content": { "pass": "要素の子孫にフォーカス可能なものはありません", "fail": { "default": "要素の子孫にフォーカス可能なものがあります", - "notHidden": "インタラクティブなコントロールの内側の要素に負の tabindex を指定しても、('aria-hidden=true' が指定されている場合も)支援技術がその要素にフォーカスできないようにはなりません" + "notHidden": "インタラクティブなコントロールの内側の要素に負のtabindexを指定しても、('aria-hidden=true'が指定されている場合も)支援技術がその要素にフォーカスできないようにはなりません" }, "incomplete": "要素に子孫があるか判定できませんでした" }, @@ -693,7 +734,7 @@ "fail": "ページにはレベル1の見出しがなければなりません" }, "page-has-main": { - "pass": "ドキュメントに最低1つのmainランドマークがあります", + "pass": "ドキュメントには少なくとも1つのmainランドマークがあります", "fail": "ドキュメントにmainランドマークがありません" }, "page-no-duplicate-banner": { @@ -709,34 +750,34 @@ "fail": "ドキュメントに複数のmainランドマークがあります" }, "tabindex": { - "pass": "要素に0より大きいtabindexは存在していません", - "fail": "要素に0より大きいtabindexが存在しています" + "pass": "要素に0より大きいtabindexは存在しません", + "fail": "要素に0より大きいtabindexが存在します" }, "alt-space-value": { "pass": "要素に妥当なalt属性値があります", - "fail": "要素にスペース文字のみを含んだalt属性があり、これはすべてのスクリーン・リーダーに無視されません" + "fail": "要素にスペース文字のみを含んだalt属性があり、これは必ずしもすべてのスクリーン・リーダーに無視されません" }, "duplicate-img-label": { "pass": "要素の既存のテキストと<img>の代替テキストは重複していません", "fail": "要素が既存のテキストと重複した代替テキストの存在する<img>要素を含んでいます" }, "explicit-label": { - "pass": "フォームの要素に明確な<label>が存在しています", - "fail": "フォームの要素に明確な<label>が存在していません", - "incomplete": "フォームの要素に明確な <label> があるか判定できませんでした" + "pass": "フォームの要素に明示的な<label>が存在します", + "fail": "フォームの要素に明示的な<label>が存在しません", + "incomplete": "フォームの要素に明示的な <label> があるか判定できませんでした" }, "help-same-as-label": { - "pass": "ヘルプテキスト(titleまたはaria-describedby)はラベルテキストと重複していません", - "fail": "ヘルプテキスト(titleまたはaria-describedby)がラベルテキストと同じです" + "pass": "ヘルプテキスト(titleまたはaria-describedby)はラベルのテキストと重複していません", + "fail": "ヘルプテキスト(titleまたはaria-describedby)がラベルのテキストと同じです" }, "hidden-explicit-label": { - "pass": "フォームの要素に視認可能で明確な<label>があります", - "fail": "フォームの要素に非表示の明確な<label>があります", - "incomplete": "フォームの要素に明確な非表示の <label> があるか判定できませんでした" + "pass": "フォームの要素に視認可能で明示的な<label>があります", + "fail": "フォームの要素に非表示の明示的な<label>があります", + "incomplete": "フォームの要素に明示的で非表示の <label> があるか判定できませんでした" }, "implicit-label": { - "pass": "フォームの要素に暗黙の(包含された)<label>が存在しています", - "fail": "フォームの要素に暗黙の(包含された)<label>が存在していません", + "pass": "フォームの要素に暗黙の(包含された)<label>が存在します", + "fail": "フォームの要素に暗黙の(包含された)<label>が存在しません", "incomplete": "フォームの要素に暗黙の(包含された)<label>が存在するか判定できません" }, "label-content-name-mismatch": { @@ -745,21 +786,21 @@ }, "multiple-label": { "pass": "フォームフィールドにlabel要素は複数ありません", - "incomplete": "複数のlabel要素は支援技術に広くサポートされていません。最初のラベルがすべての必要な情報を含んでいることを確認します。" + "incomplete": "複数のlabel要素は支援技術に広くサポートされていません。最初のラベルがすべての必要な情報を含んでいることを確認しましょう。" }, "title-only": { "pass": "フォーム要素はラベルにtitle属性だけを用いていません", "fail": "フォーム要素のラベルにtitle属性だけを用いています" }, "landmark-is-unique": { - "pass": "ランドマークは一意のロール又はロール/ラベル/タイトル (例: アクセシブルな名前) の組み合わせがなければなりません", - "fail": "ランドマークを識別可能にするため、ランドマークには一意の aria-label、aria-labelledby、または title がなければなりません。" + "pass": "ランドマークには一意のロール又はロール/ラベル/タイトル (例: アクセシブルな名前) の組み合わせがなければなりません", + "fail": "ランドマークを識別可能にするため、ランドマークには一意のaria-label、aria-labelledby、またはtitleがなければなりません。" }, "has-lang": { - "pass": "<html>要素にlang属性が存在しています", + "pass": "<html>要素にlang属性が存在します", "fail": { - "noXHTML": "HTMLページで xml:lang 属性は有効ではありません、 lang 属性を使用してください。", - "noLang": "<html> 要素に lang 属性が指定されていません" + "noXHTML": "HTMLページでxml:lang属性は有効ではありません、lang属性を使用してください。", + "noLang": "<html>要素にlang属性が指定されていません" } }, "valid-lang": { @@ -771,34 +812,31 @@ "fail": "langおよびxml:lang属性に同じ基本言語を指定していません" }, "dlitem": { - "pass": "説明リスト項目に<dl>親要素が存在しています", - "fail": "説明リスト項目に<dl>親要素が存在していません" + "pass": "説明リスト項目に<dl>親要素が存在します", + "fail": "説明リスト項目に<dl>親要素が存在しません" }, "listitem": { - "pass": "リスト項目に<ul>、<ol>、またはrole=\"list\"親要素が存在しています", + "pass": "リスト項目に<ul>、<ol>、またはrole=\"list\"の親要素が存在します", "fail": { - "default": "リスト項目に <ul>、<ol> 親要素が存在していません", - "roleNotValid": "リスト項目にロールまたは role=\"list\" のついていない<ul>、<ol> 親要素が存在していません" + "default": "リスト項目に <ul>、<ol> 親要素が存在しません", + "roleNotValid": "リスト項目にroleが指定されていない<ul>や<ol>親要素、またはrole=\"list\"の親要素が存在しません" } }, "only-dlitems": { - "pass": "リスト要素に<dt>または<dd>要素内で許可された直接の子要素のみが存在しています", - "fail": "リスト要素に<dt>または<dd>要素内で許可されていない直接の子要素が存在しています" + "pass": "dl要素内には許可されている直接の子要素、<dt>、<dd>および<div>要素のみが存在します", + "fail": "dl要素内に許可されていない直接の子要素が存在します: ${data.values}" }, "only-listitems": { - "pass": "リスト要素に<li>要素内で許可された直接の子要素のみが存在しています", - "fail": { - "default": "リスト要素に<li>要素内で許可されていない直接の子要素が存在しています", - "roleNotValid": "リスト要素に許可されていないロールを持った直接の子要素が存在しています: ${data.roles}" - } + "pass": "リスト要素内には許可されている直接の子要素、<li>要素のみが存在します", + "fail": "リスト要素内に許可されていない直接の子要素が存在します: ${data.values}" }, "structured-dlitems": { - "pass": "空ではない場合、要素に<dt>および<dd>要素の両方が存在しています", - "fail": "空ではない場合、要素に少なくとも1つの<dt>要素に続く、少なくとも1つの<dd>要素が存在していません" + "pass": "空ではない場合、要素に<dt>および<dd>要素の両方が存在します", + "fail": "空ではない場合、要素に少なくとも1つの<dt>要素に続く、少なくとも1つの<dd>要素が存在しません" }, "caption": { - "pass": "マルチメディア要素にキャプショントラックが存在しています", - "incomplete": "要素にキャプションが存在することを確認します" + "pass": "マルチメディア要素にキャプショントラックが存在します", + "incomplete": "要素にキャプションが存在することを確認しましょう" }, "frame-tested": { "pass": "iframeはaxe-coreでテストされました", @@ -808,12 +846,12 @@ "no-autoplay-audio": { "pass": "<video> または <audio> は許可された時間より長く音声を出力しない、もしくはコントールするメカニズムを持っています", "fail": "<video> または <audio> は許可された時間より長く音声を出力し、かつコントロールするメカニズムを持っていません", - "incomplete": "<video> または <audio> が許可された時間より長く音声を出力しない、またはコントロールするメカニズムを提供していることを確認します" + "incomplete": "<video> または <audio> が許可された時間より長く音声を出力しない、またはコントロールするメカニズムを提供していることを確認しましょう" }, "css-orientation-lock": { - "pass": "ディスプレイは操作可能であり、向きを固定する機能は存在しません", - "fail": "CSSオリエンテーションロックが適用されており、ディスプレイ操作を不可能にしています", - "incomplete": "CSSオリエンテーションロックが特定できません" + "pass": "ディスプレイは操作可能で、向きは固定されていません", + "fail": "CSSにより向きが固定されており、ディスプレイ操作を不可能にしています", + "incomplete": "CSSで向きが固定されているかどうか特定できません" }, "meta-viewport-large": { "pass": "<meta>タグはモバイルデバイスでの大幅な拡大を阻止しません", @@ -824,32 +862,32 @@ "fail": "<meta>タグの${data}がモバイルデバイスでの拡大を無効にします" }, "target-offset": { - "pass": "ターゲットに最も近い隣接要素からのオフセットが十分にあリます (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)", - "fail": "ターゲットの最も近い隣接要素からのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)", + "pass": "ターゲットに最も近い隣接要素からのオフセットが十分にあリます (${data.closestOffset}pxであり、${data.minOffset}px以上です)", + "fail": "ターゲットの最も近い隣接要素からのオフセットが不十分です (${data.closestOffset}pxですが、${data.minOffset}px以上でなければなりません)", "incomplete": { - "default": "tabindexが負の要素において、最も近い隣接要素からのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)。これがターゲットである要素か確認します", - "nonTabbableNeighbor": "ターゲットのtabindexが負の隣接ノードからのオフセットが不十分です (${data.closestOffset} px は少なくとも ${data.minOffset} px でなければなりません)。隣接要素がターゲットか確認します" + "default": "tabindexが負の要素において、最も近い隣接要素からのオフセットが不十分です (${data.closestOffset}pxですが、${data.minOffset}px以上でなければなりません)。これがターゲットであるか確認しましょう", + "nonTabbableNeighbor": "ターゲットの隣接ノードで、tabindexが負のものからのオフセットが不十分です (${data.closestOffset}pxですが、${data.minOffset}px以上でなければなりません)。隣接要素がターゲットであるか確認しましょう" } }, "target-size": { "pass": { - "default": "コントロールには十分なサイズがあります (${data.width} px x ${data.height} pxであり、${data.minSize} px x ${data.minSize} px 以上です)", + "default": "コントロールには十分なサイズがあります (${data.width}px x ${data.height}pxであり、${data.minSize}px x ${data.minSize}px以上です)", "obscured": "コントロールは完全に隠されていてクリックできないため無視されます" }, "fail": { - "default": "ターゲットのサイズが不十分です (${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)", - "partiallyObscured": "ターゲットは部分的に隠れているためサイズが不十分です (最小スペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)" + "default": "ターゲットのサイズが不十分です (${data.width}px x ${data.height}pxですが、${data.minSize}px x ${data.minSize}px以上でなければなりません)", + "partiallyObscured": "ターゲットは部分的に隠れているためサイズが不十分です (最小スペースは${data.width}px x ${data.height}pxですが、少なくとも${data.minSize}px x ${data.minSize}pxでなければなりません)" }, "incomplete": { - "default": "tabindex が負の要素のサイズが不十分です (${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。これがターゲットの要素であることを確認します", + "default": "tabindexが負の要素のサイズが不十分です (${data.width}px x ${data.height}pxですが、${data.minSize}px x ${data.minSize}px以上でなければなりません)。これがターゲットであるか確認しましょう", "contentOverflow": "コンテンツがオーバーフローしたため、要素のサイズを正確に決定できませんでした", - "partiallyObscured": "tabindex が負の要素は、部分的に隠れているためサイズが不十分です (最小のスペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。これがターゲットの要素であることを確認します", - "partiallyObscuredNonTabbable": "ターゲットのサイズが不十分です。これは tabindex が負の隣接オブジェクトによって部分的に隠されているためです (最小のスペースは ${data.width} px x ${data.height} pxですが、少なくとも ${data.minSize} px x ${data.minSize} px でなければなりません)。隣接要素がターゲットの要素であることを確認します" + "partiallyObscured": "tabindexが負の要素は、部分的に隠れているためサイズが不十分です (最小のスペースは${data.width}px x ${data.height}pxですが、${data.minSize}px x ${data.minSize}px以上でなければなりません)。これがターゲットであるか確認しましょう", + "partiallyObscuredNonTabbable": "ターゲットのサイズが不十分です。これはtabindexが負の隣接オブジェクトによって部分的に隠されているためです (最小のスペースは${data.width}px x ${data.height}pxですが、${data.minSize}px x ${data.minSize}px以上でなければなりません)。隣接要素がターゲットであるか確認しましょう" } }, "header-present": { - "pass": "ページにheaderが存在しています", - "fail": "ページにheaderが存在していません" + "pass": "ページに見出しが存在します", + "fail": "ページに見出しが存在しません" }, "heading-order": { "pass": "見出しの順序が有効です", @@ -858,27 +896,27 @@ }, "identical-links-same-purpose": { "pass": "同じ名前で異なるURLに遷移する他のリンクはありません", - "incomplete": "リンクが同じ目的を持っていること、または意図的に不明瞭になっていることを確認します" + "incomplete": "リンクが同じ目的を持っていること、または意図的に不明瞭になっていることを確認しましょう" }, "internal-link-present": { - "pass": "有効なスキップリンクが存在しています", - "fail": "有効なスキップリンクが存在していません" + "pass": "有効なスキップリンクが存在します", + "fail": "有効なスキップリンクが存在しません" }, "landmark": { - "pass": "ページにランドマーク領域が存在しています", - "fail": "ページにランドマーク領域が存在していません" + "pass": "ページにランドマーク領域が存在します", + "fail": "ページにランドマーク領域が存在しません" }, "meta-refresh-no-exceptions": { "pass": "<meta>タグはすぐにページを更新しません", - "fail": "<meta>タグは時限によるページの更新を強制します" + "fail": "<meta>タグは一定時間経過後に強制的にページを更新します" }, "meta-refresh": { "pass": "<meta>タグはすぐにページを更新しません", - "fail": "<meta>タグは時限によるページの更新を強制します" + "fail": "<meta>タグは一定時間経過後に強制的にページを更新します (20時間未満)" }, "p-as-heading": { "pass": "<p>要素が見出しとしてスタイル付けされていません", - "fail": "スタイル付けされたp要素ではなく、見出し要素を使用すべきです", + "fail": "スタイル付けされた<p>要素ではなく、見出し要素を使用すべきです", "incomplete": "<p> 要素に見出しのスタイル付けがされているかどうか判定できません" }, "region": { @@ -886,7 +924,7 @@ "fail": "ページの一部のコンテンツがランドマークに含まれていません" }, "skip-link": { - "pass": "スキップリンクのターゲットが存在しています", + "pass": "スキップリンクのターゲットが存在します", "incomplete": "スキップリンクのターゲットは活性化された時に表示されるべきです", "fail": "スキップリンクのターゲットがありません" }, @@ -899,12 +937,12 @@ "fail": "ドキュメントに同じid属性を持つ有効な要素があります: ${data}" }, "duplicate-id-aria": { - "pass": "ドキュメントに同じid属性を持つARIAまたはラベルに参照された要素はありません", - "fail": "ドキュメントに同じid属性を持つARIAに参照された複数の要素があります: ${data}" + "pass": "ARIAまたはラベルが参照している要素で、同じid属性を持つものはありません", + "fail": "ARIAが参照している要素で、複数の要素で同じid属性を持つものがあります: ${data}" }, "duplicate-id": { - "pass": "ドキュメントに同じid属性を共有する静的な要素はありません", - "fail": "ドキュメントに同じid属性を共有する静的な要素が複数存在します" + "pass": "ドキュメントにid属性が同じ静的な要素はありません", + "fail": "ドキュメントにid属性が同じ静的な要素が複数存在します" }, "aria-label": { "pass": "aria-label属性が存在し、空ではありません", @@ -916,52 +954,52 @@ "incomplete": "aria-labelledby属性が、存在する要素を参照していることを確認しましょう" }, "avoid-inline-spacing": { - "pass": "テキストの間隔に影響する '!important' のついたインラインスタイルは指定されていません", + "pass": "テキストの間隔に影響する'!important'のついたインラインスタイルは指定されていません", "fail": { - "singular": "ほとんどのブラウザーで上書きすることはサポートされていないため、インラインスタイル ${data.values} から '!important' を削除します", - "plural": "ほとんどのブラウザーで上書きすることはサポートされていないため、インラインスタイル ${data.values} から '!important' を削除します" + "singular": "ほとんどのブラウザーで上書きすることはサポートされていないため、インラインスタイル${data.values}から'!important'を削除します", + "plural": "ほとんどのブラウザーで上書きすることはサポートされていないため、インラインスタイル${data.values}から'!important'を削除します" } }, "button-has-visible-text": { - "pass": "要素にスクリーン・リーダーが認識可能な内部テキストが存在しています", - "fail": "要素にスクリーン・リーダーが認識可能な内部テキストが存在していません", + "pass": "要素内にスクリーン・リーダーが認識可能なテキストが存在します", + "fail": "要素内にスクリーン・リーダーが認識可能なテキストが存在しません", "incomplete": "要素に子ノードがあるか判定できません" }, "doc-has-title": { - "pass": "ドキュメントに空ではない<title>要素が存在しています", - "fail": "ドキュメントに空ではない<title>要素が存在していません" + "pass": "ドキュメントに空ではない<title>要素が存在します", + "fail": "ドキュメントに空ではない<title>要素が存在しません" }, "exists": { - "pass": "要素は存在していません", - "incomplete": "要素が存在しています" + "pass": "要素は存在しません", + "incomplete": "要素が存在します" }, "has-alt": { - "pass": "要素にalt属性が存在しています", - "fail": "要素にalt属性が存在していません" + "pass": "要素にalt属性が指定されています", + "fail": "要素にalt属性が指定されていません" }, "has-visible-text": { - "pass": "要素にスクリーン・リーダーが認識可能なテキストが存在しています", - "fail": "要素にスクリーン・リーダーが認識可能なテキストが存在していません", + "pass": "要素にスクリーン・リーダーが認識可能なテキストが存在します", + "fail": "要素にスクリーン・リーダーが認識可能なテキストが存在しません", "incomplete": "要素に子ノードがあるか判定できません" }, "important-letter-spacing": { - "pass": "文字の間隔(letter-spacing)の属性が '!important' に設定されていない、または最低条件を満たしています", - "fail": "文字の間隔(letter-spacing)の属性には '!important' を設定しない、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + "pass": "style属性中のletter-spacingに'!important'が指定されていない、または最低条件を満たしています", + "fail": "style属性中のletter-spacingには'!important'を指定しない、または${data.minValue}em以上(現在は${data.value}em)にしてください" }, "important-line-height": { - "pass": "行間(line-height)に '!important' が設定されていません、または最低条件を満たしています", - "fail": "行間(line-height)には '!important' を使用しないでください、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + "pass": "style属性中のline-heightに'!important'が指定されていない、または最低条件を満たしています", + "fail": "style属性中のline-heightには'!important'を指定しない、または${data.minValue}em以上(現在は${data.value}em)にしてください" }, "important-word-spacing": { - "pass": "単語の間隔(word-spacing)に '!important' が設定されていません、または最低条件を満たしています", - "fail": "単語の間隔(word-spacing)には '!important' を使用しないでください、または ${data.minValue} em以上(現在は ${data.value} em)にしてください" + "pass": "style属性中のword-spacingに'!important'が指定されていない、または最低条件を満たしています", + "fail": "style属性中のword-spacingには'!important'を指定しない、または${data.minValue}em以上(現在は${data.value}em)にしてください" }, "is-on-screen": { "pass": "要素が表示されていません", "fail": "要素が表示されています" }, "non-empty-alt": { - "pass": "要素に空ではないalt属性が存在しています", + "pass": "要素に空ではないalt属性が指定されています", "fail": { "noAttr": "要素にalt属性が指定されていません", "emptyAttr": "要素に空のalt属性が指定されています" @@ -969,27 +1007,27 @@ }, "non-empty-if-present": { "pass": { - "default": "要素に value 属性が存在しません", - "has-label": "要素に空ではない value 属性が存在しています" + "default": "要素にvalue属性が指定されていません", + "has-label": "要素に空ではないvalue属性が指定されています" }, - "fail": "要素にvalue属性が存在し、value属性が空です" + "fail": "要素にvalue属性が指定されていますが、value属性が空です" }, "non-empty-placeholder": { "pass": "要素にplaceholder属性が指定されています", "fail": { - "noAttr": "要素にplacehoder属性が指定されていません", - "emptyAttr": "要素に空のplacehoder属性が指定されています" + "noAttr": "要素にplaceholder属性が指定されていません", + "emptyAttr": "要素に空のplaceholder属性が指定されています" } }, "non-empty-title": { - "pass": "要素にtitle属性が存在しています", + "pass": "要素にtitle属性が指定されています", "fail": { "noAttr": "要素にtitle属性が指定されていません", "emptyAttr": "要素に空のtitle属性が指定されています" } }, "non-empty-value": { - "pass": "要素に空ではないvalue属性が存在しています", + "pass": "要素に空ではないvalue属性が指定されています", "fail": { "noAttr": "要素にvalue属性が指定されていません", "emptyAttr": "要素に空のvalue属性が指定されています" @@ -999,10 +1037,10 @@ "pass": "要素のデフォルトのセマンティクスはrole=\"${data.role}\"で上書きされました", "fail": { "default": "要素のデフォルトのセマンティクスはrole=\"none\"またはrole=\"presentation\"で上書きされませんでした", - "globalAria": "要素にはARIAのグローバル属性が指定されているため、role=\"none\"またはrole=\"presentation\"にはなりません", + "globalAria": "要素にはグローバルなARIA属性が指定されているため、role=\"none\"またはrole=\"presentation\"にはなりません", "focusable": "要素がフォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません", - "both": "要素にはARIAのグローバル属性が指定されており、フォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません", - "iframe": "${data.nodeName} 要素にプレゼンテーション用のroleを持つ 'title' 属性を使用すると、スクリーンリーダー間で動作が一貫しません" + "both": "要素にはグローバナルARIA属性が指定されており、フォーカス可能なため、role=\"none\"またはrole=\"presentation\"にはなりません", + "iframe": "プレゼンテーション用のロールを指定した${data.nodeName}要素に'title'属性を使用すると、スクリーンリーダー間で動作が一貫しません" } }, "role-none": { @@ -1014,15 +1052,15 @@ "fail": "要素のデフォルトのセマンティクスはrole=\"presentation\"で上書きされませんでした" }, "svg-non-empty-title": { - "pass": "要素の子要素にtitle要素が存在します", + "pass": "要素にタイトルを示す子要素が存在します", "fail": { - "noTitle": "要素の子要素にtitle要素が存在しません", + "noTitle": "要素にタイトルを示す子要素が存在しません", "emptyTitle": "子要素のtitle要素が空です" }, - "incomplete": "要素の子要素にtitle要素があるか判定できません" + "incomplete": "要素にタイトルを示す子要素が存在するか判定できません" }, "caption-faked": { - "pass": "テーブルの一行目がキャプションとして使用されていません", + "pass": "テーブルの1行目がキャプションとして使用されていません", "fail": "テーブルの最初の子はテーブルセルではなく、キャプションであるべきです" }, "html5-scope": { @@ -1039,8 +1077,8 @@ "fail": "scope属性の値は'row'または'col'のみです" }, "td-has-header": { - "pass": "すべての空ではないデータセルにテーブルヘッダーが存在しています", - "fail": "いくつかの空ではないデータセルにテーブルヘッダーが存在していません" + "pass": "すべての空ではないデータセルにテーブルヘッダーが存在します", + "fail": "いくつかの空ではないデータセルにテーブルヘッダーが存在しません" }, "td-headers-attr": { "pass": "headers属性はテーブル内の他のセルを参照するためだけに使用されています", @@ -1066,5 +1104,5 @@ "failureMessage": "次のすべてを修正します:{{~it:value}}\n {{=value.split('\\n').join('\\n ')}}{{~}}" } }, - "incompleteFallbackMessage": "次のすべてを修正します:{{~it:value}}\n {{=value.split('\\n').join('\\n ')}}{{~}}" + "incompleteFallbackMessage": "axeは原因を特定できませんでした。要素のインスペクターを使いましょう!" } diff --git a/locales/pl.json b/locales/pl.json index f27606db0a..72f6dec42a 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -2,7 +2,7 @@ "lang": "pl", "rules": { "accesskeys": { - "description": "Każdy atrybut accessskey jest unikalny.", + "description": "Wartość każdego atrybutu accessskey jest unikalna.", "help": "Wartość atrybutu accessskey musi być unikalna." }, "area-alt": { @@ -10,95 +10,111 @@ "help": "Elementy aktywne <area> muszą mieć tekst alternatywny." }, "aria-allowed-attr": { - "description": "Użyte atrybuty ARIA są dozwolone dla roli elementu.", + "description": "Użyte atrybuty ARIA są dozwolone dla elementu z określoną rolą.", "help": "Elementy mogą używać tylko dozwolonych atrybutów ARIA." }, "aria-allowed-role": { "description": "Atrybut role ma odpowiednią wartość dla danego elementu.", "help": "Rola ARIA musi być odpowiednia dla danego elementu." }, + "aria-braille-equivalent": { + "description": "Upewnij się, że aria-braillelabel i aria-brailleroledescription mają odpowiednik niebrajlowski", + "help": "Atrybuty aria-braille muszą mieć odpowiednik niebrajlowski." + }, "aria-command-name": { - "description": "Każdy przycisk, łącze i pozycja menu (menuitem) ARIA ma dostępną nazwę.", - "help": "Polecenia ARIA muszą mieć dostępną nazwę." + "description": "Każdy element button, link i menuitem ARIA ma dostępną nazwę.", + "help": "Przyciski, łącza, pozycje menu ARIA muszą mieć dostępną nazwę." + }, + "aria-conditional-attr": { + "description": "Upewnij się, że atrybuty ARIA są używane zgodnie ze specyfikacją roli elementu.", + "help": "Atrybuty ARIA muszą być używane w sposób określony dla roli elementu" + }, + "aria-deprecated-role": { + "description": "Upewnij się, że elementy nie używają przestarzałych ról.", + "help": "Przestarzałe role ARIA nie mogą być używane." }, "aria-dialog-name": { "description": "Każde okno dialogowe ARIA i węzeł alertdialog ma dostępną nazwę.", "help": "Okno dialogowe ARIA i węzły alertdialog muszą mieć dostępną nazwę." }, "aria-hidden-body": { - "description": "Element <body> nie ma atrybutu aria-hidden='true'.", - "help": "Element <body> nie może mieć atrybutu aria-hidden='true'." + "description": "Element body nie ma atrybutu aria-hidden='true'.", + "help": "Element body nie może mieć atrybutu aria-hidden='true'." }, "aria-hidden-focus": { - "description": "Elementy z aria-hidden nie mogą obejmować elementów przyjmujących fokus.", + "description": "Elementy z aria-hidden=true nie mogą obejmować elementów przyjmujących fokus.", "help": "Ukryty element ARIA nie może zawierać elementów przyjmujących fokus." }, "aria-input-field-name": { - "description": "Każde pole wejściowe ARIA ma dostępną nazwę.", - "help": "Pola wejściowe ARIA muszą mieć dostępną nazwę." + "description": "Każde pole ARIA do wprowadzania danych ma dostępną nazwę.", + "help": "Pola ARIA do wprowadzania danych muszą mieć dostępną nazwę." }, "aria-meter-name": { - "description": "Każdy licznik (meter) oparty na ARIA ma dostępną nazwę.", - "help": "Liczniki (meter) ARIA muszą mieć dostępną nazwę." + "description": "Każdy element ARIA meter (licznik) ma dostępną nazwę.", + "help": "Liczniki ARIA (meter) muszą mieć dostępne nazwy." }, "aria-progressbar-name": { - "description": "Każdy pasek postępu (progressbar) ARIA ma dostępną nazwę.", - "help": "Paski postępu (progressbar) ARIA muszą mieć dostępną nazwę." + "description": "Każdy element ARIA progressbar (pasek postępu) ma dostępną nazwę.", + "help": "Paski postępu ARIA (progressbar) muszą mieć dostępne nazwy." + }, + "aria-prohibited-attr": { + "description": "Upewnij się, że atrybuty ARIA nie są zabronione dla roli elementu.", + "help": "Elementy mogą używać tylko dozwolonych atrybutów ARIA." }, "aria-required-attr": { - "description": "Elementy z rolami ARIA mają wszystkie wymagane atrybuty ARIA.", + "description": "Elementy z rolą ARIA mają wszystkie wymagane atrybuty aria-*", "help": "Wymagane atrybuty ARIA muszą istnieć." }, "aria-required-children": { - "description": "Elementy z rolą ARIA, które wymagają ról potomnych, zawierają je.", + "description": "Elementy z atrybutem ARIA role, które muszą zawierać elementy potomne z wymaganym atrybutem role, zawierają je.", "help": "Niektóre role ARIA muszą obejmować określone dzieci." }, "aria-required-parent": { - "description": "Elementy z rolą ARIA, które wymagają ról nadrzędnych, są zawarte w elementach z takimi rolami.", - "help": "Niektóre role ARIA muszą być wewnątrz określonych elementów rodziców." + "description": "Elementy z atrybutem ARIA role, które wymagają elementu rodzica z atrybutem role, są zawarte w elementach z takimi rolami.", + "help": "Elementy z niektórymi atrybutami role ARIA muszą znajdować się wewnątrz nadrzędnego elementu rodzica z wymaganym atrybutem role." }, "aria-roledescription": { "description": "Atrybut aria-roledescription jest używany tylko w elementach, które mają rolę określoną domyślnie lub jawnie.", "help": "Użyj aria-roledescription w elementach o roli semantycznej." }, "aria-roles": { - "description": "Wszystkie elementy z atrybutem roli używają prawidłowej wartości tego atrybutu.", - "help": "Stosowane role ARIA muszą być zgodne z obowiązującymi wartościami." + "description": "Wartości atrybutu role są poprawne.", + "help": "Stosowane role ARIA muszą mieć poprawne wartości." }, "aria-text": { - "description": "Atrybut \"role=text\" jest używany dla elementów, które nie mają potomków przyjmujących fokus", - "help": "\"role=text\" nie powinien mieć potomków przyjmujących fokus." + "description": "Atrybut role=\"text\" jest używany dla elementów, które nie mają potomków przyjmujących fokus", + "help": "Element z atrybutem role=\"text\" nie może mieć potomków przyjmujących fokus." }, "aria-toggle-field-name": { - "description": "Każdy przełącznik (toggle) ARIA ma dostępną nazwę.", - "help": "Przełączniki (toggle) ARIA muszą mieć dostępną nazwę." + "description": "Każdy element ARIA toggle (przełącznik) ma dostępną nazwę.", + "help": "Przełączniki ARIA (toggle) muszą mieć dostępną nazwę." }, "aria-tooltip-name": { - "description": "Każda podpowiedź (tooltip) ARIA ma dostępną nazwę.", - "help": "Podpowiedzi (tooltip) ARIA muszą mieć dostępną nazwę." + "description": "Każdy element ARIA tooltip (podpowiedź) ma dostępną nazwę.", + "help": "Podpowiedzi ARIA (tooltip) muszą mieć dostępną nazwę." }, "aria-treeitem-name": { - "description": "Każdy węzeł drzewa elementów (treeitem) ARIA ma dostępną nazwę.", - "help": "Węzły drzewa elementów ARIA muszą mieć dostępną nazwę." + "description": "Każdy element ARIA treeitem (węzeł drzewa) ma dostępną nazwę.", + "help": "Węzły drzewa elementów ARIA (treeitem) muszą mieć dostępną nazwę." }, "aria-valid-attr-value": { "description": "Wszystkie atrybuty ARIA mają poprawne wartości.", "help": "Atrybuty ARIA muszą mieć poprawne wartości." }, "aria-valid-attr": { - "description": "Atrybuty, które rozpoczynają się od aria-, są poprawnymi atrybutami ARIA.", - "help": "Atrybuty ARIA muszą być mieć poprawne nazwy." + "description": "Wszystkie atrybuty aria-* mają poprawne nazwy.", + "help": "Atrybuty ARIA muszą mieć poprawne nazwy." }, "audio-caption": { "description": "Elementy <audio> mają napisy rozszerzone.", "help": "Elementy <audio> muszą mieć ścieżkę z napisami." }, "autocomplete-valid": { - "description": "Atrybut autocomplete jest poprawny i odpowiedni dla pola formularza.", + "description": "Pola formularza, które zbierają dane osobowe, mają poprawne atrybuty autocomplete", "help": "Atrybut autocomplete musi być użyty poprawnie." }, "avoid-inline-spacing": { - "description": "Odstępy w tekście ustawione za pomocą atrybutów stylu mogą być regulowane za pomocą własnych arkuszy stylów.", + "description": "Odstępy w tekście można regulować za pomocą własnych arkuszy stylów.", "help": "Odstępy w tekście muszą być regulowane za pomocą własnych arkuszy stylów." }, "blink": { @@ -106,21 +122,21 @@ "help": "Elementy <blink> są przestarzałe i nie mogą być używane." }, "button-name": { - "description": "Przyciski mają odróżniający je tekst.", - "help": "Przyciski muszą mieć odróżniający je tekst." + "description": "Każdy przycisk ma odróżniającą go dostępną nazwę.", + "help": "Przyciski muszą mieć odróżniający je tekst nazwy." }, "bypass": { "description": "Każda strona ma co najmniej jeden mechanizm, który pozwala ominąć nawigację i przejść od razu do treści.", - "help": "Strona musi mieć środki do ominięcia powtarzających się bloków treści." + "help": "Strona musi mieć środki do ominięcia powtarzających bloków treści." + }, + "color-contrast-enhanced": { + "description": "Kontrast między kolorami pierwszego planu i tła spełnia wyższe progi współczynnika kontrastu WCAG 2 AAA.", + "help": "Elementy muszą spełniać wyższe progi współczynnika kontrastu kolorów" }, "color-contrast": { "description": "Kontrast między kolorami pierwszego planu i tła spełnia progi kontrastu WCAG 2 AA.", "help": "Elementy muszą mieć wystarczający kontrast kolorów." }, - "color-contrast-enhanced": { - "description": "Kontrast między kolorami pierwszego planu i tła spełnia progi kontrastu WCAG 2 AAA.", - "help": "Elementy muszą mieć wystarczający kontrast kolorów." - }, "css-orientation-lock": { "description": "Treść nie jest przypisana do żadnej konkretnej orientacji wyświetlacza i można ją obsługiwać we wszystkich orientacjach wyświetlacza.", "help": "Zapytania medialne nie są wykorzystywane do blokowania orientacji wyświetlacza." @@ -139,7 +155,7 @@ }, "duplicate-id-active": { "description": "Wartość każdego atrybutu id aktywnych elementów jest unikalna.", - "help": "ID aktywnych elementów muszą być unikalne." + "help": "ID aktywnych elementów, które otrzymują fokus, muszą być unikalne." }, "duplicate-id-aria": { "description": "Każdy atrybut id używany w ARIA i w etykietach jest unikalny.", @@ -162,31 +178,31 @@ "help": "Elementy w porządku otrzymywania fokusu muszą mieć rolę odpowiednią dla treści interaktywnych." }, "form-field-multiple-labels": { - "description": "Pole formularza nie ma wielu etykiet (elementów label).", - "help": "Pole formularza nie powinno zawierać wielu elementów label." + "description": "Żadne pole formularza nie ma wielu etykiet (elementów label).", + "help": "Pole formularza nie może mieć wielu elementów label." }, "frame-focusable-content": { - "description": "Elementy <frame> i <iframe> z treścią przyjmującą fokus nie mają tabindex=-1", - "help": "Ramki z treścią przyjmującą fokus nie mogą mieć tabindex=-1" + "description": "Elementy <frame> i <iframe> z treścią przyjmującą fokus nie mają tabindex=-1.", + "help": "Ramki z treścią przyjmującą fokus nie mogą mieć tabindex=-1." }, "frame-tested": { "description": "Elementy <iframe> i <frame> muszą być testowane ze skryptem axe-core.", "help": "Ramki muszą być testowane ze skryptem axe-core." }, "frame-title-unique": { - "description": "Elementy <frame> i <frame> mają unikalny atrybut title.", - "help": "Ramki (frame) muszą mieć unikalny atrybut title." + "description": "Elementy <iframe> i <frame> mają unikalny atrybut title.", + "help": "Ramki muszą mieć unikalny atrybut title." }, "frame-title": { "description": "Elementy <iframe> i <frame> mają niepusty atrybut title.", - "help": "Ramki muszą mieć atrybut title." + "help": "Ramki muszą mieć niepusty atrybut title." }, "heading-order": { "description": "Kolejność nagłówków jest semantycznie poprawna.", "help": "Poziomy nagłówków powinny wzrastać tylko o jeden." }, "hidden-content": { - "description": "Informuje użytkowników o ukrytych treściach.", + "description": "Na stronie są ukryte treści.", "help": "Ukrytych treści na stronie nie można analizować." }, "html-has-lang": { @@ -211,7 +227,7 @@ }, "image-redundant-alt": { "description": "Tekst alternatywny obrazu nie jest powtarzany w tekście.", - "help": "Alternatywny tekst obrazów nie powinien być powtarzany w tekście." + "help": "Tekst alternatywny obrazów nie powinien być powtarzany w tekście." }, "input-button-name": { "description": "Przyciski input type=button mają odróżniający je tekst.", @@ -238,7 +254,7 @@ "help": "Punkt orientacyjny banner nie może być zawarty wewnątrz innego obszaru kluczowego." }, "landmark-complementary-is-top-level": { - "description": "Obszar aside lub obszar z role=complementary są obszarami kluczowymi najwyższego poziomu.", + "description": "Obszary aside lub z role=complementary są obszarami kluczowymi najwyższego poziomu.", "help": "Punkt orientacyjny complementary nie może być zawarty wewnątrz innego obszaru kluczowego." }, "landmark-contentinfo-is-top-level": { @@ -266,12 +282,12 @@ "help": "Dokument może mieć tylko jeden obszar kluczowy main." }, "landmark-unique": { - "help": "Punkty orientacyjne mają unikalną rolę lub kombinację roli/etykiety/tytułu (tj. dostępną nazwę).", - "description": "Punkty orientacyjne (obszary kluczowe) są unikalne." + "help": "Punkty orientacyjne są unikalne", + "description": "Punkty orientacyjne (obszary kluczowe) mają unikalną rolę lub kombinację roli/etykiety/tytułu (tj. dostępną nazwę)." }, "link-in-text-block": { "description": "Łącza można rozróżniać bez opierania się na kolorze.", - "help": "Łącza muszą być odróżnialne od otaczającego je tekstu w sposób, który nie opiera się na kolorze." + "help": "Łącza muszą być odróżnialne od sąsiadującego tekstu w sposób, który nie opiera się na kolorze." }, "link-name": { "description": "Łącza mają odróżniający je tekst.", @@ -289,9 +305,13 @@ "description": "Elementy <marquee> nie są używane.", "help": "Elementy <marquee> są przestarzałe i nie mogą być używane." }, + "meta-refresh-no-exceptions": { + "description": "Upewnij się, że <meta http-equiv=\"refresh\"> nie jest używany do opóźnionego odświeżania", + "help": "Nie wolno stosować opóźnionego odświeżania" + }, "meta-refresh": { - "description": "<meta http-equiv=\"refresh\"> nie jest stosowane.", - "help": "Automatyczne odświeżenie strony nie może być stosowane." + "description": "Znacznik <meta http-equiv=\"refresh\"> nie jest używany do opóźnionego odświeżania.", + "help": "Opóźnione odświeżanie poniżej 20 godzin nie może być stosowane." }, "meta-viewport-large": { "description": "Element <meta name=\"viewport\"> umożliwia znaczne powiększanie.", @@ -302,36 +322,36 @@ "help": "Powiększanie i skalowanie nie może być wyłączone." }, "nested-interactive": { - "description": "Zagnieżdżone interaktywne kontrolki nie są ogłaszane przez czytniki ekranu", - "help": "Upewnij się, że kontrolki interaktywne nie są zagnieżdżone" + "description": "Upewnij się, że kontrolki interaktywne nie są zagnieżdżone, ponieważ nie zawsze są ogłaszane przez czytniki ekranu lub mogą powodować problemy technologii wspomagających z fokusem.", + "help": "Kontrolki interaktywne nie mogą być zagnieżdżone" }, "no-autoplay-audio": { - "description": "Elementy <video> lub <audio> nie odtwarzają automatycznie przez dłużej niż 3 sekundy dźwięku bez mechanizmu, który go zatrzymuje lub wycisza.", + "description": "Elementy <video> lub <audio> nie odtwarzają automatycznie dźwięku przez dłużej niż 3 sekundy bez mechanizmu, który go zatrzymuje lub wycisza.", "help": "Elementy <video> lub <audio> nie odtwarzają dźwięku automatycznie." }, "object-alt": { - "description": "Elementy <object> mają tekst alternatywny.", - "help": "Elementy <object> muszą mieć tekst zastępczy." + "description": "Elementy <object> mają tekst zastępczy.", + "help": "Elementy <object> muszą mieć alternatywę tekstową." }, "p-as-heading": { - "description": "Elementy p nie są stylizowane jako nagłówki.", - "help": "Pogrubienie, kursywa i rozmiar czcionki nie są używane do stylizacji elementów p jako nagłówków." + "description": "Pogrubienie, kursywa i rozmiar czcionki nie są używane do stylizacji elementów <p> jako nagłówków.", + "help": "Stylizowane elementy <p> nie mogą być używane jako nagłówki." }, "page-has-heading-one": { - "description": "Strona, lub co najmniej jedna z jej ramek, zawiera nagłówek pierwszego poziomu.", + "description": "Strona lub co najmniej jedna z jej ramek, zawiera nagłówek pierwszego poziomu.", "help": "Strona musi zawierać nagłówek poziomu 1." }, "presentation-role-conflict": { - "description": "Elementy z role=none lub role=presentation nie mogą kolidować z innymi rolami.", - "help": "Elementy, które mają role=none lub role=presentation, nie mogą kolidować z innymi rolami." + "description": "Elementy oznaczone jako prezentacyjne nie powinny mieć globalnego ARIA ani tabindex, aby zapewnić, że wszystkie czytniki ekranu je zignorują.", + "help": "Upewnij się, że elementy oznaczone jako prezentacyjne są konsekwentnie ignorowane." }, "region": { "description": "Cała treść strony jest objęta przez punkty orientacyjne.", "help": "Cała treść strony musi być zawarta w obszarach kluczowych." }, "role-img-alt": { - "description": "Elementy z [role=\"img\"] mają tekst alternatywny.", - "help": "Elementy z [role=\"img\"] muszą mieć tekst alternatywny." + "description": "Elementy z [role='img'] mają tekst alternatywny.", + "help": "Elementy z [role='img'] muszą mieć tekst alternatywny." }, "scope-attr-valid": { "description": "Atrybut scope w tabelach jest stosowany poprawnie.", @@ -354,32 +374,36 @@ "help": "Cel łącza pomijającego powinien istnieć i przyjmować fokus." }, "svg-img-alt": { - "description": "Elementy svg z rolami img, graphics-document lub graphics-symbol mają dostępny tekst.", - "help": "Elementy svg z rolą img mają tekst alternatywny." + "description": "Elementy <svg> z rolą img, graphics-document lub graphics-symbol mają dostępny tekst.", + "help": "Elementy svg z rolą img muszą mieć tekst alternatywny." }, "tabindex": { "description": "Wartości atrybutów tabindex nie są większe niż 0.", "help": "Elementy nie powinny mieć wartości tabindex większej niż zero." }, "table-duplicate-name": { - "description": "Tabele nie mają takiego samego streszczenia (summary) i podpisu (caption).", + "description": "Podpis tabeli (element <caption>) nie może zawierać takiego samego tekstu co atrybut summary.", "help": "Atrybut summary w tabeli ma inny tekst niż element caption." }, "table-fake-caption": { "description": "Tabele używają jako podpisu elementu <caption>.", - "help": "Komórki danych i nagłówkowe w tabeli danych nie są używane do umieszczania podpisów." + "help": "Komórki danych i nagłówkowe w tabeli danych nie są używane do umieszczania podpisu tabeli." + }, + "target-size": { + "description": "Sprawdź, czy cel dotykowy ma wystarczający rozmiar i przestrzeń wokół.", + "help": "Wszystkie cele dotykowe muszą mieć rozmiar 24px lub pozostawiać wystarczającą ilość miejsca wokół." }, "td-has-header": { - "description": "Każda niepusta komórka danych w dużej tabeli ma jeden lub więcej nagłówków tabeli.", - "help": "Wszystkie niepuste elementy td w tabelach danych większych niż 3 na 3 mają skojarzony nagłówek tabeli." + "description": "Wszystkie niepuste komórki danych w tabelach danych większych niż 3 na 3 mają jeden lub więcej nagłówków tabeli.", + "help": "Każdy niepusty element <td> w dużej tabeli musi mieć powiązany nagłówek tabeli" }, "td-headers-attr": { "description": "Każda komórka tabeli używająca atrybutu headers odwołuje się do innej komórki w tej tabeli.", "help": "Wszystkie komórki z atrybutem headers odnoszą się tylko do innych komórek tej samej tabeli." }, "th-has-data-cells": { - "description": "Każdy nagłówek tabeli w tabeli danych odnosi się do komórek danych.", - "help": "Wszystkie elementy th i elementy z role=columnheader/rowheader mają komórki danych, które opisują." + "description": "Wszystkie elementy th i elementy z role=columnheader/rowheader mają komórki danych, które opisują.", + "help": "Każdy nagłówek tabeli w tabeli danych musi odnosić się do komórek danych." }, "valid-lang": { "description": "Atrybuty lang mają poprawne wartości.", @@ -403,7 +427,8 @@ "fail": { "singular": "Atrybut ARIA nie jest dozwolony: ${data.values}.", "plural": ": Atrybuty ARIA nie są dozwolone: ${data.values}." - } + }, + "incomplete": "Sprawdź, czy nie ma problemu, jeśli atrybut ARIA jest ignorowany w tym elemencie: ${data.values}" }, "aria-allowed-role": { "pass": "Rola ARIA jest dozwolona dla danego elementu.", @@ -416,25 +441,53 @@ "plural": ": Role ARIA ${data.values} muszą być usunięte, gdy element jest widoczny, ponieważ nie są one dozwolone dla elementu." } }, + "aria-busy": { + "pass": "Element ma atrybut aria-busy", + "fail": "Element używa aria-busy=\"true\" podczas pokazywania ładowania" + }, + "aria-conditional-attr": { + "pass": "Atrybut ARIA jest dozwolony", + "fail": { + "checkbox": "Usuń aria-checked lub ustaw jego wartość na \"${data.checkState}\", aby dopasować ją do rzeczywistego stanu pola wyboru.", + "rowSingular": "Ten atrybut jest obsługiwany przez wiersze siatki, ale nie przez ${data.ownerRole}: ${data.invalidAttrs}", + "rowPlural": "Te atrybuty są obsługiwane przez wiersze siatki, ale nie przez ${data.ownerRole}: ${data.invalidAttrs}" + } + }, "aria-errormessage": { "pass": "Istnieje aria-errormessage oraz elementy referencyjne widoczne dla czytników ekranowych, które wykorzystują wspieraną technikę aria-errormessage.", "fail": { - "singular": "Wartość aria-errormessage `${data.values}` musi używać techniki ogłoszenia wiadomości (np. aria-live, aria-describedby, role=alert, etc.).", - "plural": "Wartości aria-errormessage `${data.values}` muszą używać techniki ogłoszenia wiadomości (np. aria-live, aria-describedby, role=alert, etc.)." + "singular": "Wartość aria-errormessage ${data.values} musi używać techniki ogłoszenia wiadomości (np. aria-live, aria-describedby, role=alert, etc.).", + "plural": "Wartości aria-errormessage ${data.values} muszą używać techniki ogłoszenia wiadomości (np. aria-live, aria-describedby, role=alert, etc.).", + "hidden": "Wartość aria-errormessage ${data.values} nie może odwoływać się do ukrytego elementu." }, "incomplete": { - "singular": "Upewnij się, że wartość aria-errormessage `${data.values}` odnosi się do istniejącego elementu.", - "plural": "Upewnij się, że wartości aria-errormessage `${data.values}` odnoszą się do istniejących elementów." + "singular": "Upewnij się, że wartość aria-errormessage ${data.values} odnosi się do istniejącego elementu.", + "plural": "Upewnij się, że wartości aria-errormessage ${data.values} odnoszą się do istniejących elementów.", + "idrefs": "Nie można określić, czy na stronie istnieje element aria-errormessage: ${data.values}" } }, "aria-hidden-body": { - "pass": "Nie ma żadnego atrybutu aria-hidden w elemencie <body> dokumentu.", - "fail": "Atrybutu aria-hidden=true nie może być w elemencie <body> dokumentu." + "pass": "Nie ma żadnego atrybutu aria-hidden w elemencie body dokumentu.", + "fail": "Atrybut aria-hidden=true nie może być użyty w elemencie body dokumentu." + }, + "aria-level": { + "pass": "Wartość aria-level jest poprawna", + "incomplete": "Wartości aria-level większe niż 6 nie są obsługiwane we wszystkich kombinacjach czytników ekranu i przeglądarek" }, "aria-prohibited-attr": { "pass": "Atrybut ARIA jest dozwolony", - "fail": "Atrybut ARIA nie może być użyty, dodaj atrybut role lub użyj innego elementu: ${data.values}", - "incomplete": "Atrybut ARIA nie jest dobrze obsługiwany w elemencie i zamiast niego zostanie użyta treść tekstowa: ${data.values}" + "fail": { + "hasRolePlural": "Atrybuty ${data.prohibited} nie mogą być używane z rolą \"${data.role}\".", + "hasRoleSingular": "Atrybut ${data.prohibited} nie może być użyty z rolą \"${data.role}\".", + "noRolePlural": "Atrybuty ${data.prohibited} nie mogą być używane w ${data.nodeName} bez poprawnego atrybutu roli.", + "noRoleSingular": "Atrybut ${data.prohibited} nie może być użyty w ${data.nodeName} bez poprawnego atrybutu roli." + }, + "incomplete": { + "hasRoleSingular": "Atrybut ${data.prohibited} nie jest dobrze obsługiwany przez rolę \"${data.role}\".", + "hasRolePlural": "Atrybuty ${data.prohibited} nie są dobrze obsługiwane przez role \"${data.role}\".", + "noRoleSingular": "Atrybut ${data.prohibited} nie jest dobrze obsługiwany przez ${data.nodeName} bez poprawnego atrybutu roli.", + "noRolePlural": "Atrybuty ${data.prohibited} nie są dobrze obsługiwane przez ${data.nodeName} bez poprawnego atrybutu roli." + } }, "aria-required-attr": { "pass": "Wszystkie wymagane atrybuty ARIA istnieją.", @@ -447,7 +500,8 @@ "pass": "Wymagane dzieci ARIA istnieją.", "fail": { "singular": "Wymagana rola dziecka ARIA nie istnieje: ${data.values}.", - "plural": "Wymagane role dzieci ARIA nie istnieją: ${data.values}." + "plural": "Wymagane role dzieci ARIA nie istnieją: ${data.values", + "unallowed": "Element ma dzieci, które nie są dozwolone: ${data.values}" }, "incomplete": { "singular": "Należy dodać oczekiwaną rolę dziecka ARIA: ${data.values}.", @@ -473,12 +527,15 @@ "aria-valid-attr-value": { "pass": "Wartości atrybutu ARIA są poprawne.", "fail": { - "singular": "Niepoprawna wartość atrybutu ARIA: ${data.values}.", - "plural": "Niepoprawne wartości atrybutu ARIA: ${data.values}." + "singular": "Niepoprawna wartość atrybutu ARIA: ${data.values}", + "plural": "Niepoprawne wartości atrybutu ARIA: ${data.values}" }, "incomplete": { - "noId": "Identyfikator elementu atrybutu ARIA nie istnieje na stronie: ${data.needsReview}.", - "ariaCurrent": "Wartość atrybutu ARIA jest niepoprawna i będzie traktowana jako aria-current=true: ${data.needsReview}." + "noId": "Identyfikator elementu atrybutu ARIA nie istnieje na stronie: ${data.needsReview}", + "noIdShadow": "ID elementu atrybutu ARIA nie istnieje na stronie lub jest potomkiem innego drzewa shadow DOM: ${data.needsReview}", + "ariaCurrent": "Wartość atrybutu ARIA jest niepoprawna i będzie traktowana jako \"aria-current=true\": ${data.needsReview}", + "idrefs": "Nie można określić, czy atrybut ARIA element ID istnieje na stronie: ${data.needsReview}", + "empty": "Wartość atrybutu ARIA jest ignorowana, gdy jest pusty: ${data.needsReview}" } }, "aria-valid-attr": { @@ -488,9 +545,26 @@ "plural": "Niepoprawne nazwy atrybutów ARIA: ${data.values}." } }, + "braille-label-equivalent": { + "pass": "Atrybut aria-braillelabel jest użyty w elemencie z dostępnym tekstem.", + "fail": "Atrybut aria-braillelabel jest użyty w elemencie, który nie ma dostępnego tekstu.", + "incomplete": "Nie można wyliczyć dostępnego tekstu." + }, + "braille-roledescription-equivalent": { + "pass": "Atrybut aria-brailleroledescription nie jest używany w elemencie, który nie ma dostępnego tekstu.", + "fail": { + "noRoleDescription": "Atrybut aria-brailleroledescription jest użyty w elemencie bez atrybutu aria-roledescription.", + "emptyRoleDescription": "Atrybut aria-brailleroledescription jest użyty w elemencie z pustym atrybutem aria-roledescription." + } + }, + "deprecatedrole": { + "pass": "Rola ARIA nie jest przestarzała", + "fail": "Użyta rola jest przestarzała: ${data}" + }, "fallbackrole": { "pass": "Użyto tylko jednej wartości roli.", - "fail": "Użyj tylko jednej wartości roli, ponieważ role rezerwowe nie są obsługiwane w starszych przeglądarkach." + "fail": "Użyj tylko jednej wartości roli, ponieważ role rezerwowe nie są obsługiwane w starszych przeglądarkach.", + "incomplete": "Używaj tylko roli 'presentation' lub 'none', ponieważ są one synonimami." }, "has-global-aria-attribute": { "pass": { @@ -515,8 +589,8 @@ "fail": "Element nie przyjmuje fokusu." }, "no-implicit-explicit-label": { - "pass": "Nie ma rozbieżności między <label> a dostępną nazwą.", - "incomplete": "Sprawdź, czy <label> nie musi być częścią nazwy pola ARIA ${data}." + "pass": "Nie ma rozbieżności między label a dostępną nazwą.", + "incomplete": "Sprawdź, czy label nie musi być częścią nazwy pola ARIA ${data}." }, "unsupportedrole": { "pass": "Rola ARIA jest obsługiwana.", @@ -526,9 +600,13 @@ "pass": "Element w porządku otrzymywania fokusu ma poprawną semantykę.", "fail": "Element w porządku otrzymywania fokusu ma niepoprawną semantykę." }, - "color-contrast": { - "pass": "Element ma wystarczający kontrast kolorów ${data.contrastRatio}.", - "fail": "Element ma niewystarczający kontrast kolorów: ${data.contrastRatio} (foreground color: ${data.fgColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}.", + "color-contrast-enhanced": { + "pass": "Element ma wystarczający kontrast kolorów ${data.contrastRatio}", + "fail": { + "default": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} (kolor pierwszego planu: ${data.fgColor}, kolor tła: ${data.bgColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}", + "fgOnShadowColor": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} pomiędzy kolorem pierwszego planu a kolorem cienia tekstu (kolor pierwszego planu: ${data.fgColor}, kolor cienia tekstu: ${data.shadowColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}", + "shadowOnBgColor": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} pomiędzy kolorem cienia tekstu a kolorem tła (kolor cienia tekstu: ${data.shadowColor}, kolor tła: ${data.bgColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}" + }, "incomplete": { "default": "Nie można określić współczynnika kontrastu.", "bgImage": "Nie można określić koloru tła elementu, ponieważ element ma obraz tła.", @@ -545,9 +623,16 @@ "pseudoContent": "Nie można określić koloru tła elementu, ponieważ jest to pseudoelement." } }, - "color-contrast-enhanced": { - "pass": "Element ma wystarczający kontrast kolorów ${data.contrastRatio}.", - "fail": "Element ma niewystarczający kontrast kolorów: ${data.contrastRatio} (foreground color: ${data.fgColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}.", + "color-contrast": { + "pass": { + "default": "Element ma wystarczający kontrast kolorów ${data.contrastRatio}", + "hidden": "Element jest ukryty" + }, + "fail": { + "default": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} (kolor pierwszego planu: ${data.fgColor}, kolor tła: ${data.bgColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}", + "fgOnShadowColor": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} pomiędzy kolorem pierwszego planu a kolorem cienia tekstu (kolor pierwszego planu: ${data.fgColor}, kolor cienia tekstu: ${data.shadowColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}", + "shadowOnBgColor": "Element ma niewystarczający kontrast kolorów ${data.contrastRatio} pomiędzy kolorem cienia tekstu a kolorem tła (kolor cienia tekstu: ${data.shadowColor}, kolor tła: ${data.bgColor}, rozmiar czcionki: ${data.fontSize}, waga czcionki: ${data.fontWeight}). Oczekiwany współczynnik kontrastu: ${data.expectedContrastRatio}" + }, "incomplete": { "default": "Nie można określić współczynnika kontrastu.", "bgImage": "Nie można określić koloru tła elementu, ponieważ element ma obraz tła.", @@ -564,9 +649,20 @@ "pseudoContent": "Nie można określić koloru tła elementu, ponieważ jest to pseudoelement." } }, + "link-in-text-block-style": { + "pass": "Łącza mogą być odróżnione od sąsiadującego tekstu poprzez wizualną stylizację", + "incomplete": { + "default": "Sprawdź, czy łącze wymaga stylizacji, aby odróżnić je od sąsiadującego tekstu.", + "pseudoContent": "Sprawdź, czy pseudostyl łącza jest wystarczający, aby odróżnić je od sąsiadującego tekstu." + }, + "fail": "Łącze nie ma żadnej stylizacji (np. podkreślenia), która odróżniałaby je od sąsiadującego tekstu" + }, "link-in-text-block": { - "pass": "Łącza można odróżnić od otaczającego je tekstu w inny sposób niż za pomocą koloru.", - "fail": "Łącza muszą być odróżnione od otaczającego je tekstu w inny sposób niż za pomocą koloru.", + "pass": "Łącza można odróżnić od sąsiadującego tekstu w inny sposób niż za pomocą koloru.", + "fail": { + "fgContrast": "Łącze ma niewystarczający kontrast kolorów ${data.contrastRatio}:1 z sąsiadującym tekstem. (Minimalny kontrast to: ${data.requiredContrastRatio}:1, kolor tekstu łącza: ${data.nodeColor}, kolor sąsiadującego tekstu: ${data.parentColor})", + "bgContrast": "Tło łącza ma niewystarczający kontrast kolorów ${data.contrastRatio} (Minimalny kontrast to: ${data.requiredContrastRatio}:1, kolor tła łącza: ${data.nodeBackgroundColor}, sąsiadujący kolor tła: ${data.parentBackgroundColor})" + }, "incomplete": { "default": "Nie można określić współczynnika kontrastu.", "bgContrast": "Nie można określić współczynnika kontrastu elementu. Sprawdź, czy nie ma odrębnego stylu stanów hover/fokus.", @@ -577,8 +673,8 @@ } }, "autocomplete-appropriate": { - "pass": "Wartość autocomplete jest odpowiednia dla tego typu pola wejściowego.", - "fail": "Wartość autocomplete jest niewłaściwa dla tego typu pola wejściowego." + "pass": "Wartość autocomplete jest odpowiednia dla tego typu pola formularza.", + "fail": "Wartość autocomplete jest niewłaściwa dla tego typu pola formularza." }, "autocomplete-valid": { "pass": "Atrybut autocomplete jest sformatowany poprawnie.", @@ -594,6 +690,7 @@ }, "focusable-disabled": { "pass": "W elemencie nie ma elementów przyjmujących fokus.", + "incomplete": "Sprawdź, czy elementy, na których można ustawić fokus, natychmiast otrzymują wskaźnik fokusu", "fail": "Treść z możliwością ustawiania fokusu powinna być wyłączona lub usunięta z DOM." }, "focusable-element": { @@ -611,6 +708,7 @@ }, "focusable-not-tabbable": { "pass": "Nie ma elementów przyjmujących fokus wewnątrz elementu.", + "incomplete": "Sprawdź, czy elementy, na których można ustawić fokus, natychmiast otrzymują wskaźnik fokusu", "fail": "Treść przyjmująca fokus powinna mieć tabindex=-1 lub być usunięta z DOM." }, "frame-focusable-content": { @@ -624,7 +722,10 @@ }, "no-focusable-content": { "pass": "Element nie ma potomków przyjmujących fokus", - "fail": "Element ma elementy potomne przyjmujące fokus", + "fail": { + "default": "Element ma elementy potomne przyjmujące fokus", + "notHidden": "Użycie ujemnej wartości tabindex elementu wewnątrz interaktywnej kontrolki nie zapobiega ustawianiu na elemencie fokusu przez technologie wspomagające (nawet z 'aria-hidden=true')" + }, "incomplete": "Nie można ustalić, czy element ma elementy potomne" }, "page-has-heading-one": { @@ -656,35 +757,35 @@ "fail": "Element ma atrybut alt zawierający tylko znak spacji, który nie przez wszystkie czytniki ekranu jest ignorowany ." }, "duplicate-img-label": { - "pass": "Element nie powiela tekstu istniejącego w atrybucie alt elementu <img>.", - "fail": "Element powiela tekst istniejący w atrybucie alt elementu <img>." + "pass": "Element nie powiela tekstu istniejącego w atrybucie alt elementu img.", + "fail": "Element powiela tekst istniejący w atrybucie alt elementu img." }, "explicit-label": { - "pass": "Element formularza ma jawnie określoną <label>.", - "fail": "Element formularza nie ma jawnie określonej <label>.", - "incomplete": "Nie można określić, czy element formularza ma jawnie określoną <label>." + "pass": "Element formularza ma jawnie określoną label.", + "fail": "Element formularza nie ma jawnie określonej label.", + "incomplete": "Nie można określić, czy element formularza ma jawnie określoną label." }, "help-same-as-label": { "pass": "Tekst pomocy (title lub aria-describedby) nie powiela tekstu etykiety.", "fail": "Tekst pomocy (title lub aria-describedby) jest taki sam jak tekst etykiety." }, "hidden-explicit-label": { - "pass": "Element formularza ma widoczną jednoznaczną <label>.", - "fail": "Element formularza ma jednoznaczną <label>, która jest ukryta.", - "incomplete": "Nie można określić, czy element formularza ma jednoznaczną ukrytą etykietę (<label>)." + "pass": "Element formularza ma widoczną jednoznaczną etykietę label.", + "fail": "Element formularza ma jednoznaczną etykietę label, która jest ukryta.", + "incomplete": "Nie można określić, czy element formularza ma jawną etykietę (label), gdy jest ukryta." }, "implicit-label": { - "pass": "Element formularza ma dorozumianą etykietę (jest owinięty w <label>).", - "fail": "Element formularza nie ma dorozumianej etykiety (nie jest owinięty w <label>).", - "incomplete": "Nie można określić, czy element formularza ma dorozumianą etykietę (jest owinięty w <label>)." + "pass": "Element formularza ma dorozumianą etykietę (jest owinięty w label).", + "fail": "Element formularza nie ma dorozumianej etykiety (nie jest owinięty w label).", + "incomplete": "Nie można określić, czy element formularza ma dorozumianą etykietę (jest owinięty w label)." }, "label-content-name-mismatch": { "pass": "Widoczny tekst elementu jest częścią dostępnej nazwy elementu.", - "fail": "Tekst wewnątrz elementu nie jest częścią dostępnej nazwy." + "fail": "Widoczny tekst wewnątrz elementu nie jest częścią dostępnej nazwy." }, "multiple-label": { - "pass": "Pole formularza nie ma wielu <label>.", - "incomplete": "Technologie wspomagające nie obsługują wystarczająco dobrze wielu elementów <label>. Upewnij się, że pierwsza etykieta zawiera wszystkie niezbędne informacje." + "pass": "Pole formularza nie ma wielu label.", + "incomplete": "Technologie wspomagające nie obsługują wystarczająco dobrze wielu elementów label. Upewnij się, że pierwsza etykieta zawiera wszystkie niezbędne informacje." }, "title-only": { "pass": "Element formularza nie używa wyłącznie atrybutu title jako swojej etykiety.", @@ -702,7 +803,7 @@ } }, "valid-lang": { - "pass": "Wartość atrybutu lang znajduje się na liście poprawnych kodów języków.", + "pass": "Wartość atrybutu lang jest na liście poprawnych kodów języków.", "fail": "Wartości atrybutu lang nie ma na liście poprawnych kodów języków." }, "xml-lang-mismatch": { @@ -710,30 +811,27 @@ "fail": "Atrybuty lang i xml:lang nie mają tego samego języka podstawowego." }, "dlitem": { - "pass": "Element listy opisowej ma element nadrzędny <dl>.", - "fail": "Pozycja listy opisowej nie ma elementu nadrzędnego <dl>." + "pass": "Element listy opisowej ma element nadrzędny dl.", + "fail": "Pozycja listy opisowej nie ma elementu nadrzędnego dl." }, "listitem": { - "pass": "Element listy ma <ul>, <ol> lub role=\"list\" jako bezpośredni element rodzicielski.", + "pass": "Element listy ma ul, ol lub role=\"list\" jako bezpośredni element rodzicielski.", "fail": { - "default": "Element listy nie ma nadrzędnego elementu <ul> lub <ol>.", - "roleNotValid": "Element listy nie ma nadrzędnego elementu <ul>, <ol>, ani nadrzędnego elementu z role=\"list\"." + "default": "Element listy nie ma nadrzędnego elementu ul lub ol.", + "roleNotValid": "Element listy nie ma nadrzędnego elementu ul, ol, ani nadrzędnego elementu z role=\"list\"." } }, "only-dlitems": { - "pass": "Element <dl> ma wewnątrz tylko dozwolone jako bezpośrednie elementy potomne (dzieci) elementy <dt> lub <dd> .", + "pass": "Element dl ma wewnątrz tylko dozwolone jako bezpośrednie elementy potomne (dzieci) elementy dt lub dd.", "fail": "Element listy ma wewnątrz bezpośrednie elementy dzieci, które nie są dozwolone wewnątrz listy opisowej." }, "only-listitems": { - "pass": "Element lista ma wewnątrz tylko dozwolone jako bezpośrednie elementy potomne (dzieci) elementy <li>.", - "fail": { - "default": "Element lista ma niedozwolone bezpośrednie elementy potomne (dzieci), poza elementami <li>.", - "roleNotValid": "Element lista ma bezpośrednie elementy potomne (dzieci) z rolą, która nie jest dozwolona: ${data.role}." - } + "pass": "Element lista ma wewnątrz tylko dozwolone jako bezpośrednie elementy potomne (dzieci) elementy li.", + "fail": "Element listy ma bezpośrednie elementy potomne, które nie są dozwolone: ${data.values}" }, "structured-dlitems": { - "pass": "Gdy elemet <dl> nie jest pusty, ma zarówno elementy <dt>, jak i <dd>.", - "fail": "Element nie jest pusty, ale nie ma co najmniej jednego elementu <dt>, po którym następuje co najmniej jeden element <dd>." + "pass": "Gdy elemet dl nie jest pusty, ma zarówno elementy dt, jak i dd.", + "fail": "Element nie jest pusty, ale nie ma co najmniej jednego elementu dt, po którym następuje co najmniej jeden element dd." }, "caption": { "pass": "Element multimedialny ma ścieżkę z napisami rozszerzonymi.", @@ -762,13 +860,38 @@ "pass": "Znacznik <meta> nie wyłącza powiększania na urządzeniach przenośnych.", "fail": "${data} w znaczniku <meta> wyłącza powiększanie na urządzeniach przenośnych." }, + "target-offset": { + "pass": "Cel ma wystarczające odsunięcie od swojego najbliższego sąsiada: (${data.closestOffset}px, które powinno wynosić co najmniej ${data.minOffset}px)", + "fail": "Cel ma niewystarczające odsunięcie od najbliższego sąsiada: (${data.closestOffset}px, które powinno wynosić co najmniej ${data.minOffset}px)", + "incomplete": { + "default": "Element z ujemnym tabindeksem ma niewystarczające odsunięcie od najbliższego sąsiada: (${data.closestOffset}px, które powinno wynosić co najmniej${data.minOffset}px). Czy to jest cel?", + "nonTabbableNeighbor": "Cel ma niewystarczające odsunięcie od sąsiada z ujemnym tabindeksem: (${data.closestOffset}px, które powinno wynosić co najmniej ${data.minOffset}px). Czy sąsiad jest celem?" + } + }, + "target-size": { + "pass": { + "default": "Kontrolka ma wystarczający rozmiar (${data.width}px na ${data.height}px, który powinnien wynosić co najmniej ${data.minSize}px na ${data.minSize}px)", + "obscured": "Kontrolka jest ignorowana, ponieważ jest całkowicie zasłonięta i nie można jej kliknąć." + }, + "fail": { + "default": "Cel ma niewystarczający rozmiar (${data.width}px na ${data.height}px, który powinnien wynosić co najmniej ${data.minSize}px na ${data.minSize}px)", + "partiallyObscured": "Cel ma niewystarczający rozmiar, ponieważ jest częściowo przesłonięty (najmniejsza przestrzeń to ${data.width}px na ${data.height}px, powinna wynosić ${data.minSize}px na ${data.minSize}px)" + }, + "incomplete": { + "default": "Element z ujemnym tabindex ma niewystarczający rozmiar (${data.width}px na ${data.height}px, powinnien wynosić co najmniej ${data.minSize}px na ${data.minSize}px). Czy to jest cel?", + "contentOverflow": "Rozmiar elementu nie mógł być dokładnie określony z powodu przepełnienia zawartości", + "partiallyObscured": "Element z ujemnym tabindex ma niewystarczający rozmiar, ponieważ jest częściowo przesłonięty (najmniejsza przestrzeń to ${data.width}px na ${data.height}px, powinna wynosić co najmniej ${data.minSize}px na ${data.minSize}px). Czy to jest cel?", + "partiallyObscuredNonTabbable": "Cel ma niewystarczający rozmiar, ponieważ jest częściowo przesłonięty przez sąsiada o ujemnym tabindeksie (najmniejsza przestrzeń to ${data.width}px na ${data.height}px, powinna wynosić co najmniej ${data.minSize}px na ${data.minSize}px). Czy sąsiad jest celem?" + } + }, "header-present": { "pass": "Strona ma nagłówek.", "fail": "Strona nie ma nagłówka." }, "heading-order": { "pass": "Kolejność nagłówków jest poprawna.", - "fail": "Kolejność nagłówków jest niepoprawna." + "fail": "Kolejność nagłówków jest niepoprawna.", + "incomplete": "Nie można określić poprzedniego nagłówka" }, "identical-links-same-purpose": { "pass": "Nie ma żadnych innych łączy o tej samej nazwie, które kierują na inny adres URL.", @@ -782,13 +905,18 @@ "pass": "Strona ma punkt orientacyjny (obszar kluczowy).", "fail": "Strona nie ma żadnego punktu orientacyjnego (obszaru kluczowego)." }, + "meta-refresh-no-exceptions": { + "pass": "Znacznik <meta> nie powoduje natychmiastowego odświeżenia strony", + "fail": "Znacznik <meta> tag wymusza czasowe odświeżenie strony" + }, "meta-refresh": { "pass": "Znacznik <meta> nie odświeża od razu strony.", "fail": "Znacznik <meta> wymusza odświeżenie strony." }, "p-as-heading": { "pass": "Elementy <p> nie są stylizowane na nagłówki.", - "fail": "Zamiast stylizowanych na nagłówki elementów p muszą być używane nagłówki semantyczne." + "fail": "Zamiast stylizowanych na nagłówki elementów <p> muszą być użyte nagłówki semantyczne.", + "incomplete": "Nie można określić, czy elementy <p> są stylizowane na nagłówki" }, "region": { "pass": "Cała treść strony jest zawarta w obszarach kluczowych.", @@ -825,16 +953,16 @@ "incomplete": "Spowoduj, aby istniał element, do którego istnieje odwołanie w atrybucie aria-labelldeby." }, "avoid-inline-spacing": { - "pass": "Nie określono żadnych stylów inline z '!important', które wpływają na odstępy w tekście.", + "pass": "Nie określono żadnych stylów wewnętrznych (inline) z '!important', które wpływają na odstępy w tekście.", "fail": { "singular": "Usuń dyrektywę '!important' ze stylu inline ${data.values}, ponieważ nadpisywanie tego nie jest obsługiwane przez większość przeglądarek.", "plural": "Usuń dyrektywy '!important' ze stylów inline ${data.values}, ponieważ nadpisywanie tego nie jest obsługiwane przez większość przeglądarek." } }, "button-has-visible-text": { - "pass": "Element ma tekst wewnętrzny widoczny dla czytników ekranu.", + "pass": "Element ma tekst wewnętrzny widoczny dla czytników ekranu.", "fail": "Element nie ma wewnętrznego tekstu, który jest widoczny dla czytników ekranu.", - "incomplete": "Nie można określić, czy element ma elementy dzieci." + "incomplete": "Nie można określić, czy element ma elementy potomne." }, "doc-has-title": { "pass": "Dokument ma niepusty element <title>.", @@ -853,6 +981,18 @@ "fail": "Element nie ma tekstu, który jest widoczny dla czytników ekranu.", "incomplete": "Nie można określić, czy element ma elementy dzieci." }, + "important-letter-spacing": { + "pass": "Odstępy między literami (letter-spacing) w atrybucie style nie są ustawione na !important lub spełniają minimum", + "fail": "Odstępy między literami w atrybucie style nie mogą używać !important lub muszą mieć ${data.minValue}em (aktualnie: ${data.value}em)" + }, + "important-line-height": { + "pass": "Właściwość line-height w atrybucie style nie jest ustawiona na !important lub spełnia minimum.", + "fail": "Właściwość line-height w atrybucie style nie może używać !important lub musi mieć ${data.minValue}em (aktualnie: ${data.value}em)." + }, + "important-word-spacing": { + "pass": "Odstępy miedzy wyrazami (word-spacing) w atrybucie style nie są ustawione na !important lub spełniają minimum", + "fail": "Odstępy miedzy wyrazami (word-spacing) w atrybucie style nie mogą używać !important lub muszą mieć ${data.minValue}em (aktualnie: ${data.value}em)" + }, "is-on-screen": { "pass": "Element nie jest widoczny.", "fail": "Element jest widoczny." @@ -898,7 +1038,8 @@ "default": "Domyślna semantyka elementu nie została nadpisana przez role=\"none\" ani role=\"presentation\".", "globalAria": "Rola elementu nie jest prezentacyjna, ponieważ ma on ogólny atrybut ARIA.", "focusable": "Rola elementu nie jest prezentacyjna, ponieważ może on przyjmować fokus.", - "both": "Rola elementu nie jest prezentacyjna, ponieważ ma on ogólny atrybut ARIA i może przyjmować fokus." + "both": "Rola elementu nie jest prezentacyjna, ponieważ ma on ogólny atrybut ARIA i może przyjmować fokus.", + "iframe": "Użycie atrybutu \"title\" na elemencie ${data.nodeName} z rolą prezentacyjną zachowuje się niespójnie pomiędzy czytnikami ekranu." } }, "role-none": { @@ -913,7 +1054,7 @@ "pass": "Element ma dziecko, które jest tytułem.", "fail": { "noTitle": "Element nie ma dziecka, które jest tytułem.", - "emptyTitle": "Tytuł elementu dziecka jest pusty." + "emptyTitle": "Element title dziecka jest pusty." }, "incomplete": "Nie można ustalić, czy element ma dziecko, które jest tytułem." }, @@ -927,7 +1068,8 @@ }, "same-caption-summary": { "pass": "Treści atrybutu summary i elementu <caption> nie są powielane.", - "fail": "Treści atrybutu summary i elementu <caption> są identyczne." + "fail": "Treści atrybutu summary i elementu <caption> są identyczne.", + "incomplete": "Nie można określić, czy element <table> ma caption" }, "scope-value": { "pass": "Atrybut scope ma poprawną wartość.", diff --git a/package-lock.json b/package-lock.json index 9d8655064d..fe85d3e9d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "axe-core", - "version": "4.7.2", + "version": "4.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "axe-core", - "version": "4.7.2", + "version": "4.8.0", "license": "MPL-2.0", "devDependencies": { "@axe-core/webdriverjs": "^4.5.2", @@ -44,6 +44,7 @@ "html-entities": "1.x", "http-server": "^14.1.1", "husky": "^8.0.3", + "inquirer": "^8.2.5", "jquery": "^3.6.3", "jsdoc": "^3.6.11", "jsdom": "^21.0.0", @@ -63,6 +64,7 @@ "mocha": "^10.2.0", "node-notifier": "^10.0.1", "npm-run-all": "^4.1.5", + "outdent": "^0.8.0", "prettier": "^2.8.2", "proxyquire": "^2.1.3", "revalidator": "^0.3.1", @@ -2635,6 +2637,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -2671,6 +2693,31 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -2858,6 +2905,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -3001,6 +3072,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -3101,6 +3178,18 @@ "node": ">=8" } }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -3117,6 +3206,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -3977,6 +4075,27 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -4998,6 +5117,32 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -6440,6 +6585,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -6511,6 +6676,61 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -6702,6 +6922,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -8508,6 +8737,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -9115,6 +9350,29 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -9143,6 +9401,12 @@ "os-tmpdir": "^1.0.0" } }, + "node_modules/outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10180,6 +10444,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11625,6 +11898,15 @@ "resolved": "git+ssh://git@github.com/w3c/wcag-act-rules.git#2341a1ba4d47bc4cdccd5a3b7534e67b52c59002", "dev": true }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/weakmap-polyfill": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/weakmap-polyfill/-/weakmap-polyfill-2.0.4.tgz", @@ -13964,6 +14246,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -13991,6 +14279,30 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -14142,6 +14454,16 @@ "update-browserslist-db": "^1.0.9" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -14242,6 +14564,12 @@ "supports-color": "^7.1.0" } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -14311,6 +14639,12 @@ "restore-cursor": "^3.1.0" } }, + "cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true + }, "cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -14321,6 +14655,12 @@ "string-width": "^5.0.0" } }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -15003,6 +15343,23 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + } + } + }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -15784,6 +16141,28 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -16875,6 +17254,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -16931,6 +17316,54 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -17059,6 +17492,12 @@ "is-extglob": "^2.1.1" } }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -18419,6 +18858,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -18884,6 +19329,23 @@ "word-wrap": "^1.2.3" } }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -18906,6 +19368,12 @@ "os-tmpdir": "^1.0.0" } }, + "outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -19686,6 +20154,12 @@ "glob": "^7.1.3" } }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20805,6 +21279,15 @@ "dev": true, "from": "wcag-act-rules@github:w3c/wcag-act-rules#2341a1b" }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "weakmap-polyfill": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/weakmap-polyfill/-/weakmap-polyfill-2.0.4.tgz", diff --git a/package.json b/package.json index 3caafd20e6..ed49ca6607 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "axe-core", "description": "Accessibility engine for automated Web UI testing", - "version": "4.7.2", + "version": "4.8.0", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -66,7 +66,7 @@ } }, "scripts": { - "start": "http-server -p 9876 --silent", + "start": "http-server -a \"\" -p 9876 --silent", "develop": "grunt dev --force", "api-docs": "jsdoc --configure .jsdoc.json", "build": "grunt", @@ -144,6 +144,7 @@ "html-entities": "1.x", "http-server": "^14.1.1", "husky": "^8.0.3", + "inquirer": "^8.2.5", "jquery": "^3.6.3", "jsdoc": "^3.6.11", "jsdom": "^21.0.0", @@ -163,6 +164,7 @@ "mocha": "^10.2.0", "node-notifier": "^10.0.1", "npm-run-all": "^4.1.5", + "outdent": "^0.8.0", "prettier": "^2.8.2", "proxyquire": "^2.1.3", "revalidator": "^0.3.1", diff --git a/sri-history.json b/sri-history.json index 9dfb98395f..81c7ee8286 100644 --- a/sri-history.json +++ b/sri-history.json @@ -350,5 +350,9 @@ "4.7.2": { "axe.js": "sha256-EE+7F84cDAG4yvmWb/Cbb3WmQaNAGj4FwU4cKxJZqjg=", "axe.min.js": "sha256-VTWaX4yE65/ofnyeohwMLLKvU1GEYDoWOCbKlcqFMs8=" + }, + "4.8.0": { + "axe.js": "sha256-Rd1U88z7mahhAvxYhchoCfwWxV8R/H+BcSoX6XTQ0gY=", + "axe.min.js": "sha256-1oZiCfWnUt2rwqLWzpkoxlcEaOH53/QFYke0jo+KecA=" } } diff --git a/test/act-rules/aria-state-or-property-permitted-5c01ea.spec.js b/test/act-rules/aria-state-or-property-permitted-5c01ea.spec.js index 6a2e047d12..10b74e3fa7 100644 --- a/test/act-rules/aria-state-or-property-permitted-5c01ea.spec.js +++ b/test/act-rules/aria-state-or-property-permitted-5c01ea.spec.js @@ -1,5 +1,5 @@ require('./act-runner.js')({ id: '5c01ea', title: 'ARIA state or property is permitted', - axeRules: ['aria-allowed-attr'] + axeRules: ['aria-allowed-attr', 'aria-prohibited-attr'] }); diff --git a/test/assets/ZeroWidth0Char.woff b/test/assets/ZeroWidth0Char.woff new file mode 100644 index 0000000000..3c26a6e1ed Binary files /dev/null and b/test/assets/ZeroWidth0Char.woff differ diff --git a/test/checks/aria/braille-label-equivalent.js b/test/checks/aria/braille-label-equivalent.js new file mode 100644 index 0000000000..b98439bc28 --- /dev/null +++ b/test/checks/aria/braille-label-equivalent.js @@ -0,0 +1,51 @@ +describe('braille-label-equivalent tests', () => { + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkContext = axe.testUtils.MockCheckContext(); + const checkEvaluate = getCheckEvaluate('braille-label-equivalent'); + + afterEach(() => { + checkContext.reset(); + }); + + it('returns true without aria-braillelabel', () => { + const params = checkSetup('<img id="target" alt="" />'); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-braillelabel is empty', () => { + const params = checkSetup( + '<img id="target" alt="" aria-braillelabel="" />' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-braillelabel is whitespace-only', () => { + const params = checkSetup( + '<img id="target" alt="" aria-braillelabel=" \r\t\n " />' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + describe('when aria-braillelabel has text', () => { + it('returns false when the accessible name is empty', () => { + const params = checkSetup(` + <img id="target" alt="" aria-braillelabel="foo" /> + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('returns false when the accessible name has only whitespace', () => { + const params = checkSetup(` + <img id="target" alt=" \r\t\n " aria-braillelabel="foo" /> + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when the accessible name is not empty', () => { + const params = checkSetup(` + <img id="target" alt="foo" aria-braillelabel="foo" /> + `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + }); +}); diff --git a/test/checks/aria/braille-roledescription-equivalent.js b/test/checks/aria/braille-roledescription-equivalent.js new file mode 100644 index 0000000000..3dda24c32d --- /dev/null +++ b/test/checks/aria/braille-roledescription-equivalent.js @@ -0,0 +1,80 @@ +describe('braille-roledescription-equivalent tests', () => { + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkContext = axe.testUtils.MockCheckContext(); + const checkEvaluate = getCheckEvaluate('braille-roledescription-equivalent'); + + afterEach(() => { + checkContext.reset(); + }); + + it('returns true without aria-brailleroledescription', () => { + const params = checkSetup('<div id="target"></div>'); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-brailleroledecription is empty', () => { + const params = checkSetup( + '<div id="target" aria-brailleroledescription=""></div>' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-brailleroledecription is whitespace-only', () => { + const params = checkSetup( + '<div id="target" aria-brailleroledescription=" \r\t\n "></div>' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + describe('when aria-brailleroledescription has text', () => { + it('returns false without aria-roledescription', () => { + const params = checkSetup(` + <div + id="target" + aria-brailleroledescription="foo" + ></div> + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { messageKey: 'noRoleDescription' }); + }); + + it('returns false when aria-roledescription is empty', () => { + const params = checkSetup(` + <div + id="target" + aria-roledescription="" + aria-brailleroledescription="foo" + ></div> + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'emptyRoleDescription' + }); + }); + + it('returns false when aria-roledescription has only whitespace', () => { + const params = checkSetup(` + <div + id="target" + aria-roledescription=" \r\t\n " + aria-brailleroledescription="foo" + ></div> + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'emptyRoleDescription' + }); + }); + + it('returns true when aria-roledescription is not empty', () => { + const params = checkSetup(` + <div + id="target" + aria-roledescription="foo" + aria-brailleroledescription="foo" + ></div> + `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + }); +}); diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index 1b7165b127..fe5a15ff82 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -69,7 +69,12 @@ describe('aria-required-children', () => { it('should pass all existing required children when all required', () => { const params = checkSetup( - '<div id="target" role="menu"><li role="none"></li><li role="menuitem">Item 1</li><div role="menuitemradio">Item 2</div><div role="menuitemcheckbox">Item 3</div></div>' + `<div id="target" role="menu"> + <li role="none"></li> + <li role="menuitem">Item 1</li> + <div role="menuitemradio">Item 2</div> + <div role="menuitemcheckbox">Item 3</div> + </div>` ); assert.isTrue(requiredChildrenCheck.apply(checkContext, params)); }); @@ -293,6 +298,20 @@ describe('aria-required-children', () => { assert.isTrue(requiredChildrenCheck.apply(checkContext, params)); }); + it('should ignore hidden children inside the group', () => { + const params = checkSetup(` + <div role="menu" id="target"> + <ul role="group"> + <li style="display: none">hidden</li> + <li aria-hidden="true">hidden</li> + <li style="visibility: hidden" aria-hidden="true">hidden</li> + <li role="menuitem">Menuitem</li> + </ul> + </div> + `); + assert.isTrue(requiredChildrenCheck.apply(checkContext, params)); + }); + it('should fail when role allows group and group does not have required child', () => { const params = checkSetup( '<div role="menu" id="target"><ul role="group"><li>Menuitem</li></ul></div>' @@ -329,19 +348,6 @@ describe('aria-required-children', () => { }); describe('options', () => { - it('should return undefined instead of false when the role is in options.reviewEmpty', () => { - const params = checkSetup('<div role="grid" id="target"></div>', { - reviewEmpty: [] - }); - assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); - - // Options: - params[1] = { - reviewEmpty: ['grid'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); - it('should not throw when options is incorrect', () => { const params = checkSetup('<div role="row" id="target"></div>'); @@ -358,88 +364,110 @@ describe('aria-required-children', () => { assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); }); - it('should return undefined when the element has empty children', () => { - const params = checkSetup( - '<div role="listbox" id="target"><div></div></div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); + describe('reviewEmpty', () => { + it('should return undefined instead of false when the role is in options.reviewEmpty', () => { + const params = checkSetup('<div role="grid" id="target"></div>', { + reviewEmpty: [] + }); + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + + // Options: + params[1] = { + reviewEmpty: ['grid'] + }; + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return false when the element has empty child with role', () => { - const params = checkSetup( - '<div role="listbox" id="target"><div role="grid"></div></div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return undefined when the element has empty children', () => { + const params = checkSetup( + '<div role="listbox" id="target"><div></div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return undefined when there is a empty text node', () => { - const params = checkSetup( - '<div role="listbox" id="target"> <!-- empty --> \n\t </div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return false when the element has empty child with role', () => { + const params = checkSetup( + '<div role="listbox" id="target"><div role="grid"></div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return false when there is a non-empty text node', () => { - const params = checkSetup( - '<div role="listbox" id="target"> hello </div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return undefined when there is a empty text node', () => { + const params = checkSetup( + '<div role="listbox" id="target"> <!-- empty --> \n\t </div>', + { reviewEmpty: ['listbox'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return undefined when the element has empty child with role=presentation', () => { - const params = checkSetup( - '<div role="listbox" id="target"><div role="presentation"></div></div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return false when there is a non-empty text node', () => { + const params = checkSetup( + '<div role="listbox" id="target"> hello </div>', + { reviewEmpty: ['listbox'] } + ); + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return undefined when the element has empty child with role=none', () => { - const params = checkSetup( - '<div role="listbox" id="target"><div role="none"></div></div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return undefined when the element has empty child with role=presentation', () => { + const params = checkSetup( + '<div role="listbox" id="target"><div role="presentation"></div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return undefined when the element has hidden children', () => { - const params = checkSetup( - `<div role="menu" id="target"> - <div role="menuitem" hidden></div> - <div role="none" hidden></div> - <div role="list" hidden></div> - </div>` - ); - params[1] = { - reviewEmpty: ['menu'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); - }); + it('should return false when role=none child has visible content', () => { + const params = checkSetup( + '<div role="listbox" id="target"><div role="none">hello</div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isFalse(requiredChildrenCheck.apply(checkContext, params)); + }); + + it('should return undefined when role=none child has hidden content', () => { + const params = checkSetup( + `<div role="listbox" id="target"> + <div role="none"> + <h1 style="display:none">hello</h1> + <h1 aria-hidden="true">hello</h1> + <h1 style="visibility:hidden" aria-hidden="true">hello</h1> + </div> + </div>`, + { reviewEmpty: ['listbox'] } + ); + + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); + + it('should return undefined when the element has empty child with role=none', () => { + const params = checkSetup( + '<div role="listbox" id="target"><div role="none"></div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); + + it('should return undefined when the element has hidden children', () => { + const params = checkSetup( + `<div role="menu" id="target"> + <div role="menuitem" hidden></div> + <div role="none" hidden></div> + <div role="list" hidden></div> + </div>`, + { reviewEmpty: ['menu'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); - it('should return undefined when the element has empty child and aria-label', () => { - const params = checkSetup( - '<div role="listbox" id="target" aria-label="listbox"><div></div></div>' - ); - params[1] = { - reviewEmpty: ['listbox'] - }; - assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + it('should return undefined when the element has empty child and aria-label', () => { + const params = checkSetup( + '<div role="listbox" id="target" aria-label="listbox"><div></div></div>', + { reviewEmpty: ['listbox'] } + ); + assert.isUndefined(requiredChildrenCheck.apply(checkContext, params)); + }); }); }); }); diff --git a/test/checks/aria/valid-attr-value.js b/test/checks/aria/valid-attr-value.js index 7182f0460c..a9e3a3afcc 100644 --- a/test/checks/aria/valid-attr-value.js +++ b/test/checks/aria/valid-attr-value.js @@ -222,6 +222,10 @@ describe('aria-valid-attr-value', function () { }); describe('null values', function () { + afterEach(() => { + axe.reset(); + }); + it('returns undefined when a boolean attribute is null', function () { var vNode = queryFixture( '<div id="target" role="checkbox" aria-checked></div>' @@ -249,6 +253,15 @@ describe('aria-valid-attr-value', function () { }); it('returns false for empty string values that are not allowed to be empty', function () { + axe.configure({ + standards: { + ariaAttrs: { + 'aria-valuetext': { + allowEmpty: false + } + } + } + }); var vNode = queryFixture( '<div id="target" aria-valuetext="" role="range"></div>' ); diff --git a/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js index d6f5fcb627..312674d66c 100644 --- a/test/checks/color/color-contrast.js +++ b/test/checks/color/color-contrast.js @@ -1020,5 +1020,21 @@ describe('color-contrast', function () { ); assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); + + it('incompletes if text-shadow is only on part of the text', function () { + var params = checkSetup(` + <div id="target" style=" + background-color: #aaa; + color:#666; + text-shadow: 1px 1px #000; + "> Hello world </div> + `); + + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._relatedNodes, []); + assert.deepEqual(checkContext._data, { + messageKey: 'complexTextShadows' + }); + }); }); }); diff --git a/test/checks/color/link-in-text-block-style.js b/test/checks/color/link-in-text-block-style.js index 2fdb94fc4a..cc8e3d812d 100644 --- a/test/checks/color/link-in-text-block-style.js +++ b/test/checks/color/link-in-text-block-style.js @@ -1,67 +1,64 @@ -describe('link-in-text-block-style', function () { - 'use strict'; +describe('link-in-text-block-style', () => { + const fixture = document.getElementById('fixture'); + const shadowSupport = axe.testUtils.shadowSupport; + let styleElm; - var fixture = document.getElementById('fixture'); - var shadowSupport = axe.testUtils.shadowSupport; - var styleElm; - - var checkContext = axe.testUtils.MockCheckContext(); + const checkContext = axe.testUtils.MockCheckContext(); const { queryFixture } = axe.testUtils; const linkInBlockStyleCheck = axe.testUtils.getCheckEvaluate( 'link-in-text-block-style' ); - before(function () { + before(() => { styleElm = document.createElement('style'); document.head.appendChild(styleElm); }); - var defaultStyle = { + const defaultStyle = { color: 'black', textDecoration: 'none' }; - beforeEach(function () { + beforeEach(() => { createStyleString('p', defaultStyle); }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; styleElm.innerHTML = ''; checkContext.reset(); }); - after(function () { + after(() => { styleElm.parentNode.removeChild(styleElm); }); function createStyleString(selector, outerStyle) { // Merge style with the default - var prop; - var styleObj = {}; - for (prop in defaultStyle) { + const styleObj = {}; + for (const prop in defaultStyle) { if (defaultStyle.hasOwnProperty(prop)) { styleObj[prop] = defaultStyle[prop]; } } - for (prop in outerStyle) { + for (const prop in outerStyle) { if (outerStyle.hasOwnProperty(prop)) { styleObj[prop] = outerStyle[prop]; } } - var cssLines = Object.keys(styleObj) - .map(function (prop) { + const cssLines = Object.keys(styleObj) + .map(prop => { // Make camelCase prop dash separated - var cssPropName = prop + const cssPropName = prop .trim() .split(/(?=[A-Z])/g) - .reduce(function (prop, propPiece) { - if (!prop) { + .reduce((name, propPiece) => { + if (!name) { return propPiece; } else { - return prop + '-' + propPiece.toLowerCase(); + return name + '-' + propPiece.toLowerCase(); } }, null); @@ -76,8 +73,8 @@ describe('link-in-text-block-style', function () { function getLinkElm(linkStyle) { // Get a random id and build the style strings - var linkId = 'linkid-' + Math.floor(Math.random() * 100000); - var parId = 'parid-' + Math.floor(Math.random() * 100000); + const linkId = 'linkid-' + Math.floor(Math.random() * 100000); + const parId = 'parid-' + Math.floor(Math.random() * 100000); createStyleString('#' + linkId, linkStyle); createStyleString('#' + parId, {}); @@ -94,14 +91,14 @@ describe('link-in-text-block-style', function () { return document.getElementById(linkId); } - describe('link default state', function () { - beforeEach(function () { + describe('link default state', () => { + beforeEach(() => { createStyleString('a', { textDecoration: 'none' }); }); - it('passes the selected node and closest ancestral block element', function () { + it('passes the selected node and closest ancestral block element', () => { fixture.innerHTML = '<div> <span style="display:block; id="parent">' + ' <p style="display:inline"><a href="" id="link">' + @@ -110,19 +107,19 @@ describe('link-in-text-block-style', function () { '</span> outside block </div>'; axe.testUtils.flatTreeSetup(fixture); - var linkElm = document.getElementById('link'); + const linkElm = document.getElementById('link'); assert.isFalse(linkInBlockStyleCheck.call(checkContext, linkElm)); }); (shadowSupport.v1 ? it : xit)( 'works with the block outside the shadow tree', - function () { - var parentElm = document.createElement('div'); - var shadow = parentElm.attachShadow({ mode: 'open' }); + () => { + const parentElm = document.createElement('div'); + const shadow = parentElm.attachShadow({ mode: 'open' }); shadow.innerHTML = '<a href="" style="text-decoration:underline;">Link</a>'; - var linkElm = shadow.querySelector('a'); + const linkElm = shadow.querySelector('a'); fixture.appendChild(parentElm); axe.testUtils.flatTreeSetup(fixture); @@ -133,33 +130,33 @@ describe('link-in-text-block-style', function () { (shadowSupport.v1 ? it : xit)( 'works with the link inside the shadow tree slot', - function () { - var div = document.createElement('div'); + () => { + const div = document.createElement('div'); div.setAttribute('style', 'text-decoration:none;'); div.innerHTML = '<a href="" style="text-decoration:underline;">Link</a>'; - var shadow = div.attachShadow({ mode: 'open' }); + const shadow = div.attachShadow({ mode: 'open' }); shadow.innerHTML = '<p><slot></slot></p>'; fixture.appendChild(div); axe.testUtils.flatTreeSetup(fixture); - var linkElm = div.querySelector('a'); + const linkElm = div.querySelector('a'); assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); } ); }); - describe('links distinguished through style', function () { - it('returns false if link style matches parent', function () { - var linkElm = getLinkElm({}); + describe('links distinguished through style', () => { + it('returns false if link style matches parent', () => { + const linkElm = getLinkElm({}); assert.isFalse(linkInBlockStyleCheck.call(checkContext, linkElm)); assert.equal(checkContext._relatedNodes[0], linkElm.parentNode); assert.isNull(checkContext._data); }); - it('returns true if link has underline', function () { - var linkElm = getLinkElm({ + it('returns true if link has underline', () => { + const linkElm = getLinkElm({ textDecoration: 'underline' }); assert.isTrue(linkInBlockStyleCheck.call(checkContext, linkElm)); @@ -167,7 +164,7 @@ describe('link-in-text-block-style', function () { assert.isNull(checkContext._data); }); - it('returns undefined when the link has a :before pseudo element', function () { + it('returns undefined when the link has a :before pseudo element', () => { const link = queryFixture(` <style> a:before { content: '🔗'; } @@ -181,7 +178,7 @@ describe('link-in-text-block-style', function () { assert.equal(checkContext._relatedNodes[0], link.parentNode); }); - it('returns undefined when the link has a :after pseudo element', function () { + it('returns undefined when the link has a :after pseudo element', () => { const link = queryFixture(` <style> a:after { content: ""; } @@ -195,7 +192,7 @@ describe('link-in-text-block-style', function () { assert.equal(checkContext._relatedNodes[0], link.parentNode); }); - it('does not return undefined when the pseudo element content is none', function () { + it('does not return undefined when the pseudo element content is none', () => { const link = queryFixture(` <style> a:after { content: none; position: absolute; } diff --git a/test/checks/color/link-in-text-block.js b/test/checks/color/link-in-text-block.js index 46fe3e362a..027d055791 100644 --- a/test/checks/color/link-in-text-block.js +++ b/test/checks/color/link-in-text-block.js @@ -1,62 +1,59 @@ -describe('link-in-text-block', function () { - 'use strict'; +describe('link-in-text-block', () => { + const fixture = document.getElementById('fixture'); + const shadowSupport = axe.testUtils.shadowSupport; + let styleElm; - var fixture = document.getElementById('fixture'); - var shadowSupport = axe.testUtils.shadowSupport; - var styleElm; + const checkContext = axe.testUtils.MockCheckContext(); - var checkContext = axe.testUtils.MockCheckContext(); - - before(function () { + before(() => { styleElm = document.createElement('style'); document.head.appendChild(styleElm); }); - var defaultStyle = { + const defaultStyle = { color: '#000', textDecoration: 'none' }; - beforeEach(function () { + beforeEach(() => { createStyleString('p', defaultStyle); }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; styleElm.innerHTML = ''; checkContext.reset(); }); - after(function () { + after(() => { styleElm.parentNode.removeChild(styleElm); }); function createStyleString(selector, outerStyle) { // Merge style with the default - var prop; - var styleObj = {}; - for (prop in defaultStyle) { + const styleObj = {}; + for (const prop in defaultStyle) { if (defaultStyle.hasOwnProperty(prop)) { styleObj[prop] = defaultStyle[prop]; } } - for (prop in outerStyle) { + for (const prop in outerStyle) { if (outerStyle.hasOwnProperty(prop)) { styleObj[prop] = outerStyle[prop]; } } - var cssLines = Object.keys(styleObj) - .map(function (prop) { + const cssLines = Object.keys(styleObj) + .map(prop => { // Make camelCase prop dash separated - var cssPropName = prop + const cssPropName = prop .trim() .split(/(?=[A-Z])/g) - .reduce(function (prop, propPiece) { - if (!prop) { + .reduce((name, propPiece) => { + if (!name) { return propPiece; } else { - return prop + '-' + propPiece.toLowerCase(); + return name + '-' + propPiece.toLowerCase(); } }, null); @@ -71,8 +68,8 @@ describe('link-in-text-block', function () { function getLinkElm(linkStyle, paragraphStyle) { // Get a random id and build the style strings - var linkId = 'linkid-' + Math.floor(Math.random() * 100000); - var parId = 'parid-' + Math.floor(Math.random() * 100000); + const linkId = 'linkid-' + Math.floor(Math.random() * 100000); + const parId = 'parid-' + Math.floor(Math.random() * 100000); createStyleString('#' + linkId, linkStyle); createStyleString('#' + parId, paragraphStyle); @@ -89,14 +86,14 @@ describe('link-in-text-block', function () { return document.getElementById(linkId); } - describe('link default state', function () { - beforeEach(function () { + describe('link default state', () => { + beforeEach(() => { createStyleString('a', { color: '#100' // insufficient contrast }); }); - it('passes the selected node and closest ancestral block element', function () { + it('passes the selected node and closest ancestral block element', () => { fixture.innerHTML = '<div> <span style="display:block; color: #010" id="parent">' + ' <p style="display:inline"><a href="" id="link">' + @@ -105,7 +102,7 @@ describe('link-in-text-block', function () { '</span> outside block </div>'; axe.testUtils.flatTreeSetup(fixture); - var linkElm = document.getElementById('link'); + const linkElm = document.getElementById('link'); assert.isFalse( axe.testUtils @@ -117,16 +114,16 @@ describe('link-in-text-block', function () { (shadowSupport.v1 ? it : xit)( 'works with the block outside the shadow tree', - function () { - var parentElm = document.createElement('div'); + () => { + const parentElm = document.createElement('div'); parentElm.setAttribute( 'style', 'color:#100; background-color:#FFFFFF;' ); - var shadow = parentElm.attachShadow({ mode: 'open' }); + const shadow = parentElm.attachShadow({ mode: 'open' }); shadow.innerHTML = '<a href="" style="color:#000; background-color:#FFFFFF; text-decoration:none;">Link</a>'; - var linkElm = shadow.querySelector('a'); + const linkElm = shadow.querySelector('a'); fixture.appendChild(parentElm); axe.testUtils.flatTreeSetup(fixture); @@ -142,17 +139,17 @@ describe('link-in-text-block', function () { (shadowSupport.v1 ? it : xit)( 'works with the link inside the shadow tree slot', - function () { - var div = document.createElement('div'); + () => { + const div = document.createElement('div'); div.setAttribute('style', 'color:#100; background-color:#FFFFFF;'); div.innerHTML = '<a href="" style="color:#000;background-color:#FFFFFF;">Link</a>'; - var shadow = div.attachShadow({ mode: 'open' }); + const shadow = div.attachShadow({ mode: 'open' }); shadow.innerHTML = '<p><slot></slot></p>'; fixture.appendChild(div); axe.testUtils.flatTreeSetup(fixture); - var linkElm = div.querySelector('a'); + const linkElm = div.querySelector('a'); assert.isFalse( axe.testUtils @@ -164,15 +161,15 @@ describe('link-in-text-block', function () { ); }); - describe('links distinguished through color', function () { - beforeEach(function () { + describe('links distinguished through color', () => { + beforeEach(() => { createStyleString('a:active, a:focus', { textDecoration: 'underline' }); }); - it('returns undefined if the background contrast can not be determined', function () { - var linkElm = getLinkElm( + it('returns undefined if the background contrast can not be determined', () => { + const linkElm = getLinkElm( {}, { color: '#000010', @@ -192,8 +189,8 @@ describe('link-in-text-block', function () { assert.equal(checkContext._relatedNodes[0], linkElm.parentNode); }); - it('returns false with fgContrast key if nodes have insufficient foreground contrast and same background color', function () { - var linkElm = getLinkElm( + it('returns false with fgContrast key if nodes have insufficient foreground contrast and same background color', () => { + const linkElm = getLinkElm( { color: 'black' }, @@ -209,8 +206,8 @@ describe('link-in-text-block', function () { assert.equal(checkContext._data.messageKey, 'fgContrast'); }); - it('returns false with fgContrast key if nodes have insufficient foreground contrast and insufficient background color', function () { - var linkElm = getLinkElm( + it('returns false with fgContrast key if nodes have insufficient foreground contrast and insufficient background color', () => { + const linkElm = getLinkElm( { color: 'black', backgroundColor: 'white' @@ -228,8 +225,8 @@ describe('link-in-text-block', function () { assert.equal(checkContext._data.messageKey, 'fgContrast'); }); - it('returns false with bgContrast key if nodes have same foreground color and insufficient background contrast', function () { - var linkElm = getLinkElm( + it('returns false with bgContrast key if nodes have same foreground color and insufficient background contrast', () => { + const linkElm = getLinkElm( { color: 'black', backgroundColor: 'white' @@ -248,8 +245,8 @@ describe('link-in-text-block', function () { assert.equal(checkContext._relatedNodes[0], linkElm.parentNode); }); - it('returns true if nodes have sufficient foreground contrast and insufficient background contrast', function () { - var linkElm = getLinkElm( + it('returns true if nodes have sufficient foreground contrast and insufficient background contrast', () => { + const linkElm = getLinkElm( { color: 'black', backgroundColor: 'white' @@ -267,8 +264,8 @@ describe('link-in-text-block', function () { assert.equal(checkContext._relatedNodes[0], linkElm.parentNode); }); - it('returns true if nodes have insufficient foreground contrast and sufficient background contrast', function () { - var linkElm = getLinkElm( + it('returns true if nodes have insufficient foreground contrast and sufficient background contrast', () => { + const linkElm = getLinkElm( { color: 'black', backgroundColor: 'white' @@ -286,7 +283,7 @@ describe('link-in-text-block', function () { assert.equal(checkContext._relatedNodes[0], linkElm.parentNode); }); - it('should return the proper values stored in data (fgContrast)', function () { + it('should return the proper values stored in data (fgContrast)', () => { fixture.innerHTML = '<div> <span style="display:block; color: #100" id="parent">' + ' <p style="display:inline"><a href="" id="link">' + @@ -295,7 +292,7 @@ describe('link-in-text-block', function () { '</span> outside block </div>'; axe.testUtils.flatTreeSetup(fixture); - var linkElm = document.getElementById('link'); + const linkElm = document.getElementById('link'); axe.testUtils .getCheckEvaluate('link-in-text-block') @@ -310,8 +307,8 @@ describe('link-in-text-block', function () { }); }); - it('should return the proper values stored in data (bgContrast)', function () { - var linkElm = getLinkElm( + it('should return the proper values stored in data (bgContrast)', () => { + const linkElm = getLinkElm( { color: 'black', backgroundColor: 'white' @@ -338,7 +335,7 @@ describe('link-in-text-block', function () { describe('options.allowSameColor', () => { it('when true, passes when link and text colors are identical', () => { - var linkElm = getLinkElm( + const linkElm = getLinkElm( { color: 'black' }, @@ -355,7 +352,7 @@ describe('link-in-text-block', function () { }); it('when false, fails when link and text colors are identical', () => { - var linkElm = getLinkElm( + const linkElm = getLinkElm( { color: 'black' }, diff --git a/test/checks/keyboard/page-no-duplicate.js b/test/checks/keyboard/page-no-duplicate.js index fd0533332c..a54677e7e8 100644 --- a/test/checks/keyboard/page-no-duplicate.js +++ b/test/checks/keyboard/page-no-duplicate.js @@ -1,29 +1,26 @@ -describe('page-no-duplicate', function () { - 'use strict'; +describe('page-no-duplicate', () => { + const fixture = document.getElementById('fixture'); + const checkContext = new axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const shadowSupported = axe.testUtils.shadowSupport.v1; - var fixture = document.getElementById('fixture'); - var checkContext = new axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var shadowSupported = axe.testUtils.shadowSupport.v1; + const check = checks['page-no-duplicate-main']; - var check = checks['page-no-duplicate-main']; - - afterEach(function () { - fixture.innerHTML = ''; + afterEach(() => { checkContext.reset(); }); - describe('options.selector', function () { - it('throws if there is no selector', function () { - assert.throws(function () { - var params = checkSetup('<div id="target"></div>', undefined); + describe('options.selector', () => { + it('throws if there is no selector', () => { + assert.throws(() => { + const params = checkSetup('<div id="target"></div>', undefined); assert.isFalse(check.evaluate.apply(checkContext, params)); }); }); - it('should return false if there is more than one element matching the selector', function () { - var options = { selector: 'main' }; - var params = checkSetup( + it('should return false if there is more than one element matching the selector', () => { + const options = { selector: 'main' }; + const params = checkSetup( '<div><main id="target"></main><main id="dup"></main></div>', options ); @@ -35,33 +32,33 @@ describe('page-no-duplicate', function () { ); }); - it('should return true if there is only one element matching the selector', function () { - var options = { selector: 'main' }; - var params = checkSetup('<div role="main" id="target"></div>', options); + it('should return true if there is only one element matching the selector', () => { + const options = { selector: 'main' }; + const params = checkSetup('<div role="main" id="target"></div>', options); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should return true if there are no element matching the selector', function () { - var options = { selector: 'footer' }; - var params = checkSetup( + it('should return true if there are no element matching the selector', () => { + const options = { selector: 'footer' }; + const params = checkSetup( '<div><main id="target"></main><main></main></div>', options ); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should return true if there is more than one element matching the selector but only one is visible', function () { - var options = { selector: 'main' }; - var params = checkSetup( + it('should return true if there is more than one element matching the selector but only one is visible', () => { + const options = { selector: 'main' }; + const params = checkSetup( '<div><main id="target"></main><main id="dup" style="display:none;"></main></div>', options ); assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should return true if there is more than one element matching the selector but only one is visible to screenreaders', function () { - var options = { selector: 'main' }; - var params = checkSetup( + it('should return true if there is more than one element matching the selector but only one is visible to screenreaders', () => { + const options = { selector: 'main' }; + const params = checkSetup( '<div><main id="target" aria-hidden="true"></main><main id="dup"></main></div>', options ); @@ -70,17 +67,17 @@ describe('page-no-duplicate', function () { (shadowSupported ? it : xit)( 'should return false if there is a second matching element inside the shadow dom', - function () { - var options = { selector: 'main' }; - var div = document.createElement('div'); + () => { + const options = { selector: 'main' }; + const div = document.createElement('div'); div.innerHTML = '<div id="shadow"></div><main id="target"></main>'; - var shadow = div + const shadow = div .querySelector('#shadow') .attachShadow({ mode: 'open' }); shadow.innerHTML = '<main></main>'; axe.testUtils.fixtureSetup(div); - var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.isFalse( check.evaluate.call(checkContext, vNode.actualNode, options, vNode) @@ -93,18 +90,18 @@ describe('page-no-duplicate', function () { (shadowSupported ? it : xit)( 'should return true if there is a second matching element inside the shadow dom but only one is visible to screenreaders', - function () { - var options = { selector: 'main' }; - var div = document.createElement('div'); + () => { + const options = { selector: 'main' }; + const div = document.createElement('div'); div.innerHTML = '<div id="shadow"></div><main id="target" aria-hidden="true"></main>'; - var shadow = div + const shadow = div .querySelector('#shadow') .attachShadow({ mode: 'open' }); shadow.innerHTML = '<main></main>'; axe.testUtils.fixtureSetup(div); - var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.isTrue( check.evaluate.call(checkContext, vNode.actualNode, options, vNode) @@ -116,13 +113,13 @@ describe('page-no-duplicate', function () { ); }); - describe('option.nativeScopeFilter', function () { - it('should ignore element contained in a nativeScopeFilter match', function () { - var options = { + describe('option.nativeScopeFilter', () => { + it('should ignore element contained in a nativeScopeFilter match', () => { + const options = { selector: 'footer', nativeScopeFilter: 'main' }; - var params = checkSetup( + const params = checkSetup( '<div><footer id="target"></footer>' + '<main><footer></footer></main>' + '</div>', @@ -131,12 +128,12 @@ describe('page-no-duplicate', function () { assert.isTrue(check.evaluate.apply(checkContext, params)); }); - it('should not ignore element contained in a nativeScopeFilter match with their roles redefined', function () { - var options = { + it('should not ignore element contained in a nativeScopeFilter match with their roles redefined', () => { + const options = { selector: 'footer, [role="contentinfo"]', nativeScopeFilter: 'main' }; - var params = checkSetup( + const params = checkSetup( '<div><footer id="target"></footer>' + '<main><div role="contentinfo"></div></main>' + '</div>', @@ -145,12 +142,12 @@ describe('page-no-duplicate', function () { assert.isFalse(check.evaluate.apply(checkContext, params)); }); - it('should pass when there are two elements and the first is contained within a nativeSccopeFilter', function () { - var options = { + it('should pass when there are two elements and the first is contained within a nativeSccopeFilter', () => { + const options = { selector: 'footer, [role="contentinfo"]', nativeScopeFilter: 'article' }; - var params = checkSetup( + const params = checkSetup( '<article>' + '<footer id="target">Article footer</footer>' + '</article>' + @@ -162,19 +159,95 @@ describe('page-no-duplicate', function () { (shadowSupported ? it : xit)( 'elements if its ancestor is outside the shadow DOM tree', - function () { - var options = { + () => { + const options = { selector: 'footer', nativeScopeFilter: 'header' }; - var div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = '<header id="shadow"></header><footer id="target"></footer>'; div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = '<footer></footer>'; axe.testUtils.fixtureSetup(div); - var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + + assert.isTrue( + check.evaluate.call(checkContext, vNode.actualNode, options, vNode) + ); + } + ); + }); + + describe('options.role', () => { + it('should pass when element does not match the role', () => { + const options = { + selector: 'footer', + role: 'contentinfo' + }; + const params = checkSetup( + `<div> + <footer id="target"></footer> + <div role="main"> + <footer></footer> + </div> + </div>`, + options + ); + assert.isTrue(check.evaluate.apply(checkContext, params)); + }); + + it('should fail when element matches the role', () => { + const options = { + selector: 'footer', + role: 'contentinfo' + }; + const params = checkSetup( + `<div> + <footer id="target"></footer> + <div> + <footer id="fail"></footer> + </div> + </div>`, + options + ); + assert.isFalse(check.evaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._relatedNodes, [ + fixture.querySelector('#fail') + ]); + }); + + it('should pass when there are two elements and the first does not match the role', () => { + const options = { + selector: 'footer, [role="contentinfo"]', + role: 'contentinfo' + }; + const params = checkSetup( + `<article> + <footer id="target">Article footer</footer> + </article> + <footer>Body footer</footer>`, + options + ); + assert.isTrue(check.evaluate.apply(checkContext, params)); + }); + + (shadowSupported ? it : xit)( + "should pass if element's ancestor is outside the shadow DOM tree", + () => { + const options = { + selector: 'footer', + role: 'contentinfo' + }; + + const div = document.createElement('div'); + div.innerHTML = + '<article id="shadow"></article><footer id="target"></footer>'; + div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML = + '<footer></footer>'; + axe.testUtils.fixtureSetup(div); + const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.isTrue( check.evaluate.call(checkContext, vNode.actualNode, options, vNode) diff --git a/test/checks/mobile/target-offset.js b/test/checks/mobile/target-offset.js index 0baceac82e..f160baa10f 100644 --- a/test/checks/mobile/target-offset.js +++ b/test/checks/mobile/target-offset.js @@ -1,28 +1,26 @@ -describe('target-offset tests', function () { - 'use strict'; +describe('target-offset tests', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkEvaluate = getCheckEvaluate('target-offset'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var check = checks['target-offset']; - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('returns true when there are no other nearby targets', function () { - var checkArgs = checkSetup( + it('returns true when there are no other nearby targets', () => { + const checkArgs = checkSetup( '<a href="#" id="target" style="' + 'display: inline-block; width:16px; height:16px;' + '">x</a>' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('returns true when the offset is 24px', function () { - var checkArgs = checkSetup( + it('returns true when the offset is 24px', () => { + const checkArgs = checkSetup( '<a href="#" id="target" style="' + 'display: inline-block; width:16px; height:16px; margin-right: 8px' + '">x</a>' + @@ -31,14 +29,14 @@ describe('target-offset tests', function () { '">x</a>' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); describe('when the offset is insufficient', () => { - it('returns false for targets in the tab order', function () { - var checkArgs = checkSetup( + it('returns false for targets in the tab order', () => { + const checkArgs = checkSetup( '<a href="#" id="target" style="' + 'display: inline-block; width:16px; height:16px; margin-right: 7px' + '">x</a>' + @@ -47,14 +45,14 @@ describe('target-offset tests', function () { '">x</a>' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 23, 0.2); + assert.closeTo(checkContext._data.closestOffset, 22, 0.2); }); - it('returns undefined for targets not in the tab order', function () { - var checkArgs = checkSetup( + it('returns undefined for targets not in the tab order', () => { + const checkArgs = checkSetup( '<a href="#" id="target" tabindex="-1" style="' + 'display: inline-block; width:16px; height:16px; margin-right: 7px' + '">x</a>' + @@ -63,15 +61,15 @@ describe('target-offset tests', function () { '">x</a>' ); - assert.isUndefined(check.evaluate.apply(checkContext, checkArgs)); + assert.isUndefined(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 23, 0.2); + assert.closeTo(checkContext._data.closestOffset, 22, 0.2); }); }); - it('ignores non-widget elements as neighbors', function () { - var checkArgs = checkSetup( + it('ignores non-widget elements as neighbors', () => { + const checkArgs = checkSetup( '<a href="#" id="target" style="' + 'display: inline-block; width:16px; height:16px; margin-right: 7px' + '">x</a>' + @@ -80,13 +78,13 @@ describe('target-offset tests', function () { '">x</div>' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('ignores non-focusable widget elements as neighbors', function () { - var checkArgs = checkSetup( + it('ignores non-focusable widget elements as neighbors', () => { + const checkArgs = checkSetup( '<a href="#" id="target" style="' + 'display: inline-block; width:16px; height:16px; margin-right: 7px' + '">x</a>' + @@ -95,13 +93,13 @@ describe('target-offset tests', function () { '">x</button>' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('sets all elements that are too close as related nodes', function () { - var checkArgs = checkSetup( + it('sets all elements that are too close as related nodes', () => { + const checkArgs = checkSetup( '<a href="#" id="left" style="' + 'display: inline-block; width:16px; height:16px;' + '">x</a>' + @@ -112,11 +110,11 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x</a>' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); @@ -124,7 +122,7 @@ describe('target-offset tests', function () { describe('when neighbors are focusable but not tabbable', () => { it('returns undefined if all neighbors are not tabbable', () => { - var checkArgs = checkSetup( + const checkArgs = checkSetup( '<a href="#" id="left" tabindex="-1" style="' + 'display: inline-block; width:16px; height:16px;' + '">x</a>' + @@ -135,19 +133,19 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x</a>' ); - assert.isUndefined(check.evaluate.apply(checkContext, checkArgs)); + assert.isUndefined(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.messageKey, 'nonTabbableNeighbor'); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); }); it('returns false if some but not all neighbors are not tabbable', () => { - var checkArgs = checkSetup( + const checkArgs = checkSetup( '<a href="#" id="left" style="' + 'display: inline-block; width:16px; height:16px;' + '">x</a>' + @@ -158,12 +156,12 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x</a>' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js index 2c23ddc084..fb4d13b3e9 100644 --- a/test/checks/navigation/region.js +++ b/test/checks/navigation/region.js @@ -18,6 +18,7 @@ describe('region', function () { afterEach(function () { fixture.innerHTML = ''; checkContext.reset(); + axe.reset(); }); it('should return true when content is inside the region', function () { @@ -28,6 +29,23 @@ describe('region', function () { assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); }); + it('should return true when a region role is added to standards', () => { + axe.configure({ + standards: { + ariaRoles: { + feed: { + type: 'landmark' + } + } + } + }); + var checkArgs = checkSetup( + '<div role="feed" id="target">This is random content.</div>' + + '<div role="main"><h1 id="mainheader">Introduction</h1></div>' + ); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + it('should return false when img content is outside the region', function () { var checkArgs = checkSetup( '<img id="target" src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>' diff --git a/test/checks/shared/inline-style-property.js b/test/checks/shared/inline-style-property.js index 2d6b1ab308..9d93d30f02 100644 --- a/test/checks/shared/inline-style-property.js +++ b/test/checks/shared/inline-style-property.js @@ -1,35 +1,34 @@ -describe('inline-style-property tests', function () { - 'use strict'; - var fixture = document.getElementById('fixture'); - var checkSetup = axe.testUtils.checkSetup; +describe('inline-style-property tests', () => { + const fixture = document.getElementById('fixture'); + const checkSetup = axe.testUtils.checkSetup; - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - describe('important-letter-spacing check', function () { - var checkEvaluate = axe.testUtils.getCheckEvaluate( + describe('important-letter-spacing check', () => { + const checkEvaluate = axe.testUtils.getCheckEvaluate( 'important-letter-spacing' ); - var checkContext = axe.testUtils.MockCheckContext(); - afterEach(function () { + const checkContext = axe.testUtils.MockCheckContext(); + afterEach(() => { checkContext.reset(); }); - it('is true when the property is not set in the style attribute', function () { - var params = checkSetup( + it('is true when the property is not set in the style attribute', () => { + const params = checkSetup( '<p style="width: 60%" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is false when letter-spacing is less than 0.12em and !important', function () { - var params = checkSetup( + it('is false when letter-spacing is less than 0.12em and !important', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0.1, @@ -37,20 +36,20 @@ describe('inline-style-property tests', function () { }); }); - it('is true when !important is not used', function () { - var params = checkSetup( + it('is true when !important is not used', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is true when letter-spacing is 0.15 times the font-size', function () { - var params = checkSetup( + it('is true when letter-spacing is 0.15 times the font-size', () => { + const params = checkSetup( '<p style="letter-spacing: 0.15em !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 0.15, @@ -58,16 +57,16 @@ describe('inline-style-property tests', function () { }); }); - it('uses the highest priority value if multiple are set', function () { - var style = [ + it('uses the highest priority value if multiple are set', () => { + const style = [ 'letter-spacing: 0.15em !important', 'letter-spacing: 0.1em !important', 'letter-spacing: 0.2em' ].join('; '); - var params = checkSetup( + const params = checkSetup( '<p style="' + style + '" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0.1, @@ -75,12 +74,12 @@ describe('inline-style-property tests', function () { }); }); - describe('handles different font-sizes', function () { - it('is true when the font is 0.15 time the spacing', function () { - var params = checkSetup( + describe('handles different font-sizes', () => { + it('is true when the font is 0.15 time the spacing', () => { + const params = checkSetup( '<p style="font-size: 20px; letter-spacing: 3px !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 0.15, @@ -88,11 +87,11 @@ describe('inline-style-property tests', function () { }); }); - it('is false when the font is 0.10 times the spacing', function () { - var params = checkSetup( + it('is false when the font is 0.10 times the spacing', () => { + const params = checkSetup( '<p style="font-size: 30px; letter-spacing: 3px !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0.1, @@ -101,12 +100,12 @@ describe('inline-style-property tests', function () { }); }); - describe('with non-number values', function () { - it('is false when `normal` (which is 0) is used along with !important', function () { - var params = checkSetup( + describe('with non-number values', () => { + it('is false when `normal` (which is 0) is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: normal !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0, @@ -114,11 +113,11 @@ describe('inline-style-property tests', function () { }); }); - it('is false when `initial` (meaning `normal`) is used along with !important', function () { - var params = checkSetup( + it('is false when `initial` (meaning `normal`) is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: initial !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0, @@ -126,12 +125,12 @@ describe('inline-style-property tests', function () { }); }); - it('is true when `inherited` is used along with !important', function () { - var params = checkSetup( + it('is true when `inherited` is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em">' + '<span style="letter-spacing: inherit !important;" id="target">Hello world</span</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 'inherit', @@ -139,12 +138,12 @@ describe('inline-style-property tests', function () { }); }); - it('is true when `unset` is used along with !important', function () { - var params = checkSetup( + it('is true when `unset` is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em">' + '<span style="letter-spacing: unset !important;" id="target">Hello world</span</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 'unset', @@ -152,12 +151,12 @@ describe('inline-style-property tests', function () { }); }); - it('is true when `revert` is used along with !important', function () { - var params = checkSetup( + it('is true when `revert` is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em">' + '<span style="letter-spacing: revert !important;" id="target">Hello world</span</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 'revert', @@ -165,12 +164,12 @@ describe('inline-style-property tests', function () { }); }); - it('is true when `revert-layer` is used along with !important', function () { - var params = checkSetup( + it('is true when `revert-layer` is used along with !important', () => { + const params = checkSetup( '<p style="letter-spacing: 0.1em">' + '<span style="letter-spacing: revert-layer !important;" id="target">Hello world</span</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 'revert-layer', @@ -180,38 +179,38 @@ describe('inline-style-property tests', function () { }); }); - describe('important-word-spacing check', function () { - var checkEvaluate = axe.testUtils.getCheckEvaluate( + describe('important-word-spacing check', () => { + const checkEvaluate = axe.testUtils.getCheckEvaluate( 'important-word-spacing' ); - var checkContext = axe.testUtils.MockCheckContext(); - afterEach(function () { + const checkContext = axe.testUtils.MockCheckContext(); + afterEach(() => { checkContext.reset(); }); - it('is true when word-spacing is not set in the style attribute', function () { - var params = checkSetup( + it('is true when word-spacing is not set in the style attribute', () => { + const params = checkSetup( '<p style="width: 60%" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is true when below 0.16em and not !important', function () { - var params = checkSetup( + it('is true when below 0.16em and not !important', () => { + const params = checkSetup( '<p style="word-spacing: 0.1em" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is false when below 0.16em and !important', function () { - var params = checkSetup( + it('is false when below 0.16em and !important', () => { + const params = checkSetup( '<p style="word-spacing: 0.1em !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 0.1, @@ -219,11 +218,11 @@ describe('inline-style-property tests', function () { }); }); - it('is true when 0.16em and !important', function () { - var params = checkSetup( + it('is true when 0.16em and !important', () => { + const params = checkSetup( '<p style="word-spacing: 0.16em !important" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 0.16, @@ -232,40 +231,42 @@ describe('inline-style-property tests', function () { }); }); - describe('important-line-height check', function () { - var checkEvaluate = axe.testUtils.getCheckEvaluate('important-line-height'); - var checkContext = axe.testUtils.MockCheckContext(); - afterEach(function () { + describe('important-line-height check', () => { + const checkEvaluate = axe.testUtils.getCheckEvaluate( + 'important-line-height' + ); + const checkContext = axe.testUtils.MockCheckContext(); + afterEach(() => { checkContext.reset(); }); - it('is true when line-height is not set in the style attribute', function () { - var params = checkSetup( + it('is true when line-height is not set in the style attribute', () => { + const params = checkSetup( '<p style="width: 60%" id="target">Hello world</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is true when below 1.5em and not !important', function () { - var params = checkSetup( + it('is true when below 1.5em and not !important', () => { + const params = checkSetup( '<p style="line-height: 1.2em; max-width: 200px;" id="target">' + ' The toy brought back fond memories of being lost in the rain forest.' + '</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is false when below 1.5em and !important', function () { - var params = checkSetup( + it('is false when below 1.5em and !important', () => { + const params = checkSetup( '<p style="line-height: 1.2em !important; max-width: 200px;" id="target">' + ' The toy brought back fond memories of being lost in the rain forest.' + '</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 1.2, @@ -273,13 +274,13 @@ describe('inline-style-property tests', function () { }); }); - it('is true when 1.5em and !important', function () { - var params = checkSetup( + it('is true when 1.5em and !important', () => { + const params = checkSetup( '<p style="line-height: 1.5em !important; max-width: 200px;" id="target">' + ' The toy brought back fond memories of being lost in the rain forest.' + '</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 1.5, @@ -287,13 +288,13 @@ describe('inline-style-property tests', function () { }); }); - it('returns the 1em for `normal !important`', function () { - var params = checkSetup( + it('returns the 1em for `normal !important`', () => { + const params = checkSetup( '<p style="line-height: normal !important; max-width: 200px;" id="target">' + ' The toy brought back fond memories of being lost in the rain forest.' + '</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 1, @@ -301,24 +302,24 @@ describe('inline-style-property tests', function () { }); }); - it('is true for single line texts', function () { - var params = checkSetup( + it('is true for single line texts', () => { + const params = checkSetup( '<p style="line-height: 1.2em !important; max-width: 200px;" id="target">' + ' Short' + '</p>' ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); }); - describe('With options configured for font-size', function () { - var checkEvaluate = axe.testUtils.getCheckEvaluate( + describe('With options configured for font-size', () => { + const checkEvaluate = axe.testUtils.getCheckEvaluate( 'important-letter-spacing' ); - var checkContext = axe.testUtils.MockCheckContext(); - var options = { + const checkContext = axe.testUtils.MockCheckContext(); + const options = { cssProperty: 'font-size', minValue: 16, maxValue: 42, @@ -326,16 +327,16 @@ describe('inline-style-property tests', function () { noImportant: false }; - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('is false when !important and below the minValue', function () { - var params = checkSetup( + it('is false when !important and below the minValue', () => { + const params = checkSetup( '<p style="font-size: 12px !important" id="target">Hello world</p>', options ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 12, @@ -344,12 +345,12 @@ describe('inline-style-property tests', function () { }); }); - it('is false when !important and above the maxValue', function () { - var params = checkSetup( + it('is false when !important and above the maxValue', () => { + const params = checkSetup( '<p style="font-size: 43px !important" id="target">Hello world</p>', options ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 43, @@ -358,23 +359,23 @@ describe('inline-style-property tests', function () { }); }); - it('is true when not !important', function () { - var params = checkSetup( + it('is true when not !important', () => { + const params = checkSetup( '<p style="font-size: 12px" id="target">Hello world</p>', options ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.isNull(checkContext._data); }); - it('is false when not !important and {noImportant: true}', function () { - var opt = Object.assign({}, options, { noImportant: true }); - var params = checkSetup( + it('is false when not !important and {noImportant: true}', () => { + const opt = Object.assign({}, options, { noImportant: true }); + const params = checkSetup( '<p style="font-size: 12px" id="target">Hello world</p>', opt ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 12, @@ -383,18 +384,18 @@ describe('inline-style-property tests', function () { }); }); - it('returns the normal value when `normal` is used', function () { + it('returns the normal value when `normal` is used', () => { // Using line-height, since font-size cannot be normal - var options = { + const opts = { cssProperty: 'line-height', normalValue: 3, minValue: 5 }; - var params = checkSetup( + const params = checkSetup( '<p style="line-height: normal !important" id="target">Hello world</p>', - options + opts ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._data, { value: 3, @@ -402,12 +403,12 @@ describe('inline-style-property tests', function () { }); }); - it('is true when above the minValue', function () { - var params = checkSetup( + it('is true when above the minValue', () => { + const params = checkSetup( '<p style="font-size: 16px !important" id="target">Hello world</p>', options ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 16, @@ -416,13 +417,13 @@ describe('inline-style-property tests', function () { }); }); - it('ignores minValue when not a number', function () { - var opt = Object.assign({}, options, { minValue: '16' }); - var params = checkSetup( + it('ignores minValue when not a number', () => { + const opt = Object.assign({}, options, { minValue: '16' }); + const params = checkSetup( '<p style="font-size: 12px !important" id="target">Hello world</p>', opt ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 12, @@ -430,13 +431,13 @@ describe('inline-style-property tests', function () { }); }); - it('ignores maxValue when not a number', function () { - var opt = Object.assign({}, options, { maxValue: '42' }); - var params = checkSetup( + it('ignores maxValue when not a number', () => { + const opt = Object.assign({}, options, { maxValue: '42' }); + const params = checkSetup( '<p style="font-size: 50px !important" id="target">Hello world</p>', opt ); - var result = checkEvaluate.apply(checkContext, params); + const result = checkEvaluate.apply(checkContext, params); assert.isTrue(result); assert.deepEqual(checkContext._data, { value: 50, diff --git a/test/commons/aria/get-accessible-refs.js b/test/commons/aria/get-accessible-refs.js index 57941a5336..00820526e3 100644 --- a/test/commons/aria/get-accessible-refs.js +++ b/test/commons/aria/get-accessible-refs.js @@ -80,6 +80,34 @@ describe('aria.getAccessibleRefs', function () { assert.deepEqual(getAccessibleRefs(node), [ref]); }); + describe('when JavaScript object names are used as IDs', function () { + const ids = [ + 'prototype', + 'constructor', + '__proto__', + 'Element', + 'nodeName', + 'valueOf', + 'toString' + ]; + for (const id of ids) { + it(`does not break with id="${id}"`, function () { + setLookup({ 'aria-bar': { type: 'idrefs' } }); + fixture.innerHTML = `<div id="ref" aria-bar="${ids.join( + ' ' + )}"></div><i id="${id}"></i>`; + + var node = document.getElementById(id); + var ref = document.getElementById('ref'); + assert.deepEqual( + getAccessibleRefs(node), + [ref], + `Not equal for ID ${id}` + ); + }); + } + }); + (shadowSupport ? it : xit)('works inside shadow DOM', function () { setLookup({ 'aria-bar': { type: 'idref' } }); fixture.innerHTML = '<div id="foo"></div>'; diff --git a/test/commons/aria/validate-attr-value.js b/test/commons/aria/validate-attr-value.js index b7be5cf07e..c251fca158 100644 --- a/test/commons/aria/validate-attr-value.js +++ b/test/commons/aria/validate-attr-value.js @@ -1,31 +1,29 @@ -describe('aria.validateAttrValue', function () { - 'use strict'; - - var fixture = document.getElementById('fixture'); - var shadowSupport = axe.testUtils.shadowSupport; - var node; +describe('aria.validateAttrValue', () => { + const fixture = document.getElementById('fixture'); + const shadowSupport = axe.testUtils.shadowSupport; + let node; - function setAttr(node, attrName, attrValue) { - node.setAttribute(attrName, attrValue); + function setAttr(elm, attrName, attrValue) { + elm.setAttribute(attrName, attrValue); axe.teardown(); - return axe.setup(node); + return axe.setup(elm); } - beforeEach(function () { + beforeEach(() => { node = document.createElement('div'); }); - afterEach(function () { + afterEach(() => { axe.reset(); }); - it('should return true if there is no matching attribute (future-compat???)', function () { + it('should return true if there is no matching attribute (future-compat???)', () => { setAttr(node, 'unknown-attr', 'hello'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'unknown-attr')); }); - it('works on virtual nodes', function () { + it('works on virtual nodes', () => { axe.configure({ standards: { ariaAttrs: { @@ -37,14 +35,14 @@ describe('aria.validateAttrValue', function () { } } }); - var vNode = axe.testUtils.queryFixture( + const vNode = axe.testUtils.queryFixture( '<div id="target" cats="valid"></div>' ); assert.isTrue(axe.commons.aria.validateAttrValue(vNode, 'cats')); }); - describe('allowEmpty', function () { - beforeEach(function () { + describe('allowEmpty', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -82,7 +80,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('returns true for empty attributes with allowEmpty:true', function () { + it('returns true for empty attributes with allowEmpty:true', () => { setAttr(node, 'cats', ''); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); @@ -105,7 +103,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'horses')); }); - it('returns true for whitespace-only attributes with allowEmpty:true', function () { + it('returns true for whitespace-only attributes with allowEmpty:true', () => { setAttr(node, 'cats', ' \r\n\t '); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); @@ -129,9 +127,9 @@ describe('aria.validateAttrValue', function () { }); }); - describe('schema defintions', function () { - describe('enumerated values', function () { - beforeEach(function () { + describe('schema defintions', () => { + describe('enumerated values', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -144,7 +142,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('should validate against enumerated .values if present', function () { + it('should validate against enumerated .values if present', () => { setAttr(node, 'cats', 'valid'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); @@ -154,21 +152,21 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); }); - it('should be case-insensitive for enumerated values', function () { + it('should be case-insensitive for enumerated values', () => { setAttr(node, 'cats', 'vaLiD'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); }); - it('should reject empty strings', function () { + it('should reject empty strings', () => { setAttr(node, 'cats', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); }); }); - describe('idref', function () { - beforeEach(function () { + describe('idref', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -180,7 +178,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('should validate the referenced node exists', function () { + it('should validate the referenced node exists', () => { fixture.innerHTML = '<div id="target"></div>'; setAttr(node, 'dogs', 'target'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'dogs')); @@ -189,8 +187,8 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'dogs')); }); - it('should work in shadow DOM', function () { - var shadEl; + it('should work in shadow DOM', () => { + let shadEl; if (shadowSupport.v1) { // shadow DOM v1 - note: v0 is compatible with this code, so no need @@ -209,14 +207,14 @@ describe('aria.validateAttrValue', function () { } }); - it('returns false if empty without allowEmpty: true', function () { + it('returns false if empty without allowEmpty: true', () => { setAttr(node, 'dogs', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'dogs')); }); }); - describe('idrefs', function () { - beforeEach(function () { + describe('idrefs', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -228,46 +226,46 @@ describe('aria.validateAttrValue', function () { }); }); - it('should return false when a single referenced node is not found', function () { + it('should return false when a single referenced node is not found', () => { setAttr(node, 'goats', 'invalid'); // target2 not found assert.isFalse(axe.commons.aria.validateAttrValue(node, 'goats')); }); - it('should return false when no referenced element is found', function () { + it('should return false when no referenced element is found', () => { fixture.innerHTML = '<div id="target"></div>'; setAttr(node, 'goats', 'target2 target3'); // target2 not found assert.isFalse(axe.commons.aria.validateAttrValue(node, 'goats')); }); - it('should return true when at least one referenced element is found', function () { + it('should return true when at least one referenced element is found', () => { fixture.innerHTML = '<div id="target"></div>'; setAttr(node, 'goats', 'target target2'); // target2 not found assert.isTrue(axe.commons.aria.validateAttrValue(node, 'goats')); }); - it('should return true when all targets are found', function () { + it('should return true when all targets are found', () => { fixture.innerHTML = '<div id="target"></div><div id="target2"></div>'; setAttr(node, 'goats', 'target target2'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'goats')); }); - it('should not fail on weird whitespace', function () { + it('should not fail on weird whitespace', () => { fixture.innerHTML = '<div id="target"></div><div id="target2"></div>'; setAttr(node, 'goats', ' \t \ttarget \t target2 '); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'goats')); }); - it('returns false if empty without allowEmpty: true', function () { + it('returns false if empty without allowEmpty: true', () => { setAttr(node, 'goats', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'goats')); }); }); - describe('string', function () { - beforeEach(function () { + describe('string', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -279,19 +277,19 @@ describe('aria.validateAttrValue', function () { }); }); - it('returns true for non-empty strings', function () { + it('returns true for non-empty strings', () => { setAttr(node, 'cows', 'hi'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cows')); }); - it('returns false for non-empty strings without allowEmpty:true', function () { + it('returns false for non-empty strings without allowEmpty:true', () => { setAttr(node, 'cows', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cows')); }); }); - describe('decimal', function () { - beforeEach(function () { + describe('decimal', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -303,7 +301,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('should allow, but not require, a preceeding sign', function () { + it('should allow, but not require, a preceeding sign', () => { setAttr(node, 'sheep', '+1.12'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -314,7 +312,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should make the decimal separator optional', function () { + it('should make the decimal separator optional', () => { setAttr(node, 'sheep', '+1'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -325,7 +323,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should make the whole number optional', function () { + it('should make the whole number optional', () => { setAttr(node, 'sheep', '+.1'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -336,7 +334,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should make the right-side optional', function () { + it('should make the right-side optional', () => { setAttr(node, 'sheep', '+1.'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -347,7 +345,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should validate the entire string', function () { + it('should validate the entire string', () => { setAttr(node, 'sheep', ' +1.12 '); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -358,7 +356,7 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should only allow for numbers', function () { + it('should only allow for numbers', () => { setAttr(node, 'sheep', '+a.12'); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -369,7 +367,7 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('should require at least one number', function () { + it('should require at least one number', () => { setAttr(node, 'sheep', '+.'); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); @@ -389,14 +387,14 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); }); - it('returns false for empty strings without allowEmpty:true', function () { + it('returns false for empty strings without allowEmpty:true', () => { setAttr(node, 'sheep', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'sheep')); }); }); - describe('int', function () { - beforeEach(function () { + describe('int', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -408,7 +406,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('should only allow for numbers by an optional preceding sign', function () { + it('should only allow for numbers by an optional preceding sign', () => { setAttr(node, 'pigs', '+1234234'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'pigs')); @@ -419,7 +417,7 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'pigs')); }); - it('should return true for value greater than or equal to minValue', function () { + it('should return true for value greater than or equal to minValue', () => { axe.configure({ standards: { ariaAttrs: { @@ -441,12 +439,12 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'pigs')); }); - it('returns false for empty strings without allowEmpty:true', function () { + it('returns false for empty strings without allowEmpty:true', () => { setAttr(node, 'pigs', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'pigs')); }); - it('should return false for value less than the minValue', function () { + it('should return false for value less than the minValue', () => { axe.configure({ standards: { ariaAttrs: { @@ -463,8 +461,8 @@ describe('aria.validateAttrValue', function () { }); }); - describe('boolean', function () { - beforeEach(function () { + describe('boolean', () => { + beforeEach(() => { axe.configure({ standards: { ariaAttrs: { @@ -476,7 +474,7 @@ describe('aria.validateAttrValue', function () { }); }); - it('returns true for boolean value', function () { + it('returns true for boolean value', () => { setAttr(node, 'horses', 'true'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'horses')); @@ -484,13 +482,13 @@ describe('aria.validateAttrValue', function () { assert.isTrue(axe.commons.aria.validateAttrValue(node, 'horses')); }); - it('should be case-insensitive', function () { + it('should be case-insensitive', () => { setAttr(node, 'horses', 'trUE'); assert.isTrue(axe.commons.aria.validateAttrValue(node, 'horses')); }); - it('returns false for non-boolean values', function () { + it('returns false for non-boolean values', () => { setAttr(node, 'horses', 'hi'); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'horses')); @@ -498,7 +496,7 @@ describe('aria.validateAttrValue', function () { assert.isFalse(axe.commons.aria.validateAttrValue(node, 'horses')); }); - it('returns false for non-empty strings without allowEmpty:true', function () { + it('returns false for non-empty strings without allowEmpty:true', () => { setAttr(node, 'horses', ''); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'horses')); }); @@ -508,8 +506,8 @@ describe('aria.validateAttrValue', function () { function makeShadowTreeVAV(node) { 'use strict'; - var root = node.attachShadow({ mode: 'open' }); - var div = document.createElement('div'); + const root = node.attachShadow({ mode: 'open' }); + const div = document.createElement('div'); div.className = 'parent'; root.appendChild(div); div.appendChild(createContentVAV()); @@ -517,7 +515,7 @@ function makeShadowTreeVAV(node) { function createContentVAV() { 'use strict'; - var group = document.createElement('div'); + const group = document.createElement('div'); group.innerHTML = '<label id="mylabel">Label</label>' + '<input id="myinput" aria-labelledby="mylabel" type="text" />' + diff --git a/test/commons/color/element-is-distinct.js b/test/commons/color/element-is-distinct.js index bbec67d928..413482bb30 100644 --- a/test/commons/color/element-is-distinct.js +++ b/test/commons/color/element-is-distinct.js @@ -1,50 +1,49 @@ -describe('color.elementIsDistinct', function () { - 'use strict'; - var styleElm, elementIsDistinct; +describe('color.elementIsDistinct', () => { + let styleElm; + let elementIsDistinct; - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); - before(function () { + before(() => { styleElm = document.createElement('style'); document.head.appendChild(styleElm); }); - var defaultStyle = { + const defaultStyle = { color: '#000', textDecoration: 'none' }; function createStyleString(selector, outerStyle) { // Merge style with the default - var prop; - var styleObj = {}; - for (prop in defaultStyle) { + const styleObj = {}; + for (const prop in defaultStyle) { if (defaultStyle.hasOwnProperty(prop)) { styleObj[prop] = defaultStyle[prop]; } } - for (prop in outerStyle) { + for (const prop in outerStyle) { if (outerStyle.hasOwnProperty(prop)) { styleObj[prop] = outerStyle[prop]; } } - var cssLines = Object.keys(styleObj) - .map(function (prop) { + const cssLines = Object.keys(styleObj) + .map(prop => { // Make camelCase prop dash separated - var cssPropName = prop + const cssPropName = prop .trim() .split(/(?=[A-Z])/g) - .reduce(function (prop, propPiece) { - if (!prop) { + .reduce((name, propPiece) => { + if (!name) { return propPiece; } else { - return prop + '-' + propPiece.toLowerCase(); + return name + '-' + propPiece.toLowerCase(); } }, null); // Return indented line of style code - return ' ' + cssPropName + ':' + styleObj[prop] + ';'; + return ' ' + cssPropName + ':' + styleObj[prop] + ';'; }) .join('\n'); @@ -54,8 +53,8 @@ describe('color.elementIsDistinct', function () { function getLinkElm(linkStyle, paragraphStyle) { // Get a random id and build the style string - var linkId = 'linkid-' + Math.floor(Math.random() * 100000); - var parId = 'parid-' + Math.floor(Math.random() * 100000); + const linkId = 'linkid-' + Math.floor(Math.random() * 100000); + const parId = 'parid-' + Math.floor(Math.random() * 100000); createStyleString('#' + linkId, linkStyle); createStyleString('#' + parId, paragraphStyle); @@ -74,92 +73,92 @@ describe('color.elementIsDistinct', function () { }; } - beforeEach(function () { + beforeEach(() => { createStyleString('p', defaultStyle); elementIsDistinct = axe.commons.color.elementIsDistinct; }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; styleElm.innerHTML = ''; }); - after(function () { + after(() => { styleElm.parentNode.removeChild(styleElm); }); - it('returns false without style adjustments', function () { - var elms = getLinkElm({}); - var result = elementIsDistinct(elms.link, elms.par); + it('returns false without style adjustments', () => { + const elms = getLinkElm({}); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns true with background-image set', function () { - var elms = getLinkElm({ + it('returns true with background-image set', () => { + const elms = getLinkElm({ background: 'url(icon.png) no-repeat' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns true with border: dashed 1px black', function () { - var elms = getLinkElm({ + it('returns true with border: dashed 1px black', () => { + const elms = getLinkElm({ border: 'dashed 1px black' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns true with border-bottom: dashed 1px black', function () { - var elms = getLinkElm({ + it('returns true with border-bottom: dashed 1px black', () => { + const elms = getLinkElm({ borderBottom: 'dashed 1px black' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns false with border: solid 0px black', function () { - var elms = getLinkElm({ + it('returns false with border: solid 0px black', () => { + const elms = getLinkElm({ border: 'solid 0px black' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns false with border: none 1px black', function () { - var elms = getLinkElm({ + it('returns false with border: none 1px black', () => { + const elms = getLinkElm({ border: 'none 1px black' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns false with border: solid 1px transparent', function () { - var elms = getLinkElm({ + it('returns false with border: solid 1px transparent', () => { + const elms = getLinkElm({ border: 'solid 1px transparent' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns true with outline: solid 1px black', function () { - var elms = getLinkElm({ + it('returns true with outline: solid 1px black', () => { + const elms = getLinkElm({ outline: 'solid 1px black' }); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns true if font-weight is different', function () { - var elms = getLinkElm( + it('returns true if font-weight is different', () => { + const elms = getLinkElm( { fontWeight: 'bold' }, @@ -168,12 +167,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns false if font-weight is the same', function () { - var elms = getLinkElm( + it('returns false if font-weight is the same', () => { + const elms = getLinkElm( { fontWeight: 'bold' }, @@ -182,12 +181,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('compares font numbers and labels correctly', function () { - var elms = getLinkElm( + it('compares font numbers and labels correctly', () => { + const elms = getLinkElm( { fontWeight: 'bold' }, @@ -196,12 +195,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns true if text-decoration is different', function () { - var elms = getLinkElm( + it('returns true if text-decoration is different', () => { + const elms = getLinkElm( { textDecoration: 'underline' }, @@ -210,12 +209,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns false if text-decoration is the same', function () { - var elms = getLinkElm( + it('returns false if text-decoration is the same', () => { + const elms = getLinkElm( { textDecoration: 'underline' }, @@ -224,12 +223,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); - it('returns true if font-size is different', function () { - var elms = getLinkElm( + it('returns true if font-size is different', () => { + const elms = getLinkElm( { fontSize: '14px' }, @@ -238,12 +237,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns true if font-family is different', function () { - var elms = getLinkElm( + it('returns true if font-family is different', () => { + const elms = getLinkElm( { fontFamily: 'Arial' }, @@ -252,12 +251,12 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isTrue(result); }); - it('returns false if the first font-family is identical', function () { - var elms = getLinkElm( + it('returns false if the first font-family is identical', () => { + const elms = getLinkElm( { fontFamily: 'Arial-black, Arial' }, @@ -266,7 +265,7 @@ describe('color.elementIsDistinct', function () { } ); - var result = elementIsDistinct(elms.link, elms.par); + const result = elementIsDistinct(elms.link, elms.par); assert.isFalse(result); }); }); diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index fb76e89328..12852c69df 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -4,18 +4,34 @@ describe('color.getBackgroundColor', function () { var fixture = document.getElementById('fixture'); var shadowSupported = axe.testUtils.shadowSupport.v1; - var origBodyBg; - var origHtmlBg; - before(function () { - origBodyBg = document.body.style.background; - origHtmlBg = document.documentElement.style.background; + /** + * Assert that two Colors are close-to-equal. + * @param {axe.commons.color.Color[]} actual + * @param {axe.commons.color.Color[]} expected + * @param {number} threshold How much each RGB value may differ by + * @param {number} alphaThreshold How much the alpha channel may differ by + */ + function assertColorsClose( + actual, + expected, + threshold = 0.5, + alphaThreshold = 0.1 + ) { + assert.closeTo(actual.red, expected.red, threshold, 'red'); + assert.closeTo(actual.green, expected.green, threshold, 'green'); + assert.closeTo(actual.blue, expected.blue, threshold, 'blue'); + assert.closeTo(actual.alpha, expected.alpha, alphaThreshold, 'alpha'); + } + + beforeEach(function () { + // This normalizes the default mocha behavior of setting a different background + // based on prefers-color-scheme settings. + document.body.style.background = '#fff'; + document.documentElement.style.background = 'unset'; }); afterEach(function () { - document.body.style.background = origBodyBg; - document.documentElement.style.background = origHtmlBg; - axe.commons.color.incompleteData.clear(); axe._tree = undefined; }); @@ -31,10 +47,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(128, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -55,11 +68,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [target, pos]); }); @@ -75,10 +84,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [target, under]); }); @@ -101,11 +107,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [target, under]); }); @@ -120,10 +122,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [target, parent]); }); @@ -138,10 +137,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [target, parent]); }); @@ -155,10 +151,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(102, 153, 51, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); }); it('should apply opacity from an ancestor not in the element stack', function () { @@ -173,10 +166,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(102, 153, 51, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); }); it('should return null if containing parent has a background image and is non-opaque', function () { @@ -195,16 +185,13 @@ describe('color.getBackgroundColor', function () { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'bgImage'); }); - it('should return white if transparency goes all the way up to document', function () { + it('should return body color if transparency goes all the way up to document', function () { fixture.innerHTML = '<div id="target" style="height: 10px; width: 30px;">'; var target = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target); var expected = new axe.commons.color.Color(255, 255, 255, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); }); it('should return null if there is a background image', function () { @@ -272,10 +259,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(0, 128, 0, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [target]); }); @@ -324,9 +308,8 @@ describe('color.getBackgroundColor', function () { document.getElementById('target'), [] ); - assert.equal(Math.round(actual.blue), 255); - assert.equal(Math.round(actual.red), 255); - assert.equal(Math.round(actual.green), 255); + var expected = new axe.commons.color.Color(255, 255, 255); + assertColorsClose(actual, expected); }); it('should return null if an absolutely positioned element partially obsures background', function () { @@ -362,10 +345,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -384,10 +364,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -406,10 +383,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -428,10 +402,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -450,10 +421,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -472,10 +440,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -491,10 +456,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.notEqual(bgNodes, [parent]); }); @@ -508,10 +470,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(0, 0, 0, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); }); it('handles nested inline elements in the middle of a text', function () { @@ -527,10 +486,8 @@ describe('color.getBackgroundColor', function () { var bgNodes = []; axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); - assert.equal(actual.red, 0); - assert.equal(actual.green, 255); - assert.equal(actual.blue, 255); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(0, 255, 255, 1); + assert.deepEqual(actual, expected); }); it('should return null for inline elements with position:absolute', function () { @@ -560,10 +517,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.notEqual(bgNodes, [parent]); }); @@ -586,10 +540,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -609,10 +560,7 @@ describe('color.getBackgroundColor', function () { var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -630,10 +578,7 @@ describe('color.getBackgroundColor', function () { var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -655,10 +600,7 @@ describe('color.getBackgroundColor', function () { assert.deepEqual(bgNodes, [shifted]); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('should return null when encountering background images during visual traversal', function () { @@ -715,10 +657,7 @@ describe('color.getBackgroundColor', function () { var expected = new axe.commons.color.Color(0, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns negative z-index elements when body has a background', function () { @@ -736,10 +675,7 @@ describe('color.getBackgroundColor', function () { var expected = new axe.commons.color.Color(0, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('should return null for negative z-index element when html and body have a background', function () { @@ -768,11 +704,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(fixture, []); var expected = new axe.commons.color.Color(255, 255, 255, 1); - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('should return the body bgColor when content does not overlap', function () { @@ -783,11 +715,8 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getBackgroundColor(target, []); - - assert.closeTo(actual.red, 255, 0); - assert.closeTo(actual.green, 255, 0); - assert.closeTo(actual.blue, 255, 0); - assert.closeTo(actual.alpha, 1, 0); + var expected = new axe.commons.color.Color(255, 255, 255, 1); + assert.deepEqual(actual, expected); }); it('should return the html canvas inherited from body bgColor when element content does not overlap with body', function () { @@ -801,17 +730,16 @@ describe('color.getBackgroundColor', function () { document.body.style.background = '#000'; document.body.style.margin = 0; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var actual = axe.commons.color.getBackgroundColor(target, []); - - assert.closeTo(actual.red, 0, 0); - assert.closeTo(actual.green, 0, 0); - assert.closeTo(actual.blue, 0, 0); - assert.closeTo(actual.alpha, 1, 0); - - document.body.style.height = originalHeight; - document.body.style.margin = originalMargin; + try { + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var actual = axe.commons.color.getBackgroundColor(target, []); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); + } finally { + document.body.style.height = originalHeight; + document.body.style.margin = originalMargin; + } }); it('should return the html canvas bgColor when element content does not overlap with body', function () { @@ -824,16 +752,15 @@ describe('color.getBackgroundColor', function () { document.body.style.background = '#0f0'; document.documentElement.style.background = '#f00'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#target'); - var actual = axe.commons.color.getBackgroundColor(target, []); - - assert.closeTo(actual.red, 255, 0); - assert.closeTo(actual.green, 0, 0); - assert.closeTo(actual.blue, 0, 0); - assert.closeTo(actual.alpha, 1, 0); - - document.body.style.height = originalHeight; + try { + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var actual = axe.commons.color.getBackgroundColor(target, []); + var expected = new axe.commons.color.Color(255, 0, 0, 1); + assert.deepEqual(actual, expected); + } finally { + document.body.style.height = originalHeight; + } }); it('should apply mix-blend-mode', function () { @@ -870,10 +797,8 @@ describe('color.getBackgroundColor', function () { var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, []); - assert.closeTo(actual.red, 0, 0); - assert.closeTo(actual.green, 0, 0); - assert.closeTo(actual.blue, 0, 0); - assert.closeTo(actual.alpha, 1, 0); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); } ); @@ -890,11 +815,8 @@ describe('color.getBackgroundColor', function () { var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, [], false); - - assert.equal(actual.red, 0); - assert.equal(actual.green, 0); - assert.equal(actual.blue, 0); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); } ); @@ -911,11 +833,7 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, []); var expected = new axe.commons.color.Color(0, 0, 0, 1); - - assert.equal(actual.red, expected.red); - assert.equal(actual.green, expected.green); - assert.equal(actual.blue, expected.blue); - assert.equal(actual.alpha, expected.alpha); + assert.deepEqual(actual, expected); } ); @@ -933,10 +851,8 @@ describe('color.getBackgroundColor', function () { var target = shadow.querySelector('#shadowTarget'); axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, []); - assert.equal(actual.red, 255); - assert.equal(actual.green, 255); - assert.equal(actual.blue, 255); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(255, 255, 255, 1); + assert.deepEqual(actual, expected); } ); @@ -954,10 +870,8 @@ describe('color.getBackgroundColor', function () { var elm2 = document.querySelector('#elm2'); axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(elm2, []); - assert.equal(actual.red, 0); - assert.equal(actual.blue, 0); - assert.equal(actual.green, 0); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); } ); @@ -983,10 +897,8 @@ describe('color.getBackgroundColor', function () { var elm3 = shadow2.querySelector('#elm3'); axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(elm3, []); - assert.closeTo(actual.red, 128, 2); - assert.closeTo(actual.blue, 128, 2); - assert.closeTo(actual.green, 128, 2); - assert.closeTo(actual.alpha, 1, 0); + var expected = new axe.commons.color.Color(128, 128, 128, 1); + assertColorsClose(actual, expected, 2, 0); } ); @@ -1002,10 +914,8 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, []); - assert.equal(actual.red, 0); - assert.equal(actual.green, 0); - assert.equal(actual.blue, 0); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); } ); @@ -1036,10 +946,8 @@ describe('color.getBackgroundColor', function () { axe.testUtils.flatTreeSetup(fixture); var linkElm = div.querySelector('a'); var actual = axe.commons.color.getBackgroundColor(linkElm, []); - assert.equal(actual.red, 0); - assert.equal(actual.green, 0); - assert.equal(actual.blue, 0); - assert.equal(actual.alpha, 1); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); } ); @@ -1056,10 +964,7 @@ describe('color.getBackgroundColor', function () { // is 128 without the shadow var expected = new axe.commons.color.Color(145, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); assert.deepEqual(bgNodes, [parent]); }); @@ -1072,11 +977,29 @@ describe('color.getBackgroundColor', function () { var bgNodes = []; axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); + var expected = new axe.commons.color.Color(0, 0, 0, 1); + assert.deepEqual(actual, expected); + }); + + it('filters small shadows, while keeping larger ones', () => { + fixture.innerHTML = ` + <div id="parent" style="height: 40px; width: 30px; background-color: #800000;"> + <div id="target" style=" + height: 20px; + width: 15px; + text-shadow: red 0 0 1em, black 1px 1px 0;">foo + </div></div> + `; + var target = fixture.querySelector('#target'); + var parent = fixture.querySelector('#parent'); + var bgNodes = []; + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, bgNodes); - assert.equal(actual.red, 0); - assert.equal(actual.green, 0); - assert.equal(actual.blue, 0); - assert.equal(actual.alpha, 1); + // is 128 without the shadow + var expected = new axe.commons.color.Color(145, 0, 0, 1); + assertColorsClose(actual, expected); + assert.deepEqual(bgNodes, [parent]); }); it('ignores text-shadows thinner than shadowOutlineEmMax', function () { @@ -1091,10 +1014,7 @@ describe('color.getBackgroundColor', function () { // is 128 without the shadow var expected = new axe.commons.color.Color(145, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); describe('body and document', function () { @@ -1108,11 +1028,7 @@ describe('color.getBackgroundColor', function () { [] ); var expected = new axe.commons.color.Color(255, 0, 0, 1); - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns the body background even when the body is MUCH larger than the screen', function () { @@ -1126,10 +1042,7 @@ describe('color.getBackgroundColor', function () { ); var expected = new axe.commons.color.Color(255, 0, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns the html background', function () { @@ -1145,10 +1058,7 @@ describe('color.getBackgroundColor', function () { document.body.removeAttribute('style'); var expected = new axe.commons.color.Color(0, 255, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns the html background when body does not cover the element', function () { @@ -1164,10 +1074,7 @@ describe('color.getBackgroundColor', function () { ); var expected = new axe.commons.color.Color(0, 255, 0, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns the body background when body does cover the element', function () { @@ -1182,10 +1089,7 @@ describe('color.getBackgroundColor', function () { ); var expected = new axe.commons.color.Color(0, 0, 255, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); it('returns both the html and body background if the body has alpha', function () { @@ -1200,10 +1104,7 @@ describe('color.getBackgroundColor', function () { ); var expected = new axe.commons.color.Color(0, 128, 128, 1); - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assertColorsClose(actual, expected); }); }); }); diff --git a/test/commons/color/get-foreground-color.js b/test/commons/color/get-foreground-color.js index 50b7ea5526..06b31cc752 100644 --- a/test/commons/color/get-foreground-color.js +++ b/test/commons/color/get-foreground-color.js @@ -10,6 +10,12 @@ describe('color.getForegroundColor', () => { assert.closeTo(actual.alpha, expected.alpha, margin / 255); } + beforeEach(() => { + // This normalizes the default mocha behavior of setting a different background + // based on prefers-color-scheme settings. + document.body.style.background = '#fff'; + }); + afterEach(() => { axe.commons.color.incompleteData.clear(); document.body.scrollTop = 0; diff --git a/test/commons/color/get-stroke-colors-from-shadows.js b/test/commons/color/get-stroke-colors-from-shadows.js new file mode 100644 index 0000000000..ec22a85f2d --- /dev/null +++ b/test/commons/color/get-stroke-colors-from-shadows.js @@ -0,0 +1,151 @@ +describe('axe.commons.color.getStrokeColorsFromShadow', () => { + const { getStrokeColorsFromShadows, parseTextShadows } = axe.commons.color; + + it('should return an empty array if no shadows are passed', () => { + const colors = getStrokeColorsFromShadows([]); + assert.deepEqual(colors, []); + }); + + it('combines multiple shadows without blur into a single stroke', () => { + const shadows = parseTextShadows(` + 0 -2px #F00, + 2px 0 #F00, + 0 2px #F00, + -2px 0 #F00 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.deepEqual(shadowColors, [ + { + red: 255, + green: 0, + blue: 0, + alpha: 1 + } + ]); + }); + + it('returns empty when only one side is covered by the shadow', () => { + const shadows = parseTextShadows(`0 1px #000`); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 0); + }); + + it('returns null when two sides is covered by the shadow', () => { + const shadows = parseTextShadows(`1px 0 #000, 0 1px #000`); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.isNull(shadowColors); + }); + + it('returns null when three sides is covered by the shadow', () => { + const shadows = parseTextShadows(`1px 0 #000, 0 1px #000, -1px 0 #000`); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.isNull(shadowColors); + }); + + it('skips shadows offset with 0.5px or less', () => { + const shadows = parseTextShadows(` + .5px 0 #000, + 0 .4px #000, + -0.3px 0 #000, + 0 -0.2px #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 0); + }); + + it('applies an alpha value to shadows of 1.5px or less', () => { + const shadows = parseTextShadows(` + 0 -1.5px #000, + 1.4px 0 #000, + 0 1.3px #000, + -1.2px 0 #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 0); + assert.equal(shadowColors[0].green, 0); + assert.equal(shadowColors[0].blue, 0); + assert.closeTo(shadowColors[0].alpha, 0.46, 0.01); + }); + + it('applies an alpha value if not all shadows are offset by more than 1.5px', () => { + const shadows = parseTextShadows(` + 0 -2px #000, + 2px 0 #000, + 0 1.5px #000, + -1.5px 0 #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 0); + assert.equal(shadowColors[0].green, 0); + assert.equal(shadowColors[0].blue, 0); + assert.closeTo(shadowColors[0].alpha, 0.46, 0.01); + }); + + it('multiplies alpha value for each shadow', () => { + const shadows = parseTextShadows(` + 0 -1px #000, + 0 -1px #000, + 1px 0 #000, + 1px 0 #000, + 0 1px #000, + 0 1px #000, + -1px 0 #000, + -1px 0 #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 0); + assert.equal(shadowColors[0].green, 0); + assert.equal(shadowColors[0].blue, 0); + assert.closeTo(shadowColors[0].alpha, 0.7, 0.01); + }); + + it('double-counts shadows on corners', () => { + // Corner-shadows overlap on the sides of letters, increasing alpha + const shadows = parseTextShadows(` + -1px -1px #000, + 1px -1px #000, + 1px 1px #000, + -1px 1px #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 0); + assert.equal(shadowColors[0].green, 0); + assert.equal(shadowColors[0].blue, 0); + assert.closeTo(shadowColors[0].alpha, 0.7, 0.01); + }); + + it('applies an alpha value if not all sides are offset by more than 1.5px', () => { + const shadows = parseTextShadows(` + -1.5px -2px #000, + 2px 1.5px #000 + `); + const shadowColors = getStrokeColorsFromShadows(shadows); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 0); + assert.equal(shadowColors[0].green, 0); + assert.equal(shadowColors[0].blue, 0); + assert.closeTo(shadowColors[0].alpha, 0.46, 0.01); + }); + + describe('options.ignoreEdgeCount: true', () => { + it('returns empty when two sides is covered by the shadow', () => { + const shadows = parseTextShadows(`1px 0 #000, 0 1px #000`); + const shadowColors = getStrokeColorsFromShadows(shadows, { + ignoreEdgeCount: true + }); + assert.deepEqual(shadowColors, []); + }); + + it('returns empty when three sides is covered by the shadow', () => { + const shadows = parseTextShadows(`1px 0 #000, 0 1px #000, -1px 0 #000`); + const shadowColors = getStrokeColorsFromShadows(shadows, { + ignoreEdgeCount: true + }); + assert.deepEqual(shadowColors, []); + }); + }); +}); diff --git a/test/commons/color/get-text-shadow-colors.js b/test/commons/color/get-text-shadow-colors.js index 0a70f6333b..eb46db25ff 100644 --- a/test/commons/color/get-text-shadow-colors.js +++ b/test/commons/color/get-text-shadow-colors.js @@ -142,7 +142,7 @@ describe('axe.commons.color.getTextShadowColors', function () { assert.equal(shadowColors[0].blue, 0); }); - it('does not return shadows with a ratio less than minRatio', function () { + it('returns null if a shadows has a ratio less than minRatio', function () { fixture.innerHTML = '<span style="text-shadow: ' + '0 0 1em #F00, 0 0 0.5em #0F0, 1px 1px 0.2em #00F;' + @@ -150,10 +150,7 @@ describe('axe.commons.color.getTextShadowColors', function () { var span = fixture.querySelector('span'); var shadowColors = getTextShadowColors(span, { minRatio: 0.5 }); - - assert.lengthOf(shadowColors, 2); - assert.equal(shadowColors[0].red, 255); - assert.equal(shadowColors[1].green, 255); + assert.isNull(shadowColors); }); it('does not return shadows with a ratio less than maxRatio', function () { @@ -192,4 +189,86 @@ describe('axe.commons.color.getTextShadowColors', function () { assert.equal(shadowColors[0].blue, 0); assert.equal(shadowColors[0].alpha, 0); }); + + describe('with shadows that combine to a stroke', () => { + const opt = { minRatio: 0.01 }; + it('combines multiple shadows without blur into a single stroke', () => { + fixture.innerHTML = ` + <span style="text-shadow: + 0 -2px #F00, + 2px 0 #F00, + 0 2px #F00, + -2px 0 #F00; + ">Hello world</span> + `; + const shadowColors = getTextShadowColors(fixture.firstElementChild, opt); + assert.deepEqual(shadowColors, [ + { + red: 255, + green: 0, + blue: 0, + alpha: 1 + } + ]); + }); + + it('only combines shadows thinner than minRatio', () => { + const minRatio = 0.1; // .1em (has to be greater than 1px, or this will be null) + fixture.innerHTML = ` + <span style="text-shadow: + 0 -2px ${minRatio}em #000, + 2px 0 ${minRatio}em #000, + 0 2px ${minRatio}em #000, + -2px 0 ${minRatio}em #000; + ">Hello wold</span> + `; + const shadowColors = getTextShadowColors(fixture.firstElementChild, { + minRatio + }); + assert.lengthOf(shadowColors, 4); + }); + + it('places the combined shadow in the correct order with shadows', () => { + fixture.innerHTML = ` + <span style="text-shadow: + 0 -1px #000, 1px 0 #000, 0 1px #000, -1px 0 #000, + 0 0 1em #111, + 0 -1px #222, 1px 0 #222, 0 1px #222, -1px 0 #222, + 1px 1px 0.2em #333; + ">Hello wold</span> + `; + const shadowColors = getTextShadowColors(fixture.firstElementChild, opt); + const hexColors = shadowColors.map(color => color.toHexString()[1]); + assert.deepEqual(['0', '1', '2', '3'], hexColors); + }); + + it('returns null when when thin shadows are not all the way around the text', () => { + fixture.innerHTML = ` + <span style="text-shadow: + 0 -2px #000, + 2px 0 #000, + 0 2px #000; + ">Hello wold</span> + `; + const shadowColors = getTextShadowColors(fixture.firstElementChild, opt); + assert.isNull(shadowColors); + }); + + it('ignores partial strokes with ignoreEdgeCount: true', () => { + fixture.innerHTML = ` + <span style="text-shadow: + 2px 1px 2px #F00, + 0 -2px #000, + 2px 0 #000, + 0 2px #000; + ">Hello wold</span> + `; + const shadowColors = getTextShadowColors(fixture.firstElementChild, { + ...opt, + ignoreEdgeCount: true + }); + assert.lengthOf(shadowColors, 1); + assert.equal(shadowColors[0].red, 255); + }); + }); }); diff --git a/test/commons/color/parse-text-shadows.js b/test/commons/color/parse-text-shadows.js new file mode 100644 index 0000000000..6452ca9e9e --- /dev/null +++ b/test/commons/color/parse-text-shadows.js @@ -0,0 +1,76 @@ +describe('axe.commons.color.parseTextShadows', () => { + const { parseTextShadows } = axe.commons.color; + + it('should return an empty array if no text-shadow is provided', () => { + const shadows = parseTextShadows(' \t\n'); + assert.equal(shadows.length, 0); + }); + + it('should return an array of objects with `pixels` and `colorStr` properties', () => { + const shadows = parseTextShadows('1px 2px 3px #000, 4px 5px 6px #fff'); + assert.equal(shadows.length, 2); + + assert.deepEqual(shadows[0], { + pixels: [1, 2, 3], + colorStr: '#000' + }); + + assert.deepEqual(shadows[1], { + pixels: [4, 5, 6], + colorStr: '#fff' + }); + }); + + it('accepts color at the start of the shadow', () => { + const shadows = parseTextShadows('#000 1px 2px 3px, #fff 4px 5px 6px'); + assert.deepEqual(shadows[0], { + pixels: [1, 2, 3], + colorStr: '#000' + }); + assert.deepEqual(shadows[1], { + pixels: [4, 5, 6], + colorStr: '#fff' + }); + }); + + it('accepts named colors', () => { + const shadows = parseTextShadows('red 1px 2px 3px, blue 4px 5px 6px'); + assert.deepEqual(shadows[0], { + pixels: [1, 2, 3], + colorStr: 'red' + }); + assert.deepEqual(shadows[1], { + pixels: [4, 5, 6], + colorStr: 'blue' + }); + }); + + it('accepts different color functions', () => { + const shadows = parseTextShadows(` + rgb(0, 202, 148) 0px 0px, + hsl(165.58deg 100% 35.07% / .5) 0px 0px, + lch(65 49.19 167.45) 0px 0px, + oklch(60% 0.57 181) 0px 0px, + lab(65 -48.01 10.69) 0px 0px, + color(xyz-d65 0.26 0.44 0.35 / 1) 0px 0px, + `); + assert.equal(shadows[0].colorStr, 'rgb(0, 202, 148)'); + assert.equal(shadows[1].colorStr, 'hsl(165.58deg 100% 35.07% / .5)'); + assert.equal(shadows[2].colorStr, 'lch(65 49.19 167.45)'); + assert.equal(shadows[3].colorStr, 'oklch(60% 0.57 181)'); + assert.equal(shadows[4].colorStr, 'lab(65 -48.01 10.69)'); + assert.equal(shadows[5].colorStr, 'color(xyz-d65 0.26 0.44 0.35 / 1)'); + }); + + it('sets default blur to 0 when omitted', () => { + const shadows = parseTextShadows('1px 2px #000, 4px 5px #fff'); + assert.deepEqual(shadows[0], { + pixels: [1, 2, 0], + colorStr: '#000' + }); + assert.deepEqual(shadows[1], { + pixels: [4, 5, 0], + colorStr: '#fff' + }); + }); +}); diff --git a/test/commons/dom/create-grid.js b/test/commons/dom/create-grid.js index 5b344d3a55..6073a3357a 100644 --- a/test/commons/dom/create-grid.js +++ b/test/commons/dom/create-grid.js @@ -1,13 +1,14 @@ // Additional tests for createGrid are part of createRectStack tests, // which is what createGrid was originally part of -describe('create-grid', function () { - var fixture; - var createGrid = axe.commons.dom.createGrid; - var fixtureSetup = axe.testUtils.fixtureSetup; - var gridSize = axe.constants.gridSize; +describe('create-grid', () => { + let fixture; + const createGrid = axe.commons.dom.createGrid; + const fixtureSetup = axe.testUtils.fixtureSetup; + const queryFixture = axe.testUtils.queryFixture; + const gridSize = axe.constants.gridSize; function findPositions(grid, vNode) { - var positions = []; + const positions = []; grid.loopGridPosition(grid.boundaries, (cell, position) => { if (cell.includes(vNode)) { positions.push(position); @@ -16,12 +17,12 @@ describe('create-grid', function () { return positions; } - it('returns the grid size', function () { + it('returns the grid size', () => { axe.setup(); assert.equal(createGrid(), axe.constants.gridSize); }); - it('sets ._grid to nodes in the grid', function () { + it('sets ._grid to nodes in the grid', () => { fixture = fixtureSetup('<span>Hello world</span>'); assert.isUndefined(fixture._grid); assert.isUndefined(fixture.children[0]._grid); @@ -31,21 +32,21 @@ describe('create-grid', function () { assert.equal(fixture._grid, fixture.children[0]._grid); }); - it('adds elements to the correct cell in the grid', function () { + it('adds elements to the correct cell in the grid', () => { fixture = fixtureSetup('<span>Hello world</span>'); createGrid(); - var positions = findPositions(fixture._grid, fixture.children[0]); + const positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [{ col: 0, row: 0 }]); }); - it('adds large elements to multiple cell', function () { + it('adds large elements to multiple cell', () => { fixture = fixtureSetup( '<span style="display: inline-block; width: 300px; height: 300px;">' + 'Hello world</span>' ); createGrid(); - var positions = findPositions(fixture._grid, fixture.children[0]); + const positions = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(positions, [ { col: 0, row: 0 }, { col: 1, row: 0 }, @@ -54,38 +55,112 @@ describe('create-grid', function () { ]); }); - describe('hidden elements', function () { - beforeEach(function () { + it('only adds the visible non-overflow area of large elements', () => { + const vNode = queryFixture(` + <div style="overflow: hidden; width: 300px; height: 300px;"> + <span id="target" style="display: inline-block; width: 1000px; height: 1000px;">' + + 'Hello world</span> + </div> + `); + createGrid(); + + const positions = findPositions(vNode._grid, vNode); + assert.deepEqual(positions, [ + { col: 0, row: 0 }, + { col: 1, row: 0 }, + { col: 0, row: 1 }, + { col: 1, row: 1 } + ]); + }); + + describe('stackingOrder', () => { + it('adds stacking context information', () => { + fixture = fixtureSetup('<span>Hello world</span>'); + createGrid(); + const vNode = fixture.children[0]; + assert.lengthOf(vNode._stackingOrder, 1); + // purposefully do not test stackLevel and treeOrder values as they are + // implementation details that can easily change + assert.hasAllKeys(vNode._stackingOrder[0], [ + 'stackLevel', + 'treeOrder', + 'vNode' + ]); + assert.typeOf(vNode._stackingOrder[0].stackLevel, 'number'); + assert.typeOf(vNode._stackingOrder[0].treeOrder, 'number'); + assert.isNull(vNode._stackingOrder[0].vNode); // root stack + }); + + // when to use z-index values can be tested though + it('sets the stack level equal to the z-index of positioned elements', () => { + const vNode = queryFixture( + '<div id="target" style="position: absolute; z-index: 100">Hello world</div>' + ); + createGrid(); + assert.equal(vNode._stackingOrder[0].stackLevel, 100); + }); + + it("ignores z-index on elements that aren't positioned", () => { + const vNode = queryFixture( + '<div id="target" style="opacity: 0.2; z-index: 100">Hello world</div>' + ); + createGrid(); + assert.notEqual(vNode._stackingOrder[0].stackLevel, 100); + }); + + it('uses z-index on children of flex elements', () => { + const vNode = queryFixture( + '<div style="display: flex"><div id="target" style="z-index: 100">Hello world</div></div>' + ); + createGrid(); + assert.equal(vNode._stackingOrder[0].stackLevel, 100); + }); + + it('creates multiple stacking context when they are nested', () => { + const vNode = queryFixture(` + <div style="opacity: 0.2"> + <div style="transform: translate(42px, 18px);"> + <div id="target">Hello world</div> + </div> + </div> + `); + createGrid(); + assert.lengthOf(vNode._stackingOrder, 2); + }); + }); + + describe('hidden elements', () => { + beforeEach(() => { // Ensure the fixture itself is part of the grid, even if its content isn't document .querySelector('#fixture') .setAttribute('style', 'min-height: 10px'); }); - it('does not add hidden elements', function () { + it('does not add hidden elements', () => { fixture = fixtureSetup('<div style="display: none">hidden</div>'); createGrid(); - var position = findPositions(fixture._grid, fixture.children[0]); + const position = findPositions(fixture._grid, fixture.children[0]); assert.isEmpty(position); assert.isUndefined(fixture.children[0]._grid); }); - it('does not add off screen elements', function () { + it('does not add off screen elements', () => { fixture = fixtureSetup( '<div style="position: fixed; top: -3em">off screen</div>' ); createGrid(); - var position = findPositions(fixture._grid, fixture.children[0]); + const position = findPositions(fixture._grid, fixture.children[0]); assert.isEmpty(position); assert.isUndefined(fixture.children[0]._grid); }); - it('does add partially on screen elements', function () { + it('does add partially on screen elements', () => { fixture = fixtureSetup( '<div style="position: fixed; top: -1em; min-height: 2em">off screen</div>' ); createGrid(); - var position = findPositions(fixture._grid, fixture.children[0]); + const position = findPositions(fixture._grid, fixture.children[0]); assert.deepEqual(position, [ { col: 0, row: -1 }, { col: 0, row: 0 } @@ -102,7 +177,7 @@ describe('create-grid', function () { document.body.removeAttribute('style'); }); - it('adds elements vertically scrolled out of view', function () { + it('adds elements vertically scrolled out of view', () => { const gridScroll = 2; fixture = fixtureSetup(`<div id="scroller" style="height: ${gridSize}px; width: ${gridSize}px; overflow: scroll"> @@ -121,12 +196,12 @@ describe('create-grid', function () { createGrid(); childElms.forEach((child, index) => { assert.isDefined(child._grid, `Expect child ${index} to be defined`); - var position = findPositions(child._grid, child); + const position = findPositions(child._grid, child); assert.deepEqual(position, [{ col: 0, row: index - gridScroll }]); }); }); - it('adds elements horizontally scrolled out of view', function () { + it('adds elements horizontally scrolled out of view', () => { const gridScroll = 2; fixture = fixtureSetup(`<div id="scroller" style="width: ${gridSize}px; overflow: scroll"> @@ -147,60 +222,60 @@ describe('create-grid', function () { createGrid(); childElms.forEach((child, index) => { assert.isDefined(child._grid, `Expect child ${index} to be defined`); - var position = findPositions(child._grid, child); + const position = findPositions(child._grid, child); assert.deepEqual(position, [{ col: index - gridScroll, row: 0 }]); }); }); }); - describe('subGrids', function () { - it('sets the .subGrid property', function () { + describe('subGrids', () => { + it('sets the .subGrid property', () => { fixture = fixtureSetup( '<div style="overflow: scroll; height: 100px;">' + '<span style="display: inline-block; height: 300px" id="x">x</span>' + '</div>' ); - var vOverflow = fixture.children[0]; + const vOverflow = fixture.children[0]; assert.isUndefined(vOverflow._subGrid); createGrid(); assert.isDefined(vOverflow._subGrid); assert.notEqual(vOverflow._grid, vOverflow._subGrid); }); - it('sets the ._grid of children as the subGrid', function () { + it('sets the ._grid of children as the subGrid', () => { fixture = fixtureSetup( '<div style="overflow: scroll; height: 100px;">' + '<span style="display: inline-block; height: 300px" id="x">x</span>' + '</div>' ); createGrid(); - var vOverflow = fixture.children[0]; - var vSpan = vOverflow.children[0]; + const vOverflow = fixture.children[0]; + const vSpan = vOverflow.children[0]; assert.equal(vOverflow._subGrid, vSpan._grid); }); - it('does not add scrollable children to the root grid', function () { + it('does not add scrollable children to the root grid', () => { fixture = fixtureSetup( '<div style="overflow: scroll; height: 100px;">' + '<span style="display: inline-block; height: 300px" id="x">x</span>' + '</div>' ); createGrid(); - var vSpan = fixture.children[0].children[0]; - var position = findPositions(fixture._grid, vSpan); + const vSpan = fixture.children[0].children[0]; + const position = findPositions(fixture._grid, vSpan); assert.isEmpty(position); }); - it('adds scrollable children to the subGrid', function () { + it('adds scrollable children to the subGrid', () => { fixture = fixtureSetup( '<div style="overflow: scroll; height: 100px;">' + '<span style="display: inline-block; height: 300px" id="x">x</span>' + '</div>' ); createGrid(); - var vOverflow = fixture.children[0]; - var vSpan = vOverflow.children[0]; - var position = findPositions(vOverflow._subGrid, vSpan); + const vOverflow = fixture.children[0]; + const vSpan = vOverflow.children[0]; + const position = findPositions(vOverflow._subGrid, vSpan); assert.deepEqual(position, [ { col: 0, row: 0 }, { col: 0, row: 1 } diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index df466da016..3b045f7874 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -465,6 +465,28 @@ describe('dom.getElementStack', () => { assert.deepEqual(stack, []); }); + it('should correctly position children of different stacking contexts', () => { + fixture.innerHTML = ` + <header id="1" style="position: absolute; z-index: 999; height: 50px; width: 100%; top: 0"> + <div id="2" style="display: flex; position: relative; height: 50px;"> + <div id="3" style="position: relative; height: 50px; width: 100%;"></div> + </div> + <div id="4" style="display: flex; position: absolute; height: 50px; width: 100%; top: 0;"> + <div id="5" style="position: absolute; transform: translate(0, -50%);"> + <div id="6" style="display: flex;"> + <span id="target">Hello World</span> + </div> + </div> + </div> + </header> + `; + + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#target'); + const stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['target', '6', '5', '4', '3', '2', '1']); + }); + it('should throw error if element midpoint-x exceeds the grid', () => { fixture.innerHTML = '<div id="target">Hello World</div>'; axe.testUtils.flatTreeSetup(fixture); diff --git a/test/commons/dom/get-target-rects.js b/test/commons/dom/get-target-rects.js new file mode 100644 index 0000000000..f25abe2918 --- /dev/null +++ b/test/commons/dom/get-target-rects.js @@ -0,0 +1,83 @@ +describe('get-target-rects', () => { + const getTargetRects = axe.commons.dom.getTargetRects; + const { queryFixture } = axe.testUtils; + + it('returns the bounding rect when unobscured', () => { + const vNode = queryFixture('<button id="target">x</button>'); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [vNode.actualNode.getBoundingClientRect()]); + }); + + it('returns subset rect when obscured', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px;"></div> + `); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [new DOMRect(10, 5, 20, 40)]); + }); + + it('ignores elements with "pointer-events: none"', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px; pointer-events: none"></div> + `); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [vNode.actualNode.getBoundingClientRect()]); + }); + + it("ignores elements that don't overlap the target", () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 60px; top: 0; width: 50px; height: 50px;"></div> + `); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [vNode.actualNode.getBoundingClientRect()]); + }); + + it('ignores non-tabbable descendants of the target', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px"> + <div style="position: absolute; left: 5px; top: 5px; width: 50px; height: 50px;"></div> + </button> + `); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [vNode.actualNode.getBoundingClientRect()]); + }); + + it('returns each unobscured area', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px;"></div> + <div style="position: absolute; left: 0px; top: 20px; width: 50px; height: 10px"></div> + `); + const rects = getTargetRects(vNode); + assert.deepEqual(rects, [ + new DOMRect(10, 5, 20, 15), + new DOMRect(10, 30, 20, 15) + ]); + }); + + it('returns empty if target is fully obscured', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 0; top: 0; width: 50px; height: 50px;"></div> + `); + const rects = getTargetRects(vNode); + assert.lengthOf(rects, 0); + }); + + it('returns subset rect of the target with tabbable descendant', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px"> + <div tabindex="0" style="position: absolute; left: 5px; top: 5px; width: 50px; height: 50px;"></div> + </button> + `); + const rects = getTargetRects(vNode); + console.log(JSON.stringify(rects)); + assert.deepEqual(rects, [ + new DOMRect(10, 5, 30, 7), + new DOMRect(10, 5, 7, 40) + ]); + }); +}); diff --git a/test/commons/dom/get-target-size.js b/test/commons/dom/get-target-size.js new file mode 100644 index 0000000000..10bda51300 --- /dev/null +++ b/test/commons/dom/get-target-size.js @@ -0,0 +1,47 @@ +describe('get-target-size', () => { + const getTargetSize = axe.commons.dom.getTargetSize; + const { queryFixture } = axe.testUtils; + + it('returns the bounding rect when unobscured', () => { + const vNode = queryFixture('<button id="target">x</button>'); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, vNode.actualNode.getBoundingClientRect()); + }); + + it('returns target size when obscured', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px;"></div> + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 20, 40)); + }); + + it('ignores elements with "pointer-events: none"', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px; pointer-events: none"></div> + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, vNode.actualNode.getBoundingClientRect()); + }); + + it("ignores elements that don't overlap the target", () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 60px; top: 0; width: 50px; height: 50px;"></div> + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, vNode.actualNode.getBoundingClientRect()); + }); + + it('returns the largest unobscured area', () => { + const vNode = queryFixture(` + <button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px">x</button> + <div style="position: absolute; left: 30px; top: 0; width: 50px; height: 50px;"></div> + <div style="position: absolute; left: 0px; top: 0px; width: 50px; height: 10px"></div> + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 10, 20, 35)); + }); +}); diff --git a/test/commons/dom/get-visible-child-text-rects.js b/test/commons/dom/get-visible-child-text-rects.js index 242e9453be..126e79d037 100644 --- a/test/commons/dom/get-visible-child-text-rects.js +++ b/test/commons/dom/get-visible-child-text-rects.js @@ -14,20 +14,20 @@ describe('dom.getVisibleChildTextRects', () => { } /** - * Asset that two DOMRect arrays are equal. - * @param {DOMRect[]} rectAs - * @param {DOMRect[]} rectBs + * Assert that two DOMRect arrays are equal. + * @param {DOMRect[]} actualRects + * @param {DOMRect[]} expectedRects */ - function assertRectsEqual(rectAs, rectBs) { - assert.equal(rectAs.length, rectBs.length); - rectAs.forEach((rect, index) => { - const rectA = rectAs[index]; - const rectB = rectBs[index]; - - assert.approximately(rectA.left, rectB.left, 1); - assert.approximately(rectA.top, rectB.top, 1); - assert.approximately(rectA.width, rectB.width, 1); - assert.approximately(rectA.height, rectB.height, 1); + function assertRectsEqual(actualRects, expectedRects) { + assert.equal(actualRects.length, expectedRects.length); + actualRects.forEach((rect, index) => { + const actual = actualRects[index]; + const expected = expectedRects[index]; + + assert.approximately(actual.left, expected.left, 1, 'left'); + assert.approximately(actual.top, expected.top, 1, 'top'); + assert.approximately(actual.width, expected.width, 1, 'width'); + assert.approximately(actual.height, expected.height, 1, 'height'); }); } diff --git a/test/commons/dom/visually-sort.js b/test/commons/dom/visually-sort.js index 1b943d30f4..897f614215 100644 --- a/test/commons/dom/visually-sort.js +++ b/test/commons/dom/visually-sort.js @@ -1,21 +1,108 @@ // This method is mostly tested through color-contrast integrations -describe('visually-sort', function () { +describe('visually-sort', () => { 'use strict'; - var fixtureSetup = axe.testUtils.fixtureSetup; - var visuallySort = axe.commons.dom.visuallySort; + const fixture = document.querySelector('#fixture'); + const visuallySort = axe.commons.dom.visuallySort; + const querySelectorAll = axe.utils.querySelectorAll; + let root; - it('returns 1 if B overlaps A', function () { - var rootNode = fixtureSetup('<a><b>bar</b></a>'); - var vNodeA = rootNode.children[0]; - var vNodeB = vNodeA.children[0]; - assert.equal(visuallySort(vNodeA, vNodeB), 1); + beforeEach(() => { + fixture.innerHTML = ` + <header id="1" style="position: absolute; z-index: 999; height: 50px; width: 100%;"> + <div id="2" style="display: flex; position: relative; height: 50px;"> + <div id="3" style="position: relative; height: 50px; width: 100%;"></div> + </div> + </header> + <div id="4" style="position: absolute; z-index: 10; height: 50px; width: 100%;"> + <div id="5" style="position: absolute; transform: translate(0, -50%);"> + <div id="6"> + <div id="7" style="float: left">Text</div> + <div id="8">Text</div> + <span id="9">Text</span> + <div id="10">Text</div> + <div id="shadow-host"></div> + </div> + </div> + </div> + `; + const shadowRoot = fixture + .querySelector('#shadow-host') + .attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '<div id="shadow-child">Text</div>'; + root = axe.setup(fixture); }); - it('returns -1 if A overlaps B', function () { - var rootNode = fixtureSetup('<b><a>bar</a></b>'); - var vNodeB = rootNode.children[0]; - var vNodeA = vNodeB.children[0]; - assert.equal(visuallySort(vNodeA, vNodeB), -1); + /* + Array.sort() return meanings: + + compareFn(a, b) | return value sort order + ----------------|----------------------- + > 0 | sort a after b, e.g. [b, a] + < 0 | sort a before b, e.g. [a, b] + === 0 | keep original order of a and b + */ + + it('sorts a higher stack before a lower stack', () => { + const vNodeA = querySelectorAll(root, '#1')[0]; + const vNodeB = querySelectorAll(root, '#4')[0]; + + assert.isBelow(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts a lower stack after a higher stack', () => { + const vNodeA = querySelectorAll(root, '#4')[0]; + const vNodeB = querySelectorAll(root, '#1')[0]; + + assert.isAbove(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts a child stack before a parent stack', () => { + const vNodeA = querySelectorAll(root, '#6')[0]; + const vNodeB = querySelectorAll(root, '#4')[0]; + + assert.isBelow(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts a parent stack after a child stack', () => { + const vNodeA = querySelectorAll(root, '#4')[0]; + const vNodeB = querySelectorAll(root, '#6')[0]; + + assert.isAbove(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts a child of a higher stack before a child of a lower stack', () => { + const vNodeA = querySelectorAll(root, '#3')[0]; + const vNodeB = querySelectorAll(root, '#7')[0]; + + assert.isBelow(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts a child of a lower stack after a child of a higher stack', () => { + const vNodeA = querySelectorAll(root, '#7')[0]; + const vNodeB = querySelectorAll(root, '#3')[0]; + + assert.isAbove(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts elements by tree order when in the same stack', () => { + const vNodeA = querySelectorAll(root, '#8')[0]; + const vNodeB = querySelectorAll(root, '#10')[0]; + + assert.isAbove(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts floated elements before other elements of the same stack', () => { + const vNodeA = querySelectorAll(root, '#7')[0]; + const vNodeB = querySelectorAll(root, '#8')[0]; + + assert.isBelow(visuallySort(vNodeA, vNodeB), 0); + }); + + it('sorts shadow DOM elements by tree order when in the same stack', () => { + const vNodeA = querySelectorAll(root, '#8')[0]; + const vNodeB = querySelectorAll(root, '#shadow-host')[0].children[0]; + + assert.isAbove(visuallySort(vNodeA, vNodeB), 0); }); }); diff --git a/test/commons/matches/from-function.js b/test/commons/matches/from-function.js index c8f0bc880a..e3a1228a6e 100644 --- a/test/commons/matches/from-function.js +++ b/test/commons/matches/from-function.js @@ -1,47 +1,47 @@ -describe('matches.fromFunction', function () { - var fromFunction = axe.commons.matches.fromFunction; +describe('matches.fromFunction', () => { + const fromFunction = axe.commons.matches.fromFunction; function noop() {} - it('throws an error when the matcher is a number', function () { - assert.throws(function () { + it('throws an error when the matcher is a number', () => { + assert.throws(() => { fromFunction(noop, 123); }); }); - it('throws an error when the matcher is a string', function () { - assert.throws(function () { + it('throws an error when the matcher is a string', () => { + assert.throws(() => { fromFunction(noop, 'foo'); }); }); - it('throws an error when the matcher is an array', function () { - assert.throws(function () { + it('throws an error when the matcher is an array', () => { + assert.throws(() => { fromFunction(noop, ['foo']); }); }); - it('throws an error when the matcher is a RegExp', function () { - assert.throws(function () { + it('throws an error when the matcher is a RegExp', () => { + assert.throws(() => { fromFunction(noop, /foo/); }); }); - describe('with object matches', function () { - var keyMap = {}; + describe('with object matches', () => { + let keyMap = {}; function getValue(key) { return key; } - it('passes every object key to the getValue function once', function () { - var keys = ['foo', 'bar', 'baz']; - function getValue(key) { - var index = keys.indexOf(key); + it('passes every object key to the getValue function once', () => { + const keys = ['foo', 'bar', 'baz']; + function getValueOnce(key) { + const index = keys.indexOf(key); assert.notEqual(index, -1); keys.splice(index, 1); return key; } - fromFunction(getValue, { + fromFunction(getValueOnce, { foo: 'foo', bar: 'bar', baz: 'baz' @@ -49,7 +49,7 @@ describe('matches.fromFunction', function () { assert.lengthOf(keys, 0); }); - it('returns true if every value is matched', function () { + it('returns true if every value is matched', () => { keyMap = { foo: 'foo', bar: 'bar', @@ -58,7 +58,7 @@ describe('matches.fromFunction', function () { assert.isTrue(fromFunction(getValue, keyMap)); }); - it('returns false if any value is not matched', function () { + it('returns false if any value is not matched', () => { keyMap = { foo: 'foo', bar: 'bar', @@ -74,7 +74,7 @@ describe('matches.fromFunction', function () { ); }); - it('returns true if there are no keys', function () { + it('returns true if there are no keys', () => { assert.isTrue(fromFunction(getValue, {})); }); }); diff --git a/test/commons/math/get-offset.js b/test/commons/math/get-offset.js index 9214d6a861..073c8d0649 100644 --- a/test/commons/math/get-offset.js +++ b/test/commons/math/get-offset.js @@ -1,104 +1,75 @@ -describe('getOffset', function () { - 'use strict'; - var fixtureSetup = axe.testUtils.fixtureSetup; - var getOffset = axe.commons.math.getOffset; - var round = 0.2; +describe('getOffset', () => { + const fixtureSetup = axe.testUtils.fixtureSetup; + const getOffset = axe.commons.math.getOffset; + const round = 0.2; - // Return the diagonal of a square of size X, or rectangle of size X * Y - function getDiagonal(x, y) { - y = typeof y === 'number' ? y : x; - return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); - } - - it('returns with + spacing for horizontally adjacent elms', function () { - var fixture = fixtureSetup( - '<a style="width:30px; margin-right:10px; display:inline-block"> </a>' + - '<b style="width:20px; display:inline-block"> </b>' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), 40, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); - }); - - it('returns closest horizontal distance for elements horizontally aligned', function () { - var fixture = fixtureSetup( - '<a style="width:30px; height:30px; margin-right:10px; display:inline-block"> </a>' + - '<b style="width:20px; height:20px; top:5px; position:relative; display:inline-block"> </b>' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40, 5), round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns center to edge of circle when both are undersized', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 50px; left: 0"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 38, round); }); - it('returns height + spacing for vertically adjacent elms', function () { - var fixture = fixtureSetup( - '<a style="height:30px; margin:10px 0; display:block"> </a>' + - '<b style="height:20px; display:block"> </b>' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), 40, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns center to edge of square when one is undersized', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: 50px; left: 0"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 45, round); }); - it('returns closest vertical distance for elements horizontally aligned', function () { - var fixture = fixtureSetup( - '<a style="height:30px; margin:10px 0; display:block"> </a>' + - '<b style="height:20px; margin: 0 10px; display:block"> </b>' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40, 10), round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns center to corner of square when at a diagonal', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: 50px; left: 50px"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 63.6, round); }); - it('returns corner to corner distance for diagonal elms', function () { - var fixture = fixtureSetup( - '<a style="width: 30px; height:30px; margin:10px 30px; display:block"> </a>' + - '<b style="width: 20px; height:20px; display:block"> </b>' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40), round); - assert.closeTo(getOffset(nodeB, nodeA), getDiagonal(30), round); + it('returns 0 if nodeA is overlapped by nodeB', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.equal(getOffset(nodeA, nodeB), 0); }); - it('returns the distance to the edge when elements overlap on an edge', function () { - var fixture = fixtureSetup( - '<a style="padding-right:30px; display:inline-block">' + - '<b style="width:30px; height: 20px; display:inline-block"> </b>' + - '</a>' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), 30, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns 0 if nodeB is overlapped by nodeA', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px"> </button> + `); + const nodeA = fixture.children[3]; + const nodeB = fixture.children[1]; + assert.equal(getOffset(nodeA, nodeB), 0); }); - it('returns the shortest side of the element when an element overlaps on a corner', function () { - var fixture = fixtureSetup( - '<a style="padding:30px 30px 0 0; display:inline-block">' + - '<b style="width:30px; height: 20px; display:inline-block"> </b>' + - '</a>' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(30), round); - assert.closeTo(getOffset(nodeB, nodeA), 20, round); + it('subtracts minNeighbourRadius from center-to-center calculations', () => { + const fixture = fixtureSetup(` + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 50px; left: 0"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB, 30), 20, round); }); - it('returns smallest diagonal if elmA fully covers elmB', function () { - var fixture = fixtureSetup( - '<a style="padding:10px; display:inline-block">' + - '<b style="width:20px; height: 30px; display:inline-block"> </b>' + - '</a>' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(10), round); - assert.closeTo(getOffset(nodeB, nodeA), 10, round); + it('returns 0 if center of nodeA is enclosed by nodeB', () => { + const fixture = fixtureSetup(` + <button style="width: 50px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0"> </button> + <button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 20px;"> </button> + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.equal(getOffset(nodeA, nodeB, 30), 0); }); }); diff --git a/test/commons/math/rect-has-minimum-size.js b/test/commons/math/rect-has-minimum-size.js new file mode 100644 index 0000000000..7d93b5c83a --- /dev/null +++ b/test/commons/math/rect-has-minimum-size.js @@ -0,0 +1,28 @@ +describe('rectHasMinimumSize', () => { + const rectHasMinimumSize = axe.commons.math.rectHasMinimumSize; + + it('returns true if rect is large enough', () => { + const rect = new DOMRect(10, 20, 10, 20); + assert.isTrue(rectHasMinimumSize(10, rect)); + }); + + it('returns true for rounding margin', () => { + const rect = new DOMRect(10, 20, 9.95, 20); + assert.isTrue(rectHasMinimumSize(10, rect)); + }); + + it('returns false if width is too small', () => { + const rect = new DOMRect(10, 20, 5, 20); + assert.isFalse(rectHasMinimumSize(10, rect)); + }); + + it('returns false if height is too small', () => { + const rect = new DOMRect(10, 20, 10, 5); + assert.isFalse(rectHasMinimumSize(10, rect)); + }); + + it('returns false when below rounding margin', () => { + const rect = new DOMRect(10, 20, 9.94, 20); + assert.isFalse(rectHasMinimumSize(10, rect)); + }); +}); diff --git a/test/commons/math/split-rects.js b/test/commons/math/split-rects.js index 714e03f24d..0680b44822 100644 --- a/test/commons/math/split-rects.js +++ b/test/commons/math/split-rects.js @@ -1,92 +1,95 @@ -describe('splitRects', function () { - var splitRects = axe.commons.math.splitRects; - function createRect(x, y, width, height) { - return { - x: x, - y: y, - width: width, - height: height, - top: y, - left: x, - bottom: y + height, - right: x + width - }; - } +describe('splitRects', () => { + const splitRects = axe.commons.math.splitRects; - it('returns the original rect if there is no clipping rect', function () { - var rectA = createRect(0, 0, 100, 50); - var rects = splitRects(rectA, []); + it('returns the original rect if there is no clipping rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rects = splitRects(rectA, []); assert.lengthOf(rects, 1); assert.deepEqual(rects[0], rectA); }); - it('returns the original rect if there is no overlap', function () { - var rectA = createRect(0, 0, 100, 50); - var rectB = createRect(0, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns the original rect if there is no overlap', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(0, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 1); assert.deepEqual(rects[0], rectA); }); - describe('with one overlapping rect', function () { - it('returns one rect if overlaps covers two corners', function () { - var rectA = createRect(0, 0, 100, 50); - var rectB = createRect(40, 0, 100, 50); - var rects = splitRects(rectA, [rectB]); + describe('with one overlapping rect', () => { + it('returns one rect if overlaps covers two corners', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(40, 0, 100, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 1); - assert.deepEqual(rects[0], createRect(0, 0, 40, 50)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 40, 50)); }); - it('returns two rects if overlap covers one corner', function () { - var rectA = createRect(0, 0, 100, 100); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns two rects if overlap covers one corner', () => { + const rectA = new DOMRect(0, 0, 100, 100); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 2); - assert.deepEqual(rects[0], createRect(0, 0, 100, 50)); - assert.deepEqual(rects[1], createRect(0, 0, 50, 100)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 100, 50)); + assert.deepEqual(rects[1], new DOMRect(0, 0, 50, 100)); }); - it('returns three rects if overlap covers an edge, but no corner', function () { - var rectA = createRect(0, 0, 100, 150); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns three rects if overlap covers an edge, but no corner', () => { + const rectA = new DOMRect(0, 0, 100, 150); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 3); - assert.deepEqual(rects[0], createRect(0, 0, 100, 50)); - assert.deepEqual(rects[1], createRect(0, 100, 100, 50)); - assert.deepEqual(rects[2], createRect(0, 0, 50, 150)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 100, 50)); + assert.deepEqual(rects[1], new DOMRect(0, 100, 100, 50)); + assert.deepEqual(rects[2], new DOMRect(0, 0, 50, 150)); }); - it('returns four rects if overlap sits in the middle, touching no corner', function () { - var rectA = createRect(0, 0, 150, 150); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns four rects if overlap sits in the middle, touching no corner', () => { + const rectA = new DOMRect(0, 0, 150, 150); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 4); - assert.deepEqual(rects[0], createRect(0, 0, 150, 50)); - assert.deepEqual(rects[1], createRect(100, 0, 50, 150)); - assert.deepEqual(rects[2], createRect(0, 100, 150, 50)); - assert.deepEqual(rects[3], createRect(0, 0, 50, 150)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 150, 50)); + assert.deepEqual(rects[1], new DOMRect(100, 0, 50, 150)); + assert.deepEqual(rects[2], new DOMRect(0, 100, 150, 50)); + assert.deepEqual(rects[3], new DOMRect(0, 0, 50, 150)); + }); + + it('returns no rects if overlap covers the entire input rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(-50, -50, 400, 400); + const rects = splitRects(rectA, [rectB]); + assert.lengthOf(rects, 0); }); }); - describe('with multiple overlaps', function () { - it('can return a single rect two overlaps each cover an edge', function () { - var rectA = createRect(0, 0, 150, 50); - var rectB = createRect(0, 0, 50, 50); - var rectC = createRect(100, 0, 50, 50); - var rects = splitRects(rectA, [rectB, rectC]); + describe('with multiple overlaps', () => { + it('can return a single rect two overlaps each cover an edge', () => { + const rectA = new DOMRect(0, 0, 150, 50); + const rectB = new DOMRect(0, 0, 50, 50); + const rectC = new DOMRect(100, 0, 50, 50); + const rects = splitRects(rectA, [rectB, rectC]); assert.lengthOf(rects, 1); - assert.deepEqual(rects[0], createRect(50, 0, 50, 50)); + assert.deepEqual(rects[0], new DOMRect(50, 0, 50, 50)); }); - it('can recursively clips regions', function () { - var rectA = createRect(0, 0, 150, 100); - var rectB = createRect(0, 50, 50, 50); - var rectC = createRect(100, 50, 50, 50); - var rects = splitRects(rectA, [rectB, rectC]); + it('can recursively clips regions', () => { + const rectA = new DOMRect(0, 0, 150, 100); + const rectB = new DOMRect(0, 50, 50, 50); + const rectC = new DOMRect(100, 50, 50, 50); + const rects = splitRects(rectA, [rectB, rectC]); assert.lengthOf(rects, 3); - assert.deepEqual(rects[0], createRect(0, 0, 150, 50)); - assert.deepEqual(rects[1], createRect(50, 0, 100, 50)); - assert.deepEqual(rects[2], createRect(50, 0, 50, 100)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 150, 50)); + assert.deepEqual(rects[1], new DOMRect(50, 0, 100, 50)); + assert.deepEqual(rects[2], new DOMRect(50, 0, 50, 100)); + }); + + it('returns no rects if overlap covers the entire input rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(50, 50, 200, 200); + const rectC = new DOMRect(-50, -50, 200, 200); + const rects = splitRects(rectA, [rectB, rectC]); + assert.lengthOf(rects, 0); }); }); }); diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js index 8b053fb020..fd0552f0cd 100644 --- a/test/commons/text/accessible-text.js +++ b/test/commons/text/accessible-text.js @@ -1,129 +1,163 @@ -describe('text.accessibleTextVirtual', function () { - 'use strict'; +describe('text.accessibleTextVirtual', () => { + const fixture = document.getElementById('fixture'); + const { html, shadowSupport } = axe.testUtils; - var fixture = document.getElementById('fixture'); - var shadowSupport = axe.testUtils.shadowSupport; - - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = null; }); - it('is called through accessibleText with a DOM node', function () { - var accessibleText = axe.commons.text.accessibleText; - fixture.innerHTML = '<label><input type="button"></label>'; + it('is called through accessibleText with a DOM node', () => { + const accessibleText = axe.commons.text.accessibleText; + fixture.innerHTML = html` <label><input type="button" /></label> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('input'); + const target = fixture.querySelector('input'); assert.equal(accessibleText(target), ''); }); - it('should match the first example from the ARIA spec', function () { - fixture.innerHTML = - '<ul role="menubar">' + - ' <!-- Rule 2A: "File" label via aria-labelledby -->' + - ' <li role="menuitem" aria-haspopup="true" aria-labelledby="fileLabel" id="rule2a">' + - ' <span id="fileLabel">File</span>' + - ' <ul role="menu">' + - ' <!-- Rule 2C: "New" label via Namefrom:contents -->' + - ' <li role="menuitem" id="rule2c">New</li>' + - ' <li role="menuitem">Open…</li>' + - ' …' + - ' </ul>' + - ' </li>' + - '</ul>'; + it('should match the first example from the ARIA spec', () => { + fixture.innerHTML = html` + <ul role="menubar"> + <!-- Rule 2A: "File" label via aria-labelledby --> + <li + role="menuitem" + aria-haspopup="true" + aria-labelledby="fileLabel" + id="rule2a" + > + <span id="fileLabel">File</span> + <ul role="menu"> + <!-- Rule 2C: "New" label via Namefrom:contents --> + <li role="menuitem" id="rule2c">New</li> + <li role="menuitem">Open…</li> + … + </ul> + </li> + </ul> + `; axe.testUtils.flatTreeSetup(fixture); - var rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0]; - var rule2c = axe.utils.querySelectorAll(axe._tree, '#rule2c')[0]; + const rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0]; + const rule2c = axe.utils.querySelectorAll(axe._tree, '#rule2c')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(rule2a), 'File'); assert.equal(axe.commons.text.accessibleTextVirtual(rule2c), 'New'); }); - it('should match the second example from the ARIA spec', function () { - fixture.innerHTML = - '<fieldset>' + - ' <legend>Meeting alarms</legend>' + - ' <!-- Rule 2A: "Beep" label given by native HTML label element -->' + - ' <input type="checkbox" id="beep"> <label for="beep">Beep</label> <br>' + - ' <input type="checkbox" id="mtgTitle"> <label for="mtgTitle">Display the meeting title</label> <br>' + - ' <!-- Rule 2B -->' + - ' <input type="checkbox" id="flash">' + - ' <label for="flash">' + - ' Flash the screen' + - ' <!-- Rule 2A: label of text input given by aria-label, "Number of times to flash screen" -->' + - ' <input type="text" value="3" size="2" id="numTimes" title="Number of times to flash screen">' + - ' times' + - ' </label>' + - '</fieldset>'; + it('should match the second example from the ARIA spec', () => { + fixture.innerHTML = html` + <fieldset> + <legend>Meeting alarms</legend> + <!-- Rule 2A: "Beep" label given by native HTML label element --> + <input type="checkbox" id="beep" /> <label for="beep">Beep</label> + <br /> + <input type="checkbox" id="mtgTitle" /> + <label for="mtgTitle">Display the meeting title</label> <br /> + <!-- Rule 2B --> + <input type="checkbox" id="flash" /> + <label for="flash"> + Flash the screen + <!-- Rule 2A: label of text input given by aria-label, "Number of times to flash screen" --> + <input + type="text" + value="3" + size="2" + id="numTimes" + title="Number of times to flash screen" + /> + times + </label> + </fieldset> + `; axe.testUtils.flatTreeSetup(fixture); - var rule2a = axe.utils.querySelectorAll(axe._tree, '#beep')[0]; - var rule2b = axe.utils.querySelectorAll(axe._tree, '#flash')[0]; + const rule2a = axe.utils.querySelectorAll(axe._tree, '#beep')[0]; + const rule2b = axe.utils.querySelectorAll(axe._tree, '#flash')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(rule2a), 'Beep'); - // Chrome 72: "Flash the screen 3 times" - // Firefox 62: "Flash the screen 3 times" - // Safari 12.0: "Flash the screen 3 times" assert.equal( axe.commons.text.accessibleTextVirtual(rule2b), 'Flash the screen 3 times' ); }); - it('should use aria-labelledby if present', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use aria-labelledby if present', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is a label' ); }); - it('should use recusive aria-labelledby properly', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1 t1label" aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use recusive aria-labelledby properly', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1 t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'ARIA Label This is a label' ); }); - it('should include hidden text referred to with aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1label" style="display:none">This is a ' + - '<span style="visibility:hidden">hidden </span>' + - '<span aria-hidden="true">secret</span></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t1" aria-labelledby="t1label">'; + it('should include hidden text referred to with aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1label" style="display:none"> + This is a + <span style="visibility:hidden">hidden </span> + <span aria-hidden="true">secret</span> + </div> + <label for="t1">HTML Label</label> + <input type="text" id="t1" aria-labelledby="t1label" />' + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is a hidden secret' ); }); - it('should allow setting the initial includeHidden value', function () { - fixture.innerHTML = - '<label id="lbl1" style="display:none;">hidden label</label>'; + it('should allow setting the initial includeHidden value', () => { + fixture.innerHTML = html` + <label id="lbl1" style="display:none;">hidden label</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target, { includeHidden: false @@ -139,470 +173,577 @@ describe('text.accessibleTextVirtual', function () { ); }); - it('should use aria-label if present with no labelledby', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use aria-label if present with no labelledby', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input type="text" value="the value" aria-label="ARIA Label" id="t1" /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'ARIA Label'); }); - it('should use alt on imgs with no ARIA', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'id="t1"> of <i>everything</i></div>' + - '<img alt="Alt text goes here" id="target">' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use alt on imgs with no ARIA', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is <input type="text" value="the value" id="t1" /> of + <i>everything</i> + </div> + <img alt="Alt text goes here" id="target" /> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Alt text goes here' ); }); - it('should use alt on image inputs with no ARIA', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'id="t1"> of <i>everything</i></div>' + - '<input type="image" alt="Alt text goes here" id="target">' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use alt on image inputs with no ARIA', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is <input type="text" value="the value" id="t1" /> of + <i>everything</i> + </div> + <input type="image" alt="Alt text goes here" id="target" /> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" />' + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Alt text goes here' ); }); - it('should use not use alt on text inputs with no ARIA', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'id="t1"> of <i>everything</i></div>' + - '<input type="text" alt="Alt text goes here" id="target">' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use not use alt on text inputs with no ARIA', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is <input type="text" value="the value" id="t1" /> of + <i>everything</i> + </div> + <input type="text" alt="Alt text goes here" id="target" /> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); - it('should use HTML label if no ARIA information', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use HTML label if no ARIA information', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is <input type="text" value="the value" id="t1" /> of + <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'HTML Label'); }); - it('should handle last ditch title attribute', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i title="italics"></i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should handle last ditch title attribute', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i title="italics"></i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of italics' ); }); - it('should handle totally empty elements', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i></i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should handle totally empty elements', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i></i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of' ); }); - it('should handle author name-from roles properly', function () { - fixture.innerHTML = - '<div id="t2label" role="heading">This is ' + - ' <input type="text" value="the value" ' + - ' aria-labelledby="t1label" aria-label="ARIA Label" id="t1">' + - ' of <i role="alert">everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should handle author name-from roles properly', () => { + fixture.innerHTML = html` + <div id="t2label" role="heading"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i role="alert">everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; - // Chrome 86: This is This is a label of - // Firefox 82: This is ARIA Label everything - // Safari 14.0: This is This is a label of everything + const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; + // Chrome 114: "This is the value of " + // Firefox 115: "This is ARIA Label the value everything" + // Safari 16.5: This is the value This is a label of everything assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of everything' ); }); - it('should only show each node once when label is before input', function () { - fixture.innerHTML = - '<div id="target"><label for="tb1">My form input</label>' + - '<input type="text" id="tb1"></div>'; + it('should only show each node once when label is before input', () => { + fixture.innerHTML = html` + <div id="target"> + <label for="tb1">My form input</label> <input type="text" id="tb1" /> + </div> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'My form input' ); }); - it('should only show each node once when label follows input', function () { - fixture.innerHTML = - '<div id="target">' + - '<input type="text" id="tb1"></div>' + - '<label for="tb1">My form input</label>'; + it('should only show each node once when label follows input', () => { + fixture.innerHTML = html` + <div id="target"> + <input type="text" id="tb1" /> + </div> + <label for="tb1">My form input</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'My form input' ); }); - it('should handle nested inputs in normal context', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should handle nested inputs in normal context', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of everything' ); }); - it('should use handle nested inputs properly in labelledby context', function () { - // Chrome 72: This is This is a label of everything - // Firefox 62: This is ARIA Label the value of everything - // Safari 12.0: THis is This is a label of everything - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use handle nested inputs properly in labelledby context', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + // Chrome 114: This is the value of everything + // Firefox 115: This is ARIA Label the value of everything + // Safari 16.5: THis is This is a label of everything assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is ARIA Label of everything' ); }); - it('should use ignore hidden inputs', function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="hidden" value="the value" ' + - 'Label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use ignore hidden inputs', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is <input type="hidden" value="the value" Label" id="t1"> of + <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is of everything' ); }); - it('should use handle inputs with no type as if they were text inputs', function () { - fixture.innerHTML = - '<div id="t2label">This is <input value="the value" ' + - 'aria-labelledby="t1label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use handle inputs with no type as if they were text inputs', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input value="the value" aria-labelledby="t1label" id="t1" /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is the value of everything" - // Safari 12.0: "This is This is a label of everything" + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + // Chrome 114: "This is the value of everything" + // Firefox 115: "This is the value of everything" + // Safari 16.5: "This is This is a label of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything' ); }); - it('should use handle nested selects properly in labelledby context', function () { - fixture.innerHTML = - '<div id="t2label">This is <select multiple ' + - 'aria-labelledby="t1label" id="t1">' + - '<option selected>first</option><option>second</option><option selected>third</option>' + - '</select> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use handle nested selects properly in labelledby context', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <select multiple aria-labelledby="t1label" id="t1"> + <option selected>first</option> + <option>second</option> + <option selected>third</option> + </select> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is of everything" - // Safari 12.0: "This is first third label of" + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + // Chrome 114: "This is first third of everything" + // Firefox 115: "This is of everything" + // Safari 16.5: "This is first third of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), - 'This is of everything' + 'This is first third of everything' ); }); - it('should use handle nested textareas properly in labelledby context', function () { - fixture.innerHTML = - '<div id="t2label">This is <textarea ' + - 'aria-labelledby="t1label" id="t1">the value</textarea> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use handle nested textareas properly in labelledby context', () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <textarea aria-labelledby="t1label" id="t1">the value</textarea> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; - // Chrome 70: "This is This is a label of everything" - // Firefox 62: "This is ARIA Label the value of everything" - // Safari 12.0: "This is This is a label of everything" + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + // Chrome 114: "This is the value of everything" + // Firefox 115: "This is the value of everything" + // Safari 16.5: "This is This is a label of everything" assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything' ); }); - it('should use handle ARIA labels properly in labelledby context', function () { - fixture.innerHTML = - '<div id="t2label">This <span aria-label="not a span">span</span>' + - ' is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" id="t1"> of <i>everything</i></div>' + - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + it('should use handle ARIA labels properly in labelledby context', () => { + fixture.innerHTML = html` <div id="t2label"> + This + <span aria-label="not a span">span</span> + is + <input + type="text" + value="the value" + aria-labelledby="t1label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" />`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'This not a span is the value of everything' ); }); - it('should come up empty if input is labeled only by select options', function () { - fixture.innerHTML = - '<label for="target">' + - '<select id="select">' + - ' <option selected="selected">Chosen</option>' + - ' <option>Not Selected</option>' + - '</select>' + - '</label>' + - '<input id="target" type="text" />'; + it('should come up empty if input is labeled only by select options', () => { + fixture.innerHTML = html` + <label for="target"> + <select id="select"> + <option selected="selected">Chosen</option> + <option>Not Selected</option> + </select> + </label> + <input id="target" type="text" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "Chosen" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); - it("should be empty if input is labeled by labeled select (ref'd string labels have spotty support)", function () { - fixture.innerHTML = - '<label for="select">My Select</label>' + - '<label for="target">' + - '<select id="select">' + - ' <option selected="selected">Chosen</option>' + - ' <option>Not Selected</option>' + - '</select>' + - '</label>' + - '<input id="target" type="text" />'; + it("should be empty if input is labeled by labeled select (ref'd string labels have spotty support)", () => { + fixture.innerHTML = html` + <label for="select">My Select</label> + <label for="target"> + <select id="select"> + <option selected="selected">Chosen</option> + <option>Not Selected</option> + </select> + </label> + <input id="target" type="text" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); - it('should be empty for an empty label wrapping a select', function () { - fixture.innerHTML = - '<label>' + - '<span class="label"></span>' + - '<select id="target">' + - '<option value="1" selected="selected">Please choose a region</option>' + - '<option value="2">Coastal</option>' + - '<option value="3">Forest</option>' + - '<option value="4">Grasslands</option>' + - '<option value="5">Mountains</option>' + - '</select>' + - '</label>'; + it('should be empty for an empty label wrapping a select', () => { + fixture.innerHTML = html` + <label> + <span class="label"></span> + <select id="target"> + <option value="1" selected="selected">Please choose a region</option> + <option value="2">Coastal</option> + <option value="3">Forest</option> + <option value="4">Grasslands</option> + <option value="5">Mountains</option> + </select> + </label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); - it('should not return select options if input is aria-labelled by a select', function () { - fixture.innerHTML = - '<label>' + - '<select id="select">' + - ' <option selected="selected">Chosen</option>' + - ' <option>Not Selected</option>' + - '</select>' + - '</label>' + - '<input aria-labelledby="select" type="text" id="target" />'; + it('should not return select options if input is aria-labelled by a select', () => { + fixture.innerHTML = html` + <label> + <select id="select"> + <option selected="selected">Chosen</option> + <option>Not Selected</option> + </select> + </label> + <input aria-labelledby="select" type="text" id="target" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; + // Chrome 114: "Chosen" + // Firefox 115: "Chosen" + // Safari 16.5: "Chosen" + assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Chosen'); }); - it('shoud properly fall back to title', function () { - fixture.innerHTML = '<a href="#" role="presentation" title="Hello"></a>'; + it('shoud properly fall back to title', () => { + fixture.innerHTML = html` + <a href="#" role="presentation" title="Hello"></a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=presentation on anchors', function () { - fixture.innerHTML = '<a href="#" role="presentation">Hello</a>'; + it('should give text even for role=presentation on anchors', () => { + fixture.innerHTML = html` <a href="#" role="presentation">Hello</a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=presentation on buttons', function () { - fixture.innerHTML = '<button role="presentation">Hello</button>'; + it('should give text even for role=presentation on buttons', () => { + fixture.innerHTML = html` <button role="presentation">Hello</button> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=presentation on summary', function () { - fixture.innerHTML = '<summary role="presentation">Hello</summary>'; + it('should give text even for role=presentation on summary', () => { + fixture.innerHTML = html` <summary role="presentation">Hello</summary> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('shoud properly fall back to title', function () { - fixture.innerHTML = '<a href="#" role="none" title="Hello"></a>'; + it('shoud properly fall back to title', () => { + fixture.innerHTML = html` <a href="#" role="none" title="Hello"></a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=none on anchors', function () { - fixture.innerHTML = '<a href="#" role="none">Hello</a>'; + it('should give text even for role=none on anchors', () => { + fixture.innerHTML = html` <a href="#" role="none">Hello</a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=none on buttons', function () { - fixture.innerHTML = '<button role="none">Hello</button>'; + it('should give text even for role=none on buttons', () => { + fixture.innerHTML = html` <button role="none">Hello</button> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should give text even for role=none on summary', function () { - fixture.innerHTML = '<summary role="none">Hello</summary>'; + it('should give text even for role=none on summary', () => { + fixture.innerHTML = html` <summary role="none">Hello</summary> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should not add extra spaces around phrasing elements', function () { - fixture.innerHTML = '<a href="#">Hello<span>World</span></a>'; + it('should not add extra spaces around phrasing elements', () => { + fixture.innerHTML = html` <a href="#">Hello<span>World</span></a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'HelloWorld'); }); - it('should add spaces around non-phrasing elements', function () { - fixture.innerHTML = '<a href="#">Hello<div>World</div></a>'; + it('should add spaces around non-phrasing elements', () => { + fixture.innerHTML = html` + <a href="#" + >Hello + <div>World</div></a + > + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello World'); }); - it('should not look at scripts', function () { + it('should not look at scripts', () => { fixture.innerHTML = - '<a href="#"><script> var ajiasdf = true; </script></a>'; + '<a href="#"><script> const ajiasdf = true; </script></a>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); - it('should use <label> for input buttons', function () { - fixture.innerHTML = '<label><input type="button"></label>'; + it('should use <label> for input buttons', () => { + fixture.innerHTML = html` <label><input type="button" /></label> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); - it('should not stop when attributes contain whitespace', function () { + it('should not stop when attributes contain whitespace', () => { fixture.innerHTML = '<button aria-label=" " aria-labelledby=" ">Hello World</button>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello World'); }); (!!document.fonts ? it : xit)( 'should allow ignoring icon ligatures', - function (done) { - var materialFont = new FontFace( + done => { + const materialFont = new FontFace( 'Material Icons', 'url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2)' ); - materialFont.load().then(function () { + materialFont.load().then(() => { document.fonts.add(materialFont); fixture.innerHTML = '<button id="target">Hello World<span style="font-family: \'Material Icons\'">delete</span></button>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; try { assert.equal( axe.commons.text.accessibleTextVirtual(target, { @@ -620,22 +761,33 @@ describe('text.accessibleTextVirtual', function () { (shadowSupport.v1 ? it : xit)( 'should only find aria-labelledby element in the same context ', - function () { - fixture.innerHTML = - '<div id="t2label">This is <input type="text" value="the value" ' + - 'aria-labelledby="t1label" aria-label="ARIA Label" id="t1"> of <i>everything</i></div>' + - '<div id="shadow"></div>'; - - var shadow = document + () => { + fixture.innerHTML = html` + <div id="t2label"> + This is + <input + type="text" + value="the value" + aria-labelledby="t1label" + aria-label="ARIA Label" + id="t1" + /> + of <i>everything</i> + </div> + <div id="shadow"></div> + `; + + const shadow = document .getElementById('shadow') .attachShadow({ mode: 'open' }); - shadow.innerHTML = - '<div id="t1label">This is a <b>label</b></div>' + - '<label for="t1">HTML Label</label>' + - '<input type="text" id="t2" aria-labelledby="t2label">'; + shadow.innerHTML = html` + <div id="t1label">This is a <b>label</b></div> + <label for="t1">HTML Label</label> + <input type="text" id="t2" aria-labelledby="t2label" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; + const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'ARIA Label' @@ -645,16 +797,16 @@ describe('text.accessibleTextVirtual', function () { (shadowSupport.v1 ? it : xit)( 'should find attributes within a shadow tree', - function () { - fixture.innerHTML = '<div id="shadow"></div>'; + () => { + fixture.innerHTML = html` <div id="shadow"></div> `; - var shadow = document + const shadow = document .getElementById('shadow') .attachShadow({ mode: 'open' }); shadow.innerHTML = '<input type="text" id="t1" title="I will be king">'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'I will be king' @@ -664,17 +816,17 @@ describe('text.accessibleTextVirtual', function () { (shadowSupport.v1 ? it : xit)( 'should find attributes within a slot on the shadow tree', - function () { + () => { fixture.innerHTML = '<div id="shadow"><input type="text" id="t1" title="you will be queen"></div>'; - var shadow = document + const shadow = document .getElementById('shadow') .attachShadow({ mode: 'open' }); shadow.innerHTML = '<slot></slot>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'you will be queen' @@ -684,18 +836,19 @@ describe('text.accessibleTextVirtual', function () { (shadowSupport.v1 ? it : xit)( 'should find fallback content for shadow DOM', - function () { - fixture.innerHTML = '<div id="shadow"></div>'; + () => { + fixture.innerHTML = html` <div id="shadow"></div> `; - var shadow = document + const shadow = document .getElementById('shadow') .attachShadow({ mode: 'open' }); - shadow.innerHTML = - '<input type="text" id="t1">' + - '<label for="t1"><slot>Fallback content heroes</slot></label>'; + shadow.innerHTML = html` + <input type="text" id="t1" /> + <label for="t1"><slot>Fallback content heroes</slot></label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Fallback content heroes' @@ -703,110 +856,126 @@ describe('text.accessibleTextVirtual', function () { } ); - describe('figure', function () { - it('should check aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div>' + - '<figure aria-labelledby="t1">Not part of a11yName <figcaption>Fail</figcaption></figure>'; + describe('figure', () => { + it('should check aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <figure aria-labelledby="t1"> + Not part of a11yName + <figcaption>Fail</figcaption> + </figure> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should check aria-label', function () { - fixture.innerHTML = - '<figure aria-label="Hello">Not part of a11yName <figcaption>Fail</figcaption></figure>'; + it('should check aria-label', () => { + fixture.innerHTML = html` + <figure aria-label="Hello"> + Not part of a11yName + <figcaption>Fail</figcaption> + </figure> + '; + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should check the figures figcaption', function () { + it('should check the figures figcaption', () => { fixture.innerHTML = '<figure>Not part of a11yName <figcaption>Hello</figcaption></figure>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should check title on figure', function () { + it('should check title on figure', () => { fixture.innerHTML = '<figure title="Hello">Not part of a11yName <figcaption></figcaption></figure>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should fall back to innerText of figure', function () { - fixture.innerHTML = '<figure>Hello<figcaption></figcaption></figure>'; + it('should fall back to innerText of figure', () => { + fixture.innerHTML = html` + <figure> + Hello + <figcaption></figcaption> + </figure> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); (shadowSupport.v1 ? it : xit)( 'should check within the composed (shadow) tree', - function () { - var node = document.createElement('div'); + () => { + const node = document.createElement('div'); node.innerHTML = 'Hello'; - var shadowRoot = node.attachShadow({ mode: 'open' }); + const shadowRoot = node.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '<figure>Not part of a11yName <figcaption><slot></slot></figcaption></figure>'; fixture.appendChild(node); axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'figure')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); } ); }); - describe('img', function () { - it('should work with aria-labelledby attribute', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<img aria-labelledby="t1 t2">'; + describe('img', () => { + it('should work with aria-labelledby attribute', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <img aria-labelledby="t1 t2" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with aria-label attribute', function () { - fixture.innerHTML = '<img aria-label="Hello World">'; + it('should work with aria-label attribute', () => { + fixture.innerHTML = html` <img aria-label="Hello World" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with alt attribute', function () { - fixture.innerHTML = '<img alt="Hello World">'; + it('should work with alt attribute', () => { + fixture.innerHTML = html` <img alt="Hello World" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with title attribute', function () { - fixture.innerHTML = '<img title="Hello World">'; + it('should work with title attribute', () => { + fixture.innerHTML = html` <img title="Hello World" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'img')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' @@ -814,65 +983,65 @@ describe('text.accessibleTextVirtual', function () { }); }); - describe('input buttons', function () { - it('should find value for input type=button', function () { - fixture.innerHTML = '<input type="button" value="Hello">'; + describe('input buttons', () => { + it('should find value for input type=button', () => { + fixture.innerHTML = html` <input type="button" value="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should find value for input type=reset', function () { - fixture.innerHTML = '<input type="reset" value="Hello">'; + it('should find value for input type=reset', () => { + fixture.innerHTML = html` <input type="reset" value="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should find value for input type=submit', function () { - fixture.innerHTML = '<input type="submit" value="Hello">'; + it('should find value for input type=submit', () => { + fixture.innerHTML = html` <input type="submit" value="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should provide a default value for input type="submit"', function () { - fixture.innerHTML = '<input type="submit">'; + it('should provide a default value for input type="submit"', () => { + fixture.innerHTML = html` <input type="submit" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), target.actualNode.value || 'Submit' ); }); - it('should provide a default value for input type="reset"', function () { - fixture.innerHTML = '<input type="reset">'; + it('should provide a default value for input type="reset"', () => { + fixture.innerHTML = html` <input type="reset" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; - var defaultText = axe.commons.text.accessibleTextVirtual(target); + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const defaultText = axe.commons.text.accessibleTextVirtual(target); assert.isString(defaultText); assert.equal(defaultText, target.actualNode.value || 'Reset'); }); - it('should find title for input type=button', function () { - fixture.innerHTML = '<input type="button" title="Hello">'; + it('should find title for input type=button', () => { + fixture.innerHTML = html` <input type="button" title="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); - it('should find title for input type=reset', function () { - fixture.innerHTML = '<input type="reset" title="Hello">'; + it('should find title for input type=reset', () => { + fixture.innerHTML = html` <input type="reset" title="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; // IE does not use title; but will use default value instead assert.equal( axe.commons.text.accessibleTextVirtual(target), @@ -880,11 +1049,11 @@ describe('text.accessibleTextVirtual', function () { ); }); - it('should find title for input type=submit', function () { - fixture.innerHTML = '<input type="submit" title="Hello">'; + it('should find title for input type=submit', () => { + fixture.innerHTML = html` <input type="submit" title="Hello" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; // Again, default value takes precedence over title assert.equal( axe.commons.text.accessibleTextVirtual(target), @@ -893,73 +1062,77 @@ describe('text.accessibleTextVirtual', function () { }); }); - describe('tables', function () { - it('should work with aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<table aria-labelledby="t1 t2"></table>'; + describe('tables', () => { + it('should work with aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <table aria-labelledby="t1 t2"></table> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with aria-label', function () { - fixture.innerHTML = '<table aria-label="Hello World"></table>'; + it('should work with aria-label', () => { + fixture.innerHTML = html` <table aria-label="Hello World"></table> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with the caption element', function () { + it('should work with the caption element', () => { fixture.innerHTML = '<table><caption>Hello World</caption><tr><td>Stuff</td></tr></table>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with the title attribute', function () { - fixture.innerHTML = '<table title="Hello World"></table>'; + it('should work with the title attribute', () => { + fixture.innerHTML = html` <table title="Hello World"></table> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should work with the summary attribute', function () { - fixture.innerHTML = '<table summary="Hello World"></table>'; + it('should work with the summary attribute', () => { + fixture.innerHTML = html` <table summary="Hello World"></table> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should prefer summary attribute over title attribute', function () { - // Chrome 70: "Hello world" - // Firefox 62: "Hello world" - // Safari 12.0: "FAIL" - fixture.innerHTML = '<table summary="Hello World" title="FAIL"></table>'; + it('should prefer summary attribute over title attribute', () => { + // Chrome 114: "Hello world" + // Firefox 115: "Hello world" + // Safari 16.5: "Hello world" + fixture.innerHTML = html` + <table summary="Hello World" title="FAIL"></table> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'table')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' @@ -967,20 +1140,20 @@ describe('text.accessibleTextVirtual', function () { }); }); - describe('text inputs', function () { - var types = ['text', 'password', 'search', 'tel', 'email', 'url', null]; + describe('text inputs', () => { + const types = ['text', 'password', 'search', 'tel', 'email', 'url', null]; - it('should find aria-labelledby', function () { + it('should find aria-labelledby', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<input' + - t + - ' aria-labelledby="t1 t2">'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <input ${t} aria-labelledby="t1 t2" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -989,13 +1162,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find aria-label', function () { + it('should find aria-label', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = '<input' + t + ' aria-label="Hello World">'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<input ${t} aria-label="Hello World">`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1004,14 +1177,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find an implicit label', function () { + it('should find an implicit label', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = - '<label for="t1">Hello World' + '<input' + t + '></label>'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<label for="t1">Hello World<input ${t}></label>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1020,14 +1192,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find an explicit label', function () { + it('should find an explicit label', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = - '<label for="t1">Hello World</label>' + '<input' + t + ' id="t1">'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<label for="t1">Hello World</label><input ${t} id="t1">`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1036,14 +1207,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find implicit labels with id that does not match to a label', function () { + it('should find implicit labels with id that does not match to a label', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = - '<label for="t1">Hello World' + '<input' + t + ' id="foo"></label>'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<label for="t1">Hello World<input ${t} id="foo"></label>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1052,13 +1222,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find a placeholder attribute', function () { + it('should find a placeholder attribute', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = '<input' + t + ' placeholder="Hello World">'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<input ${t} placeholder="Hello World">`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1067,13 +1237,13 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should find a title attribute', function () { + it('should find a title attribute', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = '<input' + t + ' title="Hello World">'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<input ${t} title="Hello World">`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World', @@ -1082,223 +1252,236 @@ describe('text.accessibleTextVirtual', function () { }); }); - it('should otherwise be empty string', function () { + it('should otherwise be empty string', () => { types.forEach(function (type) { - var t = type ? ' type="' + type + '"' : ''; - fixture.innerHTML = '<input' + t + '>'; + const t = type ? ' type="' + type + '"' : ''; + fixture.innerHTML = `<input ${t}>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); }); }); - describe('textarea', function () { - it('should find aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<textarea aria-labelledby="t1 t2"></textarea>'; + describe('textarea', () => { + it('should find aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <textarea aria-labelledby="t1 t2"></textarea> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find aria-label', function () { - fixture.innerHTML = '<textarea aria-label="Hello World"></textarea>'; + it('should find aria-label', () => { + fixture.innerHTML = html` + <textarea aria-label="Hello World"></textarea> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find an implicit label', function () { - fixture.innerHTML = - '<label for="t1">Hello World' + '<textarea></textarea></label>'; + it('should find an implicit label', () => { + fixture.innerHTML = `<label for="t1">Hello World<textarea></textarea></label>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find an explicit label', function () { - fixture.innerHTML = - '<label for="t1">Hello World</label>' + '<textarea id="t1"></textarea>'; + it('should find an explicit label', () => { + fixture.innerHTML = `<label for="t1">Hello World</label><textarea id="t1"></textarea>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find a placeholder attribute', function () { - fixture.innerHTML = '<textarea placeholder="Hello World"></textarea>'; + it('should find a placeholder attribute', () => { + fixture.innerHTML = html` + <textarea placeholder="Hello World"></textarea> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find a title attribute', function () { - fixture.innerHTML = '<textarea title="Hello World"></textarea>'; + it('should find a title attribute', () => { + fixture.innerHTML = html` <textarea title="Hello World"></textarea> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should otherwise be empty string', function () { - fixture.innerHTML = '<textarea></textarea>'; + it('should otherwise be empty string', () => { + fixture.innerHTML = html` <textarea></textarea> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'textarea')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); }); - describe('image inputs', function () { - it('should find aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<input type="image" aria-labelledby="t1 t2">'; + describe('image inputs', () => { + it('should find aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <input type="image" aria-labelledby="t1 t2" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find aria-label', function () { - fixture.innerHTML = '<input type="image" aria-label="Hello World">'; + it('should find aria-label', () => { + fixture.innerHTML = html` + <input type="image" aria-label="Hello World" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find an alt attribute', function () { - fixture.innerHTML = '<input type="image" alt="Hello World">'; + it('should find an alt attribute', () => { + fixture.innerHTML = html` <input type="image" alt="Hello World" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find a title attribute', function () { - fixture.innerHTML = '<input type="image" title="Hello World">'; + it('should find a title attribute', () => { + fixture.innerHTML = html` <input type="image" title="Hello World" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should otherwise be "Submit" string', function () { - fixture.innerHTML = '<input type="image">'; + it('should otherwise be "Submit" string', () => { + fixture.innerHTML = html` <input type="image" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'input')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Submit'); }); }); - describe('a', function () { - it('should find aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<a aria-labelledby="t1 t2"></a>'; + describe('a', () => { + it('should find aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <a aria-labelledby="t1 t2"></a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find aria-label', function () { - fixture.innerHTML = '<a aria-label="Hello World"></a>'; + it('should find aria-label', () => { + fixture.innerHTML = html` <a aria-label="Hello World"></a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should check subtree', function () { - fixture.innerHTML = '<a><span>Hello<span> World</span></span></a>'; + it('should check subtree', () => { + fixture.innerHTML = html` + <a + ><span>Hello<span> World</span></span></a + > + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find a title attribute', function () { - fixture.innerHTML = '<a title="Hello World"></a>'; + it('should find a title attribute', () => { + fixture.innerHTML = html` <a title="Hello World"></a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should otherwise be empty string', function () { - fixture.innerHTML = '<a></a>'; + it('should otherwise be empty string', () => { + fixture.innerHTML = html` <a></a> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); - it('should use text from a table with a single cell and role=presentation', function () { - fixture.innerHTML = - '<a href="example.html">' + - '<table role="presentation">' + - '<tr>' + - '<td>' + - 'Descriptive Link Text' + - '</td>' + - '</tr>' + - '</table>' + - '</a>'; - axe.testUtils.flatTreeSetup(fixture); - - var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; + it('should use text from a table with a single cell and role=presentation', () => { + fixture.innerHTML = html` + <a href="example.html"> + <table role="presentation"> + <tr> + <td>Descriptive Link Text</td> + </tr> + </table> + </a> + `; + axe.testUtils.flatTreeSetup(fixture); + + const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Descriptive Link Text' @@ -1306,65 +1489,67 @@ describe('text.accessibleTextVirtual', function () { }); }); - describe('button', function () { - it('should find aria-labelledby', function () { - fixture.innerHTML = - '<div id="t1">Hello</div><div id="t2">World</div>' + - '<button aria-labelledby="t1 t2"></button>'; + describe('button', () => { + it('should find aria-labelledby', () => { + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + <button aria-labelledby="t1 t2"></button> + `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find aria-label', function () { - fixture.innerHTML = '<button aria-label="Hello World"></button>'; + it('should find aria-label', () => { + fixture.innerHTML = html` <button aria-label="Hello World"></button> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should check subtree', function () { + it('should check subtree', () => { fixture.innerHTML = '<button><span>Hello<span> World</span></span></button>'; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should find a title attribute', function () { - fixture.innerHTML = '<button title="Hello World"></button>'; + it('should find a title attribute', () => { + fixture.innerHTML = html` <button title="Hello World"></button> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal( axe.commons.text.accessibleTextVirtual(target), 'Hello World' ); }); - it('should otherwise be empty string', function () { - fixture.innerHTML = '<button></button>'; + it('should otherwise be empty string', () => { + fixture.innerHTML = html` <button></button> `; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; + const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); }); - describe('text level semantics', function () { - var tags = [ + describe('text level semantics', () => { + const tags = [ 'em', 'strong', 'small', @@ -1393,67 +1578,70 @@ describe('text.accessibleTextVirtual', function () { 'wbr' ]; - it('should find aria-labelledby', function () { + it('should find aria-labelledby', () => { tags.forEach(function (tag) { - fixture.innerHTML = '<div id="t1">Hello</div><div id="t2">World</div>'; + fixture.innerHTML = html` + <div id="t1">Hello</div> + <div id="t2">World</div> + `; axe.testUtils.flatTreeSetup(fixture); - var elm = document.createElement(tag); + const elm = document.createElement(tag); elm.setAttribute('aria-labelledby', 't1 t2'); elm.style.display = 'inline'; // Firefox hides some of these elements because reasons... fixture.appendChild(elm); axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.getNodeFromTree(elm); - var result = axe.commons.text.accessibleTextVirtual(target); + const target = axe.utils.getNodeFromTree(elm); + const result = axe.commons.text.accessibleTextVirtual(target); assert.equal(result, 'Hello World', tag); }); }); - it('should find aria-label', function () { + it('should find aria-label', () => { tags.forEach(function (tag) { - var elm = document.createElement(tag); + const elm = document.createElement(tag); elm.setAttribute('aria-label', 'Hello World'); elm.style.display = 'inline'; // Firefox hack, see above fixture.appendChild(elm); axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.getNodeFromTree(elm); - var result = axe.commons.text.accessibleTextVirtual(target); + const target = axe.utils.getNodeFromTree(elm); + const result = axe.commons.text.accessibleTextVirtual(target); assert.equal(result, 'Hello World', tag); }); }); - it('should find a title attribute', function () { + it('should find a title attribute', () => { tags.forEach(function (tag) { - var elm = document.createElement(tag); + const elm = document.createElement(tag); elm.setAttribute('title', 'Hello World'); elm.style.display = 'inline'; // Firefox hack, see above fixture.appendChild(elm); axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.getNodeFromTree(elm); - var result = axe.commons.text.accessibleTextVirtual(target); + const target = axe.utils.getNodeFromTree(elm); + const result = axe.commons.text.accessibleTextVirtual(target); assert.equal(result, 'Hello World', tag); }); }); - it('should otherwise be empty string', function () { + it('should otherwise be empty string', () => { tags.forEach(function (tag) { - fixture.innerHTML = '<' + tag + '></' + tag + '>'; + fixture.innerHTML = `<${tag}></${tag}>`; axe.testUtils.flatTreeSetup(fixture); - var target = axe.utils.querySelectorAll(axe._tree, tag)[0]; + const target = axe.utils.querySelectorAll(axe._tree, tag)[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); }); }); - it('inserts a space before img alt text', function () { - var accessibleText = axe.commons.text.accessibleText; + it('inserts a space before img alt text', () => { + const accessibleText = axe.commons.text.accessibleText; fixture.innerHTML = '<a id="test" href="../images/index.html">Images tool test page<img id="img18" aria-label="aria-label text" alt="logo" src="../images/Accessibility.jpg" width="50" height="50"></a>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal( accessibleText(target), 'Images tool test page aria-label text' @@ -1461,1526 +1649,1934 @@ describe('text.accessibleTextVirtual', function () { }); }); - describe('text.accessibleText acceptance tests', function () { + describe('text.accessibleText acceptance tests', () => { 'use strict'; // Tests borrowed from the AccName 1.1 testing docs // https://www.w3.org/wiki/AccName_1.1_Testable_Statements#Name_test_case_539 - var ariaValuetext = xit; // Not acc supported - var pseudoText = xit; // Not acc supported + const ariaValuetext = xit; // Not acc supported + const pseudoText = xit; // Not acc supported - var fixture = document.getElementById('fixture'); - var accessibleText = axe.commons.text.accessibleText; - var _unsupported; + const accessibleText = axe.commons.text.accessibleText; + let _unsupported; - before(function () { + before(() => { _unsupported = axe.commons.text.unsupported.accessibleNameFromFieldValue; axe.commons.text.unsupported.accessibleNameFromFieldValue = []; }); - after(function () { + after(() => { axe.commons.text.unsupported.accessibleNameFromFieldValue = _unsupported; }); - afterEach(function () { - fixture.innerHTML = ''; + afterEach(() => { + fixture.innerHTML = html``; axe._tree = null; }); - it('passes test 1', function () { - fixture.innerHTML = '<input type="button" aria-label="Rich" id="test">'; + it('passes test 1', () => { + fixture.innerHTML = html` + <input type="button" aria-label="Rich" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Rich'); }); - it('passes test 2', function () { - fixture.innerHTML = - '<div id="ID1">Rich\'s button</div>' + - '<input type="button" aria-labelledby="ID1" id="test">'; + it('passes test 2', () => { + fixture.innerHTML = html` + <div id="ID1">Rich's button</div> + <input type="button" aria-labelledby="ID1" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), "Rich's button"); }); - it('passes test 3', function () { - fixture.innerHTML = - '<div id="ID1">Rich\'s button</div>' + - '<input type="button" aria-label="bar" aria-labelledby="ID1" id="test"/>'; + it('passes test 3', () => { + fixture.innerHTML = html` + <div id="ID1">Rich's button</div> + <input type="button" aria-label="bar" aria-labelledby="ID1" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), "Rich's button"); }); - it('passes test 4', function () { - fixture.innerHTML = '<input type="reset" id="test"/>'; + it('passes test 4', () => { + fixture.innerHTML = html` <input type="reset" id="test" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Reset'); }); - it('passes test 5', function () { - fixture.innerHTML = '<input type="button" id="test" value="foo"/>'; + it('passes test 5', () => { + fixture.innerHTML = html` <input type="button" id="test" value="foo" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 6', function () { + it('passes test 6', () => { fixture.innerHTML = '<input src="baz.html" type="image" id="test" alt="foo"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 7', function () { - fixture.innerHTML = - '<label for="test">States:</label>' + '<input type="text" id="test"/>'; + it('passes test 7', () => { + fixture.innerHTML = html` + <label for="test">States:</label> + <input type="text" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'States:'); }); - it('passes test 8', function () { - fixture.innerHTML = - '<label for="test">' + - 'foo' + - '<input type="text" value="David"/>' + - '</label>' + - '<input type="text" id="test" value="baz"/>'; + it('passes test 8', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="text" id="test" value="baz" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 9', function () { - fixture.innerHTML = - '<label for="test">' + - 'crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="text" id="test" value="baz"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 9', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="text" id="test" value="baz" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - ariaValuetext('passes test 10', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="text" id="test" value="baz"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 10', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="text" id="test" value="baz" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - it('passes test 11', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="text" id="test" value="baz"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 11', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="text" id="test" value="baz" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 12', function () { + it('passes test 12', () => { fixture.innerHTML = '<input type="text" id="test" title="crazy" value="baz"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - pseudoText('passes test 13', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="text" id="test"/>'; + pseudoText('passes test 13', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="text" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 14', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' [data-after]:after { content: attr(data-after); }' + - '</style>' + - '<label for="test" data-after="test content"></label>' + - '<input type="text" id="test">'; + pseudoText('passes test 14', () => { + fixture.innerHTML = html` + <style type="text/css"> + [data-after]:after { + content: attr(data-after); + } + </style> + <label for="test" data-after="test content"></label> + <input type="text" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'test content'); }); - it('passes test 15', function () { - fixture.innerHTML = '<img id="test" src="foo.jpg" aria-label="1"/>'; + it('passes test 15', () => { + fixture.innerHTML = html` + <img id="test" src="foo.jpg" aria-label="1" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), '1'); }); - it('passes test 16', function () { + it('passes test 16', () => { fixture.innerHTML = '<img id="test" src="foo.jpg" aria-label="1" alt="a" title="t"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), '1'); }); // To the best of my knowledge, this test is incorrect // Chrome and Firefox seem to return "peanuts", so does axe-core. - xit('passes test 17', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="test">' + - '<img aria-labelledby="test" src="foo.jpg"/>'; + xit('passes test 17', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="test" /> + <img aria-labelledby="test" src="foo.jpg" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), ''); }); - it('passes test 18', function () { + it('passes test 18', () => { fixture.innerHTML = '<img id="test" aria-labelledby="test" src="foo.jpg"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), ''); }); // To the best of my knowledge, this test is incorrect // Chrome and Firefox seem to return "peanuts", so does axe-core. - xit('passes test 19', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="test">' + - '<img aria-labelledby="test" aria-label="1" src="foo.jpg"/>'; + xit('passes test 19', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="test" /> + <img aria-labelledby="test" aria-label="1" src="foo.jpg" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), ''); }); - it('passes test 20', function () { + it('passes test 20', () => { fixture.innerHTML = '<img id="test" aria-labelledby="test" aria-label="1" src="foo.jpg"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), '1'); }); - it('passes test 21', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="ID1">' + - '<input type="text" value="popcorn" id="ID2">' + - '<input type="text" value="apple jacks" id="ID3">' + - '<img aria-labelledby="ID1 ID2 ID3" id="test" src="foo.jpg"/>'; + it('passes test 21', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="ID1" /> + <input type="text" value="popcorn" id="ID2" /> + <input type="text" value="apple jacks" id="ID3" /> + <img aria-labelledby="ID1 ID2 ID3" id="test" src="foo.jpg" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'peanuts popcorn apple jacks'); }); - it('passes test 22', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="ID1">' + - '<img id="test" aria-label="l" aria-labelledby="test ID1" src="foo.jpg"/>'; + it('passes test 22', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="ID1" /> + <img + id="test" + aria-label="l" + aria-labelledby="test ID1" + src="foo.jpg" + /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'l peanuts'); }); - it('passes test 23', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="ID1">' + - '<input type="text" value="popcorn" id="ID2">' + - '<img id="test" aria-label="l" aria-labelledby="test ID1 ID2" src="foo.jpg"/>'; + it('passes test 23', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="ID1" /> + <input type="text" value="popcorn" id="ID2" /> + <img + id="test" + aria-label="l" + aria-labelledby="test ID1 ID2" + src="foo.jpg" + /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'l peanuts popcorn'); }); - it('passes test 24', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="ID1">' + - '<input type="text" value="popcorn" id="ID2">' + - '<input type="text" value="apple jacks" id="ID3">' + - '<img id="test" aria-label="l" aria-labelledby="test ID1 ID2 ID3" alt= "a" title="t" src="foo.jpg"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 24', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="ID1" /> + <input type="text" value="popcorn" id="ID2" /> + <input type="text" value="apple jacks" id="ID3" /> + <img + id="test" + aria-label="l" + aria-labelledby="test ID1 ID2 ID3" + alt="a" + title="t" + src="foo.jpg" + /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'l peanuts popcorn apple jacks'); }); - it('passes test 25', function () { - fixture.innerHTML = - '<input type="text" value="peanuts" id="ID1">' + - '<input type="text" value="popcorn" id="ID2">' + - '<input type="text" value="apple jacks" id="ID3">' + - '<img id="test" aria-label="" aria-labelledby="test ID1 ID2 ID3" alt="" title="t" src="foo.jpg"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 25', () => { + fixture.innerHTML = html` + <input type="text" value="peanuts" id="ID1" /> + <input type="text" value="popcorn" id="ID2" /> + <input type="text" value="apple jacks" id="ID3" /> + <img + id="test" + aria-label="" + aria-labelledby="test ID1 ID2 ID3" + alt="" + title="t" + src="foo.jpg" + /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 't peanuts popcorn apple jacks'); }); - it('passes test 26', function () { - fixture.innerHTML = - '<div id="test" aria-labelledby="ID1">foo</div>' + - '<span id="ID1">bar</span>'; + it('passes test 26', () => { + fixture.innerHTML = html` + <div id="test" aria-labelledby="ID1">foo</div> + <span id="ID1">bar</span> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'bar'); }); - it('passes test 27', function () { - fixture.innerHTML = '<div id="test" aria-label="Tag">foo</div>'; + it('passes test 27', () => { + fixture.innerHTML = html` <div id="test" aria-label="Tag">foo</div> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Tag'); }); - it('passes test 28', function () { - fixture.innerHTML = - '<div id="test" aria-labelledby="ID1" aria-label="Tag">foo</div>' + - '<span id="ID1">bar</span>'; + it('passes test 28', () => { + fixture.innerHTML = html` + <div id="test" aria-labelledby="ID1" aria-label="Tag">foo</div> + <span id="ID1">bar</span> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'bar'); }); - it('passes test 29', function () { - fixture.innerHTML = - '<div id="test" aria-labelledby="ID0 ID1" aria-label="Tag">foo</div>' + - '<span id="ID0">bar</span>' + - '<span id="ID1">baz</span>'; + it('passes test 29', () => { + fixture.innerHTML = html` + <div id="test" aria-labelledby="ID0 ID1" aria-label="Tag">foo</div> + <span id="ID0">bar</span> + <span id="ID1">baz</span> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'bar baz'); }); // Should only pass in strict mode - it('passes test 30', function () { - fixture.innerHTML = '<div id="test">Div with text</div>'; + it('passes test 30', () => { + fixture.innerHTML = html` <div id="test">Div with text</div> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target, { strict: true }), ''); }); - it('passes test 31', function () { - fixture.innerHTML = '<div id="test" role="button">foo</div>'; + it('passes test 31', () => { + fixture.innerHTML = html` <div id="test" role="button">foo</div> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 32', function () { - fixture.innerHTML = - '<div id="test" role="button" title="Tag" style="outline:medium solid black; width:2em; height:1em;">' + - '</div>'; + it('passes test 32', () => { + fixture.innerHTML = html` + <div + id="test" + role="button" + title="Tag" + style="outline:medium solid black; width:2em; height:1em;" + ></div> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Tag'); }); - it('passes test 33', function () { - fixture.innerHTML = - '<div id="ID1">foo</div>' + - '<a id="test" href="test.html" aria-labelledby="ID1">bar</a>'; + it('passes test 33', () => { + fixture.innerHTML = html` + <div id="ID1">foo</div> + <a id="test" href="test.html" aria-labelledby="ID1">bar</a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 34', function () { - fixture.innerHTML = - '<a id="test" href="test.html" aria-label="Tag">ABC</a>'; + it('passes test 34', () => { + fixture.innerHTML = html` + <a id="test" href="test.html" aria-label="Tag">ABC</a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Tag'); }); - it('passes test 35', function () { - fixture.innerHTML = - '<a href="test.html" id="test" aria-labelledby="ID1" aria-label="Tag">foo</a>' + - '<p id="ID1">bar</p>'; + it('passes test 35', () => { + fixture.innerHTML = html` + <a href="test.html" id="test" aria-labelledby="ID1" aria-label="Tag" + >foo</a + > + <p id="ID1">bar</p> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'bar'); }); - it('passes test 36', function () { - fixture.innerHTML = - '<a href="test.html" id="test" aria-labelledby="test ID1" aria-label="Tag"></a>' + - '<p id="ID1">foo</p>'; + it('passes test 36', () => { + fixture.innerHTML = html` + <a + href="test.html" + id="test" + aria-labelledby="test ID1" + aria-label="Tag" + ></a> + <p id="ID1">foo</p> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Tag foo'); }); - it('passes test 37', function () { - fixture.innerHTML = '<a href="test.html" id="test">ABC</a>'; + it('passes test 37', () => { + fixture.innerHTML = html` <a href="test.html" id="test">ABC</a> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'ABC'); }); - it('passes test 38', function () { - fixture.innerHTML = '<a href="test.html" id="test" title="Tag"></a>'; + it('passes test 38', () => { + fixture.innerHTML = html` + <a href="test.html" id="test" title="Tag"></a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Tag'); }); - it('passes test 39', function () { - fixture.innerHTML = - '<input id="test" type="text" aria-labelledby="ID1 ID2 ID3">' + - '<p id="ID1">foo</p>' + - '<p id="ID2">bar</p>' + - '<p id="ID3">baz</p>'; + it('passes test 39', () => { + fixture.innerHTML = html` + <input id="test" type="text" aria-labelledby="ID1 ID2 ID3" /> + <p id="ID1">foo</p> + <p id="ID2">bar</p> + <p id="ID3">baz</p> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - it('passes test 40', function () { - fixture.innerHTML = - '<input id="test" type="text" aria-label="bar" aria-labelledby="ID1 test">' + - '<div id="ID1">foo</label>'; + it('passes test 40', () => { + fixture.innerHTML = html` + <input id="test" type="text" aria-label="bar" aria-labelledby="ID1 test"> + <div id="ID1">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar'); }); - it('passes test 41', function () { - fixture.innerHTML = - '<input id="test" type="text"/>' + '<label for="test">foo</label>'; + it('passes test 41', () => { + fixture.innerHTML = html` + <input id="test" type="text" /> + <label for="test">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 42', function () { - fixture.innerHTML = - '<input type="password" id="test">' + '<label for="test">foo</label>'; + it('passes test 42', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 43', function () { - fixture.innerHTML = - '<input type="checkbox" id="test">' + - '<label for="test">foo</label></body>'; + it('passes test 43', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test"> + <label for="test">foo</label></body> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 44', function () { - fixture.innerHTML = - '<input type="radio" id="test">' + '<label for="test">foo</label>'; + it('passes test 44', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 45', function () { - fixture.innerHTML = - '<input type="file" id="test">' + '<label for="test">foo</label>'; + it('passes test 45', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 46', function () { - fixture.innerHTML = - '<input type="image" id="test">' + '<label for="test">foo</label>'; + it('passes test 46', () => { + fixture.innerHTML = html` + <input type="image" id="test" /> + <label for="test">foo</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 47', function () { - fixture.innerHTML = - '<input type="checkbox" id="test">' + - '<label for="test">foo<input type="text" value="bar">baz</label>'; + it('passes test 47', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test">foo<input type="text" value="bar" />baz</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - it('passes test 48', function () { - fixture.innerHTML = - '<input type="text" id="test">' + - '<label for="test">foo<input type="text" value="bar">baz</label>'; + it('passes test 48', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test">foo<input type="text" value="bar" />baz</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - it('passes test 49', function () { - fixture.innerHTML = - '<input type="password" id="test">' + - '<label for="test">foo<input type="text" value="bar">baz</label>'; + it('passes test 49', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test">foo<input type="text" value="bar" />baz</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - it('passes test 50', function () { - fixture.innerHTML = - '<input type="radio" id="test">' + - '<label for="test">foo<input type="text" value="bar">baz</label>'; + it('passes test 50', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test">foo<input type="text" value="bar" />baz</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - it('passes test 51', function () { - fixture.innerHTML = - '<input type="file" id="test">' + - '<label for="test">foo <input type="text" value="bar"> baz</label>'; + it('passes test 51', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test">foo <input type="text" value="bar" /> baz</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - pseudoText('passes test 52', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test" title="bar"><input id="test" type="text" name="test" title="buz"></label> ' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 52', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" title="bar" + ><input id="test" type="text" name="test" title="buz" + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - pseudoText('passes test 53', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test" title="bar"><input id="test" type="password" name="test" title="buz"></label> ' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 53', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" title="bar" + ><input id="test" type="password" name="test" title="buz" + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo bar baz'); }); - pseudoText('passes test 54', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test"><input id="test" type="checkbox" name="test" title=" bar "></label>' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 54', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" + ><input id="test" type="checkbox" name="test" title=" bar " + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo baz'); }); - pseudoText('passes test 55', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test"><input id="test" type="radio" name="test" title=" bar "></label> ' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 55', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" + ><input id="test" type="radio" name="test" title=" bar " + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo baz'); }); - pseudoText('passes test 56', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test"><input id="test" type="file" name="test" title="bar"></label>' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 56', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" + ><input id="test" type="file" name="test" title="bar" + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo baz'); }); - pseudoText('passes test 57', function () { - fixture.innerHTML = - '<style type="text/css">' + - ' label:before { content: "foo"; }' + - ' label:after { content: "baz"; }' + - '</style>' + - '<form>' + - ' <label for="test"><input id="test" type="image" src="foo.jpg" name="test" title="bar"></label>' + - '</form>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 57', () => { + fixture.innerHTML = html` + <style type="text/css"> + label:before { + content: 'foo'; + } + label:after { + content: 'baz'; + } + </style> + <form> + <label for="test" + ><input + id="test" + type="image" + src="foo.jpg" + name="test" + title="bar" + /></label> + </form> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo baz'); }); - it('passes test 58', function () { - fixture.innerHTML = - '<label for="test">States:</label>' + - '<input type="password" id="test"/>'; + it('passes test 58', () => { + fixture.innerHTML = html` + <label for="test">States:</label> + <input type="password" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'States:'); }); - it('passes test 59', function () { - fixture.innerHTML = - '<label for="test">States:</label>' + - '<input type="checkbox" id="test"/>'; + it('passes test 59', () => { + fixture.innerHTML = html` + <label for="test">States:</label> + <input type="checkbox" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'States:'); }); - it('passes test 60', function () { + it('passes test 60', () => { fixture.innerHTML = - '<label for="test">States:</label>' + '<input type="radio" id="test"/>'; + '<label for="test">States:</label><input type="radio" id="test"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'States:'); }); - it('passes test 61', function () { + it('passes test 61', () => { fixture.innerHTML = - '<label for="test">File:</label>' + '<input type="file" id="test"/>'; + '<label for="test">File:</label><input type="file" id="test"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'File:'); }); - it('passes test 62', function () { - fixture.innerHTML = - '<label for="test">States:</label>' + - '<input type="image" id="test" src="foo.jpg"/>'; + it('passes test 62', () => { + fixture.innerHTML = html` + <label for="test">States:</label> + <input type="image" id="test" src="foo.jpg" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'States:'); }); - it('passes test 63', function () { - fixture.innerHTML = - '<label for="test">' + - ' foo' + - ' <input type="text" value="David"/>' + - '</label>' + - '<input type="password" id="test" value="baz"/>'; + it('passes test 63', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="password" id="test" value="baz" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 64', function () { - fixture.innerHTML = - '<label for="test">' + - ' foo' + - ' <input type="text" value="David"/>' + - '</label>' + - '<input type="checkbox" id="test"/>'; + it('passes test 64', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="checkbox" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 65', function () { - fixture.innerHTML = - '<label for="test">' + - ' foo' + - ' <input type="text" value="David"/>' + - '</label>' + - '<input type="radio" id="test"/>'; + it('passes test 65', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="radio" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 66', function () { - fixture.innerHTML = - '<label for="test">' + - ' foo' + - ' <input type="text" value="David"/>' + - '</label>' + - '<input type="file" id="test"/>'; + it('passes test 66', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="file" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 67', function () { - fixture.innerHTML = - '<label for="test">' + - ' foo' + - ' <input type="text" value="David"/>' + - '</label>' + - '<input type="image" id="test" src="foo.jpg"/>'; + it('passes test 67', () => { + fixture.innerHTML = html` + <label for="test"> + foo + <input type="text" value="David" /> + </label> + <input type="image" id="test" src="foo.jpg" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo David'); }); - it('passes test 68', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="password" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 68', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="password" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 69', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="checkbox" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 69', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="checkbox" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 70', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="radio" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 70', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="radio" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 71', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="file" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 71', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="file" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 72', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <select name="member" size="1" role="menu" tabindex="0">' + - ' <option role="menuitem" value="beard" selected="true">clown</option>' + - ' <option role="menuitem" value="scuba">rich</option>' + - ' </select>' + - '</label> ' + - '<input type="image" id="test" src="foo.jpg"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 72', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <select name="member" size="1" role="menu" tabindex="0"> + <option role="menuitem" value="beard" selected="true">clown</option> + <option role="menuitem" value="scuba">rich</option> + </select> + </label> + <input type="image" id="test" src="foo.jpg" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - ariaValuetext('passes test 73', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="password" value="baz" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 73', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="password" value="baz" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - ariaValuetext('passes test 74', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday"' + - ' aria-valuemin="1" aria-valuemax="7" aria-valuenow="4"></div>' + - '</label>' + - '<input type="checkbox" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 74', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="checkbox" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - ariaValuetext('passes test 75', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday"' + - ' aria-valuemin="1" aria-valuemax="7" aria-valuenow="4"></div>' + - '</label>' + - '<input type="radio" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 75', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="radio" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - ariaValuetext('passes test 76', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday"' + - ' aria-valuemin="1" aria-valuemax="7" aria-valuenow="4"></div>' + - '</label>' + - '<input type="file" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 76', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="file" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - ariaValuetext('passes test 77', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuetext="Monday" aria-valuemin="1"' + - ' aria-valuemax="7" aria-valuenow="4"></div>' + - '</label>' + - '<input type="image" src="foo.jpg" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + ariaValuetext('passes test 77', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuetext="Monday" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="image" src="foo.jpg" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy Monday'); }); - it('passes test 78', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="password" id="test" value="baz"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 78', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="password" id="test" value="baz" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 79', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="checkbox" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 79', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="checkbox" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 80', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="radio" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 80', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="radio" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 81', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="file" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 81', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="file" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 82', function () { - fixture.innerHTML = - '<label for="test">' + - ' crazy' + - ' <div role="spinbutton" aria-valuemin="1" aria-valuemax="7" aria-valuenow="4">' + - ' </div>' + - '</label>' + - '<input type="image" src="foo.jpg" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 82', () => { + fixture.innerHTML = html` + <label for="test"> + crazy + <div + role="spinbutton" + aria-valuemin="1" + aria-valuemax="7" + aria-valuenow="4" + ></div> + </label> + <input type="image" src="foo.jpg" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy 4'); }); - it('passes test 83', function () { + it('passes test 83', () => { fixture.innerHTML = '<input type="password" id="test" title="crazy" value="baz"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 84', function () { - fixture.innerHTML = '<input type="checkbox" id="test" title="crazy"/>'; + it('passes test 84', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" title="crazy" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 85', function () { - fixture.innerHTML = '<input type="radio" id="test" title="crazy"/>'; + it('passes test 85', () => { + fixture.innerHTML = html` + <input type="radio" id="test" title="crazy" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 86', function () { - fixture.innerHTML = '<input type="file" id="test" title="crazy"/>'; + it('passes test 86', () => { + fixture.innerHTML = html` <input type="file" id="test" title="crazy" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - it('passes test 87', function () { + it('passes test 87', () => { fixture.innerHTML = '<input type="image" src="foo.jpg" id="test" title="crazy"/>'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'crazy'); }); - pseudoText('passes test 88', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="password" id="test"/>'; + pseudoText('passes test 88', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="password" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 89', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="checkbox" id="test"/>'; + pseudoText('passes test 89', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="checkbox" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 90', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="radio" id="test"/>'; + pseudoText('passes test 90', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="radio" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 91', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="file" id="test"/>'; + pseudoText('passes test 91', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="file" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 92', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content:"fancy "; }' + - '</style>' + - '<label for="test">fruit</label>' + - '<input type="image" src="foo.jpg" id="test"/>'; + pseudoText('passes test 92', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'fancy '; + } + </style> + <label for="test">fruit</label> + <input type="image" src="foo.jpg" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 93', function () { - fixture.innerHTML = - '<style>' + - ' label:after { content:" fruit"; }' + - '</style>' + - '<label for="test">fancy</label>' + - '<input type="password" id="test"/>'; + pseudoText('passes test 93', () => { + fixture.innerHTML = html` + <style> + label:after { + content: ' fruit'; + } + </style> + <label for="test">fancy</label> + <input type="password" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 94', function () { - fixture.innerHTML = - '<style>' + - ' label:after { content:" fruit"; }' + - '</style>' + - '<label for="test">fancy</label>' + - '<input type="checkbox" id="test"/>'; + pseudoText('passes test 94', () => { + fixture.innerHTML = html` + <style> + label:after { + content: ' fruit'; + } + </style> + <label for="test">fancy</label> + <input type="checkbox" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 95', function () { - fixture.innerHTML = - '<style>' + - ' label:after { content:" fruit"; }' + - '</style>' + - '<label for="test">fancy</label>' + - '<input type="radio" id="test"/>'; + pseudoText('passes test 95', () => { + fixture.innerHTML = html` + <style> + label:after { + content: ' fruit'; + } + </style> + <label for="test">fancy</label> + <input type="radio" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 96', function () { - fixture.innerHTML = - '<style>' + - ' label:after { content:" fruit"; }' + - '</style>' + - '<label for="test">fancy</label>' + - '<input type="file" id="test"/>'; + pseudoText('passes test 96', () => { + fixture.innerHTML = html` + <style> + label:after { + content: ' fruit'; + } + </style> + <label for="test">fancy</label> + <input type="file" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - pseudoText('passes test 97', function () { - fixture.innerHTML = - '<style>' + - ' label:after { content:" fruit"; }' + - '</style>' + - '<label for="test">fancy</label>' + - '<input type="image" src="foo.jpg" id="test"/>'; + pseudoText('passes test 97', () => { + fixture.innerHTML = html` + <style> + label:after { + content: ' fruit'; + } + </style> + <label for="test">fancy</label> + <input type="image" src="foo.jpg" id="test" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'fancy fruit'); }); - it('passes test 98', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1</li>' + - ' <li role="option">2</li>' + - ' <li role="option">3</li>' + - ' </ul>' + - ' </div>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 72: "Flash the screen 1 times" - // Safari 12.0: "Flash the screen 1 times" - // Firefox 62: "Flash the screen 1 times" + it('passes test 98', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >Flash the screen + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 99', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">Flash the screen ' + - ' <span role="menu">' + - ' <span role="menuitem" aria-selected="true">1</span>' + - ' <span role="menuitem" hidden>2</span>' + - ' <span role="menuitem" hidden>3</span>' + - ' </span>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 99', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >Flash the screen + <span role="menu"> + <span role="menuitem" aria-selected="true">1</span> + <span role="menuitem" hidden>2</span> + <span role="menuitem" hidden>3</span> + </span> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen times.'); }); - it('passes test 100', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">Flash the screen ' + - ' <select size="1">' + - ' <option selected="selected">1</option>' + - ' <option>2</option>' + - ' <option>3</option>' + - ' </select>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 100', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >Flash the screen + <select size="1"> + <option selected="selected">1</option> + <option>2</option> + <option>3</option> + </select> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 101', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">foo <input role="slider" type="range" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz ' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 101', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >foo + <input + role="slider" + type="range" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 102', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">foo <input role="spinbutton" type="number" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 102', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >foo + <input + role="spinbutton" + type="number" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 103', function () { - fixture.innerHTML = '<input type="checkbox" id="test" title="foo" />'; + it('passes test 103', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" title="foo" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 104', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1 </li>' + - ' <li role="option">2 </li>' + - ' <li role="option">3 </li>' + - ' </ul>' + - ' </div>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 104', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >Flash the screen + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 105', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">Flash the screen ' + - ' <span role="menu">' + - ' <span role="menuitem" aria-selected="true">1</span>' + - ' <span role="menuitem" hidden>2</span>' + - ' <span role="menuitem" hidden>3</span>' + - ' </span>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 105', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >Flash the screen + <span role="menu"> + <span role="menuitem" aria-selected="true">1</span> + <span role="menuitem" hidden>2</span> + <span role="menuitem" hidden>3</span> + </span> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen times.'); }); - it('passes test 106', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">Flash the screen ' + - ' <select size="1">' + - ' <option selected="selected">1</option>' + - ' <option>2</option>' + - ' <option>3</option>' + - ' </select>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 106', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >Flash the screen + <select size="1"> + <option selected="selected">1</option> + <option>2</option> + <option>3</option> + </select> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 107', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">foo <input role="slider" type="range" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 107', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >foo + <input + role="slider" + type="range" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 108', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">foo <input role="spinbutton" type="number" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 108', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >foo + <input + role="spinbutton" + type="number" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 109', function () { - fixture.innerHTML = '<input type="file" id="test" title="foo" />'; + it('passes test 109', () => { + fixture.innerHTML = html` <input type="file" id="test" title="foo" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 110', function () { + it('passes test 110', () => { fixture.innerHTML = '<input type="image" src="test.png" id="test" title="foo" />'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 111', function () { - fixture.innerHTML = - '<input type="password" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1</li>' + - ' <li role="option">2</li>' + - ' <li role="option">3</li>' + - ' </ul>' + - ' </div>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 111', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test" + >Flash the screen + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 112', function () { - fixture.innerHTML = - '<input type="password" id="test" />' + - '<label for="test">Flash the screen ' + - ' <span role="menu">' + - ' <span role="menuitem" aria-selected="true">1</span>' + - ' <span role="menuitem" hidden>2</span>' + - ' <span role="menuitem" hidden>3</span>' + - ' </span>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 112', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test" + >Flash the screen + <span role="menu"> + <span role="menuitem" aria-selected="true">1</span> + <span role="menuitem" hidden>2</span> + <span role="menuitem" hidden>3</span> + </span> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen times.'); }); - it('passes test 113', function () { - fixture.innerHTML = - '<input type="password" id="test" />' + - '<label for="test">Flash the screen ' + - ' <select size="1">' + - ' <option selected="selected">1</option>' + - ' <option>2</option>' + - ' <option>3</option>' + - ' </select>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 113', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test" + >Flash the screen + <select size="1"> + <option selected="selected">1</option> + <option>2</option> + <option>3</option> + </select> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 114', function () { - fixture.innerHTML = - '<input type="password" id="test" />' + - '<label for="test">foo <input role="slider" type="range" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 114', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test" + >foo + <input + role="slider" + type="range" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 115', function () { - fixture.innerHTML = - '<input type="password" id="test" />' + - '<label for="test">foo <input role="spinbutton" type="number" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 115', () => { + fixture.innerHTML = html` + <input type="password" id="test" /> + <label for="test" + >foo + <input + role="spinbutton" + type="number" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 116', function () { - fixture.innerHTML = '<input type="password" id="test" title="foo" />'; + it('passes test 116', () => { + fixture.innerHTML = html` + <input type="password" id="test" title="foo" /> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 117', function () { - fixture.innerHTML = - '<input type="radio" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1</li>' + - ' <li role="option">2</li>' + - ' <li role="option">3</li>' + - ' </ul>' + - ' </div>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 117', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test" + >Flash the screen + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 118', function () { - fixture.innerHTML = - '<input type="radio" id="test" />' + - '<label for="test">Flash the screen ' + - ' <span role="menu">' + - ' <span role="menuitem" aria-selected="true">1</span>' + - ' <span role="menuitem" hidden>2</span>' + - ' <span role="menuitem" hidden>3</span>' + - ' </span>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 118', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test" + >Flash the screen + <span role="menu"> + <span role="menuitem" aria-selected="true">1</span> + <span role="menuitem" hidden>2</span> + <span role="menuitem" hidden>3</span> + </span> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen times.'); }); - it('passes test 119', function () { - fixture.innerHTML = - '<input type="radio" id="test" />' + - '<label for="test">Flash the screen ' + - ' <select size="1">' + - ' <option selected="selected">1</option>' + - ' <option>2</option>' + - ' <option>3</option>' + - ' </select>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 72: "Flash the screen 1 times" - // Firefox 62: "Flash the screen 1 times" - // Safari 12.0: "Flash the screen 1 times" + it('passes test 119', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test" + >Flash the screen + <select size="1"> + <option selected="selected">1</option> + <option>2</option> + <option>3</option> + </select> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 120', function () { - fixture.innerHTML = - '<input type="radio" id="test" />' + - '<label for="test">foo <input role="slider" type="range" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 70: Foo 5 baz - // Firefox 62: Foo 5 baz - // Safari 12.0: Foo 5 baz + it('passes test 120', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test" + >foo + <input + role="slider" + type="range" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 121', function () { - fixture.innerHTML = - '<input type="radio" id="test" />' + - '<label for="test">foo <input role="spinbutton" type="number" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 121', () => { + fixture.innerHTML = html` + <input type="radio" id="test" /> + <label for="test" + >foo + <input + role="spinbutton" + type="number" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 122', function () { - fixture.innerHTML = '<input type="radio" id="test" title="foo" />'; + it('passes test 122', () => { + fixture.innerHTML = html` <input type="radio" id="test" title="foo" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); - it('passes test 123', function () { - fixture.innerHTML = - '<input type="text" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1</li>' + - ' <li role="option">2</li>' + - ' <li role="option">3</li>' + - ' </ul>' + - ' </div>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 72: "Flash the screen 1 times." - // Firefox 62: "Flash the screen 1 times." - // Safari 12.0: "Flash the screen 1 times." + it('passes test 123', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test" + >Flash the screen + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 124', function () { - fixture.innerHTML = - '<input type="text" id="test" />' + - '<label for="test">Flash the screen ' + - ' <span role="menu">' + - ' <span role="menuitem" aria-selected="true">1</span>' + - ' <span role="menuitem" hidden>2</span>' + - ' <span role="menuitem" hidden>3</span>' + - ' </span>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 124', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test" + >Flash the screen + <span role="menu"> + <span role="menuitem" aria-selected="true">1</span> + <span role="menuitem" hidden>2</span> + <span role="menuitem" hidden>3</span> + </span> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen times.'); }); - it('passes test 125', function () { - fixture.innerHTML = - '<input type="text" id="test" />' + - '<label for="test">Flash the screen ' + - ' <select size="1">' + - ' <option selected="selected">1</option>' + - ' <option>2</option>' + - ' <option>3</option>' + - ' </select>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 72: Flash the screen 1 times - // Firefox 62: Flash the screen 1 times - // Safari 12.0: Flash the screen 1 times + it('passes test 125', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test" + >Flash the screen + <select size="1"> + <option selected="selected">1</option> + <option>2</option> + <option>3</option> + </select> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 126', function () { - fixture.innerHTML = - '<input type="text" id="test" />' + - '<label for="test">foo <input role="slider" type="range" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 126', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test" + >foo + <input + role="slider" + type="range" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 127', function () { - fixture.innerHTML = - '<input type="text" id="test" />' + - '<label for="test">foo <input role="spinbutton" type="number" value="5" min="1" max="10" aria-valuenow="5" aria-valuemin="1" aria-valuemax="10"> baz' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 127', () => { + fixture.innerHTML = html` + <input type="text" id="test" /> + <label for="test" + >foo + <input + role="spinbutton" + type="number" + value="5" + min="1" + max="10" + aria-valuenow="5" + aria-valuemin="1" + aria-valuemax="10" + /> + baz + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo 5 baz'); }); - it('passes test 128', function () { - fixture.innerHTML = '<input type="text" id="test" title="foo" />'; + it('passes test 128', () => { + fixture.innerHTML = html` <input type="text" id="test" title="foo" /> `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'foo'); }); // Skip from 128 - 138 as those are name description cases - it('passes test 139', function () { - fixture.innerHTML = - '<style>' + - ' .hidden { display: none; }' + - '</style>' + - '<div id="test" role="link" tabindex="0">' + - ' <span aria-hidden="true"><i> Hello, </i></span>' + - ' <span>My</span> name is' + - ' <div><img src="file.jpg" title="Bryan" alt="" role="presentation" /></div>' + - ' <span role="presentation" aria-label="Eli">' + - ' <span aria-label="Garaventa">Zambino</span>' + - ' </span>' + - ' <span>the weird.</span>' + - ' (QED)' + - ' <span class="hidden"><i><b>and don\'t you forget it.</b></i></span>' + - ' <table>' + - ' <tr>' + - ' <td>Where</td>' + - ' <td style="visibility:hidden;"><div>in</div></td>' + - ' <td><div style="display:none;">the world</div></td>' + - ' <td>are my marbles?</td>' + - ' </tr>' + - ' </table>' + - '</div>'; - - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); - // Chrome 72: "My name is Eli the weird. (QED) Where are my marbles?" - // Safari 12.0: "My name is Eli the weird. (QED) Where are my marbles?" - // Firefox 62: "Hello, My name is Eli the weird. (QED)" + it('passes test 139', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <div id="test" role="link" tabindex="0"> + <span aria-hidden="true"><i> Hello, </i></span> + <span>My</span> name is + <div> + <img src="file.jpg" title="Bryan" alt="" role="presentation" /> + </div> + <span role="presentation" aria-label="Eli"> + <span aria-label="Garaventa">Zambino</span> + </span> + <span>the weird.</span> + (QED) + <span class="hidden" + ><i><b>and don't you forget it.</b></i></span + > + <table> + <tr> + <td>Where</td> + <td style="visibility:hidden;"><div>in</div></td> + <td><div style="display:none;">the world</div></td> + <td>are my marbles?</td> + </tr> + </table> + </div> + `; + + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); + // Chrome 114: "My name is Garaventa the weird. (QED) Where are my marbles?"" + // Firefox 115: "My name is Bryan Eli the weird. (QED) Where are my marbles?" + // Safari 16.5: "My name is Eli the weird. (QED) Where are my marbles?" assert.equal( accessibleText(target), 'My name is Eli the weird. (QED) Where are my marbles?' ); }); - it('passes test 140', function () { - fixture.innerHTML = - '<style>' + - ' .hidden { display: none; }' + - '</style>' + - '<input id="test" type="text" aria-labelledby="lblId" />' + - '<div id="lblId" >' + - ' <span aria-hidden="true"><i> Hello, </i></span>' + - ' <span>My</span> name is' + - ' <div><img src="file.jpg" title="Bryan" alt="" role="presentation" /></div>' + - ' <span role="presentation" aria-label="Eli">' + - ' <span aria-label="Garaventa">Zambino</span>' + - ' </span>' + - ' <span>the weird.</span>' + - ' (QED)' + - ' <span class="hidden"><i><b>and don\'t you forget it.</b></i></span>' + - ' <table>' + - ' <tr>' + - ' <td>Where</td>' + - ' <td style="visibility:hidden;"><div>in</div></td>' + - ' <td><div style="display:none;">the world</div></td>' + - ' <td>are my marbles?</td>' + - ' </tr>' + - ' </table>' + - '</div>'; - - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 140', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <input id="test" type="text" aria-labelledby="lblId" /> + <div id="lblId"> + <span aria-hidden="true"><i> Hello, </i></span> + <span>My</span> name is + <div> + <img src="file.jpg" title="Bryan" alt="" role="presentation" /> + </div> + <span role="presentation" aria-label="Eli"> + <span aria-label="Garaventa">Zambino</span> + </span> + <span>the weird.</span> + (QED) + <span class="hidden" + ><i><b>and don't you forget it.</b></i></span + > + <table> + <tr> + <td>Where</td> + <td style="visibility:hidden;"><div>in</div></td> + <td><div style="display:none;">the world</div></td> + <td>are my marbles?</td> + </tr> + </table> + </div> + `; + + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal( accessibleText(target), 'My name is Eli the weird. (QED) Where are my marbles?' @@ -2989,260 +3585,331 @@ describe('text.accessibleTextVirtual', function () { // Disabling this, axe has a buggy implicitName computation // shouldn't be a big deal - xit('passes test 141', function () { - fixture.innerHTML = - '<style>' + - ' .hidden { display: none; }' + - '</style>' + - '<input type="text" id="test" />' + - '<label for="test" id="label">' + - ' <span aria-hidden="true"><i> Hello, </i></span>' + - ' <span>My</span> name is' + - ' <div><img src="file.jpg" title="Bryan" alt="" role="presentation" /></div>' + - ' <span role="presentation" aria-label="Eli">' + - ' <span aria-label="Garaventa">Zambino</span>' + - ' </span>' + - ' <span>the weird.</span>' + - ' (QED)' + - ' <span class="hidden"><i><b>and don\'t you forget it.</b></i></span>' + - ' <table>' + - ' <tr>' + - ' <td>Where</td>' + - ' <td style="visibility:hidden;"><div>in</div></td>' + - ' <td><div style="display:none;">the world</div></td>' + - ' <td>are my marbles?</td>' + - ' </tr>' + - ' </table>' + - '</label>'; - - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + xit('passes test 141', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <input type="text" id="test" /> + <label for="test" id="label"> + <span aria-hidden="true"><i> Hello, </i></span> + <span>My</span> name is + <div> + <img src="file.jpg" title="Bryan" alt="" role="presentation" /> + </div> + <span role="presentation" aria-label="Eli"> + <span aria-label="Garaventa">Zambino</span> + </span> + <span>the weird.</span> + (QED) + <span class="hidden" + ><i><b>and don't you forget it.</b></i></span + > + <table> + <tr> + <td>Where</td> + <td style="visibility:hidden;"><div>in</div></td> + <td><div style="display:none;">the world</div></td> + <td>are my marbles?</td> + </tr> + </table> + </label> + `; + + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal( accessibleText(target), 'My name is Eli the weird. (QED) Where are my marbles?' ); }); - it('passes test 143', function () { - fixture.innerHTML = - ' <style>' + - ' .hidden { display: none; }' + - ' </style>' + - ' <div>' + - ' <input id="test" type="text" aria-labelledby="lbl1 lbl2" aria-describedby="descId" />' + - ' <span>' + - ' <span aria-hidden="true" id="lbl1">Important</span>' + - ' <span class="hidden">' + - ' <span aria-hidden="true" id="lbl2">stuff</span>' + - ' </span>' + - ' </span>' + - ' </div>' + - ' <div class="hidden">' + - ' <div id="descId">' + - ' <span aria-hidden="true"><i> Hello, </i></span>' + - ' <span>My</span> name is' + - ' <div><img src="file.jpg" title="Bryan" alt="" role="presentation" /></div>' + - ' <span role="presentation" aria-label="Eli">' + - ' <span aria-label="Garaventa">Zambino</span>' + - ' </span>' + - ' <span>the weird.</span>' + - ' (QED)' + - ' <span class="hidden"><i><b>and don\'t you forget it.</b></i></span>' + - ' <table>' + - ' <tr>' + - ' <td>Where</td>' + - ' <td style="visibility:hidden;"><div>in</div></td>' + - ' <td><div style="display:none;">the world</div></td>' + - ' <td>are my marbles?</td>' + - ' </tr>' + - ' </table>' + - ' </div>' + - ' </div>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 143', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <div> + <input + id="test" + type="text" + aria-labelledby="lbl1 lbl2" + aria-describedby="descId" + /> + <span> + <span aria-hidden="true" id="lbl1">Important</span> + <span class="hidden"> + <span aria-hidden="true" id="lbl2">stuff</span> + </span> + </span> + </div> + <div class="hidden"> + <div id="descId"> + <span aria-hidden="true"><i> Hello, </i></span> + <span>My</span> name is + <div> + <img src="file.jpg" title="Bryan" alt="" role="presentation" /> + </div> + <span role="presentation" aria-label="Eli"> + <span aria-label="Garaventa">Zambino</span> + </span> + <span>the weird.</span> + (QED) + <span class="hidden" + ><i><b>and don't you forget it.</b></i></span + > + <table> + <tr> + <td>Where</td> + <td style="visibility:hidden;"><div>in</div></td> + <td><div style="display:none;">the world</div></td> + <td>are my marbles?</td> + </tr> + </table> + </div> + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Important stuff'); }); - it('passes test 144', function () { + it('passes test 144', () => { fixture.innerHTML = '<input id="test" role="combobox" type="text" title="Choose your language" value="English">'; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Choose your language'); }); - it('passes test 145', function () { - fixture.innerHTML = - '<div id="test" role="combobox" tabindex="0" title="Choose your language.">' + - ' <span> English </span>' + - '</div>'; + it('passes test 145', () => { + fixture.innerHTML = html` + <div + id="test" + role="combobox" + tabindex="0" + title="Choose your language." + > + <span> English </span> + </div> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Choose your language.'); }); - it('passes test 147', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">Flash the screen ' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1</li>' + - ' <li role="option">2</li>' + - ' <li role="option">3</li>' + - ' </ul>' + - ' times.' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 147', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >Flash the screen + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + times. + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - pseudoText('passes test 148', function () { - fixture.innerHTML = - '<input type="checkbox" id="test" />' + - '<label for="test">Flash the screen ' + - ' <div role="textbox" contenteditable>1</div>' + - ' times.' + - '</label>'; + pseudoText('passes test 148', () => { + fixture.innerHTML = html` + <input type="checkbox" id="test" /> + <label for="test" + >Flash the screen + <div role="textbox" contenteditable>1</div> + times. + </label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 149', function () { - fixture.innerHTML = - '<label for="test">a test</label>' + - '<label>This <input type="checkbox" id="test" /> is</label>'; + it('passes test 149', () => { + fixture.innerHTML = html` + <label for="test">a test</label> + <label>This <input type="checkbox" id="test" /> is</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'a test This is'); }); - it('passes test 150', function () { - fixture.innerHTML = - '<label>This <input type="checkbox" id="test" /> is</label>' + - '<label for="test">a test</label>'; + it('passes test 150', () => { + fixture.innerHTML = html` + <label>This <input type="checkbox" id="test" /> is</label> + <label for="test">a test</label> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'This is a test'); }); - it('passes test 151', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">W<i>h<b>a</b></i>t<br>is<div>your<div>name<b>?</b></div></div></label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 151', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >W<i>h<b>a</b></i + >t<br />is + <div> + your + <div>name<b>?</b></div> + </div></label + > + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'What is your name?'); }); - pseudoText('passes test 152', function () { - fixture.innerHTML = - '<style>' + - ' label:before { content: "This"; display: block; }' + - ' label:after { content: "."; }' + - '</style>' + - '<label for="test">is a test</label>' + - '<input type="text" id="test"/>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + pseudoText('passes test 152', () => { + fixture.innerHTML = html` + <style> + label:before { + content: 'This'; + display: block; + } + label:after { + content: '.'; + } + </style> + <label for="test">is a test</label> + <input type="text" id="test" /> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'This is a test.'); }); - it('passes test 153', function () { - fixture.innerHTML = - '<style>' + - ' .hidden { display: none; }' + - '</style>' + - '<input type="file" id="test" />' + - '<label for="test">' + - ' <span class="hidden">1</span><span>2</span>' + - ' <span style="visibility: hidden;">3</span><span>4</span>' + - ' <span hidden>5</span><span>6</span>' + - ' <span aria-hidden="true">7</span><span>8</span>' + - ' <span aria-hidden="false" class="hidden">9</span><span>10</span>' + - '</label>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 153', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <input type="file" id="test" /> + <label for="test"> + <span class="hidden">1</span><span>2</span> + <span style="visibility: hidden;">3</span><span>4</span> + <span hidden>5</span><span>6</span> <span aria-hidden="true">7</span + ><span>8</span> <span aria-hidden="false" class="hidden">9</span + ><span>10</span> + </label> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), '2 4 6 8 10'); }); - it('passes test 154', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">Flash <span aria-owns="id1">the screen</span> times.</label>' + - '<div>' + - ' <div id="id1" role="combobox" aria-owns="id2">' + - ' <div role="textbox"></div>' + - ' </div>' + - '</div>' + - '<div>' + - ' <ul id="id2" role="listbox" style="list-style-type: none;">' + - ' <li role="option" >1 </li>' + - ' <li role="option" aria-selected="true">2 </li>' + - ' <li role="option">3 </li>' + - ' </ul>' + - '</div>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 154', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >Flash <span aria-owns="id1">the screen</span> times.</label + > + <div> + <div id="id1" role="combobox" aria-owns="id2"> + <div role="textbox"></div> + </div> + </div> + <div> + <ul id="id2" role="listbox" style="list-style-type: none;"> + <li role="option">1</li> + <li role="option" aria-selected="true">2</li> + <li role="option">3</li> + </ul> + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 2 times.'); }); - it('passes test 155', function () { - fixture.innerHTML = - '<input type="file" id="test" />' + - '<label for="test">Flash <span aria-owns="id1">the screen</span> times.</label>' + - '<div id="id1">' + - ' <div role="combobox">' + - ' <div role="textbox"></div>' + - ' <ul role="listbox" style="list-style-type: none;">' + - ' <li role="option" aria-selected="true">1 </li>' + - ' <li role="option">2 </li>' + - ' <li role="option">3 </li>' + - ' </ul>' + - ' </div>' + - '</div>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 155', () => { + fixture.innerHTML = html` + <input type="file" id="test" /> + <label for="test" + >Flash <span aria-owns="id1">the screen</span> times.</label + > + <div id="id1"> + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option" aria-selected="true">1</li> + <li role="option">2</li> + <li role="option">3</li> + </ul> + </div> + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Flash the screen 1 times.'); }); - it('passes test 156', function () { - fixture.innerHTML = - '<style>' + - ' .hidden { display: none; }' + - '</style>' + - '<div id="test" role="link" tabindex="0">' + - ' <span aria-hidden="true"><i> Hello, </i></span>' + - ' <span>My</span> name is' + - ' <div><img src="file.jpg" title="Bryan" alt="" role="presentation" /></div>' + - ' <span role="presentation" aria-label="Eli"><span aria-label="Garaventa">Zambino</span></span>' + - ' <span>the weird.</span>' + - ' (QED)' + - ' <span class="hidden"><i><b>and don\'t you forget it.</b></i></span>' + - '</div>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 156', () => { + fixture.innerHTML = html` + <style> + .hidden { + display: none; + } + </style> + <div id="test" role="link" tabindex="0"> + <span aria-hidden="true"><i> Hello, </i></span> + <span>My</span> name is + <div> + <img src="file.jpg" title="Bryan" alt="" role="presentation" /> + </div> + <span role="presentation" aria-label="Eli" + ><span aria-label="Garaventa">Zambino</span></span + > + <span>the weird.</span> + (QED) + <span class="hidden" + ><i><b>and don't you forget it.</b></i></span + > + </div> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'My name is Eli the weird. (QED)'); }); - it('passes test 158', function () { - fixture.innerHTML = - '<a id="test" href="#" aria-label="California"' + - ' title="San Francisco">United States</a>'; + it('passes test 158', () => { + fixture.innerHTML = html` + <a id="test" href="#" aria-label="California" title="San Francisco"> + United States + </a> + `; axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'California'); }); - it('passes test 159', function () { - fixture.innerHTML = - '<h2 id="test">' + - 'Country of origin:' + - '<input role="combobox" type="text" title="Choose your country." value="United States">' + - '</h2>'; - axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#test'); + it('passes test 159', () => { + fixture.innerHTML = html` + <h2 id="test"> + Country of origin: + <input + role="combobox" + type="text" + title="Choose your country." + value="United States" + /> + </h2> + `; + axe.testUtils.flatTreeSetup(fixture); + const target = fixture.querySelector('#test'); assert.equal(accessibleText(target), 'Country of origin: United States'); }); @@ -3261,10 +3928,10 @@ describe('text.accessibleTextVirtual', function () { ).join(' +\n ') + ';' return ` - it('passes test ${index + 1}', function () { + it('passes test ${index + 1}', () => { fixture.innerHTML = ${strings} axe.testUtils.flatTreeSetup(fixture); - var target = fixture.querySelector('#${id}'); + const target = fixture.querySelector('#${id}'); assert.equal(accessibleText(target), '${expected}'); });` }*/ diff --git a/test/commons/text/form-control-value.js b/test/commons/text/form-control-value.js index 56041ed5f3..96552ec352 100644 --- a/test/commons/text/form-control-value.js +++ b/test/commons/text/form-control-value.js @@ -1,13 +1,11 @@ -describe('text.formControlValue', function () { - var formControlValue = axe.commons.text.formControlValue; - var queryFixture = axe.testUtils.queryFixture; - var fixtureSetup = axe.testUtils.fixtureSetup; - var injectIntoFixture = axe.testUtils.injectIntoFixture; - var fixture = document.querySelector('#fixture'); +describe('text.formControlValue', () => { + const formControlValue = axe.commons.text.formControlValue; + const { queryFixture, fixtureSetup, injectIntoFixture, html } = axe.testUtils; + const fixture = document.querySelector('#fixture'); function getNodeType(node) { // Note: Inconsistent response for `node.type` across browsers, hence resolving and sanitizing using getAttribute - var nodeType = node.hasAttribute('type') + let nodeType = node.hasAttribute('type') ? axe.commons.text.sanitize(node.getAttribute('type')).toLowerCase() : 'text'; nodeType = axe.utils.validInputTypes().includes(nodeType) @@ -16,37 +14,69 @@ describe('text.formControlValue', function () { return nodeType; } - it('returns the first truthy result from text.formControlValueMethods', function () { - var target = queryFixture( - '<div id="target" role="textbox" value="foo">bar</div>' - ); - var fixture = axe.utils.querySelectorAll(axe._tree, '#fixture')[0]; - assert.equal(formControlValue(target, { startNode: fixture }), 'bar'); + it('returns the first truthy result from text.formControlValueMethods', () => { + const target = queryFixture(html` + <div id="target" role="textbox" value="foo">bar</div> + `); + const vNode = axe.utils.querySelectorAll(axe._tree, '#fixture')[0]; + assert.equal(formControlValue(target, { startNode: vNode }), 'bar'); }); - it('returns `` when the node equals context.startNode', function () { - var target = queryFixture('<input id="target" value="foo" />'); + it('returns `` when the node equals context.startNode', () => { + const target = queryFixture('<input id="target" value="foo" />'); assert.equal(formControlValue(target, { startNode: target }), ''); }); - it('returns `` when accessibleNameFromFieldValue says the role is unsupported', function () { - var target = queryFixture( - '<input id="target" value="foo" role="combobox"/>' - ); - assert.equal(formControlValue(target), ''); + describe('unsupported accessibleNameFromFieldValue', () => { + it('returns `` for unsupported progressbar role', () => { + const target = queryFixture(html` + <div id="target" role="progressbar" aria-value="foo"></div> + `); + assert.equal(formControlValue(target), ''); + }); + + it('returns the value for supported textbox role', () => { + const target = queryFixture(html` + <div id="target" role="textbox">bar</div> + `); + assert.equal(formControlValue(target), 'bar'); + }); + + it('returns the value for supported listbox role', () => { + const target = queryFixture(html` + <div id="target" role="listbox"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + <div role="option">baz</div> + </div> + `); + assert.equal(formControlValue(target), 'bar'); + }); + + it('returns the value for supported combobox role', () => { + const target = queryFixture(html` + <div id="target" role="combobox"> + <div role="textbox" id="text">nope</div> + <div role="listbox" id="list"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + </div> + </div> + `); + assert.equal(formControlValue(target), 'bar'); + }); }); - describe('nativeTextboxValue', function () { - var nativeTextboxValue = - axe.commons.text.formControlValueMethods.nativeTextboxValue; + describe('nativeTextboxValue', () => { + const { nativeTextboxValue } = axe.commons.text.formControlValueMethods; - it('returns the value of textarea elements', function () { - var target = queryFixture('<textarea>foo</textarea>', 'textarea'); + it('returns the value of textarea elements', () => { + const target = queryFixture('<textarea>foo</textarea>', 'textarea'); assert.equal(nativeTextboxValue(target), 'foo'); }); - it('returns the value of text field input elements', function () { - var formData = { + it('returns the value of text field input elements', () => { + const formData = { text: 'foo', date: '2018-12-12', 'datetime-local': '2018-12-12T12:34', @@ -60,23 +90,16 @@ describe('text.formControlValue', function () { week: '2018-W46' }; fixtureSetup( - Object.keys(formData).reduce(function (html, fieldType) { - return ( - html + - '<input type="' + - fieldType + - '" value="' + - formData[fieldType] + - '">' - ); + Object.keys(formData).reduce((code, fieldType) => { + return `${code}<input type="${fieldType}" value="${formData[fieldType]}">`; }, '') ); axe.utils .querySelectorAll(axe._tree[0], '#fixture input') .forEach(function (target) { - var expected = formData[getNodeType(target.actualNode)]; + const expected = formData[getNodeType(target.actualNode)]; assert.isDefined(expected); - var actual = nativeTextboxValue(target); + const actual = nativeTextboxValue(target); assert.equal( actual, expected, @@ -85,19 +108,19 @@ describe('text.formControlValue', function () { }); }); - it('returns `` for non-text input elements', function () { - fixtureSetup( - '<input type="button" value="foo">' + - '<input type="checkbox" value="foo">' + - '<input type="file" value="foo">' + - '<input type="hidden" value="foo">' + - '<input type="image" value="foo">' + - '<input type="password" value="foo">' + - '<input type="radio" value="foo">' + - '<input type="reset" value="foo">' + - '<input type="submit" value="foo">' + - '<input type="color" value="#000000">' - ); + it('returns `` for non-text input elements', () => { + fixtureSetup(html` + <input type="button" value="foo" /> + <input type="checkbox" value="foo" /> + <input type="file" value="foo" /> + <input type="hidden" value="foo" /> + <input type="image" value="foo" /> + <input type="password" value="foo" /> + <input type="radio" value="foo" /> + <input type="reset" value="foo" /> + <input type="submit" value="foo" /> + <input type="color" value="#000000" /> + `); axe.utils .querySelectorAll(axe._tree[0], '#fixture input') .forEach(function (target) { @@ -116,17 +139,17 @@ describe('text.formControlValue', function () { }); }); - it('returns the value of DOM nodes', function () { + it('returns the value of DOM nodes', () => { fixture.innerHTML = '<input value="foo">'; axe.utils.getFlattenedTree(fixture); assert.equal(nativeTextboxValue(fixture.querySelector('input')), 'foo'); }); - it('returns `` for other elements', function () { + it('returns `` for other elements', () => { // some random elements: ['div', 'span', 'h1', 'output', 'summary', 'style', 'template'].forEach( function (nodeName) { - var target = document.createElement(nodeName); + const target = document.createElement(nodeName); target.value = 'foo'; // That shouldn't do anything fixture.appendChild(target); axe.utils.getFlattenedTree(fixture); @@ -136,82 +159,81 @@ describe('text.formControlValue', function () { }); }); - describe('nativeSelectValue', function () { - var nativeSelectValue = - axe.commons.text.formControlValueMethods.nativeSelectValue; + describe('nativeSelectValue', () => { + const { nativeSelectValue } = axe.commons.text.formControlValueMethods; - it('returns the selected option text', function () { - var target = queryFixture( - '<select id="target">' + - ' <option>foo</option>' + - ' <option value="bar" selected>baz</option>' + - '</select>' - ); + it('returns the selected option text', () => { + const target = queryFixture(html` + <select id="target"> + <option>foo</option> + <option value="bar" selected>baz</option> + </select> + `); assert.equal(nativeSelectValue(target), 'baz'); }); - it('returns the selected option text after selection', function () { - injectIntoFixture( - '<select id="target">' + - ' <option value="foo" selected>foo</option>' + - ' <option value="bar">baz</option>' + - '</select>' - ); + it('returns the selected option text after selection', () => { + injectIntoFixture(html` + <select id="target"> + <option value="foo" selected>foo</option> + <option value="bar">baz</option> + </select> + `); fixture.querySelector('#target').value = 'bar'; - var rootNode = axe.setup(fixture); - var target = axe.utils.querySelectorAll(rootNode, '#target')[0]; + const rootNode = axe.setup(fixture); + const target = axe.utils.querySelectorAll(rootNode, '#target')[0]; assert.equal(nativeSelectValue(target), 'baz'); }); - it('returns multiple options, space seperated', function () { + it('returns multiple options, space separated', () => { // Can't apply multiple "selected" props without setting "multiple" - var target = queryFixture( - '<select id="target" multiple>' + - ' <option>oof</option>' + - ' <option selected>foo</option>' + - ' <option>rab</option>' + - ' <option selected>bar</option>' + - ' <option>zab</option>' + - ' <option selected>baz</option>' + - '</select>' - ); + const target = queryFixture(html` + <select id="target" multiple> + <option>oof</option> + <option selected>foo</option> + <option>rab</option> + <option selected>bar</option> + <option>zab</option> + <option selected>baz</option> + </select> + `); assert.equal(nativeSelectValue(target), 'foo bar baz'); }); - it('returns options from within optgroup elements', function () { - var target = queryFixture( - '<select id="target" multiple>' + - ' <option>oof</option>' + - ' <option selected>foo</option>' + - ' <optgroup>' + - ' <option>rab</option>' + - ' <option selected>bar</option>' + - ' </optgroup>' + - ' <optgroup>' + - ' <option>zab</option>' + - ' <option selected>baz</option>' + - ' </optgroup>' + - '</select>' - ); + it('returns options from within optgroup elements', () => { + const target = queryFixture(html` + <select id="target" multiple> + <option>oof</option> + <option selected>foo</option> + <optgroup> + <option>rab</option> + <option selected>bar</option> + </optgroup> + <optgroup> + <option>zab</option> + <option selected>baz</option> + </optgroup> + </select> + `); assert.equal(nativeSelectValue(target), 'foo bar baz'); }); - it('returns the first option when there are no selected options', function () { + it('returns the first option when there are no selected options', () => { // Browser automatically selectes the first option - var target = queryFixture( - '<select id="target">' + - ' <option>foo</option>' + - ' <option>baz</option>' + - '</select>' - ); + const target = queryFixture(html` + <select id="target"> + <option>foo</option> + <option>baz</option> + </select> + `); assert.equal(nativeSelectValue(target), 'foo'); }); - it('returns `` for other elements', function () { + it('returns `` for other elements', () => { // some random elements: ['div', 'span', 'h1', 'output', 'summary', 'style', 'template'].forEach( function (nodeName) { - var target = document.createElement(nodeName); + const target = document.createElement(nodeName); target.value = 'foo'; // That shouldn't do anything fixture.appendChild(target); axe.utils.getFlattenedTree(fixture); @@ -221,217 +243,232 @@ describe('text.formControlValue', function () { }); }); - describe('ariaTextboxValue', function () { - var ariaTextboxValue = + describe('ariaTextboxValue', () => { + const ariaTextboxValue = axe.commons.text.formControlValueMethods.ariaTextboxValue; - it('returns the text of role=textbox elements', function () { - var target = queryFixture('<div id="target" role="textbox">foo</div>'); + it('returns the text of role=textbox elements', () => { + const target = queryFixture('<div id="target" role="textbox">foo</div>'); assert.equal(ariaTextboxValue(target), 'foo'); }); - it('returns `` for elements without role=textbox', function () { - var target = queryFixture('<div id="target" role="combobox">foo</div>'); + it('returns `` for elements without role=textbox', () => { + const target = queryFixture('<div id="target" role="combobox">foo</div>'); assert.equal(ariaTextboxValue(target), ''); }); - it('ignores text hidden with CSS', function () { - var target = queryFixture( - '<div id="target" role="textbox">' + - '<span>foo</span>' + - '<span style="display: none;">bar</span>' + - '<span style="visibility: hidden;">baz</span>' + - '</div>' - ); + it('ignores text hidden with CSS', () => { + const target = queryFixture(html` + <div id="target" role="textbox"> + <span>foo</span> + <span style="display: none;">bar</span> + <span style="visibility: hidden;">baz</span> + </div> + `); assert.equal(ariaTextboxValue(target), 'foo'); }); - it('ignores elements with hidden content', function () { - var target = queryFixture( - '<div id="target" role="textbox">' + - '<span>span</span>' + - '<style>style</style>' + - '<template>template</template>' + - '<script>script</script>' + - '<!-- comment -->' + - '<h1>h1</h1>' + - '</div>' - ); - assert.equal(ariaTextboxValue(target), 'spanh1'); + it('ignores elements with hidden content', () => { + const target = queryFixture(html` + <div id="target" role="textbox"> + <span>span</span> + <style> + style { + } + </style> + <template>template</template> + <script> + script; + </script> + <!-- comment --> + <h1>h1</h1> + </div> + `); + assert.equal(ariaTextboxValue(target), 'span h1'); }); - it('does not return HTML or comments', function () { - var target = queryFixture( - '<div id="target" role="textbox">' + - '<i>foo</i>' + - '<!-- comment -->' + - '</div>' - ); + it('does not return HTML or comments', () => { + const target = queryFixture(html` + <div id="target" role="textbox"> + <i>foo</i> + <!-- comment --> + </div> + `); assert.equal(ariaTextboxValue(target), 'foo'); }); - it('returns the entire text content if the textbox is hidden', function () { - var target = queryFixture( - '<div id="target" role="textbox" style="display:none">' + - // Yes, this is how it works in browsers :-( - '<style>[role=texbox] { display: none }</style>' + - '</div>' - ); - assert.equal(ariaTextboxValue(target), '[role=texbox] { display: none }'); + it('returns the entire text content if the textbox is hidden', () => { + // Yes, this is how it works in browsers :-( + const target = queryFixture(html` + <div id="target" role="textbox" style="display:none"> + <style> + [role='texbox'] { + display: none; + } + </style> + </div> + `); + const text = ariaTextboxValue(target).replace(/\s+/g, ' ').trim(); + assert.equal(text, "[role='texbox'] { display: none; }"); }); }); - describe('ariaListboxValue', function () { - var ariaListboxValue = + describe('ariaListboxValue', () => { + const ariaListboxValue = axe.commons.text.formControlValueMethods.ariaListboxValue; - it('returns the selected option when the element is a listbox', function () { - var target = queryFixture( - '<div id="target" role="listbox">' + - ' <div role="option">foo</div>' + - ' <div role="option" aria-selected="true">bar</div>' + - ' <div role="option">baz</div>' + - '</div>' - ); + it('returns the selected option when the element is a listbox', () => { + const target = queryFixture(html` + <div id="target" role="listbox"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + <div role="option">baz</div> + </div> + `); assert.equal(ariaListboxValue(target), 'bar'); }); - it('returns `` when the element is not a listbox', function () { - var target = queryFixture( - '<div id="target" role="combobox">' + - ' <div role="option">foo</div>' + - ' <div role="option" aria-selected="true">bar</div>' + - ' <div role="option">baz</div>' + - '</div>' - ); + it('returns `` when the element is not a listbox', () => { + const target = queryFixture(html` + <div id="target" role="combobox"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + <div role="option">baz</div> + </div> + `); assert.equal(ariaListboxValue(target), ''); }); - it('returns `` when there is no selected option', function () { - var target = queryFixture( - '<div id="target" role="listbox">' + - ' <div role="option">foo</div>' + - ' <div role="option">bar</div>' + - ' <div role="option">baz</div>' + - '</div>' - ); + it('returns `` when there is no selected option', () => { + const target = queryFixture(html` + <div id="target" role="listbox"> + <div role="option">foo</div> + <div role="option">bar</div> + <div role="option">baz</div> + </div> + `); assert.equal(ariaListboxValue(target), ''); }); - it('returns `` when aria-selected is not true option', function () { - var target = queryFixture( - '<div id="target" role="listbox">' + - ' <div role="option" aria-selected="false">foo</div>' + - ' <div role="option" aria-selected="TRUE">bar</div>' + - ' <div role="option" aria-selected="yes">baz</div>' + - ' <div role="option" aria-selected="selected">fiz</div>' + - '</div>' - ); + it('returns `` when aria-selected is not true option', () => { + const target = queryFixture(html` + <div id="target" role="listbox"> + <div role="option" aria-selected="false">foo</div> + <div role="option" aria-selected="TRUE">bar</div> + <div role="option" aria-selected="yes">baz</div> + <div role="option" aria-selected="selected">fiz</div> + </div> + `); assert.equal(ariaListboxValue(target), ''); }); - it('returns selected options from aria-owned', function () { - var target = queryFixture( - '<div id="target" role="listbox" aria-owns="opt1 opt2 opt3"></div>' + - '<div role="option" id="opt1">foo</div>' + - '<div role="option" id="opt2" aria-selected="true">bar</div>' + - '<div role="option" id="opt3">baz</div>' - ); + it('returns selected options from aria-owned', () => { + const target = queryFixture(html` + <div id="target" role="listbox" aria-owns="opt1 opt2 opt3"></div> + <div role="option" id="opt1">foo</div> + <div role="option" id="opt2" aria-selected="true">bar</div> + <div role="option" id="opt3">baz</div> + `); assert.equal(ariaListboxValue(target), 'bar'); }); - it('ignores aria-selected for elements that are not options', function () { - var target = queryFixture( - '<div id="target" role="listbox" aria-owns="opt1 opt2 opt3"></div>' + - '<div id="opt1">foo</div>' + - '<div id="opt2" aria-selected="true">bar</div>' + - '<div id="opt3">baz</div>' - ); + it('ignores aria-selected for elements that are not options', () => { + const target = queryFixture(html` + <div id="target" role="listbox" aria-owns="opt1 opt2 opt3"></div> + <div id="opt1">foo</div> + <div id="opt2" aria-selected="true">bar</div> + <div id="opt3">baz</div> + `); assert.equal(ariaListboxValue(target), ''); }); - describe('with multiple aria-selected', function () { - it('returns the first selected option from children', function () { - var target = queryFixture( - '<div id="target" role="listbox">' + - ' <div role="option">foo</div>' + - ' <div role="option" aria-selected="true">bar</div>' + - ' <div role="option" aria-selected="true">baz</div>' + - '</div>' - ); + describe('with multiple aria-selected', () => { + it('returns the first selected option from children', () => { + const target = queryFixture(html` + <div id="target" role="listbox"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + <div role="option" aria-selected="true">baz</div> + </div> + `); assert.equal(ariaListboxValue(target), 'bar'); }); - it('returns the first selected option in aria-owned (as opposed to in the DOM order)', function () { - var target = queryFixture( - '<div id="target" role="listbox" aria-owns="opt3 opt2 opt1"></div>' + - '<div role="option" id="opt1" aria-selected="true">foo</div>' + - '<div role="option" id="opt2" aria-selected="true">bar</div>' + - '<div role="option" id="opt3">baz</div>' - ); + it('returns the first selected option in aria-owned (as opposed to in the DOM order)', () => { + const target = queryFixture(html` + <div id="target" role="listbox" aria-owns="opt3 opt2 opt1"></div> + <div role="option" id="opt1" aria-selected="true">foo</div> + <div role="option" id="opt2" aria-selected="true">bar</div> + <div role="option" id="opt3">baz</div> + `); assert.equal(ariaListboxValue(target), 'bar'); }); - it('returns the a selected child before a selected aria-owned element', function () { - var target = queryFixture( - '<div id="target" role="listbox" aria-owns="opt2 opt3">' + - ' <div role="option" aria-selected="true">foo</div>' + - '</div>' + - '<div role="option" id="opt2" aria-selected="true">bar</div>' + - '<div role="option" id="opt3">baz</div>' - ); + it('returns the a selected child before a selected aria-owned element', () => { + const target = queryFixture(html` + <div id="target" role="listbox" aria-owns="opt2 opt3"> + <div role="option" aria-selected="true">foo</div> + </div> + <div role="option" id="opt2" aria-selected="true">bar</div> + <div role="option" id="opt3">baz</div> + `); assert.equal(ariaListboxValue(target), 'foo'); }); - it('ignores aria-multiselectable=true', function () { + it('ignores aria-multiselectable=true', () => { // aria-multiselectable doesn't add additional content to the accessible name - var target = queryFixture( - '<div id="target" role="listbox" aria-owns="opt2 opt3" aria-multiselectable="true">' + - ' <div role="option" aria-selected="true">foo</div>' + - '</div>' + - '<div role="option" id="opt2" aria-selected="true">bar</div>' + - '<div role="option" id="opt3" aria-selected="true">baz</div>' - ); + const target = queryFixture(html` + <div + id="target" + role="listbox" + aria-owns="opt2 opt3" + aria-multiselectable="true" + > + <div role="option" aria-selected="true">foo</div> + </div> + <div role="option" id="opt2" aria-selected="true">bar</div> + <div role="option" id="opt3" aria-selected="true">baz</div> + `); assert.equal(ariaListboxValue(target), 'foo'); }); }); }); - describe('ariaComboboxValue', function () { - var ariaComboboxValue = - axe.commons.text.formControlValueMethods.ariaComboboxValue; - - var comboboxContent = - '<div role="textbox" id="text">nope</div>' + - '<div role="listbox" id="list">' + - ' <div role="option">foo</div>' + - ' <div role="option" aria-selected="true">bar</div>' + - '</div>'; - - it('returns the text of role=combobox elements', function () { - var target = queryFixture( - '<div id="target" role="combobox">' + comboboxContent + '</div>' - ); + describe('ariaComboboxValue', () => { + const { ariaComboboxValue } = axe.commons.text.formControlValueMethods; + + const comboboxContent = html` + <div role="textbox" id="text">nope</div> + <div role="listbox" id="list"> + <div role="option">foo</div> + <div role="option" aria-selected="true">bar</div> + </div> + `; + + it('returns the text of role=combobox elements', () => { + const target = queryFixture(html` + <div id="target" role="combobox">${comboboxContent}</div> + `); assert.equal(ariaComboboxValue(target), 'bar'); }); - it('returns `` for elements without role=combobox', function () { - var target = queryFixture( - '<div role="combobox">' + comboboxContent + '</div>', + it('returns `` for elements without role=combobox', () => { + const target = queryFixture( + `<div role="combobox">${comboboxContent}</div>`, '[role=listbox]' ); assert.equal(ariaComboboxValue(target), ''); }); - it('passes child listbox to `ariaListboxValue` and returns its result', function () { - var target = queryFixture( - '<div id="target" role="combobox">' + comboboxContent + '</div>' - ); + it('passes child listbox to `ariaListboxValue` and returns its result', () => { + const target = queryFixture(html` + <div id="target" role="combobox">${comboboxContent}</div> + `); assert.equal(ariaComboboxValue(target), 'bar'); }); - it('passes aria-owned listbox to `ariaListboxValue` and returns its result', function () { - var target = queryFixture( + it('passes aria-owned listbox to `ariaListboxValue` and returns its result', () => { + const target = queryFixture( '<div id="target" role="combobox" aria-owns="text list"></div>' + comboboxContent ); @@ -439,49 +476,43 @@ describe('text.formControlValue', function () { }); }); - describe('ariaRangeValue', function () { - var rangeRoles = ['progressbar', 'scrollbar', 'slider', 'spinbutton']; - var ariaRangeValue = + describe('ariaRangeValue', () => { + const rangeRoles = ['progressbar', 'scrollbar', 'slider', 'spinbutton']; + const ariaRangeValue = axe.commons.text.formControlValueMethods.ariaRangeValue; - it('returns `` for roles that are not ranges', function () { - var target = queryFixture('<div id="target" role="textbox">foo</div>'); + it('returns `` for roles that are not ranges', () => { + const target = queryFixture('<div id="target" role="textbox">foo</div>'); assert.equal(ariaRangeValue(target), ''); }); rangeRoles.forEach(function (role) { - describe('with ' + role, function () { - it('returns the result of aria-valuenow', function () { - var target = queryFixture( - '<div id="target" role="' + - role + - '" aria-valuenow="+123">foo</div>' - ); + describe('with ' + role, () => { + it('returns the result of aria-valuenow', () => { + const target = queryFixture(html` + <div id="target" role="${role}" aria-valuenow="+123">foo</div> + `); assert.equal(ariaRangeValue(target), '123'); }); - it('returns `0` if aria-valuenow is not a number', function () { - var target = queryFixture( - '<div id="target" role="' + role + '" aria-valuenow="abc">foo</div>' - ); + it('returns `0` if aria-valuenow is not a number', () => { + const target = queryFixture(html` + <div id="target" role="${role}" aria-valuenow="abc">foo</div> + `); assert.equal(ariaRangeValue(target), '0'); }); - it('returns decimal numbers', function () { - var target = queryFixture( - '<div id="target" role="' + - role + - '" aria-valuenow="1.5678">foo</div>' - ); + it('returns decimal numbers', () => { + const target = queryFixture(html` + <div id="target" role="${role}" aria-valuenow="1.5678">foo</div> + `); assert.equal(ariaRangeValue(target), '1.5678'); }); - it('returns negative numbers', function () { - var target = queryFixture( - '<div id="target" role="' + - role + - '" aria-valuenow="-1.0">foo</div>' - ); + it('returns negative numbers', () => { + const target = queryFixture(html` + <div id="target" role="${role}" aria-valuenow="-1.0">foo</div> + `); assert.equal(ariaRangeValue(target), '-1'); }); }); diff --git a/test/commons/text/is-icon-ligature.js b/test/commons/text/is-icon-ligature.js index ee11e2b8eb..2da2ba8ba2 100644 --- a/test/commons/text/is-icon-ligature.js +++ b/test/commons/text/is-icon-ligature.js @@ -1,67 +1,73 @@ -describe('text.isIconLigature', function () { +describe('text.isIconLigature', () => { 'use strict'; - var isIconLigature = axe.commons.text.isIconLigature; - var queryFixture = axe.testUtils.queryFixture; - var fontApiSupport = !!document.fonts; + const isIconLigature = axe.commons.text.isIconLigature; + const queryFixture = axe.testUtils.queryFixture; + const fontApiSupport = !!document.fonts; - before(function (done) { + before(done => { if (!fontApiSupport) { done(); } - var firaFont = new FontFace( + const firaFont = new FontFace( 'Fira Code', 'url(/test/assets/FiraCode-Regular.woff)' ); - var ligatureFont = new FontFace( + const ligatureFont = new FontFace( 'LigatureSymbols', 'url(/test/assets/LigatureSymbols.woff)' ); - var materialFont = new FontFace( + const materialFont = new FontFace( 'Material Icons', 'url(/test/assets/MaterialIcons.woff2)' ); - var robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)'); + const robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)'); + const zeroWidth0CharFont = new FontFace( + 'ZeroWidth0Char', + 'url(/test/assets/ZeroWidth0Char.woff)' + ); window.Promise.all([ firaFont.load(), ligatureFont.load(), materialFont.load(), - robotoFont.load() - ]).then(function () { + robotoFont.load(), + zeroWidth0CharFont.load() + ]).then(() => { document.fonts.add(firaFont); document.fonts.add(ligatureFont); document.fonts.add(materialFont); document.fonts.add(robotoFont); + document.fonts.add(zeroWidth0CharFont); done(); }); }); - it('should return false for normal text', function () { - var target = queryFixture('<div id="target">Normal text</div>'); + it('should return false for normal text', () => { + const target = queryFixture('<div id="target">Normal text</div>'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for emoji', function () { - var target = queryFixture('<div id="target">🌎</div>'); + it('should return false for emoji', () => { + const target = queryFixture('<div id="target">🌎</div>'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for non-bmp unicode', function () { - var target = queryFixture('<div id="target">◓</div>'); + it('should return false for non-bmp unicode', () => { + const target = queryFixture('<div id="target">◓</div>'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for whitespace strings', function () { - var target = queryFixture('<div id="target"> </div>'); + it('should return false for whitespace strings', () => { + const target = queryFixture('<div id="target"> </div>'); assert.isFalse(isIconLigature(target.children[0])); }); (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (fi)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">figure</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -70,8 +76,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ff)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">ffugative</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -80,8 +86,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (fl)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">flu shot</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -90,8 +96,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ffi)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">ffigure</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -100,8 +106,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ffl)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">fflu shot</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -110,16 +116,16 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for an icon ligature', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'Material Icons\'">delete</div>' ); assert.isTrue(isIconLigature(target.children[0])); } ); - (fontApiSupport ? it : it.skip)('should trim the string', function () { - var target = queryFixture( + (fontApiSupport ? it : it.skip)('should trim the string', () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto"> fflu shot </div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -127,18 +133,28 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for a font that has no character data', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'Material Icons\'">f</div>' ); assert.isTrue(isIconLigature(target.children[0])); } ); + (fontApiSupport ? it : it.skip)( + 'should return true for a font that has zero width characters', + () => { + const target = queryFixture( + '<div id="target" style="font-family: \'ZeroWidth0Char\'">0</div>' + ); + assert.isTrue(isIconLigature(target.children[0])); + } + ); + (fontApiSupport ? it : it.skip)( 'should return false for a programming text ligature', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Fira Code">!==</div>' ); assert.isFalse(isIconLigature(target.children[0])); @@ -147,8 +163,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for an icon ligature with low pixel difference', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'Material Icons\'">keyboard_arrow_left</div>' ); assert.isTrue(isIconLigature(target.children[0])); @@ -157,8 +173,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true after the 3rd time the font is an icon', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>' ); @@ -174,8 +190,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false after the 3rd time the font is not an icon', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'Roboto\'">__non-icon text__</div>' ); @@ -189,11 +205,11 @@ describe('text.isIconLigature', function () { } ); - describe('pixelThreshold', function () { + describe('pixelThreshold', () => { (fontApiSupport ? it : it.skip)( 'should allow higher percent (will not flag icon ligatures)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>' ); @@ -204,8 +220,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should allow lower percent (will flag text ligatures)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: Roboto">figure</div>' ); assert.isTrue(isIconLigature(target.children[0], 0)); @@ -213,11 +229,11 @@ describe('text.isIconLigature', function () { ); }); - describe('occurrenceThreshold', function () { + describe('occurrenceThreshold', () => { (fontApiSupport ? it : it.skip)( 'should change the number of times a font is seen before returning', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>' ); diff --git a/test/commons/text/native-text-methods.js b/test/commons/text/native-text-methods.js index df1cf5be8e..7cc6efc32d 100644 --- a/test/commons/text/native-text-methods.js +++ b/test/commons/text/native-text-methods.js @@ -1,104 +1,100 @@ -describe('text.nativeTextMethods', function () { - var text = axe.commons.text; - var nativeTextMethods = text.nativeTextMethods; - var fixtureSetup = axe.testUtils.fixtureSetup; - - describe('valueText', function () { - var valueText = nativeTextMethods.valueText; - it('returns the value of actualNode', function () { +describe('text.nativeTextMethods', () => { + const text = axe.commons.text; + const nativeTextMethods = text.nativeTextMethods; + const fixtureSetup = axe.testUtils.fixtureSetup; + + describe('valueText', () => { + const valueText = nativeTextMethods.valueText; + it('returns the value of actualNode', () => { fixtureSetup('<input value="foo" />'); - var input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; + const input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; assert.equal(valueText(input), 'foo'); }); - it('returns `` when there is no value', function () { + it('returns `` when there is no value', () => { fixtureSetup('<input />'); - var input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; + const input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; assert.equal(valueText(input), ''); }); }); - describe('buttonDefaultText', function () { - var buttonDefaultText = nativeTextMethods.buttonDefaultText; - it('returns the default button text', function () { + describe('buttonDefaultText', () => { + const buttonDefaultText = nativeTextMethods.buttonDefaultText; + it('returns the default button text', () => { fixtureSetup( '<input type="submit" />' + '<input type="image" />' + '<input type="reset" />' + '<input type="button" />' ); - var inputs = axe.utils.querySelectorAll(axe._tree[0], 'input'); + const inputs = axe.utils.querySelectorAll(axe._tree[0], 'input'); assert.equal(buttonDefaultText(inputs[0]), 'Submit'); assert.equal(buttonDefaultText(inputs[1]), 'Submit'); assert.equal(buttonDefaultText(inputs[2]), 'Reset'); assert.equal(buttonDefaultText(inputs[3]), ''); }); - it('returns `` when the element is not a button', function () { + it('returns `` when the element is not a button', () => { fixtureSetup('<input type="text">'); - var input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; + const input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; assert.equal(buttonDefaultText(input), ''); }); }); - describe('altText', function () { - var altText = nativeTextMethods.altText; - it('returns the alt attribute of actualNode', function () { + describe('altText', () => { + const altText = nativeTextMethods.altText; + it('returns the alt attribute of actualNode', () => { fixtureSetup('<img alt="foo" />'); - var img = axe.utils.querySelectorAll(axe._tree[0], 'img')[0]; + const img = axe.utils.querySelectorAll(axe._tree[0], 'img')[0]; assert.equal(altText(img), 'foo'); }); - it('returns `` when there is no alt', function () { + it('returns `` when there is no alt', () => { fixtureSetup('<img />'); - var img = axe.utils.querySelectorAll(axe._tree[0], 'img')[0]; + const img = axe.utils.querySelectorAll(axe._tree[0], 'img')[0]; assert.equal(altText(img), ''); }); }); - describe('figureText', function () { - var figureText = nativeTextMethods.figureText; - it('returns the figcaption text', function () { + describe('figureText', () => { + it('returns the figcaption text', () => { + const figureText = nativeTextMethods.figureText; fixtureSetup( '<figure>' + ' <figcaption>My caption</figcaption>' + ' some content' + '</figure>' ); - var figure = axe.utils.querySelectorAll(axe._tree[0], 'figure')[0]; + const figure = axe.utils.querySelectorAll(axe._tree[0], 'figure')[0]; assert.equal(figureText(figure), 'My caption'); }); - it('returns `` when there is no figcaption', function () { - var figureText = nativeTextMethods.figureText; - it('returns the figcaption text', function () { - fixtureSetup('<figure>' + ' some content' + '</figure>'); - var figure = axe.utils.querySelectorAll(axe._tree[0], 'figure')[0]; - assert.equal(figureText(figure), ''); - }); + it('returns `` when there is no figcaption', () => { + const figureText = nativeTextMethods.figureText; + fixtureSetup('<figure>' + ' some content' + '</figure>'); + const figure = axe.utils.querySelectorAll(axe._tree[0], 'figure')[0]; + assert.equal(figureText(figure), ''); }); - it('returns `` when if the figcaption is nested in another figure', function () { - var figureText = nativeTextMethods.figureText; - it('returns the figcaption text', function () { - fixtureSetup( - '<figure id="fig1">' + - ' <figure>' + - ' <figcaption>No caption</figcaption>' + - ' some content' + - ' </figure>' + - ' some other content' + - '</figure>' - ); - var figure = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; - assert.equal(figureText(figure), ''); - }); + it('returns `` when if the figcaption is nested in another figure', () => { + const figureText = nativeTextMethods.figureText; + fixtureSetup( + '<figure id="fig1">' + + ' <figure>' + + ' <figcaption>No caption</figcaption>' + + ' some content' + + ' </figure>' + + ' some other content' + + '</figure>' + ); + const figure = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; + assert.equal(figureText(figure), ''); }); }); - describe('tableCaptionText', function () { - var tableCaptionText = nativeTextMethods.tableCaptionText; - it('returns the table caption text', function () { + describe('tableCaptionText', () => { + const tableCaptionText = nativeTextMethods.tableCaptionText; + it('returns the table caption text', () => { fixtureSetup( '<table>' + ' <caption>My caption</caption>' + @@ -106,22 +102,22 @@ describe('text.nativeTextMethods', function () { ' <tr><td>data</td></tr>' + '</table>' ); - var table = axe.utils.querySelectorAll(axe._tree[0], 'table')[0]; + const table = axe.utils.querySelectorAll(axe._tree[0], 'table')[0]; assert.equal(tableCaptionText(table), 'My caption'); }); - it('returns `` when there is no caption', function () { + it('returns `` when there is no caption', () => { fixtureSetup( '<table>' + ' <tr><th>heading</th></tr>' + ' <tr><td>data</td></tr>' + '</table>' ); - var table = axe.utils.querySelectorAll(axe._tree[0], 'table')[0]; + const table = axe.utils.querySelectorAll(axe._tree[0], 'table')[0]; assert.equal(tableCaptionText(table), ''); }); - it('returns `` when if the caption is nested in another table', function () { + it('returns `` when if the caption is nested in another table', () => { fixtureSetup( '<table id="tbl1">' + ' <tr><td>' + @@ -133,71 +129,67 @@ describe('text.nativeTextMethods', function () { ' </th></tr>' + '</table>' ); - var table = axe.utils.querySelectorAll(axe._tree[0], '#tbl1')[0]; + const table = axe.utils.querySelectorAll(axe._tree[0], '#tbl1')[0]; assert.equal(tableCaptionText(table), ''); }); }); - describe('fieldsetLegendText', function () { - var fieldsetLegendText = nativeTextMethods.fieldsetLegendText; - it('returns the legend text', function () { + describe('fieldsetLegendText', () => { + it('returns the legend text', () => { + const fieldsetLegendText = nativeTextMethods.fieldsetLegendText; fixtureSetup( '<fieldset>' + ' <legend>My legend</legend>' + ' some content' + '</fieldset>' ); - var fieldset = axe.utils.querySelectorAll(axe._tree[0], 'fieldset')[0]; + const fieldset = axe.utils.querySelectorAll(axe._tree[0], 'fieldset')[0]; assert.equal(fieldsetLegendText(fieldset), 'My legend'); }); - it('returns `` when there is no legend', function () { - var fieldsetLegendText = nativeTextMethods.fieldsetLegendText; - it('returns the legend text', function () { - fixtureSetup('<fieldset>' + ' some content' + '</fieldset>'); - var fieldset = axe.utils.querySelectorAll(axe._tree[0], 'fieldset')[0]; - assert.equal(fieldsetLegendText(fieldset), ''); - }); + it('returns `` when there is no legend', () => { + const fieldsetLegendText = nativeTextMethods.fieldsetLegendText; + fixtureSetup('<fieldset>' + ' some content' + '</fieldset>'); + const fieldset = axe.utils.querySelectorAll(axe._tree[0], 'fieldset')[0]; + assert.equal(fieldsetLegendText(fieldset), ''); }); - it('returns `` when if the legend is nested in another fieldset', function () { - var fieldsetLegendText = nativeTextMethods.fieldsetLegendText; - it('returns the legend text', function () { - fixtureSetup( - '<fieldset id="fig1">' + - ' <fieldset>' + - ' <legend>No legend</legend>' + - ' some content' + - ' </fieldset>' + - ' some other content' + - '</fieldset>' - ); - var fieldset = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; - assert.equal(fieldsetLegendText(fieldset), ''); - }); + it('returns `` when if the legend is nested in another fieldset', () => { + const fieldsetLegendText = nativeTextMethods.fieldsetLegendText; + fixtureSetup( + '<fieldset id="fig1">' + + ' <fieldset>' + + ' <legend>No legend</legend>' + + ' some content' + + ' </fieldset>' + + ' some other content' + + '</fieldset>' + ); + const fieldset = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; + assert.equal(fieldsetLegendText(fieldset), ''); }); }); - describe('svgTitleText', function () { - var svgTitleText = nativeTextMethods.svgTitleText; - it('returns the title text', function () { + describe('svgTitleText', () => { + const svgTitleText = nativeTextMethods.svgTitleText; + it('returns the title text', () => { fixtureSetup( '<svg>' + ' <title>My title</title>' + ' some content' + '</svg>' ); - var svg = axe.utils.querySelectorAll(axe._tree[0], 'svg')[0]; + const svg = axe.utils.querySelectorAll(axe._tree[0], 'svg')[0]; assert.equal(svgTitleText(svg), 'My title'); }); - it('returns `` when there is no title', function () { - it('returns the title text', function () { + it('returns `` when there is no title', () => { + it('returns the title text', () => { fixtureSetup('<svg>' + ' some content' + '</svg>'); - var svg = axe.utils.querySelectorAll(axe._tree[0], 'svg')[0]; + const svg = axe.utils.querySelectorAll(axe._tree[0], 'svg')[0]; assert.equal(svgTitleText(svg), ''); }); }); - it('returns `` when if the title is nested in another svg', function () { - it('returns the title text', function () { + it('returns `` when if the title is nested in another svg', () => { + it('returns the title text', () => { fixtureSetup( '<svg id="fig1">' + ' <svg>' + @@ -207,30 +199,30 @@ describe('text.nativeTextMethods', function () { ' some other content' + '</svg>' ); - var svg = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; + const svg = axe.utils.querySelectorAll(axe._tree[0], '#fig1')[0]; assert.equal(svgTitleText(svg), ''); }); }); }); - describe('singleSpace', function () { - var singleSpace = nativeTextMethods.singleSpace; - it('returns a single space', function () { + describe('singleSpace', () => { + const singleSpace = nativeTextMethods.singleSpace; + it('returns a single space', () => { assert.equal(singleSpace(), ' '); }); }); - describe('placeholderText', function () { - var placeholderText = nativeTextMethods.placeholderText; - it('returns the placeholder attribute of actualNode', function () { + describe('placeholderText', () => { + const placeholderText = nativeTextMethods.placeholderText; + it('returns the placeholder attribute of actualNode', () => { fixtureSetup('<input placeholder="foo" />'); - var input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; + const input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; assert.equal(placeholderText(input), 'foo'); }); - it('returns `` when there is no placeholder', function () { + it('returns `` when there is no placeholder', () => { fixtureSetup('<input />'); - var input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; + const input = axe.utils.querySelectorAll(axe._tree[0], 'input')[0]; assert.equal(placeholderText(input), ''); }); }); diff --git a/test/commons/text/subtree-text.js b/test/commons/text/subtree-text.js index a38d05e4bd..09b5cc41d3 100644 --- a/test/commons/text/subtree-text.js +++ b/test/commons/text/subtree-text.js @@ -1,20 +1,20 @@ -describe('text.subtreeText', function () { - var fixtureSetup = axe.testUtils.fixtureSetup; - var subtreeText = axe.commons.text.subtreeText; +describe('text.subtreeText', () => { + const fixtureSetup = axe.testUtils.fixtureSetup; + const subtreeText = axe.commons.text.subtreeText; - it('concatinated the accessible name for child elements', function () { + it('concatinated the accessible name for child elements', () => { fixtureSetup('<span>foo</span> <span>bar</span> <span>baz</span>'); - var fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; + const fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; assert.equal(subtreeText(fixture), 'foo bar baz'); }); - it('returns `` when the element is not named from contents', function () { + it('returns `` when the element is not named from contents', () => { fixtureSetup('<main>foo bar baz</main>'); - var main = axe.utils.querySelectorAll(axe._tree[0], 'main')[0]; + const main = axe.utils.querySelectorAll(axe._tree[0], 'main')[0]; assert.equal(subtreeText(main), ''); }); - it('adds spacing around "block-like" elements', function () { + it('adds spacing around "block-like" elements', () => { fixtureSetup( '<div>foo</div>' + '<h1>bar</h1>' + @@ -22,19 +22,19 @@ describe('text.subtreeText', function () { '<blockquote>fizz</blockquote>' + '<pre>buzz</pre>' ); - var fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; + const fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; assert.equal(subtreeText(fixture), 'foo bar baz fizz buzz '); }); - it('does not add spacing around "inline-like" elements', function () { + it('does not add spacing around "inline-like" elements', () => { fixtureSetup( '<a>foo</a>' + '<b>bar</b>' + '<i>baz</i>' + '<s>fizz</s>' + '<u>buzz</u>' ); - var fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; + const fixture = axe.utils.querySelectorAll(axe._tree[0], '#fixture')[0]; assert.equal(subtreeText(fixture), 'foobarbazfizzbuzz'); }); - it('returns `` for embedded content', function () { + it('returns `` for embedded content', () => { fixtureSetup( '<video>foo</video>' + '<audio>foo</audio>' + @@ -42,37 +42,67 @@ describe('text.subtreeText', function () { '<iframe>foo</iframe>' + '<svg>foo</svg>' ); - var children = axe._tree[0].children; + const children = axe._tree[0].children; assert.lengthOf(children, 5); children.forEach(function (embeddedContent) { assert.equal(subtreeText(embeddedContent), ''); }); }); - describe('context.processed', function () { - beforeEach(function () { + describe('name from author', () => { + it('returns `` with default context', () => { + fixtureSetup('<main>foo</main>'); + const div = axe.utils.querySelectorAll(axe._tree[0], 'main')[0]; + const context = {}; + assert.equal(subtreeText(div, context), ''); + }); + + it('returns content with { inLabelledByContext: true }', () => { + fixtureSetup('<main>foo</main>'); + const div = axe.utils.querySelectorAll(axe._tree[0], 'main')[0]; + const context = { inLabelledByContext: true }; + assert.equal(subtreeText(div, context), 'foo'); + }); + + it('returns content with { subtreeDescendant: true }', () => { + fixtureSetup('<main>foo</main>'); + const div = axe.utils.querySelectorAll(axe._tree[0], 'main')[0]; + const context = { subtreeDescendant: true }; + assert.equal(subtreeText(div, context), 'foo'); + }); + + it('returns `` for roles that have a value, even with inLabelledByContext and subtreeDescendant', () => { + fixtureSetup('<select><option>foo</option</select>'); + const div = axe.utils.querySelectorAll(axe._tree[0], 'select')[0]; + const context = { inLabelledByContext: true, subtreeDescendant: true }; + assert.equal(subtreeText(div, context), ''); + }); + }); + + describe('context.processed', () => { + beforeEach(() => { fixtureSetup('<h1>foo</h1>'); }); - it('appends the element to context.processed to prevent duplication', function () { - var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; - var text = h1.children[0]; - var context = { processed: [] }; + it('appends the element to context.processed to prevent duplication', () => { + const h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; + const text = h1.children[0]; + const context = { processed: [] }; subtreeText(h1, context); assert.deepEqual(context.processed, [h1, text]); }); - it('sets context.processed when it is undefined', function () { - var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; - var text = h1.children[0]; - var emptyContext = {}; + it('sets context.processed when it is undefined', () => { + const h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; + const text = h1.children[0]; + const emptyContext = {}; subtreeText(h1, emptyContext); assert.deepEqual(emptyContext.processed, [h1, text]); }); - it('returns `` when the element is in the `processed` array', function () { - var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; - var context = { + it('returns `` when the element is in the `processed` array', () => { + const h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; + const context = { processed: [h1] }; assert.equal(subtreeText(h1, context), ''); diff --git a/test/commons/text/unicode.js b/test/commons/text/unicode.js index b5d8b2489e..819e01d271 100644 --- a/test/commons/text/unicode.js +++ b/test/commons/text/unicode.js @@ -1,86 +1,94 @@ -describe('text.hasUnicode', function () { - describe('text.hasUnicode, characters of type Non Bi Multilingual Plane', function () { - it('returns false when given string is alphanumeric', function () { - var actual = axe.commons.text.hasUnicode('1 apple', { +describe('text.hasUnicode', () => { + describe('text.hasUnicode, characters of type Non Bi Multilingual Plane', () => { + it('returns false when given string is alphanumeric', () => { + const actual = axe.commons.text.hasUnicode('1 apple', { nonBmp: true }); assert.isFalse(actual); }); - it('returns false when given string is number', function () { - var actual = axe.commons.text.hasUnicode('100', { + it('returns false when given string is number', () => { + const actual = axe.commons.text.hasUnicode('100', { nonBmp: true }); assert.isFalse(actual); }); - it('returns false when given string is a sentence', function () { - var actual = axe.commons.text.hasUnicode('Earth is round', { + it('returns false when given string is a sentence', () => { + const actual = axe.commons.text.hasUnicode('Earth is round', { nonBmp: true }); assert.isFalse(actual); }); - it('returns true when given string is a phonetic extension', function () { - var actual = axe.commons.text.hasUnicode('ᴁ', { + it('returns true when given string is a phonetic extension', () => { + const actual = axe.commons.text.hasUnicode('ᴁ', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string is a combining diacritical marks supplement', function () { - var actual = axe.commons.text.hasUnicode('ᴁ', { + it('returns true when given string is a combining diacritical marks supplement', () => { + const actual = axe.commons.text.hasUnicode('ᴁ', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string is a currency symbols', function () { - var actual = axe.commons.text.hasUnicode('₨ 20000', { + it('returns true when given string is a currency symbols', () => { + const actual = axe.commons.text.hasUnicode('₨ 20000', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string has arrows', function () { - var actual = axe.commons.text.hasUnicode('← turn left', { + it('returns true when given string has arrows', () => { + const actual = axe.commons.text.hasUnicode('← turn left', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string has geometric shapes', function () { - var actual = axe.commons.text.hasUnicode('◓', { + it('returns true when given string has geometric shapes', () => { + const actual = axe.commons.text.hasUnicode('◓', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string has math operators', function () { - var actual = axe.commons.text.hasUnicode('√4 = 2', { + it('returns true when given string has math operators', () => { + const actual = axe.commons.text.hasUnicode('√4 = 2', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true when given string has windings font', function () { - var actual = axe.commons.text.hasUnicode('▽', { + it('returns true when given string has windings font', () => { + const actual = axe.commons.text.hasUnicode('▽', { nonBmp: true }); assert.isTrue(actual); }); - it('returns true for a string with characters in supplementary private use area A', function () { - var actual = axe.commons.text.hasUnicode('\uDB80\uDFFE', { + it('returns true for a string with characters in supplementary private use area A', () => { + const actual = axe.commons.text.hasUnicode('\uDB80\uDFFE', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has format unicode', () => { + // zero-width spacer character U+200B + const actual = axe.commons.text.hasUnicode('\u200BHello World', { nonBmp: true }); assert.isTrue(actual); }); }); - describe('text.hasUnicode, characters of type Emoji', function () { - it('returns false when given string is alphanumeric', function () { - var actual = axe.commons.text.hasUnicode( + describe('text.hasUnicode, characters of type Emoji', () => { + it('returns false when given string is alphanumeric', () => { + const actual = axe.commons.text.hasUnicode( '1 apple a day, keeps the doctor away', { emoji: true @@ -89,60 +97,60 @@ describe('text.hasUnicode', function () { assert.isFalse(actual); }); - it('returns false when given string is number', function () { - var actual = axe.commons.text.hasUnicode('100', { + it('returns false when given string is number', () => { + const actual = axe.commons.text.hasUnicode('100', { emoji: true }); assert.isFalse(actual); }); - it('returns false when given string is a sentence', function () { - var actual = axe.commons.text.hasUnicode('Earth is round', { + it('returns false when given string is a sentence', () => { + const actual = axe.commons.text.hasUnicode('Earth is round', { emoji: true }); assert.isFalse(actual); }); - it('returns true when given string has emoji', function () { - var actual = axe.commons.text.hasUnicode('🌎 is round', { + it('returns true when given string has emoji', () => { + const actual = axe.commons.text.hasUnicode('🌎 is round', { emoji: true }); assert.isTrue(actual); }); - it('returns true when given string has emoji', function () { - var actual = axe.commons.text.hasUnicode('plant a 🌱', { + it('returns true when given string has emoji', () => { + const actual = axe.commons.text.hasUnicode('plant a 🌱', { emoji: true }); assert.isTrue(actual); }); }); - describe('text.hasUnicode, characters of type punctuations', function () { - it('returns false when given string is number', function () { - var actual = axe.commons.text.hasUnicode('100', { + describe('text.hasUnicode, characters of type punctuations', () => { + it('returns false when given string is number', () => { + const actual = axe.commons.text.hasUnicode('100', { punctuations: true }); assert.isFalse(actual); }); - it('returns false when given string is a sentence', function () { - var actual = axe.commons.text.hasUnicode('Earth is round', { + it('returns false when given string is a sentence', () => { + const actual = axe.commons.text.hasUnicode('Earth is round', { punctuations: true }); assert.isFalse(actual); }); - it('returns true when given string has punctuations', function () { - var actual = axe.commons.text.hasUnicode("What's your name?", { + it('returns true when given string has punctuations', () => { + const actual = axe.commons.text.hasUnicode("What's your name?", { punctuations: true }); assert.isTrue(actual); }); - it('returns true for strings with money signs and odd symbols', function () { + it('returns true for strings with money signs and odd symbols', () => { ['£', '¢', '¥', '€', '§', '±'].forEach(function (str) { - var actual = axe.commons.text.hasUnicode(str, { + const actual = axe.commons.text.hasUnicode(str, { punctuations: true }); assert.isTrue(actual); @@ -150,9 +158,9 @@ describe('text.hasUnicode', function () { }); }); - describe('text.hasUnicode, has combination of unicode', function () { - it('returns false when given string is number', function () { - var actual = axe.commons.text.hasUnicode('100', { + describe('text.hasUnicode, has combination of unicode', () => { + it('returns false when given string is number', () => { + const actual = axe.commons.text.hasUnicode('100', { emoji: true, nonBmp: true, punctuations: true @@ -160,8 +168,8 @@ describe('text.hasUnicode', function () { assert.isFalse(actual); }); - it('returns true when given string has unicode characters', function () { - var actual = axe.commons.text.hasUnicode( + it('returns true when given string has unicode characters', () => { + const actual = axe.commons.text.hasUnicode( 'The ☀️ is orange, the ◓ is white.', { emoji: true, @@ -171,61 +179,80 @@ describe('text.hasUnicode', function () { ); assert.isTrue(actual); }); + + it('returns true when given format unicode characters', () => { + // zero-width spacer character U+200B + const actual = axe.commons.text.hasUnicode('\u200BHello World', { + emoji: true, + nonBmp: true, + punctuations: true + }); + assert.isTrue(actual); + }); + + it('returns true when given punctuation characters', () => { + const actual = axe.commons.text.hasUnicode('Earth!!!', { + emoji: true, + nonBmp: true, + punctuations: true + }); + assert.isTrue(actual); + }); }); }); -describe('text.removeUnicode', function () { - it('returns string by removing non BMP unicode ', function () { - var actual = axe.commons.text.removeUnicode('₨₨20000₨₨', { +describe('text.removeUnicode', () => { + it('returns string by removing non BMP unicode ', () => { + const actual = axe.commons.text.removeUnicode('₨₨20000₨₨', { nonBmp: true }); assert.equal(actual, '20000'); }); - it('returns string by removing emoji unicode ', function () { - var actual = axe.commons.text.removeUnicode('☀️Sun 🌎Earth', { + it('returns string by removing emoji unicode ', () => { + const actual = axe.commons.text.removeUnicode('☀️Sun 🌎Earth', { emoji: true }); assert.equal(actual, 'Sun Earth'); }); - it('returns string after removing punctuations from word', function () { - var actual = axe.commons.text.removeUnicode('Earth!!!', { + it('returns string after removing punctuations from word', () => { + const actual = axe.commons.text.removeUnicode('Earth!!!', { punctuations: true }); assert.equal(actual, 'Earth'); }); - it('returns string removing all punctuations', function () { - var actual = axe.commons.text.removeUnicode('<!,."\':;!>', { + it('returns string removing all punctuations', () => { + const actual = axe.commons.text.removeUnicode('<!,."\':;!>', { punctuations: true }); assert.equal(actual, ''); }); - it('returns string removing all private use unicode', function () { - var actual = axe.commons.text.removeUnicode('', { + it('returns string removing all private use unicode', () => { + const actual = axe.commons.text.removeUnicode('', { nonBmp: true }); assert.equal(actual, ''); }); - it('returns string removing all supplementary private use unicode', function () { - var actual = axe.commons.text.removeUnicode('', { + it('returns string removing all supplementary private use unicode', () => { + const actual = axe.commons.text.removeUnicode('', { nonBmp: true }); assert.equal(actual, ''); }); - it('returns the string with supplementary private use area A characters removed', function () { - var actual = axe.commons.text.removeUnicode('\uDB80\uDFFE', { + it('returns the string with supplementary private use area A characters removed', () => { + const actual = axe.commons.text.removeUnicode('\uDB80\uDFFE', { nonBmp: true }); assert.equal(actual, ''); }); - it('returns string removing combination of unicode characters', function () { - var actual = axe.commons.text.removeUnicode( + it('returns string removing combination of unicode characters', () => { + const actual = axe.commons.text.removeUnicode( 'The ☀️ is orange, the ◓ is white.', { emoji: true, @@ -235,4 +262,12 @@ describe('text.removeUnicode', function () { ); assert.equal(actual, 'The is orange the is white'); }); + + it('returns string removing format unicode', () => { + // zero-width spacer character U+200B + const actual = axe.commons.text.removeUnicode('\u200BHello World', { + nonBmp: true + }); + assert.equal(actual, 'Hello World'); + }); }); diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 7a9dfd5d77..7160c0db1c 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -1,44 +1,42 @@ -/* global Promise */ -describe('Audit', function () { - 'use strict'; - - var Audit = axe._thisWillBeDeletedDoNotUse.base.Audit; - var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; - var ver = axe.version.substring(0, axe.version.lastIndexOf('.')); - var a, getFlattenedTree; - var isNotCalled = function (err) { +describe('Audit', () => { + const Audit = axe._thisWillBeDeletedDoNotUse.base.Audit; + const Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; + const ver = axe.version.substring(0, axe.version.lastIndexOf('.')); + const { fixtureSetup } = axe.testUtils; + let audit; + const isNotCalled = function (err) { throw err || new Error('Reject should not be called'); }; - var noop = function () {}; + const noop = () => {}; - var mockChecks = [ + const mockChecks = [ { id: 'positive1-check1', - evaluate: function () { + evaluate: () => { return true; } }, { id: 'positive2-check1', - evaluate: function () { + evaluate: () => { return true; } }, { id: 'negative1-check1', - evaluate: function () { + evaluate: () => { return true; } }, { id: 'positive3-check1', - evaluate: function () { + evaluate: () => { return true; } } ]; - var mockRules = [ + const mockRules = [ { id: 'positive1', selector: 'input', @@ -69,48 +67,45 @@ describe('Audit', function () { } ]; - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); - var origAuditRun; - var origAxeUtilsPreload; + let origAuditRun; - beforeEach(function () { - a = new Audit(); + beforeEach(() => { + audit = new Audit(); mockRules.forEach(function (r) { - a.addRule(r); + audit.addRule(r); }); mockChecks.forEach(function (c) { - a.addCheck(c); + audit.addCheck(c); }); - origAuditRun = a.run; + origAuditRun = audit.run; }); - afterEach(function () { - fixture.innerHTML = ''; - axe._tree = undefined; - axe._selectCache = undefined; - a.run = origAuditRun; + afterEach(() => { + axe.teardown(); + audit.run = origAuditRun; }); - it('should be a function', function () { + it('should be a function', () => { assert.isFunction(Audit); }); - describe('defaults', function () { - it('should set noHtml', function () { - var audit = new Audit(); + describe('defaults', () => { + it('should set noHtml', () => { + audit = new Audit(); assert.isFalse(audit.noHtml); }); - it('should set allowedOrigins', function () { - var audit = new Audit(); + it('should set allowedOrigins', () => { + audit = new Audit(); assert.deepEqual(audit.allowedOrigins, [window.location.origin]); }); }); - describe('Audit#_constructHelpUrls', function () { - it('should create default help URLS', function () { - var audit = new Audit(); + describe('Audit#_constructHelpUrls', () => { + it('should create default help URLS', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -126,8 +121,8 @@ describe('Audit', function () { '/target?application=axeAPI' }); }); - it('should use changed branding', function () { - var audit = new Audit(); + it('should use changed branding', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -144,8 +139,8 @@ describe('Audit', function () { '/target?application=axeAPI' }); }); - it('should use changed application', function () { - var audit = new Audit(); + it('should use changed application', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -163,8 +158,8 @@ describe('Audit', function () { }); }); - it('does not override helpUrls of different products', function () { - var audit = new Audit(); + it('does not override helpUrls of different products', () => { + audit = new Audit(); audit.addRule({ id: 'target1', matches: 'function () {return "hello";}', @@ -207,9 +202,9 @@ describe('Audit', function () { '/target2?application=axeAPI' ); }); - it('understands prerelease type version numbers', function () { - var tempVersion = axe.version; - var audit = new Audit(); + it('understands prerelease type version numbers', () => { + const tempVersion = axe.version; + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -226,9 +221,9 @@ describe('Audit', function () { ); }); - it('matches major release versions', function () { - var tempVersion = axe.version; - var audit = new Audit(); + it('matches major release versions', () => { + const tempVersion = axe.version; + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -244,8 +239,8 @@ describe('Audit', function () { 'https://dequeuniversity.com/rules/axe/1.0/target?application=axeAPI' ); }); - it('sets the lang query if locale has been set', function () { - var audit = new Audit(); + it('sets the lang query if locale has been set', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -266,9 +261,9 @@ describe('Audit', function () { }); }); - describe('Audit#setBranding', function () { - it('should change the brand', function () { - var audit = new Audit(); + describe('Audit#setBranding', () => { + it('should change the brand', () => { + audit = new Audit(); assert.equal(audit.brand, 'axe'); assert.equal(audit.application, 'axeAPI'); audit.setBranding({ @@ -277,8 +272,8 @@ describe('Audit', function () { assert.equal(audit.brand, 'thing'); assert.equal(audit.application, 'axeAPI'); }); - it('should change the application', function () { - var audit = new Audit(); + it('should change the application', () => { + audit = new Audit(); assert.equal(audit.brand, 'axe'); assert.equal(audit.application, 'axeAPI'); audit.setBranding({ @@ -287,16 +282,16 @@ describe('Audit', function () { assert.equal(audit.brand, 'axe'); assert.equal(audit.application, 'thing'); }); - it('should change the application when passed a string', function () { - var audit = new Audit(); + it('should change the application when passed a string', () => { + audit = new Audit(); assert.equal(audit.brand, 'axe'); assert.equal(audit.application, 'axeAPI'); audit.setBranding('thing'); assert.equal(audit.brand, 'axe'); assert.equal(audit.application, 'thing'); }); - it('should call _constructHelpUrls', function () { - var audit = new Audit(); + it('should call _constructHelpUrls', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -314,8 +309,8 @@ describe('Audit', function () { '/target?application=thing' }); }); - it('should call _constructHelpUrls even when nothing changed', function () { - var audit = new Audit(); + it('should call _constructHelpUrls even when nothing changed', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -331,8 +326,8 @@ describe('Audit', function () { '/target?application=axeAPI' }); }); - it('should not replace custom set branding', function () { - var audit = new Audit(); + it('should not replace custom set branding', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -357,9 +352,9 @@ describe('Audit', function () { }); }); - describe('Audit#addRule', function () { - it('should override existing rule', function () { - var audit = new Audit(); + describe('Audit#addRule', () => { + it('should override existing rule', () => { + audit = new Audit(); audit.addRule({ id: 'target', matches: 'function () {return "hello";}', @@ -378,8 +373,8 @@ describe('Audit', function () { assert.equal(audit.rules[0].selector, 'fred'); assert.equal(audit.rules[0].matches(), 'hello'); }); - it('should otherwise push new rule', function () { - var audit = new Audit(); + it('should otherwise push new rule', () => { + audit = new Audit(); audit.addRule({ id: 'target', selector: 'bob' @@ -399,9 +394,9 @@ describe('Audit', function () { }); }); - describe('Audit#resetRulesAndChecks', function () { - it('should override newly created check', function () { - var audit = new Audit(); + describe('Audit#resetRulesAndChecks', () => { + it('should override newly created check', () => { + audit = new Audit(); assert.equal(audit.checks.target, undefined); audit.addCheck({ id: 'target', @@ -412,8 +407,8 @@ describe('Audit', function () { audit.resetRulesAndChecks(); assert.equal(audit.checks.target, undefined); }); - it('should reset locale', function () { - var audit = new Audit(); + it('should reset locale', () => { + audit = new Audit(); assert.equal(audit.lang, 'en'); audit.applyLocale({ lang: 'de' @@ -422,8 +417,8 @@ describe('Audit', function () { audit.resetRulesAndChecks(); assert.equal(audit.lang, 'en'); }); - it('should reset brand', function () { - var audit = new Audit(); + it('should reset brand', () => { + audit = new Audit(); assert.equal(audit.brand, 'axe'); audit.setBranding({ brand: 'test' @@ -432,8 +427,8 @@ describe('Audit', function () { audit.resetRulesAndChecks(); assert.equal(audit.brand, 'axe'); }); - it('should reset brand application', function () { - var audit = new Audit(); + it('should reset brand application', () => { + audit = new Audit(); assert.equal(audit.application, 'axeAPI'); audit.setBranding({ application: 'test' @@ -442,7 +437,7 @@ describe('Audit', function () { audit.resetRulesAndChecks(); assert.equal(audit.application, 'axeAPI'); }); - it('should reset brand tagExlcude', function () { + it('should reset brand tagExlcude', () => { axe._load({}); assert.deepEqual(axe._audit.tagExclude, ['experimental']); axe.configure({ @@ -452,24 +447,24 @@ describe('Audit', function () { assert.deepEqual(axe._audit.tagExclude, ['experimental']); }); - it('should reset noHtml', function () { - var audit = new Audit(); + it('should reset noHtml', () => { + audit = new Audit(); audit.noHtml = true; audit.resetRulesAndChecks(); assert.isFalse(audit.noHtml); }); - it('should reset allowedOrigins', function () { - var audit = new Audit(); + it('should reset allowedOrigins', () => { + audit = new Audit(); audit.allowedOrigins = ['hello']; audit.resetRulesAndChecks(); assert.deepEqual(audit.allowedOrigins, [window.location.origin]); }); }); - describe('Audit#addCheck', function () { - it('should create a new check', function () { - var audit = new Audit(); + describe('Audit#addCheck', () => { + it('should create a new check', () => { + audit = new Audit(); assert.equal(audit.checks.target, undefined); audit.addCheck({ id: 'target', @@ -478,8 +473,8 @@ describe('Audit', function () { assert.ok(audit.checks.target); assert.deepEqual(audit.checks.target.options, { value: 'jane' }); }); - it('should configure the metadata, if passed', function () { - var audit = new Audit(); + it('should configure the metadata, if passed', () => { + audit = new Audit(); assert.equal(audit.checks.target, undefined); audit.addCheck({ id: 'target', @@ -488,9 +483,9 @@ describe('Audit', function () { assert.ok(audit.checks.target); assert.equal(audit.data.checks.target.guy, 'bob'); }); - it('should reconfigure existing check', function () { - var audit = new Audit(); - var myTest = function () {}; + it('should reconfigure existing check', () => { + audit = new Audit(); + const myTest = () => {}; audit.addCheck({ id: 'target', evaluate: myTest, @@ -507,9 +502,9 @@ describe('Audit', function () { assert.equal(audit.checks.target.evaluate, myTest); assert.deepEqual(audit.checks.target.options, { value: 'fred' }); }); - it('should not turn messages into a function', function () { - var audit = new Audit(); - var spec = { + it('should not turn messages into a function', () => { + audit = new Audit(); + const spec = { id: 'target', evaluate: 'function () { return "blah";}', metadata: { @@ -525,9 +520,9 @@ describe('Audit', function () { assert.equal(audit.data.checks.target.messages.fail, 'it failed'); }); - it('should turn function strings into a function', function () { - var audit = new Audit(); - var spec = { + it('should turn function strings into a function', () => { + audit = new Audit(); + const spec = { id: 'target', evaluate: 'function () { return "blah";}', metadata: { @@ -544,9 +539,9 @@ describe('Audit', function () { }); }); - describe('Audit#setAllowedOrigins', function () { - it('should set allowedOrigins', function () { - var audit = new Audit(); + describe('Audit#setAllowedOrigins', () => { + it('should set allowedOrigins', () => { + audit = new Audit(); audit.setAllowedOrigins([ 'https://deque.com', 'https://dequeuniversity.com' @@ -557,8 +552,8 @@ describe('Audit', function () { ]); }); - it('should normalize <same_origin>', function () { - var audit = new Audit(); + it('should normalize <same_origin>', () => { + audit = new Audit(); audit.setAllowedOrigins(['<same_origin>', 'https://deque.com']); assert.deepEqual(audit.allowedOrigins, [ window.location.origin, @@ -566,8 +561,8 @@ describe('Audit', function () { ]); }); - it('should normalize <unsafe_all_origins>', function () { - var audit = new Audit(); + it('should normalize <unsafe_all_origins>', () => { + audit = new Audit(); audit.setAllowedOrigins([ 'https://deque.com', '<unsafe_all_origins>', @@ -577,19 +572,20 @@ describe('Audit', function () { }); }); - describe('Audit#run', function () { - it('should run all the rules', function (done) { - fixture.innerHTML = + describe('Audit#run', () => { + it('should run all the rules', done => { + fixtureSetup( '<input aria-label="monkeys" type="text">' + - '<div id="monkeys">bananas</div>' + - '<input aria-labelledby="monkeys">' + - '<blink>FAIL ME</blink>'; + '<div id="monkeys">bananas</div>' + + '<input aria-labelledby="monkeys">' + + '<blink>FAIL ME</blink>' + ); - a.run( + audit.run( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function (results) { - var expected = [ + const expected = [ { id: 'positive1', result: 'inapplicable', @@ -620,7 +616,7 @@ describe('Audit', function () { } ]; - var out = results[0].nodes[0].node.source; + const out = results[0].nodes[0].node.source; results.forEach(function (res) { // attribute order is a pain in the lower back in IE, so we're not // comparing nodes. Check.run and Rule.run do this. @@ -638,8 +634,8 @@ describe('Audit', function () { ); }); - it('should not run rules disabled by the options', function (done) { - a.run( + it('should not run rules disabled by the options', done => { + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, { rules: { @@ -656,16 +652,16 @@ describe('Audit', function () { ); }); - it('should ensure audit.run recieves preload options', function (done) { + it('should ensure audit.run recieves preload options', done => { fixture.innerHTML = '<input aria-label="yo" type="text">'; - var audit = new Audit(); + audit = new Audit(); audit.addRule({ id: 'preload1', selector: '*' }); audit.run = function (context, options, resolve, reject) { - var randomRule = this.rules[0]; + const randomRule = this.rules[0]; randomRule.run( context, options, @@ -677,7 +673,7 @@ describe('Audit', function () { ); }; - var preloadOptions = { + const preloadOptions = { preload: { assets: ['cssom'] } @@ -693,7 +689,7 @@ describe('Audit', function () { assert.lengthOf(res, 1); assert.property(res[0], 'OPTIONS_PASSED'); - var optionsPassed = res[0].OPTIONS_PASSED; + const optionsPassed = res[0].OPTIONS_PASSED; assert.property(optionsPassed, 'preload'); assert.deepEqual(optionsPassed.preload, preloadOptions); @@ -706,7 +702,7 @@ describe('Audit', function () { ); }); - it.skip('should run rules (that do not need preload) and preload assets simultaneously', function (done) { + it.skip('should run rules (that do not need preload) and preload assets simultaneously', done => { /** * Note: * overriding and resolving both check and preload with a delay, @@ -715,11 +711,11 @@ describe('Audit', function () { fixture.innerHTML = '<div id="div1"></div><div id="div2"></div>'; - var runStartTime = new Date(); - var preloadInvokedTime = new Date(); - var noPreloadCheckedInvokedTime = new Date(); - var noPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; + const runStartTime = new Date(); + const preloadInvokedTime = new Date(); + const noPreloadCheckedInvokedTime = new Date(); + const noPreloadRuleCheckEvaluateInvoked = false; + const preloadOverrideInvoked = false; // override preload method axe.utils.preload = function (options) { @@ -727,13 +723,13 @@ describe('Audit', function () { preloadOverrideInvoked = true; return new Promise(function (res, rej) { - setTimeout(function () { + setTimeout(() => { res(true); }, 2000); }); }; - var audit = new Audit(); + audit = new Audit(); // add a rule and check that does not need preload audit.addRule({ id: 'no-preload', @@ -746,8 +742,8 @@ describe('Audit', function () { evaluate: function (node, options, vNode, context) { noPreloadCheckedInvokedTime = new Date(); noPreloadRuleCheckEvaluateInvoked = true; - var ready = this.async(); - setTimeout(function () { + const ready = this.async(); + setTimeout(() => { ready(true); }, 1000); } @@ -760,13 +756,13 @@ describe('Audit', function () { preload: true }); - var preloadOptions = { + const preloadOptions = { preload: { assets: ['cssom'] } }; - var allowedDiff = 50; + const allowedDiff = 50; audit.run( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, @@ -797,13 +793,13 @@ describe('Audit', function () { ); }); - it.skip('should pass assets from preload to rule check that needs assets as context', function (done) { + it.skip('should pass assets from preload to rule check that needs assets as context', done => { fixture.innerHTML = '<div id="div1"></div><div id="div2"></div>'; - var yesPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; + const yesPreloadRuleCheckEvaluateInvoked = false; + const preloadOverrideInvoked = false; - var preloadData = { + const preloadData = { data: 'you got it!' }; // override preload method @@ -814,7 +810,7 @@ describe('Audit', function () { }); }; - var audit = new Audit(); + audit = new Audit(); // add a rule and check that does not need preload audit.addRule({ id: 'no-preload', @@ -837,7 +833,7 @@ describe('Audit', function () { } }); - var preloadOptions = { + const preloadOptions = { preload: { assets: ['cssom'] } @@ -855,10 +851,10 @@ describe('Audit', function () { assert.isTrue(preloadOverrideInvoked); // assert preload data that was passed to check - var ruleResult = results.filter(function (r) { + const ruleResult = results.filter(function (r) { return (r.id = 'yes-preload' && r.nodes.length > 0); })[0]; - var checkResult = ruleResult.nodes[0].any[0]; + const checkResult = ruleResult.nodes[0].any[0]; assert.isDefined(checkResult.data); assert.property(checkResult.data, 'cssom'); assert.deepEqual(checkResult.data.cssom, preloadData); @@ -871,12 +867,12 @@ describe('Audit', function () { ); }); - it.skip('should continue to run rules and return result when preload is rejected', function (done) { + it.skip('should continue to run rules and return result when preload is rejected', done => { fixture.innerHTML = '<div id="div1"></div><div id="div2"></div>'; - var preloadOverrideInvoked = false; - var preloadNeededCheckInvoked = false; - var rejectionMsg = + const preloadOverrideInvoked = false; + const preloadNeededCheckInvoked = false; + const rejectionMsg = 'Boom! Things went terribly wrong! (But this was intended in this test)'; // override preload method @@ -885,7 +881,7 @@ describe('Audit', function () { return Promise.reject(rejectionMsg); }; - var audit = new Audit(); + audit = new Audit(); // add a rule and check that does not need preload audit.addRule({ id: 'no-preload', @@ -908,7 +904,7 @@ describe('Audit', function () { } }); - var preloadOptions = { + const preloadOptions = { preload: { assets: ['cssom'] } @@ -929,10 +925,10 @@ describe('Audit', function () { // assert that because preload failed // cssom was not populated on context of repective check assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function (r) { + const ruleResult = results.filter(function (r) { return (r.id = 'yes-preload' && r.nodes.length > 0); })[0]; - var checkResult = ruleResult.nodes[0].any[0]; + const checkResult = ruleResult.nodes[0].any[0]; assert.isDefined(checkResult.data); assert.notProperty(checkResult.data, 'cssom'); // done @@ -942,14 +938,14 @@ describe('Audit', function () { ); }); - it('should continue to run rules and return result when axios time(s)out and rejects preload', function (done) { + it('should continue to run rules and return result when axios time(s)out and rejects preload', done => { fixture.innerHTML = '<div id="div1"></div><div id="div2"></div>'; // there is no stubbing here, // the actual axios call is invoked, and timedout immediately as timeout is set to 0.1 - var preloadNeededCheckInvoked = false; - var audit = new Audit(); + let preloadNeededCheckInvoked = false; + audit = new Audit(); // add a rule and check that does not need preload audit.addRule({ id: 'no-preload', @@ -971,8 +967,9 @@ describe('Audit', function () { return true; } }); + axe.setup(); - var preloadOptions = { + const preloadOptions = { preload: { assets: ['cssom'], timeout: 0.1 @@ -991,10 +988,10 @@ describe('Audit', function () { // assert that because preload failed // cssom was not populated on context of repective check assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function (r) { + const ruleResult = results.filter(function (r) { return (r.id = 'yes-preload' && r.nodes.length > 0); })[0]; - var checkResult = ruleResult.nodes[0].any[0]; + const checkResult = ruleResult.nodes[0].any[0]; assert.isDefined(checkResult.data); assert.notProperty(checkResult.data, 'cssom'); // done @@ -1004,16 +1001,16 @@ describe('Audit', function () { ); }); - it.skip('should assign an empty array to axe._selectCache', function (done) { - var saved = axe.utils.ruleShouldRun; - axe.utils.ruleShouldRun = function () { + it.skip('should assign an empty array to axe._selectCache', done => { + const saved = axe.utils.ruleShouldRun; + axe.utils.ruleShouldRun = () => { assert.equal(axe._selectCache.length, 0); return false; }; - a.run( + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, {}, - function () { + () => { axe.utils.ruleShouldRun = saved; done(); }, @@ -1021,13 +1018,13 @@ describe('Audit', function () { ); }); - it('should clear axe._selectCache', function (done) { - a.run( + it('should clear axe._selectCache', done => { + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, { rules: {} }, - function () { + () => { assert.isTrue(typeof axe._selectCache === 'undefined'); done(); }, @@ -1035,10 +1032,10 @@ describe('Audit', function () { ); }); - it('should not run rules disabled by the configuration', function (done) { - var a = new Audit(); - var success = true; - a.rules.push( + it('should not run rules disabled by the configuration', done => { + audit = new Audit(); + const success = true; + audit.rules.push( new Rule({ id: 'positive1', selector: '*', @@ -1046,17 +1043,17 @@ describe('Audit', function () { any: [ { id: 'positive1-check1', - evaluate: function () { + evaluate: () => { success = false; } } ] }) ); - a.run( + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, {}, - function () { + () => { assert.ok(success); done(); }, @@ -1064,11 +1061,11 @@ describe('Audit', function () { ); }); - it("should call the rule's run function", function (done) { - var targetRule = mockRules[mockRules.length - 1], - rule = axe.utils.findBy(a.rules, 'id', targetRule.id), - called = false, - orig; + it("should call the rule's run function", done => { + const targetRule = mockRules[mockRules.length - 1]; + const rule = axe.utils.findBy(audit.rules, 'id', targetRule.id); + let called = false; + let orig; fixture.innerHTML = '<a href="#">link</a>'; orig = rule.run; @@ -1076,10 +1073,10 @@ describe('Audit', function () { called = true; callback({}); }; - a.run( + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, {}, - function () { + () => { assert.isTrue(called); rule.run = orig; done(); @@ -1088,12 +1085,12 @@ describe('Audit', function () { ); }); - it('should pass the option to the run function', function (done) { - var targetRule = mockRules[mockRules.length - 1], - rule = axe.utils.findBy(a.rules, 'id', targetRule.id), - passed = false, - orig, - options; + it('should pass the option to the run function', done => { + const targetRule = mockRules[mockRules.length - 1]; + const rule = axe.utils.findBy(audit.rules, 'id', targetRule.id); + let passed = false; + let orig; + let options; fixture.innerHTML = '<a href="#">link</a>'; orig = rule.run; @@ -1104,10 +1101,10 @@ describe('Audit', function () { }; options = { rules: {} }; (options.rules[targetRule.id] = {}).data = 'monkeys'; - a.run( + audit.run( { include: [axe.utils.getFlattenedTree()[0]] }, options, - function () { + () => { assert.ok(passed); rule.run = orig; done(); @@ -1116,14 +1113,14 @@ describe('Audit', function () { ); }); - it('should skip pageLevel rules if context is not set to entire page', function () { - var audit = new Audit(); + it('should skip pageLevel rules if context is not set to entire page', () => { + audit = new Audit(); audit.rules.push( new Rule({ pageLevel: true, enabled: true, - evaluate: function () { + evaluate: () => { assert.ok(false, 'Should not run'); } }) @@ -1142,9 +1139,9 @@ describe('Audit', function () { ); }); - it('catches errors and passes them as a cantTell result', function (done) { - var err = new Error('Launch the super sheep!'); - a.addRule({ + it('catches errors and passes them as a cantTell result', done => { + const err = new Error('Launch the super sheep!'); + audit.addRule({ id: 'throw1', selector: '*', any: [ @@ -1153,15 +1150,15 @@ describe('Audit', function () { } ] }); - a.addCheck({ + audit.addCheck({ id: 'throw1-check1', - evaluate: function () { + evaluate: () => { throw err; } }); axe._tree = axe.utils.getFlattenedTree(fixture); axe._selectorData = axe.utils.getSelectorData(axe._tree); - a.run( + audit.run( { include: [axe._tree[0]] }, { runOnly: { @@ -1181,8 +1178,8 @@ describe('Audit', function () { ); }); - it('should not halt if errors occur', function (done) { - a.addRule({ + it('should not halt if errors occur', done => { + audit.addRule({ id: 'throw1', selector: '*', any: [ @@ -1191,13 +1188,13 @@ describe('Audit', function () { } ] }); - a.addCheck({ + audit.addCheck({ id: 'throw1-check1', - evaluate: function () { + evaluate: () => { throw new Error('Launch the super sheep!'); } }); - a.run( + audit.run( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, { runOnly: { @@ -1205,26 +1202,26 @@ describe('Audit', function () { values: ['throw1', 'positive1'] } }, - function () { + () => { done(); }, isNotCalled ); }); - it('should run audit.normalizeOptions to ensure valid input', function () { + it('should run audit.normalizeOptions to ensure valid input', () => { fixture.innerHTML = '<input type="text" aria-label="monkeys">' + '<div id="monkeys">bananas</div>' + '<input aria-labelledby="monkeys" type="text">' + '<blink>FAIL ME</blink>'; - var checked = 'options not validated'; + let checked = 'options not validated'; - a.normalizeOptions = function () { + audit.normalizeOptions = () => { checked = 'options validated'; }; - a.run( + audit.run( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, noop, @@ -1233,8 +1230,8 @@ describe('Audit', function () { assert.equal(checked, 'options validated'); }); - it('should halt if an error occurs when debug is set', function (done) { - a.addRule({ + it('should halt if an error occurs when debug is set', done => { + audit.addRule({ id: 'throw1', selector: '*', any: [ @@ -1243,9 +1240,9 @@ describe('Audit', function () { } ] }); - a.addCheck({ + audit.addCheck({ id: 'throw1-check1', - evaluate: function () { + evaluate: () => { throw new Error('Launch the super sheep!'); } }); @@ -1253,7 +1250,7 @@ describe('Audit', function () { // check error node requires _selectorCache to be setup axe.setup(); - a.run( + audit.run( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, { debug: true, @@ -1269,15 +1266,30 @@ describe('Audit', function () { } ); }); + + it('propagates DqElement options', async () => { + fixtureSetup('<input id="input">'); + const results = await new Promise((resolve, reject) => { + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { elementRef: true, absolutePaths: true }, + resolve, + reject + ); + }); + const { node } = results[0].nodes[0]; + assert.equal(node.element, fixture.firstChild); + assert.equal(node.selector, 'html > body > #fixture > #input'); + }); }); - describe('Audit#after', function () { - it('should run Rule#after on any rule whose result is passed in', function () { + describe('Audit#after', () => { + it('should run Rule#after on any rule whose result is passed in', () => { /*eslint no-unused-vars:0*/ - var audit = new Audit(); - var success = false; - var options = [{ id: 'hehe', enabled: true, monkeys: 'bananas' }]; - var results = [ + audit = new Audit(); + let success = false; + const options = [{ id: 'hehe', enabled: true, monkeys: 'bananas' }]; + const results = [ { id: 'hehe', monkeys: 'bananas' @@ -1301,17 +1313,17 @@ describe('Audit', function () { }); }); - describe('Audit#normalizeOptions', function () { - var axeLog; - beforeEach(function () { + describe('Audit#normalizeOptions', () => { + let axeLog; + beforeEach(() => { axeLog = axe.log; }); - afterEach(function () { + afterEach(() => { axe.log = axeLog; }); - it('returns the options object when it is valid', function () { - var opt = { + it('returns the options object when it is valid', () => { + const opt = { runOnly: { type: 'rule', values: ['positive1', 'positive2'] @@ -1320,89 +1332,89 @@ describe('Audit', function () { negative1: { enabled: false } } }; - assert(a.normalizeOptions(opt), opt); + assert(audit.normalizeOptions(opt), opt); }); - it('allows `value` as alternative to `values`', function () { - var opt = { + it('allows `value` as alternative to `values`', () => { + const opt = { runOnly: { type: 'rule', value: ['positive1', 'positive2'] } }; - var out = a.normalizeOptions(opt); + const out = audit.normalizeOptions(opt); assert.deepEqual(out.runOnly.values, ['positive1', 'positive2']); assert.isUndefined(out.runOnly.value); }); - it('allows type: rules as an alternative to type: rule', function () { - var opt = { + it('allows type: rules as an alternative to type: rule', () => { + const opt = { runOnly: { type: 'rules', values: ['positive1', 'positive2'] } }; - assert(a.normalizeOptions(opt).runOnly.type, 'rule'); + assert(audit.normalizeOptions(opt).runOnly.type, 'rule'); }); - it('allows type: tags as an alternative to type: tag', function () { - var opt = { + it('allows type: tags as an alternative to type: tag', () => { + const opt = { runOnly: { type: 'tags', values: ['positive'] } }; - assert(a.normalizeOptions(opt).runOnly.type, 'tag'); + assert(audit.normalizeOptions(opt).runOnly.type, 'tag'); }); - it('allows type: undefined as an alternative to type: tag', function () { - var opt = { + it('allows type: undefined as an alternative to type: tag', () => { + const opt = { runOnly: { values: ['positive'] } }; - assert(a.normalizeOptions(opt).runOnly.type, 'tag'); + assert(audit.normalizeOptions(opt).runOnly.type, 'tag'); }); - it('allows runOnly as an array as an alternative to type: tag', function () { - var opt = { runOnly: ['positive', 'negative'] }; - var out = a.normalizeOptions(opt); + it('allows runOnly as an array as an alternative to type: tag', () => { + const opt = { runOnly: ['positive', 'negative'] }; + const out = audit.normalizeOptions(opt); assert(out.runOnly.type, 'tag'); assert.deepEqual(out.runOnly.values, ['positive', 'negative']); }); - it('allows runOnly as an array as an alternative to type: rule', function () { - var opt = { runOnly: ['positive1', 'negative1'] }; - var out = a.normalizeOptions(opt); + it('allows runOnly as an array as an alternative to type: rule', () => { + const opt = { runOnly: ['positive1', 'negative1'] }; + const out = audit.normalizeOptions(opt); assert(out.runOnly.type, 'rule'); assert.deepEqual(out.runOnly.values, ['positive1', 'negative1']); }); - it('allows runOnly as a string as an alternative to an array', function () { - var opt = { runOnly: 'positive1' }; - var out = a.normalizeOptions(opt); + it('allows runOnly as a string as an alternative to an array', () => { + const opt = { runOnly: 'positive1' }; + const out = audit.normalizeOptions(opt); assert(out.runOnly.type, 'rule'); assert.deepEqual(out.runOnly.values, ['positive1']); }); - it('throws an error if runOnly contains both rules and tags', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error if runOnly contains both rules and tags', () => { + assert.throws(() => { + audit.normalizeOptions({ runOnly: ['positive', 'negative1'] }); }); }); - it('defaults runOnly to type: tag', function () { - var opt = { runOnly: ['fakeTag'] }; - var out = a.normalizeOptions(opt); + it('defaults runOnly to type: tag', () => { + const opt = { runOnly: ['fakeTag'] }; + const out = audit.normalizeOptions(opt); assert(out.runOnly.type, 'tag'); assert.deepEqual(out.runOnly.values, ['fakeTag']); }); - it('throws an error runOnly.values not an array', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error runOnly.values not an array', () => { + assert.throws(() => { + audit.normalizeOptions({ runOnly: { type: 'rule', values: { badProp: 'badValue' } @@ -1411,9 +1423,9 @@ describe('Audit', function () { }); }); - it('throws an error runOnly.values an empty', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error runOnly.values an empty', () => { + assert.throws(() => { + audit.normalizeOptions({ runOnly: { type: 'rule', values: [] @@ -1422,9 +1434,9 @@ describe('Audit', function () { }); }); - it('throws an error runOnly.type is unknown', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error runOnly.type is unknown', () => { + assert.throws(() => { + audit.normalizeOptions({ runOnly: { type: 'something-else', values: ['wcag2aa'] @@ -1433,9 +1445,9 @@ describe('Audit', function () { }); }); - it('throws an error when option.runOnly has an unknown rule', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error when option.runOnly has an unknown rule', () => { + assert.throws(() => { + audit.normalizeOptions({ runOnly: { type: 'rule', values: ['frakeRule'] @@ -1444,9 +1456,9 @@ describe('Audit', function () { }); }); - it("doesn't throw an error when option.runOnly has an unknown tag", function () { - assert.doesNotThrow(function () { - a.normalizeOptions({ + it("doesn't throw an error when option.runOnly has an unknown tag", () => { + assert.doesNotThrow(() => { + audit.normalizeOptions({ runOnly: { type: 'tags', values: ['fakeTag'] @@ -1455,9 +1467,9 @@ describe('Audit', function () { }); }); - it('throws an error when option.rules has an unknown rule', function () { - assert.throws(function () { - a.normalizeOptions({ + it('throws an error when option.rules has an unknown rule', () => { + assert.throws(() => { + audit.normalizeOptions({ rules: { fakeRule: { enabled: false } } @@ -1465,12 +1477,12 @@ describe('Audit', function () { }); }); - it('logs an issue when a tag is unknown', function () { - var message = ''; + it('logs an issue when a tag is unknown', () => { + let message = ''; axe.log = function (m) { message = m; }; - a.normalizeOptions({ + audit.normalizeOptions({ runOnly: { type: 'tags', values: ['unknwon-tag'] @@ -1479,12 +1491,12 @@ describe('Audit', function () { assert.include(message, 'Could not find tags'); }); - it('logs no issues for unknown WCAG level tags', function () { - var message = ''; + it('logs no issues for unknown WCAG level tags', () => { + let message = ''; axe.log = function (m) { message = m; }; - a.normalizeOptions({ + audit.normalizeOptions({ runOnly: { type: 'tags', values: ['wcag23aaa'] @@ -1493,12 +1505,12 @@ describe('Audit', function () { assert.isEmpty(message); }); - it('logs an issue when a tag is unknown, together with a wcag level tag', function () { - var message = ''; + it('logs an issue when a tag is unknown, together with a wcag level tag', () => { + let message = ''; axe.log = function (m) { message = m; }; - a.normalizeOptions({ + audit.normalizeOptions({ runOnly: { type: 'tags', values: ['wcag23aaa', 'unknwon-tag'] diff --git a/test/core/base/check.js b/test/core/base/check.js index 82a7ebe6b4..98c169745a 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -1,114 +1,112 @@ -describe('Check', function () { - 'use strict'; - - var Check = axe._thisWillBeDeletedDoNotUse.base.Check; - var CheckResult = axe._thisWillBeDeletedDoNotUse.base.CheckResult; - var metadataFunctionMap = +describe('Check', () => { + const Check = axe._thisWillBeDeletedDoNotUse.base.Check; + const CheckResult = axe._thisWillBeDeletedDoNotUse.base.CheckResult; + const metadataFunctionMap = axe._thisWillBeDeletedDoNotUse.base.metadataFunctionMap; - var noop = function () {}; + const noop = () => {}; - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); afterEach(function () { fixture.innerHTML = ''; }); - it('should be a function', function () { + it('should be a function', () => { assert.isFunction(Check); }); - describe('prototype', function () { - describe('enabled', function () { - it('should be true by default', function () { - var check = new Check({}); + describe('prototype', () => { + describe('enabled', () => { + it('should be true by default', () => { + const check = new Check({}); assert.isTrue(check.enabled); }); - it('should be set to whatever is passed in', function () { - var check = new Check({ enabled: false }); + it('should be set to whatever is passed in', () => { + const check = new Check({ enabled: false }); assert.isFalse(check.enabled); }); }); - describe('configure', function () { - it('should accept one parameter', function () { + describe('configure', () => { + it('should accept one parameter', () => { assert.lengthOf(new Check({}).configure, 1); }); - it('should override options', function () { + it('should override options', () => { Check.prototype.test = function () { return this.options; }; - var check = new Check({ + const check = new Check({ options: ['foo'] }); check.configure({ options: { value: 'fong' } }); assert.deepEqual({ value: 'fong' }, check.test()); delete Check.prototype.test; }); - it('should override evaluate', function () { + it('should override evaluate', () => { Check.prototype.test = function () { return this.evaluate(); }; - var check = new Check({ + const check = new Check({ evaluate: 'function () { return "foo"; }' }); check.configure({ evaluate: 'function () { return "fong"; }' }); assert.equal('fong', check.test()); delete Check.prototype.test; }); - it('should override after', function () { + it('should override after', () => { Check.prototype.test = function () { return this.after(); }; - var check = new Check({ + const check = new Check({ after: 'function () { return "foo"; }' }); check.configure({ after: 'function () { return "fong"; }' }); assert.equal('fong', check.test()); delete Check.prototype.test; }); - it('should override evaluate as a function', function () { + it('should override evaluate as a function', () => { Check.prototype.test = function () { return this.evaluate(); }; - var check = new Check({ - evaluate: function () { + const check = new Check({ + evaluate() { return 'foo'; } }); check.configure({ - evaluate: function () { + evaluate() { return 'fong'; } }); assert.equal('fong', check.test()); delete Check.prototype.test; }); - it('should override after as a function', function () { + it('should override after as a function', () => { Check.prototype.test = function () { return this.after(); }; - var check = new Check({ - after: function () { + const check = new Check({ + after() { return 'foo'; } }); check.configure({ - after: function () { + after() { return 'fong'; } }); assert.equal('fong', check.test()); delete Check.prototype.test; }); - it('should override evaluate as ID', function () { - metadataFunctionMap['custom-evaluate'] = function () { + it('should override evaluate as ID', () => { + metadataFunctionMap['custom-evaluate'] = () => { return 'fong'; }; Check.prototype.test = function () { return this.evaluate(); }; - var check = new Check({ + const check = new Check({ evaluate: 'function () { return "foo"; }' }); check.configure({ evaluate: 'custom-evaluate' }); @@ -116,15 +114,15 @@ describe('Check', function () { delete Check.prototype.test; delete metadataFunctionMap['custom-evaluate']; }); - it('should override after as ID', function () { - metadataFunctionMap['custom-after'] = function () { + it('should override after as ID', () => { + metadataFunctionMap['custom-after'] = () => { return 'fong'; }; Check.prototype.test = function () { return this.after(); }; - var check = new Check({ + const check = new Check({ after: 'function () { return "foo"; }' }); check.configure({ after: 'custom-after' }); @@ -132,9 +130,9 @@ describe('Check', function () { delete Check.prototype.test; delete metadataFunctionMap['custom-after']; }); - it('should error if evaluate does not match an ID', function () { + it('should error if evaluate does not match an ID', () => { function fn() { - var check = new Check({}); + const check = new Check({}); check.configure({ evaluate: 'does-not-exist' }); } @@ -143,9 +141,9 @@ describe('Check', function () { 'Function ID does not exist in the metadata-function-map: does-not-exist' ); }); - it('should error if after does not match an ID', function () { + it('should error if after does not match an ID', () => { function fn() { - var check = new Check({}); + const check = new Check({}); check.configure({ after: 'does-not-exist' }); } @@ -154,79 +152,79 @@ describe('Check', function () { 'Function ID does not exist in the metadata-function-map: does-not-exist' ); }); - it('should override enabled', function () { + it('should override enabled', () => { Check.prototype.test = function () { return this.enabled; }; - var check = new Check({ + const check = new Check({ enabled: true }); check.configure({ enabled: false }); assert.equal(false, check.test()); delete Check.prototype.test; }); - it('should NOT override id', function () { + it('should NOT override id', () => { Check.prototype.test = function () { return this.id; }; - var check = new Check({ + const check = new Check({ id: 'fong' }); check.configure({ id: 'foo' }); assert.equal('fong', check.test()); delete Check.prototype.test; }); - it('should NOT override any random property', function () { + it('should NOT override any random property', () => { Check.prototype.test = function () { return this.random; }; - var check = new Check({}); + const check = new Check({}); check.configure({ random: 'foo' }); assert.equal(undefined, check.test()); delete Check.prototype.test; }); }); - describe('run', function () { - it('should accept 5 parameters', function () { + describe('run', () => { + it('should accept 5 parameters', () => { assert.lengthOf(new Check({}).run, 5); }); - it('should pass the node through', function (done) { + it('should pass the node through', done => { new Check({ - evaluate: function (node) { + evaluate(node) { assert.equal(node, fixture); done(); } }).run(axe.utils.getFlattenedTree(fixture)[0], {}, {}, noop); }); - it('should pass the options through', function (done) { - var expected = { monkeys: 'bananas' }; + it('should pass the options through', done => { + const expected = { monkeys: 'bananas' }; new Check({ options: expected, - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, expected); done(); } }).run(fixture, {}, {}, noop); }); - it('should pass the options through modified by the ones passed into the call', function (done) { - var configured = { monkeys: 'bananas' }, + it('should pass the options through modified by the ones passed into the call', done => { + const configured = { monkeys: 'bananas' }, expected = { monkeys: 'bananas', dogs: 'cats' }; new Check({ options: configured, - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, expected); done(); } }).run(fixture, { options: expected }, {}, noop); }); - it('should normalize non-object options for internal checks', function (done) { + it('should normalize non-object options for internal checks', done => { metadataFunctionMap['custom-check'] = function (node, options) { assert.deepEqual(options, { value: 'foo' }); done(); @@ -237,24 +235,24 @@ describe('Check', function () { delete metadataFunctionMap['custom-check']; }); - it('should not normalize non-object options for external checks', function (done) { + it('should not normalize non-object options for external checks', done => { new Check({ - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, 'foo'); done(); } }).run(fixture, { options: 'foo' }, {}, noop); }); - it('should pass the context through to check evaluate call', function (done) { - var configured = { + it('should pass the context through to check evaluate call', done => { + const configured = { cssom: 'yay', source: 'this is page source', aom: undefined }; new Check({ options: configured, - evaluate: function (node, options, virtualNode, context) { + evaluate(node, options, virtualNode, context) { assert.property(context, 'cssom'); assert.deepEqual(context, configured); done(); @@ -262,33 +260,33 @@ describe('Check', function () { }).run(fixture, {}, configured, noop); }); - it('should pass the virtual node through', function (done) { - var tree = axe.utils.getFlattenedTree(fixture); + it('should pass the virtual node through', done => { + const tree = axe.utils.getFlattenedTree(fixture); new Check({ - evaluate: function (node, options, virtualNode) { + evaluate(node, options, virtualNode) { assert.equal(virtualNode, tree[0]); done(); } }).run(tree[0]); }); - it.skip('should bind context to `bindCheckResult`', function (done) { - var orig = axe.utils.checkHelper, - cb = function () { + it.skip('should bind context to `bindCheckResult`', done => { + const orig = axe.utils.checkHelper, + cb = () => { return true; }, options = {}, context = {}, result = { monkeys: 'bananas' }; - axe.utils.checkHelper = function (checkResult, options, callback) { + axe.utils.checkHelper = function (checkResult, opts, callback) { assert.instanceOf(checkResult, window.CheckResult); assert.equal(callback, cb); return result; }; new Check({ - evaluate: function () { + evaluate() { assert.deepEqual(result, this); axe.utils.checkHelper = orig; done(); @@ -296,11 +294,11 @@ describe('Check', function () { }).run(fixture, options, context, cb); }); - it('should allow for asynchronous checks', function (done) { - var data = { monkeys: 'bananas' }; + it('should allow for asynchronous checks', done => { + const data = { monkeys: 'bananas' }; new Check({ - evaluate: function () { - var ready = this.async(); + evaluate() { + const ready = this.async(); setTimeout(function () { ready(data); }, 10); @@ -312,9 +310,9 @@ describe('Check', function () { }); }); - it('should pass `null` as the parameter if not enabled', function (done) { + it('should pass `null` as the parameter if not enabled', done => { new Check({ - evaluate: function () {}, + evaluate() {}, enabled: false }).run(fixture, {}, {}, function (data) { assert.isNull(data); @@ -322,9 +320,9 @@ describe('Check', function () { }); }); - it('should pass `null` as the parameter if options disable', function (done) { + it('should pass `null` as the parameter if options disable', done => { new Check({ - evaluate: function () {} + evaluate() {} }).run( fixture, { @@ -338,9 +336,9 @@ describe('Check', function () { ); }); - it('passes a result to the resolve argument', function (done) { + it('passes a result to the resolve argument', done => { new Check({ - evaluate: function () { + evaluate() { return true; } }).run(fixture, {}, {}, function (data) { @@ -350,9 +348,9 @@ describe('Check', function () { }); }); - it('should pass errors to the reject argument', function (done) { + it('should pass errors to the reject argument', done => { new Check({ - evaluate: function () { + evaluate() { throw new Error('Grenade!'); } }).run(fixture, {}, {}, noop, function (err) { @@ -363,43 +361,43 @@ describe('Check', function () { }); }); - describe('runSync', function () { - it('should accept 3 parameters', function () { + describe('runSync', () => { + it('should accept 3 parameters', () => { assert.lengthOf(new Check({}).runSync, 3); }); - it('should pass the node through', function () { + it('should pass the node through', () => { new Check({ - evaluate: function (node) { + evaluate(node) { assert.equal(node, fixture); } }).runSync(axe.utils.getFlattenedTree(fixture)[0], {}, {}); }); - it('should pass the options through', function () { - var expected = { monkeys: 'bananas' }; + it('should pass the options through', () => { + const expected = { monkeys: 'bananas' }; new Check({ options: expected, - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, expected); } }).runSync(fixture, {}, {}); }); - it('should pass the options through modified by the ones passed into the call', function () { - var configured = { monkeys: 'bananas' }, + it('should pass the options through modified by the ones passed into the call', () => { + const configured = { monkeys: 'bananas' }, expected = { monkeys: 'bananas', dogs: 'cats' }; new Check({ options: configured, - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, expected); } }).runSync(fixture, { options: expected }, {}); }); - it('should normalize non-object options for internal checks', function (done) { + it('should normalize non-object options for internal checks', done => { metadataFunctionMap['custom-check'] = function (node, options) { assert.deepEqual(options, { value: 'foo' }); done(); @@ -410,46 +408,46 @@ describe('Check', function () { delete metadataFunctionMap['custom-check']; }); - it('should not normalize non-object options for external checks', function (done) { + it('should not normalize non-object options for external checks', done => { new Check({ - evaluate: function (node, options) { + evaluate(node, options) { assert.deepEqual(options, 'foo'); done(); } }).runSync(fixture, { options: 'foo' }, {}); }); - it('should pass the context through to check evaluate call', function () { - var configured = { + it('should pass the context through to check evaluate call', () => { + const configured = { cssom: 'yay', source: 'this is page source', aom: undefined }; new Check({ options: configured, - evaluate: function (node, options, virtualNode, context) { + evaluate(node, options, virtualNode, context) { assert.property(context, 'cssom'); assert.deepEqual(context, configured); } }).runSync(fixture, {}, configured); }); - it('should pass the virtual node through', function () { - var tree = axe.utils.getFlattenedTree(fixture); + it('should pass the virtual node through', () => { + const tree = axe.utils.getFlattenedTree(fixture); new Check({ - evaluate: function (node, options, virtualNode) { + evaluate(node, options, virtualNode) { assert.equal(virtualNode, tree[0]); } }).runSync(tree[0]); }); - it('should throw error for asynchronous checks', function () { - var data = { monkeys: 'bananas' }; + it('should throw error for asynchronous checks', () => { + const data = { monkeys: 'bananas' }; try { new Check({ - evaluate: function () { - var ready = this.async(); + evaluate() { + const ready = this.async(); setTimeout(function () { ready(data); }, 10); @@ -464,18 +462,18 @@ describe('Check', function () { } }); - it('should pass `null` as the parameter if not enabled', function () { - var data = new Check({ - evaluate: function () {}, + it('should pass `null` as the parameter if not enabled', () => { + const data = new Check({ + evaluate() {}, enabled: false }).runSync(fixture, {}, {}); assert.isNull(data); }); - it('should pass `null` as the parameter if options disable', function () { - var data = new Check({ - evaluate: function () {} + it('should pass `null` as the parameter if options disable', () => { + const data = new Check({ + evaluate() {} }).runSync( fixture, { @@ -486,9 +484,9 @@ describe('Check', function () { assert.isNull(data); }); - it('passes a result to the resolve argument', function () { - var data = new Check({ - evaluate: function () { + it('passes a result to the resolve argument', () => { + const data = new Check({ + evaluate() { return true; } }).runSync(fixture, {}, {}); @@ -496,10 +494,10 @@ describe('Check', function () { assert.isTrue(data.result); }); - it('should throw errors', function () { + it('should throw errors', () => { try { new Check({ - evaluate: function () { + evaluate() { throw new Error('Grenade!'); } }).runSync(fixture, {}, {}); @@ -510,8 +508,8 @@ describe('Check', function () { }); }); - describe('getOptions', function () { - var check; + describe('getOptions', () => { + let check; beforeEach(function () { check = new Check({ options: { @@ -520,57 +518,57 @@ describe('Check', function () { }); }); - it('should return default check options', function () { + it('should return default check options', () => { assert.deepEqual(check.getOptions(), { foo: 'bar' }); }); - it('should merge options with Check defaults', function () { - var options = check.getOptions({ hello: 'world' }); + it('should merge options with Check defaults', () => { + const options = check.getOptions({ hello: 'world' }); assert.deepEqual(options, { foo: 'bar', hello: 'world' }); }); - it('should override defaults', function () { - var options = check.getOptions({ foo: 'world' }); + it('should override defaults', () => { + const options = check.getOptions({ foo: 'world' }); assert.deepEqual(options, { foo: 'world' }); }); - it('should normalize passed in options', function () { - var options = check.getOptions('world'); + it('should normalize passed in options', () => { + const options = check.getOptions('world'); assert.deepEqual(options, { foo: 'bar', value: 'world' }); }); }); }); - describe('spec object', function () { - describe('.id', function () { - it('should be set', function () { - var spec = { + describe('spec object', () => { + describe('.id', () => { + it('should be set', () => { + const spec = { id: 'monkeys' }; assert.equal(new Check(spec).id, spec.id); }); - it('should have no default', function () { - var spec = {}; + it('should have no default', () => { + const spec = {}; assert.equal(new Check(spec).id, spec.id); }); }); - describe('.after', function () { - it('should be set', function () { - var spec = { - after: function () {} + describe('.after', () => { + it('should be set', () => { + const spec = { + after() {} }; assert.equal(new Check(spec).after, spec.after); }); - it('should have no default', function () { - var spec = {}; + it('should have no default', () => { + const spec = {}; assert.equal(new Check(spec).after, spec.after); }); - it('should be able to take a string and turn it into a function', function () { - var spec = { + it('should be able to take a string and turn it into a function', () => { + const spec = { after: 'function () {return "blah";}' }; assert.equal(typeof new Check(spec).after, 'function'); @@ -578,45 +576,45 @@ describe('Check', function () { }); }); - describe('.options', function () { - it('should be set', function () { - var spec = { + describe('.options', () => { + it('should be set', () => { + const spec = { options: { value: ['monkeys', 'bananas'] } }; assert.equal(new Check(spec).options, spec.options); }); - it('should have no default', function () { - var spec = {}; + it('should have no default', () => { + const spec = {}; assert.equal(new Check(spec).options, spec.options); }); - it('should normalize non-object options for internal checks', function () { - var spec = { + it('should normalize non-object options for internal checks', () => { + const spec = { options: 'foo' }; assert.deepEqual(new Check(spec).options, { value: 'foo' }); }); - it('should not normalize non-object options for external checks', function () { - var spec = { + it('should not normalize non-object options for external checks', () => { + const spec = { options: 'foo', - evaluate: function () {} + evaluate() {} }; assert.deepEqual(new Check(spec).options, 'foo'); }); }); - describe('.evaluate', function () { - it('should be set', function () { - var spec = { - evaluate: function () {} + describe('.evaluate', () => { + it('should be set', () => { + const spec = { + evaluate() {} }; assert.equal(typeof new Check(spec).evaluate, 'function'); assert.equal(new Check(spec).evaluate, spec.evaluate); }); - it('should turn a string into a function', function () { - var spec = { + it('should turn a string into a function', () => { + const spec = { evaluate: 'function () { return "blah";}' }; assert.equal(typeof new Check(spec).evaluate, 'function'); diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 6be3e3b76b..421c06d76d 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -1,43 +1,42 @@ -describe('Rule', function () { - 'use strict'; - - var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; - var Check = axe._thisWillBeDeletedDoNotUse.base.Check; - var metadataFunctionMap = +describe('Rule', () => { + const Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; + const Check = axe._thisWillBeDeletedDoNotUse.base.Check; + const metadataFunctionMap = axe._thisWillBeDeletedDoNotUse.base.metadataFunctionMap; - var fixture = document.getElementById('fixture'); - var noop = function () {}; - var isNotCalled = function (err) { + const fixture = document.getElementById('fixture'); + const { fixtureSetup } = axe.testUtils; + const noop = () => {}; + const isNotCalled = function (err) { throw err || new Error('Reject should not be called'); }; - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('should be a function', function () { + it('should be a function', () => { assert.isFunction(Rule); }); - it('should accept two parameters', function () { + it('should accept two parameters', () => { assert.lengthOf(Rule, 2); }); - describe('prototype', function () { - describe('gather', function () { - it('should gather nodes which match the selector', function () { - var node = document.createElement('div'); + describe('prototype', () => { + describe('gather', () => { + it('should gather nodes which match the selector', () => { + const node = document.createElement('div'); node.id = 'monkeys'; fixture.appendChild(node); - var rule = new Rule({ - selector: '#monkeys' - }), - nodes = rule.gather({ - include: [axe.utils.getFlattenedTree(fixture)[0]], - exclude: [], - frames: [] - }); + const rule = new Rule({ + selector: '#monkeys' + }); + let nodes = rule.gather({ + include: [axe.utils.getFlattenedTree(fixture)[0]], + exclude: [], + frames: [] + }); assert.lengthOf(nodes, 1); assert.equal(nodes[0].actualNode, node); @@ -52,33 +51,33 @@ describe('Rule', function () { assert.lengthOf(nodes, 0); }); - it('should return a real array', function () { - var rule = new Rule({ - selector: 'div' - }), - result = rule.gather({ - include: [axe.utils.getFlattenedTree(fixture)[0]], - exclude: [], - frames: [] - }); + it('should return a real array', () => { + const rule = new Rule({ + selector: 'div' + }); + const result = rule.gather({ + include: [axe.utils.getFlattenedTree(fixture)[0]], + exclude: [], + frames: [] + }); assert.isArray(result); }); - it('should take a context parameter', function () { - var node = document.createElement('div'); + it('should take a context parameter', () => { + const node = document.createElement('div'); fixture.appendChild(node); - var rule = new Rule({ - selector: 'div' - }), - nodes = rule.gather({ - include: [ - axe.utils.getFlattenedTree( - document.getElementById('fixture').firstChild - )[0] - ] - }); + const rule = new Rule({ + selector: 'div' + }); + const nodes = rule.gather({ + include: [ + axe.utils.getFlattenedTree( + document.getElementById('fixture').firstChild + )[0] + ] + }); assert.deepEqual( nodes.map(function (n) { @@ -88,9 +87,9 @@ describe('Rule', function () { ); }); - it('should default to all nodes if selector is not specified', function () { - var nodes = [fixture], - node = document.createElement('div'); + it('should default to all nodes if selector is not specified', () => { + const nodes = [fixture]; + let node = document.createElement('div'); fixture.appendChild(node); nodes.push(node); @@ -100,7 +99,7 @@ describe('Rule', function () { fixture.appendChild(node); nodes.push(node); - var rule = new Rule({}), + const rule = new Rule({}), result = rule.gather({ include: [ axe.utils.getFlattenedTree(document.getElementById('fixture'))[0] @@ -115,11 +114,10 @@ describe('Rule', function () { nodes ); }); - it('should exclude hidden elements', function () { - fixture.innerHTML = - '<div style="display: none"><span>HEHEHE</span></div>'; + it('should exclude hidden elements', () => { + fixtureSetup('<div style="display: none"><span>HEHEHE</span></div>'); - var rule = new Rule({}), + const rule = new Rule({}), result = rule.gather({ include: [ axe.utils.getFlattenedTree( @@ -130,19 +128,19 @@ describe('Rule', function () { assert.lengthOf(result, 0); }); - it('should include hidden elements if excludeHidden is false', function () { - fixture.innerHTML = '<div style="display: none"></div>'; + it('should include hidden elements if excludeHidden is false', () => { + fixtureSetup('<div style="display: none"></div>'); - var rule = new Rule({ - excludeHidden: false - }), - result = rule.gather({ - include: [ - axe.utils.getFlattenedTree( - document.getElementById('fixture').firstChild - )[0] - ] - }); + const rule = new Rule({ + excludeHidden: false + }); + const result = rule.gather({ + include: [ + axe.utils.getFlattenedTree( + document.getElementById('fixture').firstChild + )[0] + ] + }); assert.deepEqual( result.map(function (n) { @@ -153,29 +151,29 @@ describe('Rule', function () { }); }); - describe('run', function () { - it('should be a function', function () { + describe('run', () => { + it('should be a function', () => { assert.isFunction(Rule.prototype.run); }); - it('should run #matches', function (done) { - var div = document.createElement('div'); + it('should run #matches', done => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, - rule = new Rule({ - matches: function (node) { - assert.equal(node, div); - success = true; - return []; - } - }); + let success = false; + const rule = new Rule({ + matches: function (node) { + assert.equal(node, div); + success = true; + return []; + } + }); rule.run( { include: [axe.utils.getFlattenedTree(div)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -183,10 +181,10 @@ describe('Rule', function () { ); }); - it('should pass a virtualNode to #matches', function (done) { - var div = document.createElement('div'); + it('should pass a virtualNode to #matches', done => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, + let success = false, rule = new Rule({ matches: function (node, virtualNode) { assert.equal(virtualNode.actualNode, div); @@ -200,7 +198,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(div)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -208,10 +206,10 @@ describe('Rule', function () { ); }); - it('should pass a context to #matches', function (done) { - var div = document.createElement('div'); + it('should pass a context to #matches', done => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, + let success = false, rule = new Rule({ matches: function (node, virtualNode, context) { assert.isDefined(context); @@ -227,7 +225,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(div)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -235,13 +233,13 @@ describe('Rule', function () { ); }); - it('should handle an error in #matches', function (done) { - var div = document.createElement('div'); + it('should handle an error in #matches', done => { + const div = document.createElement('div'); div.setAttribute('style', '#fff'); fixture.appendChild(div); - var success = false, + let success = false, rule = new Rule({ - matches: function () { + matches: () => { throw new Error('this is an error'); } }); @@ -252,17 +250,17 @@ describe('Rule', function () { }, {}, isNotCalled, - function () { + () => { assert.isFalse(success); done(); } ); }); - it('should execute Check#run on its child checks - any', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#run on its child checks - any', done => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { any: ['cats'] }, @@ -283,7 +281,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -291,10 +289,10 @@ describe('Rule', function () { ); }); - it('should execute Check#run on its child checks - all', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#run on its child checks - all', done => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { all: ['cats'] }, @@ -315,7 +313,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -323,10 +321,10 @@ describe('Rule', function () { ); }); - it('should execute Check#run on its child checks - none', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#run on its child checks - none', done => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { none: ['cats'] }, @@ -348,7 +346,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isTrue(success); done(); }, @@ -356,9 +354,9 @@ describe('Rule', function () { ); }); - it('should pass the matching option to check.run', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var options = { + it('should pass the matching option to check.run', done => { + fixtureSetup('<blink>Hi</blink>'); + const options = { checks: { cats: { enabled: 'bananas', @@ -366,7 +364,7 @@ describe('Rule', function () { } } }; - var rule = new Rule( + const rule = new Rule( { none: ['cats'] }, @@ -374,9 +372,9 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - run: function (node, options, context, resolve) { - assert.equal(options.enabled, 'bananas'); - assert.equal(options.options, 'minkeys'); + run: function (node, opts, context, resolve) { + assert.equal(opts.enabled, 'bananas'); + assert.equal(opts.options, 'minkeys'); resolve(true); } } @@ -388,16 +386,16 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(document)[0]] }, options, - function () { + () => { done(); }, isNotCalled ); }); - it('should pass the matching option to check.run defined on the rule over global', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var options = { + it('should pass the matching option to check.run defined on the rule over global', done => { + fixtureSetup('<blink>Hi</blink>'); + const options = { rules: { cats: { checks: { @@ -416,7 +414,7 @@ describe('Rule', function () { } }; - var rule = new Rule( + const rule = new Rule( { id: 'cats', any: [ @@ -429,9 +427,9 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - run: function (node, options, context, resolve) { - assert.equal(options.enabled, 'apples'); - assert.equal(options.options, 'apes'); + run: function (node, opts, context, resolve) { + assert.equal(opts.enabled, 'apples'); + assert.equal(opts.options, 'apes'); resolve(true); } } @@ -443,15 +441,15 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(document)[0]] }, options, - function () { + () => { done(); }, isNotCalled ); }); - it('should filter out null results', function () { - var rule = new Rule( + it('should filter out null results', () => { + const rule = new Rule( { selector: '#fixture', any: ['cats'] @@ -460,7 +458,7 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - run: function () {} + run: () => {} } } } @@ -477,25 +475,25 @@ describe('Rule', function () { ); }); - describe.skip('DqElement', function () { - var origDqElement; - var isDqElementCalled; + describe.skip('DqElement', () => { + let origDqElement; + let isDqElementCalled; - beforeEach(function () { + beforeEach(() => { isDqElementCalled = false; origDqElement = axe.utils.DqElement; - axe.utils.DqElement = function () { + axe.utils.DqElement = () => { isDqElementCalled = true; }; - fixture.innerHTML = '<blink>Hi</blink>'; + fixtureSetup('<blink>Hi</blink>'); }); - afterEach(function () { + afterEach(() => { axe.utils.DqElement = origDqElement; }); - it('is created for matching nodes', function (done) { - var rule = new Rule( + it('is created for matching nodes', done => { + const rule = new Rule( { all: ['cats'] }, @@ -504,10 +502,10 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: true, - evaluate: function () { + evaluate: () => { return true; }, - matches: function () { + matches: () => { return true; } }) @@ -519,7 +517,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isTrue(isDqElementCalled); done(); }, @@ -527,8 +525,8 @@ describe('Rule', function () { ); }); - it('is not created for disabled checks', function (done) { - var rule = new Rule( + it('is not created for disabled checks', done => { + const rule = new Rule( { all: ['cats'] }, @@ -537,8 +535,8 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: false, - evaluate: function () {}, - matches: function () { + evaluate: () => {}, + matches: () => { return true; } }) @@ -550,7 +548,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isFalse(isDqElementCalled); done(); }, @@ -558,8 +556,8 @@ describe('Rule', function () { ); }); - it('is created for matching nodes', function (done) { - var rule = new Rule( + it('is created for matching nodes', done => { + const rule = new Rule( { all: ['cats'] }, @@ -568,7 +566,7 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: true, - evaluate: function () { + evaluate: () => { return true; } }) @@ -580,7 +578,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isTrue(isDqElementCalled); done(); }, @@ -588,8 +586,8 @@ describe('Rule', function () { ); }); - it('is not created for disabled checks', function (done) { - var rule = new Rule( + it('is not created for disabled checks', done => { + const rule = new Rule( { all: ['cats'] }, @@ -598,7 +596,7 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: false, - evaluate: function () {} + evaluate: () => {} }) } } @@ -608,7 +606,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isFalse(isDqElementCalled); done(); }, @@ -617,16 +615,16 @@ describe('Rule', function () { }); }); - it('should pass thrown errors to the reject param', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var rule = new Rule( + it('should pass thrown errors to the reject param', done => { + fixtureSetup('<blink>Hi</blink>'); + const rule = new Rule( { none: ['cats'] }, { checks: { cats: { - run: function () { + run: () => { throw new Error('Holy hand grenade'); } } @@ -648,9 +646,9 @@ describe('Rule', function () { ); }); - it('should pass reject calls to the reject param', function (done) { - fixture.innerHTML = '<blink>Hi</blink>'; - var rule = new Rule( + it('should pass reject calls to the reject param', done => { + fixtureSetup('<blink>Hi</blink>'); + const rule = new Rule( { none: ['cats'] }, @@ -679,8 +677,9 @@ describe('Rule', function () { ); }); - it('should mark checks as incomplete if reviewOnFail is set to true', function (done) { - var rule = new Rule( + it('should mark checks as incomplete if reviewOnFail is set to true', done => { + axe.setup(); + const rule = new Rule( { reviewOnFail: true, all: ['cats'], @@ -691,13 +690,13 @@ describe('Rule', function () { checks: { cats: new Check({ id: 'cats', - evaluate: function () { + evaluate: () => { return false; } }), dogs: new Check({ id: 'dogs', - evaluate: function () { + evaluate: () => { return true; } }) @@ -720,21 +719,22 @@ describe('Rule', function () { ); }); - describe('NODE rule', function () { - it('should create a RuleResult', function () { - var orig = window.RuleResult; - var success = false; + describe('NODE rule', () => { + it('should create a RuleResult', () => { + axe.setup(); + const orig = window.RuleResult; + let success = false; window.RuleResult = function (r) { this.nodes = []; assert.equal(rule, r); success = true; }; - var rule = new Rule( + const rule = new Rule( { any: [ { - evaluate: function () {}, + evaluate: () => {}, id: 'cats' } ] @@ -762,14 +762,16 @@ describe('Rule', function () { window.RuleResult = orig; }); - it('should execute rule callback', function () { - var success = false; - var rule = new Rule( + it('should execute rule callback', () => { + axe.setup(); + let success = false; + + const rule = new Rule( { any: [ { - evaluate: function () {}, + evaluate: () => {}, id: 'cats' } ] @@ -790,7 +792,7 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(document)[0]] }, {}, - function () { + () => { success = true; }, isNotCalled @@ -800,22 +802,22 @@ describe('Rule', function () { }); }); - describe('runSync', function () { - it('should be a function', function () { + describe('runSync', () => { + it('should be a function', () => { assert.isFunction(Rule.prototype.runSync); }); - it('should run #matches', function () { - var div = document.createElement('div'); + it('should run #matches', () => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, - rule = new Rule({ - matches: function (node) { - assert.equal(node, div); - success = true; - return []; - } - }); + let success = false; + const rule = new Rule({ + matches: function (node) { + assert.equal(node, div); + success = true; + return []; + } + }); try { rule.runSync( @@ -830,17 +832,17 @@ describe('Rule', function () { } }); - it('should pass a virtualNode to #matches', function () { - var div = document.createElement('div'); + it('should pass a virtualNode to #matches', () => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, - rule = new Rule({ - matches: function (node, virtualNode) { - assert.equal(virtualNode.actualNode, div); - success = true; - return []; - } - }); + let success = false; + const rule = new Rule({ + matches: function (node, virtualNode) { + assert.equal(virtualNode.actualNode, div); + success = true; + return []; + } + }); try { rule.runSync( @@ -855,19 +857,19 @@ describe('Rule', function () { } }); - it('should pass a context to #matches', function () { - var div = document.createElement('div'); + it('should pass a context to #matches', () => { + const div = document.createElement('div'); fixture.appendChild(div); - var success = false, - rule = new Rule({ - matches: function (node, virtualNode, context) { - assert.isDefined(context); - assert.hasAnyKeys(context, ['cssom', 'include', 'exclude']); - assert.lengthOf(context.include, 1); - success = true; - return []; - } - }); + let success = false; + const rule = new Rule({ + matches: function (node, virtualNode, context) { + assert.isDefined(context); + assert.hasAnyKeys(context, ['cssom', 'include', 'exclude']); + assert.lengthOf(context.include, 1); + success = true; + return []; + } + }); try { rule.runSync( @@ -882,13 +884,13 @@ describe('Rule', function () { } }); - it('should handle an error in #matches', function () { - var div = document.createElement('div'); + it('should handle an error in #matches', () => { + const div = document.createElement('div'); div.setAttribute('style', '#fff'); fixture.appendChild(div); - var success = false; - var rule = new Rule({ - matches: function () { + let success = false; + const rule = new Rule({ + matches: () => { throw new Error('this is an error'); } }); @@ -906,17 +908,17 @@ describe('Rule', function () { } }); - it('should execute Check#runSync on its child checks - any', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#runSync on its child checks - any', () => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { any: ['cats'] }, { checks: { cats: { - runSync: function () { + runSync: () => { success = true; } } @@ -937,17 +939,17 @@ describe('Rule', function () { } }); - it('should execute Check#runSync on its child checks - all', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#runSync on its child checks - all', () => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { all: ['cats'] }, { checks: { cats: { - runSync: function () { + runSync: () => { success = true; } } @@ -968,17 +970,17 @@ describe('Rule', function () { } }); - it('should execute Check#run on its child checks - none', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var success = false; - var rule = new Rule( + it('should execute Check#run on its child checks - none', () => { + fixtureSetup('<blink>Hi</blink>'); + let success = false; + const rule = new Rule( { none: ['cats'] }, { checks: { cats: { - runSync: function () { + runSync: () => { success = true; } } @@ -1000,9 +1002,9 @@ describe('Rule', function () { } }); - it('should pass the matching option to check.runSync', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var options = { + it('should pass the matching option to check.runSync', () => { + fixtureSetup('<blink>Hi</blink>'); + const options = { checks: { cats: { enabled: 'bananas', @@ -1010,7 +1012,7 @@ describe('Rule', function () { } } }; - var rule = new Rule( + const rule = new Rule( { none: ['cats'] }, @@ -1018,9 +1020,9 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - runSync: function (node, options) { - assert.equal(options.enabled, 'bananas'); - assert.equal(options.options, 'minkeys'); + runSync: function (node, opts) { + assert.equal(opts.enabled, 'bananas'); + assert.equal(opts.options, 'minkeys'); } } } @@ -1039,9 +1041,9 @@ describe('Rule', function () { } }); - it('should pass the matching option to check.runSync defined on the rule over global', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var options = { + it('should pass the matching option to check.runSync defined on the rule over global', () => { + fixtureSetup('<blink>Hi</blink>'); + const options = { rules: { cats: { checks: { @@ -1060,7 +1062,7 @@ describe('Rule', function () { } }; - var rule = new Rule( + const rule = new Rule( { id: 'cats', any: [ @@ -1073,9 +1075,9 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - runSync: function (node, options) { - assert.equal(options.enabled, 'apples'); - assert.equal(options.options, 'apes'); + runSync: function (node, opts) { + assert.equal(opts.enabled, 'apples'); + assert.equal(opts.options, 'apes'); } } } @@ -1094,8 +1096,8 @@ describe('Rule', function () { } }); - it('should filter out null results', function () { - var rule = new Rule( + it('should filter out null results', () => { + const rule = new Rule( { selector: '#fixture', any: ['cats'] @@ -1104,14 +1106,14 @@ describe('Rule', function () { checks: { cats: { id: 'cats', - runSync: function () {} + runSync: () => {} } } } ); try { - var r = rule.runSync( + const r = rule.runSync( { include: [axe.utils.getFlattenedTree(document)[0]] }, @@ -1123,25 +1125,25 @@ describe('Rule', function () { } }); - describe.skip('DqElement', function () { - var origDqElement; - var isDqElementCalled; + describe.skip('DqElement', () => { + let origDqElement; + let isDqElementCalled; - beforeEach(function () { + beforeEach(() => { isDqElementCalled = false; origDqElement = axe.utils.DqElement; - axe.utils.DqElement = function () { + axe.utils.DqElement = () => { isDqElementCalled = true; }; - fixture.innerHTML = '<blink>Hi</blink>'; + fixtureSetup('<blink>Hi</blink>'); }); - afterEach(function () { + afterEach(() => { axe.utils.DqElement = origDqElement; }); - it('is created for matching nodes', function () { - var rule = new Rule( + it('is created for matching nodes', () => { + const rule = new Rule( { all: ['cats'] }, @@ -1150,10 +1152,10 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: true, - evaluate: function () { + evaluate: () => { return true; }, - matches: function () { + matches: () => { return true; } }) @@ -1174,8 +1176,8 @@ describe('Rule', function () { } }); - it('is not created for disabled checks', function () { - var rule = new Rule( + it('is not created for disabled checks', () => { + const rule = new Rule( { all: ['cats'] }, @@ -1184,8 +1186,8 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: false, - evaluate: function () {}, - matches: function () { + evaluate: () => {}, + matches: () => { return true; } }) @@ -1206,8 +1208,8 @@ describe('Rule', function () { } }); - it('is created for matching nodes', function () { - var rule = new Rule( + it('is created for matching nodes', () => { + const rule = new Rule( { all: ['cats'] }, @@ -1216,7 +1218,7 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: true, - evaluate: function () { + evaluate: () => { return true; } }) @@ -1237,8 +1239,8 @@ describe('Rule', function () { } }); - it('is not created for disabled checks', function () { - var rule = new Rule( + it('is not created for disabled checks', () => { + const rule = new Rule( { all: ['cats'] }, @@ -1247,7 +1249,7 @@ describe('Rule', function () { cats: new Check({ id: 'cats', enabled: false, - evaluate: function () {} + evaluate: () => {} }) } } @@ -1257,15 +1259,15 @@ describe('Rule', function () { include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, - function () { + () => { assert.isFalse(isDqElementCalled); }, isNotCalled ); }); - it('should not be called when there is no actualNode', function () { - var rule = new Rule( + it('should not be called when there is no actualNode', () => { + const rule = new Rule( { all: ['cats'] }, @@ -1273,13 +1275,13 @@ describe('Rule', function () { checks: { cats: new Check({ id: 'cats', - evaluate: function () {} + evaluate: () => {} }) } } ); rule.excludeHidden = false; // so we don't call utils.isHidden - var vNode = { + const vNode = { shadowId: undefined, children: [], parent: undefined, @@ -1295,7 +1297,7 @@ describe('Rule', function () { id: null, type: 'text' }, - hasClass: function () { + hasClass: () => { return false; }, attr: function (attrName) { @@ -1310,7 +1312,7 @@ describe('Rule', function () { include: [vNode] }, {}, - function () { + () => { assert.isFalse(isDqElementCalled); }, isNotCalled @@ -1318,16 +1320,16 @@ describe('Rule', function () { }); }); - it('should pass thrown errors to the reject param', function () { - fixture.innerHTML = '<blink>Hi</blink>'; - var rule = new Rule( + it('should pass thrown errors to the reject param', () => { + fixtureSetup('<blink>Hi</blink>'); + const rule = new Rule( { none: ['cats'] }, { checks: { cats: { - runSync: function () { + runSync: () => { throw new Error('Holy hand grenade'); } } @@ -1348,8 +1350,9 @@ describe('Rule', function () { } }); - it('should mark checks as incomplete if reviewOnFail is set to true', function () { - var rule = new Rule( + it('should mark checks as incomplete if reviewOnFail is set to true', () => { + axe.setup(); + const rule = new Rule( { reviewOnFail: true, all: ['cats'], @@ -1360,13 +1363,13 @@ describe('Rule', function () { checks: { cats: new Check({ id: 'cats', - evaluate: function () { + evaluate: () => { return false; } }), dogs: new Check({ id: 'dogs', - evaluate: function () { + evaluate: () => { return true; } }) @@ -1374,7 +1377,7 @@ describe('Rule', function () { } ); - var results = rule.runSync( + const results = rule.runSync( { include: [axe.utils.getFlattenedTree(fixture)[0]] }, @@ -1386,21 +1389,21 @@ describe('Rule', function () { assert.isUndefined(results.nodes[0].none[0].result); }); - describe.skip('NODE rule', function () { - it('should create a RuleResult', function () { - var orig = window.RuleResult; - var success = false; + describe.skip('NODE rule', () => { + it('should create a RuleResult', () => { + const orig = window.RuleResult; + let success = false; window.RuleResult = function (r) { this.nodes = []; assert.equal(rule, r); success = true; }; - var rule = new Rule( + const rule = new Rule( { any: [ { - evaluate: function () {}, + evaluate: () => {}, id: 'cats' } ] @@ -1408,7 +1411,7 @@ describe('Rule', function () { { checks: { cats: { - runSync: function () {} + runSync: () => {} } } } @@ -1429,14 +1432,14 @@ describe('Rule', function () { window.RuleResult = orig; }); - it('should execute rule callback', function () { - var success = false; + it('should execute rule callback', () => { + let success = false; - var rule = new Rule( + const rule = new Rule( { any: [ { - evaluate: function () {}, + evaluate: () => {}, id: 'cats' } ] @@ -1444,7 +1447,7 @@ describe('Rule', function () { { checks: { cats: { - runSync: function () { + runSync: () => { success = true; } } @@ -1468,11 +1471,11 @@ describe('Rule', function () { }); }); - describe('after', function () { - it('should execute Check#after with options', function () { - var success = false; + describe('after', () => { + it('should execute Check#after with options', () => { + let success = false; - var rule = new Rule( + const rule = new Rule( { id: 'cats', any: ['cats'] @@ -1483,11 +1486,7 @@ describe('Rule', function () { id: 'cats', enabled: true, after: function (results, options) { - assert.deepEqual(options, { - enabled: true, - options: { dogs: true }, - absolutePaths: undefined - }); + assert.deepEqual(options, { dogs: true }); success = true; return results; } @@ -1518,10 +1517,10 @@ describe('Rule', function () { assert.isTrue(success); }); - it('should add the check node to the check result', function () { - var success = false; + it('should add the check node to the check result', () => { + let success = false; - var rule = new Rule( + const rule = new Rule( { id: 'cats', any: ['cats'] @@ -1564,8 +1563,8 @@ describe('Rule', function () { assert.isTrue(success); }); - it('should filter removed checks', function () { - var rule = new Rule( + it('should filter removed checks', () => { + const rule = new Rule( { id: 'cats', any: ['cats'] @@ -1582,7 +1581,7 @@ describe('Rule', function () { } ); - var result = rule.after( + const result = rule.after( { id: 'cats', nodes: [ @@ -1616,10 +1615,10 @@ describe('Rule', function () { assert.isTrue(result.nodes[0].all[0].result); }); - it('should combine all checks for pageLevel rules', function () { - var rule = new Rule({}); + it('should combine all checks for pageLevel rules', () => { + const rule = new Rule({}); - var result = rule.after( + const result = rule.after( { id: 'cats', pageLevel: true, @@ -1663,9 +1662,10 @@ describe('Rule', function () { }); }); - describe('after', function () { - it('should mark checks as incomplete if reviewOnFail is set to true for all', function () { - var rule = new Rule( + describe('after', () => { + it('should mark checks as incomplete if reviewOnFail is set to true for all', () => { + axe.setup(); + const rule = new Rule( { id: 'cats', reviewOnFail: true, @@ -1687,7 +1687,7 @@ describe('Rule', function () { } ); - var result = rule.after( + const result = rule.after( { id: 'cats', nodes: [ @@ -1711,8 +1711,8 @@ describe('Rule', function () { assert.isUndefined(result.nodes[0].all[0].result); }); - it('should mark checks as incomplete if reviewOnFail is set to true for any', function () { - var rule = new Rule( + it('should mark checks as incomplete if reviewOnFail is set to true for any', () => { + const rule = new Rule( { id: 'cats', reviewOnFail: true, @@ -1734,7 +1734,7 @@ describe('Rule', function () { } ); - var result = rule.after( + const result = rule.after( { id: 'cats', nodes: [ @@ -1758,8 +1758,8 @@ describe('Rule', function () { assert.isEmpty(result.nodes[0].all); }); - it('should mark checks as incomplete if reviewOnFail is set to true for none', function () { - var rule = new Rule( + it('should mark checks as incomplete if reviewOnFail is set to true for none', () => { + const rule = new Rule( { id: 'cats', reviewOnFail: true, @@ -1781,7 +1781,7 @@ describe('Rule', function () { } ); - var result = rule.after( + const result = rule.after( { id: 'cats', nodes: [ @@ -1807,143 +1807,143 @@ describe('Rule', function () { }); }); - describe('spec object', function () { - describe('.selector', function () { - it('should be set', function () { - var spec = { + describe('spec object', () => { + describe('.selector', () => { + it('should be set', () => { + const spec = { selector: '#monkeys' }; assert.equal(new Rule(spec).selector, spec.selector); }); - it('should default to *', function () { - var spec = {}; + it('should default to *', () => { + const spec = {}; assert.equal(new Rule(spec).selector, '*'); }); }); - describe('.enabled', function () { - it('should be set', function () { - var spec = { + describe('.enabled', () => { + it('should be set', () => { + const spec = { enabled: false }; assert.equal(new Rule(spec).enabled, spec.enabled); }); - it('should default to true', function () { - var spec = {}; + it('should default to true', () => { + const spec = {}; assert.isTrue(new Rule(spec).enabled); }); - it('should default to true if given a bad value', function () { - var spec = { + it('should default to true if given a bad value', () => { + const spec = { enabled: 'monkeys' }; assert.isTrue(new Rule(spec).enabled); }); }); - describe('.excludeHidden', function () { - it('should be set', function () { - var spec = { + describe('.excludeHidden', () => { + it('should be set', () => { + const spec = { excludeHidden: false }; assert.equal(new Rule(spec).excludeHidden, spec.excludeHidden); }); - it('should default to true', function () { - var spec = {}; + it('should default to true', () => { + const spec = {}; assert.isTrue(new Rule(spec).excludeHidden); }); - it('should default to true if given a bad value', function () { - var spec = { + it('should default to true if given a bad value', () => { + const spec = { excludeHidden: 'monkeys' }; assert.isTrue(new Rule(spec).excludeHidden); }); }); - describe('.pageLevel', function () { - it('should be set', function () { - var spec = { + describe('.pageLevel', () => { + it('should be set', () => { + const spec = { pageLevel: false }; assert.equal(new Rule(spec).pageLevel, spec.pageLevel); }); - it('should default to false', function () { - var spec = {}; + it('should default to false', () => { + const spec = {}; assert.isFalse(new Rule(spec).pageLevel); }); - it('should default to false if given a bad value', function () { - var spec = { + it('should default to false if given a bad value', () => { + const spec = { pageLevel: 'monkeys' }; assert.isFalse(new Rule(spec).pageLevel); }); }); - describe('.reviewOnFail', function () { - it('should be set', function () { - var spec = { + describe('.reviewOnFail', () => { + it('should be set', () => { + const spec = { reviewOnFail: true }; assert.equal(new Rule(spec).reviewOnFail, spec.reviewOnFail); }); - it('should default to false', function () { - var spec = {}; + it('should default to false', () => { + const spec = {}; assert.isFalse(new Rule(spec).reviewOnFail); }); - it('should default to false if given a bad value', function () { - var spec = { + it('should default to false if given a bad value', () => { + const spec = { reviewOnFail: 'monkeys' }; assert.isFalse(new Rule(spec).reviewOnFail); }); }); - describe('.id', function () { - it('should be set', function () { - var spec = { + describe('.id', () => { + it('should be set', () => { + const spec = { id: 'monkeys' }; assert.equal(new Rule(spec).id, spec.id); }); - it('should have no default', function () { - var spec = {}; + it('should have no default', () => { + const spec = {}; assert.equal(new Rule(spec).id, spec.id); }); }); - describe('.impact', function () { - it('should be set', function () { - var spec = { + describe('.impact', () => { + it('should be set', () => { + const spec = { impact: 'critical' }; assert.equal(new Rule(spec).impact, spec.impact); }); - it('should have no default', function () { - var spec = {}; + it('should have no default', () => { + const spec = {}; assert.isUndefined(new Rule(spec).impact); }); - it('throws if impact is invalid', function () { - assert.throws(function () { + it('throws if impact is invalid', () => { + assert.throws(() => { // eslint-disable-next-line no-new new Rule({ impact: 'hello' }); }); }); }); - describe('.any', function () { - it('should be set', function () { - var spec = { + describe('.any', () => { + it('should be set', () => { + const spec = { any: [ { name: 'monkeys' @@ -1960,9 +1960,9 @@ describe('Rule', function () { }); }); - describe('.all', function () { - it('should be set', function () { - var spec = { + describe('.all', () => { + it('should be set', () => { + const spec = { all: [ { name: 'monkeys' @@ -1979,9 +1979,9 @@ describe('Rule', function () { }); }); - describe('.none', function () { - it('should be set', function () { - var spec = { + describe('.none', () => { + it('should be set', () => { + const spec = { none: [ { name: 'monkeys' @@ -1998,162 +1998,162 @@ describe('Rule', function () { }); }); - describe('.matches', function () { - it('should be set', function () { - var spec = { - matches: function () {} + describe('.matches', () => { + it('should be set', () => { + const spec = { + matches: () => {} }; assert.equal(new Rule(spec).matches, spec.matches); }); - it('should default to prototype', function () { - var spec = {}; + it('should default to prototype', () => { + const spec = {}; assert.equal(new Rule(spec).matches, Rule.prototype.matches); }); - it('should turn a string into a function', function () { - var spec = { + it('should turn a string into a function', () => { + const spec = { matches: 'function() {return "blah";}' }; assert.equal(new Rule(spec).matches(), 'blah'); }); }); - describe('.tags', function () { - it('should be set', function () { - var spec = { + describe('.tags', () => { + it('should be set', () => { + const spec = { tags: ['foo', 'bar'] }; assert.deepEqual(new Rule(spec).tags, spec.tags); }); - it('should default to empty array', function () { - var spec = {}; + it('should default to empty array', () => { + const spec = {}; assert.deepEqual(new Rule(spec).tags, []); }); }); - describe('.actIds', function () { - it('should be set', function () { - var spec = { + describe('.actIds', () => { + it('should be set', () => { + const spec = { actIds: ['abc123', 'xyz789'] }; assert.deepEqual(new Rule(spec).actIds, spec.actIds); }); - it('should default to undefined', function () { - var spec = {}; + it('should default to undefined', () => { + const spec = {}; assert.isUndefined(new Rule(spec).actIds); }); }); }); - describe('configure', function () { - beforeEach(function () { + describe('configure', () => { + beforeEach(() => { Rule.prototype._get = function (attr) { return this[attr]; }; }); - afterEach(function () { + afterEach(() => { delete Rule.prototype._get; }); - it('should be a function that takes one argument', function () { + it('should be a function that takes one argument', () => { assert.isFunction(Rule.prototype.configure); assert.lengthOf(new Rule({}).configure, 1); }); - it('should NOT override the id', function () { - var rule = new Rule({ id: 'foo' }); + it('should NOT override the id', () => { + const rule = new Rule({ id: 'foo' }); assert.equal(rule._get('id'), 'foo'); rule.configure({ id: 'fong' }); assert.equal(rule._get('id'), 'foo'); }); - it('should NOT override a random property', function () { - var rule = new Rule({ id: 'foo' }); + it('should NOT override a random property', () => { + const rule = new Rule({ id: 'foo' }); rule.configure({ fong: 'fong' }); assert.equal(rule._get('fong'), undefined); }); - it('should override the selector', function () { - var rule = new Rule({ selector: 'foo' }); + it('should override the selector', () => { + const rule = new Rule({ selector: 'foo' }); assert.equal(rule._get('selector'), 'foo'); rule.configure({ selector: 'fong' }); assert.equal(rule._get('selector'), 'fong'); }); - it('should override excludeHidden', function () { - var rule = new Rule({ excludeHidden: false }); + it('should override excludeHidden', () => { + const rule = new Rule({ excludeHidden: false }); assert.equal(rule._get('excludeHidden'), false); rule.configure({ excludeHidden: true }); assert.equal(rule._get('excludeHidden'), true); }); - it('should override enabled', function () { - var rule = new Rule({ enabled: false }); + it('should override enabled', () => { + const rule = new Rule({ enabled: false }); assert.equal(rule._get('enabled'), false); rule.configure({ enabled: true }); assert.equal(rule._get('enabled'), true); }); - it('should override pageLevel', function () { - var rule = new Rule({ pageLevel: false }); + it('should override pageLevel', () => { + const rule = new Rule({ pageLevel: false }); assert.equal(rule._get('pageLevel'), false); rule.configure({ pageLevel: true }); assert.equal(rule._get('pageLevel'), true); }); - it('should override reviewOnFail', function () { - var rule = new Rule({ reviewOnFail: false }); + it('should override reviewOnFail', () => { + const rule = new Rule({ reviewOnFail: false }); assert.equal(rule._get('reviewOnFail'), false); rule.configure({ reviewOnFail: true }); assert.equal(rule._get('reviewOnFail'), true); }); - it('should override any', function () { - var rule = new Rule({ any: ['one', 'two'] }); + it('should override any', () => { + const rule = new Rule({ any: ['one', 'two'] }); assert.deepEqual(rule._get('any'), ['one', 'two']); rule.configure({ any: [] }); assert.deepEqual(rule._get('any'), []); }); - it('should override all', function () { - var rule = new Rule({ all: ['one', 'two'] }); + it('should override all', () => { + const rule = new Rule({ all: ['one', 'two'] }); assert.deepEqual(rule._get('all'), ['one', 'two']); rule.configure({ all: [] }); assert.deepEqual(rule._get('all'), []); }); - it('should override none', function () { - var rule = new Rule({ none: ['none', 'two'] }); + it('should override none', () => { + const rule = new Rule({ none: ['none', 'two'] }); assert.deepEqual(rule._get('none'), ['none', 'two']); rule.configure({ none: [] }); assert.deepEqual(rule._get('none'), []); }); - it('should override tags', function () { - var rule = new Rule({ tags: ['tags', 'two'] }); + it('should override tags', () => { + const rule = new Rule({ tags: ['tags', 'two'] }); assert.deepEqual(rule._get('tags'), ['tags', 'two']); rule.configure({ tags: [] }); assert.deepEqual(rule._get('tags'), []); }); - it('should override matches (doT.js function)', function () { - var rule = new Rule({ matches: 'function () {return "matches";}' }); + it('should override matches (doT.js function)', () => { + const rule = new Rule({ matches: 'function () {return "matches";}' }); assert.equal(rule._get('matches')(), 'matches'); rule.configure({ matches: 'function () {return "does not match";}' }); assert.equal(rule._get('matches')(), 'does not match'); }); - it('should override matches (metadata function name)', function () { + it('should override matches (metadata function name)', () => { axe._load({}); - metadataFunctionMap['custom-matches'] = function () { + metadataFunctionMap['custom-matches'] = () => { return 'custom-matches'; }; - metadataFunctionMap['other-matches'] = function () { + metadataFunctionMap['other-matches'] = () => { return 'other-matches'; }; - var rule = new Rule({ matches: 'custom-matches' }); + const rule = new Rule({ matches: 'custom-matches' }); assert.equal(rule._get('matches')(), 'custom-matches'); rule.configure({ matches: 'other-matches' }); @@ -2162,9 +2162,9 @@ describe('Rule', function () { delete metadataFunctionMap['custom-matches']; delete metadataFunctionMap['other-matches']; }); - it('should error if matches does not match an ID', function () { + it('should error if matches does not match an ID', () => { function fn() { - var rule = new Rule({}); + const rule = new Rule({}); rule.configure({ matches: 'does-not-exist' }); } @@ -2173,17 +2173,17 @@ describe('Rule', function () { 'Function ID does not exist in the metadata-function-map: does-not-exist' ); }); - it('should override impact', function () { - var rule = new Rule({ impact: 'minor' }); + it('should override impact', () => { + const rule = new Rule({ impact: 'minor' }); assert.equal(rule._get('impact'), 'minor'); rule.configure({ impact: 'serious' }); assert.equal(rule._get('impact'), 'serious'); }); - it('should throw if impact impact', function () { - var rule = new Rule({ impact: 'minor' }); + it('should throw if impact impact', () => { + const rule = new Rule({ impact: 'minor' }); - assert.throws(function () { + assert.throws(() => { rule.configure({ impact: 'hello' }); }); }); diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index 73e6998320..771d3693b1 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -1,25 +1,23 @@ -describe('VirtualNode', function () { - 'use strict'; +describe('VirtualNode', () => { + const VirtualNode = axe.VirtualNode; + let node; - var VirtualNode = axe.VirtualNode; - var node; - - beforeEach(function () { + beforeEach(() => { node = document.createElement('div'); }); - it('should be a function', function () { + it('should be a function', () => { assert.isFunction(VirtualNode); }); - it('should accept three parameters', function () { + it('should accept three parameters', () => { assert.lengthOf(VirtualNode, 3); }); - describe('prototype', function () { - it('should have public properties', function () { - var parent = {}; - var vNode = new VirtualNode(node, parent, 'foo'); + describe('prototype', () => { + it('should have public properties', () => { + const parent = {}; + const vNode = new VirtualNode(node, parent, 'foo'); assert.equal(vNode.shadowId, 'foo'); assert.typeOf(vNode.children, 'array'); @@ -27,10 +25,10 @@ describe('VirtualNode', function () { assert.equal(vNode.parent, parent); }); - it('should abstract Node properties', function () { + it('should abstract Node properties', () => { node = document.createElement('input'); node.id = 'monkeys'; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.isDefined(vNode.props); assert.equal(vNode.props.nodeType, 1); @@ -39,9 +37,9 @@ describe('VirtualNode', function () { assert.equal(vNode.props.type, 'text'); }); - it('should reflect selected property', function () { + it('should reflect selected property', () => { node = document.createElement('option'); - var vNode = new VirtualNode(node); + let vNode = new VirtualNode(node); assert.equal(vNode.props.selected, false); node.selected = true; @@ -49,125 +47,125 @@ describe('VirtualNode', function () { assert.equal(vNode.props.selected, true); }); - it('should lowercase type', function () { - var node = document.createElement('input'); + it('should lowercase type', () => { + node = document.createElement('input'); node.setAttribute('type', 'COLOR'); - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.equal(vNode.props.type, 'color'); }); - it('should default type to text', function () { - var node = document.createElement('input'); - var vNode = new VirtualNode(node); + it('should default type to text', () => { + node = document.createElement('input'); + const vNode = new VirtualNode(node); assert.equal(vNode.props.type, 'text'); }); - it('should default type to text if type is invalid', function () { - var node = document.createElement('input'); + it('should default type to text if type is invalid', () => { + node = document.createElement('input'); node.setAttribute('type', 'woohoo'); - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.equal(vNode.props.type, 'text'); }); - it('should lowercase nodeName', function () { - var node = { + it('should lowercase nodeName', () => { + node = { nodeName: 'FOOBAR' }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.equal(vNode.props.nodeName, 'foobar'); }); - describe('attr', function () { - it('should return the value of the given attribute', function () { + describe('attr', () => { + it('should return the value of the given attribute', () => { node.setAttribute('data-foo', 'bar'); - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.equal(vNode.attr('data-foo'), 'bar'); }); - it('should return null for text nodes', function () { + it('should return null for text nodes', () => { node.textContent = 'hello'; - var vNode = new VirtualNode(node.firstChild); + const vNode = new VirtualNode(node.firstChild); assert.isNull(vNode.attr('data-foo')); }); - it('should return null if getAttribute is not a function', function () { - var node = { + it('should return null if getAttribute is not a function', () => { + node = { nodeName: 'DIV', getAttribute: null }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.isNull(vNode.attr('data-foo')); }); }); - describe('hasAttr', function () { - it('should return true if the element has the attribute', function () { + describe('hasAttr', () => { + it('should return true if the element has the attribute', () => { node.setAttribute('foo', 'bar'); - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.isTrue(vNode.hasAttr('foo')); }); - it('should return false if the element does not have the attribute', function () { - var vNode = new VirtualNode(node); + it('should return false if the element does not have the attribute', () => { + const vNode = new VirtualNode(node); assert.isFalse(vNode.hasAttr('foo')); }); - it('should return false for text nodes', function () { + it('should return false for text nodes', () => { node.textContent = 'hello'; - var vNode = new VirtualNode(node.firstChild); + const vNode = new VirtualNode(node.firstChild); assert.isFalse(vNode.hasAttr('foo')); }); - it('should return false if hasAttribute is not a function', function () { - var node = { + it('should return false if hasAttribute is not a function', () => { + node = { nodeName: 'DIV', hasAttribute: null }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.isFalse(vNode.hasAttr('foo')); }); }); - describe('attrNames', function () { - it('should return a list of attribute names', function () { + describe('attrNames', () => { + it('should return a list of attribute names', () => { node.setAttribute('foo', 'bar'); - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.deepEqual(vNode.attrNames, ['foo']); }); - it('should work with clobbered attributes', function () { - var node = document.createElement('form'); + it('should work with clobbered attributes', () => { + node = document.createElement('form'); node.setAttribute('id', '123'); node.innerHTML = '<select name="attributes"></select>'; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.deepEqual(vNode.attrNames, ['id']); }); - it('should return an empty array if there are no attributes', function () { - var vNode = new VirtualNode(node); + it('should return an empty array if there are no attributes', () => { + const vNode = new VirtualNode(node); assert.deepEqual(vNode.attrNames, []); }); }); - describe('nodeIndex', function () { - it('increments nodeIndex when a parent is passed', function () { - var vHtml = new VirtualNode({ nodeName: 'html' }); - var vHead = new VirtualNode({ nodeName: 'head' }, vHtml); - var vTitle = new VirtualNode({ nodeName: 'title' }, vHead); - var vBody = new VirtualNode({ nodeName: 'body' }, vHtml); + describe('nodeIndex', () => { + it('increments nodeIndex when a parent is passed', () => { + const vHtml = new VirtualNode({ nodeName: 'html' }); + const vHead = new VirtualNode({ nodeName: 'head' }, vHtml); + const vTitle = new VirtualNode({ nodeName: 'title' }, vHead); + const vBody = new VirtualNode({ nodeName: 'body' }, vHtml); assert.equal(vHtml.nodeIndex, 0); assert.equal(vHead.nodeIndex, 1); @@ -175,9 +173,9 @@ describe('VirtualNode', function () { assert.equal(vBody.nodeIndex, 3); }); - it('resets nodeIndex when no parent is passed', function () { - var vHtml = new VirtualNode({ nodeName: 'html' }); - var vHead = new VirtualNode({ nodeName: 'head' }, vHtml); + it('resets nodeIndex when no parent is passed', () => { + let vHtml = new VirtualNode({ nodeName: 'html' }); + let vHead = new VirtualNode({ nodeName: 'head' }, vHtml); assert.equal(vHtml.nodeIndex, 0); assert.equal(vHead.nodeIndex, 1); @@ -189,12 +187,12 @@ describe('VirtualNode', function () { }); describe('checkbox properties', () => { - it('should reflect the checked property', function () { + it('should reflect the checked property', () => { const div = document.createElement('div'); const vDiv = new VirtualNode(div); assert.isUndefined(vDiv.props.checked); - const node = document.createElement('input'); + node = document.createElement('input'); node.setAttribute('type', 'checkbox'); const vUnchecked = new VirtualNode(node); assert.isFalse(vUnchecked.props.checked); @@ -209,7 +207,7 @@ describe('VirtualNode', function () { const vDiv = new VirtualNode(div); assert.isUndefined(vDiv.props.indeterminate); - const node = document.createElement('input'); + node = document.createElement('input'); node.setAttribute('type', 'checkbox'); const vUnchecked = new VirtualNode(node); assert.isFalse(vUnchecked.props.indeterminate); @@ -220,42 +218,42 @@ describe('VirtualNode', function () { }); }); - describe.skip('isFocusable', function () { - var commons; + describe.skip('isFocusable', () => { + let commons; - beforeEach(function () { + beforeEach(() => { commons = axe.commons = axe.commons; }); - afterEach(function () { + afterEach(() => { axe.commons = commons; }); - it('should call dom.isFocusable', function () { - var called = false; + it('should call dom.isFocusable', () => { + let called = false; axe.commons = { dom: { - isFocusable: function () { + isFocusable: () => { called = true; } } }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.isFocusable; assert.isTrue(called); }); - it('should only call dom.isFocusable once', function () { - var count = 0; + it('should only call dom.isFocusable once', () => { + let count = 0; axe.commons = { dom: { - isFocusable: function () { + isFocusable: () => { count++; } } }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.isFocusable; vNode.isFocusable; vNode.isFocusable; @@ -263,42 +261,42 @@ describe('VirtualNode', function () { }); }); - describe.skip('tabbableElements', function () { - var commons; + describe.skip('tabbableElements', () => { + let commons; - beforeEach(function () { + beforeEach(() => { commons = axe.commons = axe.commons; }); - afterEach(function () { + afterEach(() => { axe.commons = commons; }); - it('should call dom.getTabbableElements', function () { - var called = false; + it('should call dom.getTabbableElements', () => { + let called = false; axe.commons = { dom: { - getTabbableElements: function () { + getTabbableElements: () => { called = true; } } }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.tabbableElements; assert.isTrue(called); }); - it('should only call dom.getTabbableElements once', function () { - var count = 0; + it('should only call dom.getTabbableElements once', () => { + let count = 0; axe.commons = { dom: { - getTabbableElements: function () { + getTabbableElements: () => { count++; } } }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.tabbableElements; vNode.tabbableElements; vNode.tabbableElements; @@ -306,46 +304,46 @@ describe('VirtualNode', function () { }); }); - describe('getComputedStylePropertyValue', function () { - var computedStyle; + describe('getComputedStylePropertyValue', () => { + let computedStyle; - beforeEach(function () { + beforeEach(() => { computedStyle = window.getComputedStyle; }); - afterEach(function () { + afterEach(() => { window.getComputedStyle = computedStyle; }); - it('should call window.getComputedStyle and return the property', function () { - var called = false; - window.getComputedStyle = function () { + it('should call window.getComputedStyle and return the property', () => { + let called = false; + window.getComputedStyle = () => { called = true; return { - getPropertyValue: function () { + getPropertyValue: () => { return 'result'; } }; }; - var vNode = new VirtualNode(node); - var result = vNode.getComputedStylePropertyValue('prop'); + const vNode = new VirtualNode(node); + const result = vNode.getComputedStylePropertyValue('prop'); assert.isTrue(called); assert.equal(result, 'result'); }); - it('should only call window.getComputedStyle and getPropertyValue once', function () { - var computedCount = 0; - var propertyCount = 0; - window.getComputedStyle = function () { + it('should only call window.getComputedStyle and getPropertyValue once', () => { + let computedCount = 0; + let propertyCount = 0; + window.getComputedStyle = () => { computedCount++; return { - getPropertyValue: function () { + getPropertyValue: () => { propertyCount++; } }; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.getComputedStylePropertyValue('prop'); vNode.getComputedStylePropertyValue('prop'); vNode.getComputedStylePropertyValue('prop'); @@ -354,60 +352,60 @@ describe('VirtualNode', function () { }); }); - describe('clientRects', function () { - it('should call node.getClientRects', function () { - var called = false; - node.getClientRects = function () { + describe('clientRects', () => { + it('should call node.getClientRects', () => { + let called = false; + node.getClientRects = () => { called = true; return []; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.clientRects; assert.isTrue(called); }); - it('should only call node.getClientRects once', function () { - var count = 0; - node.getClientRects = function () { + it('should only call node.getClientRects once', () => { + let count = 0; + node.getClientRects = () => { count++; return []; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.clientRects; vNode.clientRects; vNode.clientRects; assert.equal(count, 1); }); - it('should filter out 0 width rects', function () { - node.getClientRects = function () { + it('should filter out 0 width rects', () => { + node.getClientRects = () => { return [{ width: 10 }, { width: 0 }, { width: 20 }]; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); assert.deepEqual(vNode.clientRects, [{ width: 10 }, { width: 20 }]); }); }); - describe('boundingClientRect', function () { - it('should call node.getBoundingClientRect', function () { - var called = false; - node.getBoundingClientRect = function () { + describe('boundingClientRect', () => { + it('should call node.getBoundingClientRect', () => { + let called = false; + node.getBoundingClientRect = () => { called = true; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.boundingClientRect; assert.isTrue(called); }); - it('should only call node.getBoundingClientRect once', function () { - var count = 0; - node.getBoundingClientRect = function () { + it('should only call node.getBoundingClientRect once', () => { + let count = 0; + node.getBoundingClientRect = () => { count++; }; - var vNode = new VirtualNode(node); + const vNode = new VirtualNode(node); vNode.boundingClientRect; vNode.boundingClientRect; vNode.boundingClientRect; diff --git a/test/core/public/finish-run.js b/test/core/public/finish-run.js index 8abe92f935..1199b2153d 100644 --- a/test/core/public/finish-run.js +++ b/test/core/public/finish-run.js @@ -104,7 +104,10 @@ describe('axe.finishRun', function () { it('can report violations results', function (done) { fixture.innerHTML = '<div aria-label="foo"></div>'; axe - .runPartial({ include: [['#fixture']] }, { runOnly: 'aria-allowed-attr' }) + .runPartial( + { include: [['#fixture']] }, + { runOnly: 'aria-prohibited-attr' } + ) .then(function (result) { return axe.finishRun([result]); }) @@ -186,7 +189,7 @@ describe('axe.finishRun', function () { allResults.push(results); return axe.runPartial( { include: [['#fail']] }, - { runOnly: 'aria-allowed-attr' } + { runOnly: 'aria-prohibited-attr' } ); }) .then(function (results) { @@ -209,6 +212,40 @@ describe('axe.finishRun', function () { .catch(done); }); + it('rejects with sync reporter errors', async () => { + axe.addReporter('throwing', () => { + throw new Error('Something went wrong'); + }); + const options = { reporter: 'throwing' }; + + fixture.innerHTML = '<h1>Hello world</h1>'; + const partial = await axe.runPartial('#fixture', options); + try { + await axe.finishRun([partial], options); + assert.fail('Should have thrown'); + } catch (err) { + assert.equal(err.message, 'Something went wrong'); + } + }); + + it('rejects with async reporter errors', async () => { + axe.addReporter('throwing', (results, options, resolve, reject) => { + setTimeout(() => { + reject(new Error('Something went wrong')); + }, 10); + }); + const options = { reporter: 'throwing' }; + + fixture.innerHTML = '<h1>Hello world</h1>'; + const partial = await axe.runPartial('#fixture', options); + try { + await axe.finishRun([partial], options); + assert.fail('Should have thrown'); + } catch (err) { + assert.equal(err.message, 'Something went wrong'); + } + }); + describe('frames', function () { function createIframe(html, parent) { return new Promise(function (resolve) { diff --git a/test/core/public/plugins.js b/test/core/public/plugins.js index d4b7009722..7c35d10430 100644 --- a/test/core/public/plugins.js +++ b/test/core/public/plugins.js @@ -1,10 +1,8 @@ -describe('plugins', function () { - 'use strict'; - +describe('plugins', () => { function createFrames(callback) { - var frame, - num = 2, - loaded = 0; + let frame; + const num = 2; + let loaded = 0; function onLoad() { loaded++; @@ -26,29 +24,29 @@ describe('plugins', function () { fixture.appendChild(frame); } - var fixture = document.getElementById('fixture'); + const fixture = document.getElementById('fixture'); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._audit = null; }); - beforeEach(function () { + beforeEach(() => { axe._load({ rules: [] }); }); - it('Should have registerPlugin function', function () { + it('Should have registerPlugin function', () => { assert.ok(axe.registerPlugin); assert.equal(typeof axe.registerPlugin, 'function'); }); - it('should have an empty set of plugins', function () { + it('should have an empty set of plugins', () => { assert.deepEqual({}, axe.plugins); }); - it('should add a plugin to the plugins list and a command to the audit commands', function () { + it('should add a plugin to the plugins list and a command to the audit commands', () => { axe.registerPlugin({ id: 'my-plugin', run: 'run', @@ -63,9 +61,9 @@ describe('plugins', function () { assert.equal(axe.plugins['my-plugin']._run, 'run'); assert.equal(axe._audit.commands['my-command'], 'callback'); }); - describe('Plugin class', function () { - it('should call the run function of the registered plugin, when run is called', function () { - var called = false; + describe('Plugin class', () => { + it('should call the run function of the registered plugin, when run is called', () => { + let called = false; axe.registerPlugin({ id: 'my-plugin', run: function (id, action, options) { @@ -89,13 +87,13 @@ describe('plugins', function () { ); }); }); - describe('Plugin.protoype.run', function () { - afterEach(function () { + describe('Plugin.protoype.run', () => { + afterEach(() => { fixture.innerHTML = ''; axe._audit = null; axe.plugins = {}; }); - beforeEach(function () { + beforeEach(() => { axe._load({ rules: [] }); @@ -124,12 +122,12 @@ describe('plugins', function () { }); axe.plugins.multi.add({ id: 'hideall', - cleanup: function (done) { + cleanup: done => { done(); }, run: function (options, callback) { - var frames; - var q = axe.utils.queue(); + let frames; + const q = axe.utils.queue(); frames = axe.utils.toArray( document.querySelectorAll('iframe, frame') @@ -150,13 +148,13 @@ describe('plugins', function () { }); }); - q.defer(function (done) { + q.defer(done => { // implementation done('ola!'); }); q.then(function (data) { // done with all the frames - var results = []; + let results = []; data.forEach(function (datum) { if (datum) { results = results.concat(datum); @@ -167,15 +165,15 @@ describe('plugins', function () { } }); }); - it('should work without frames', function (done) { + it('should work without frames', done => { axe.plugins.multi.run('hideall', 'run', {}, function (results) { assert.deepEqual(results, ['ola!']); done(); }); }); - it('should work with frames', function (done) { - createFrames(function () { - setTimeout(function () { + it('should work with frames', done => { + createFrames(() => { + setTimeout(() => { axe.plugins.multi.run('hideall', 'run', {}, function (results) { assert.deepEqual(results, ['ola!', 'ola!', 'ola!', 'ola!', 'ola!']); done(); @@ -183,13 +181,13 @@ describe('plugins', function () { }, 500); }); }); - it("should call the implementation's cleanup function", function (done) { - var called = false; - axe.plugins.multi.cleanup = function (done) { + it("should call the implementation's cleanup function", done => { + let called = false; + axe.plugins.multi.cleanup = doneFn => { called = true; - done(); + doneFn(); }; - axe.plugins.multi.cleanup(function () { + axe.plugins.multi.cleanup(() => { assert.ok(called); done(); }); diff --git a/test/core/public/run-partial.js b/test/core/public/run-partial.js index 515de33548..3d99e5662f 100644 --- a/test/core/public/run-partial.js +++ b/test/core/public/run-partial.js @@ -55,6 +55,14 @@ describe('axe.runPartial', function () { .catch(done); }); + it('ignores { elementRef: true } option', async () => { + const options = { elementRef: true }; + const result = await axe.runPartial(options); + for (const nodeResult of result.results[0].nodes) { + assert.isUndefined(nodeResult.node.element); + } + }); + describe('result', function () { var partialResult; before(function (done) { @@ -91,6 +99,24 @@ describe('axe.runPartial', function () { assert.hasAllKeys(checkResult.node, dqElementKeys); }); + it('does not return DqElement objects', () => { + for (const result of partialResult.results) { + for (const nodeResult of result.nodes) { + assert.notInstanceOf(nodeResult.node, DqElement); + const checks = [ + ...nodeResult.any, + ...nodeResult.all, + ...nodeResult.none + ]; + for (const check of checks) { + for (const relatedNode of check.relatedNodes) { + assert.notInstanceOf(relatedNode, DqElement); + } + } + } + } + }); + it('can be serialized using JSON.stringify', function () { assert.doesNotThrow(function () { JSON.stringify(partialResult); diff --git a/test/core/public/run-rules.js b/test/core/public/run-rules.js index 778b236482..d04b853a80 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -43,11 +43,9 @@ describe('runRules', function () { } var fixture = document.getElementById('fixture'); - var memoizedFns; var isNotCalled; beforeEach(function () { - memoizedFns = axe._memoizedFns.slice(); isNotCalled = function (err) { throw err || new Error('Reject should not be called'); }; @@ -56,8 +54,7 @@ describe('runRules', function () { afterEach(function () { fixture.innerHTML = ''; axe._audit = null; - axe._tree = undefined; - axe._memoizedFns = memoizedFns; + axe.teardown(); }); it('should work', function (done) { @@ -92,7 +89,7 @@ describe('runRules', function () { assert.lengthOf(r[0].passes, 3); done(); }, - isNotCalled + err => done(err) ); }, 500); }); @@ -228,7 +225,8 @@ describe('runRules', function () { "/div[@id='target']" ], source: '<div id="target"></div>', - nodeIndexes: [12, 14] + nodeIndexes: [12, 14], + fromFrame: true }, any: [ { @@ -271,7 +269,8 @@ describe('runRules', function () { ], source: '<div id="foo">\n <div id="bar"></div>\n </div>', - nodeIndexes: [12, 9] + nodeIndexes: [12, 9], + fromFrame: true }, any: [ { @@ -290,7 +289,8 @@ describe('runRules', function () { ], source: '<div id="foo">\n <div id="bar"></div>\n </div>', - nodeIndexes: [12, 9] + nodeIndexes: [12, 9], + fromFrame: true } ] } @@ -538,7 +538,8 @@ describe('runRules', function () { ], xpath: ["/div[@id='target']"], source: '<div id="target">Target!</div>', - nodeIndexes: [12] + nodeIndexes: [12], + fromFrame: false }, impact: 'moderate', any: [ @@ -582,7 +583,8 @@ describe('runRules', function () { 'html > body > div:nth-child(1) > div:nth-child(1)' ], source: '<div id="target">Target!</div>', - nodeIndexes: [12] + nodeIndexes: [12], + fromFrame: false }, any: [ { @@ -599,7 +601,8 @@ describe('runRules', function () { ], xpath: ["/div[@id='target']"], source: '<div id="target">Target!</div>', - nodeIndexes: [12] + nodeIndexes: [12], + fromFrame: false } ] } @@ -847,7 +850,7 @@ describe('runRules', function () { setTimeout(function () { axe._runRules( document, - { iframes: false }, + { iframes: false, elementRef: true }, function (r) { assert.lengthOf(r[0].passes, 1); assert.equal( diff --git a/test/core/public/run.js b/test/core/public/run.js index 39cdd9d433..f3e376045e 100644 --- a/test/core/public/run.js +++ b/test/core/public/run.js @@ -201,6 +201,28 @@ describe('axe.run', function () { done(); }); }); + + it('rejects with sync reporter errors', done => { + axe.addReporter('throwing', () => { + throw new Error('Something went wrong'); + }); + axe.run({ reporter: 'throwing' }, err => { + assert.equal(err.message, 'Something went wrong'); + done(); + }); + }); + + it('rejects with async reporter errors', done => { + axe.addReporter('throwing', (results, options, resolve, reject) => { + setTimeout(() => { + reject(new Error('Something went wrong')); + }, 10); + }); + axe.run({ reporter: 'throwing' }, err => { + assert.equal(err.message, 'Something went wrong'); + done(); + }); + }); }); describe('promise result', function () { diff --git a/test/core/public/setup.js b/test/core/public/setup.js index 085848db4b..242f2b544e 100644 --- a/test/core/public/setup.js +++ b/test/core/public/setup.js @@ -32,6 +32,11 @@ describe('axe.setup', function () { assert.exists(axe._selectorData); }); + it('takes documentElement when passed the document', () => { + axe.setup(document); + assert.equal(axe._tree[0].actualNode, document.documentElement); + }); + it('should throw if called twice in a row', function () { function fn() { axe.setup(); diff --git a/test/core/reporters/helpers/process-aggregate.js b/test/core/reporters/helpers/process-aggregate.js index 5cdbbc9ee9..32f4f072c1 100644 --- a/test/core/reporters/helpers/process-aggregate.js +++ b/test/core/reporters/helpers/process-aggregate.js @@ -2,6 +2,7 @@ describe('helpers.processAggregate', function () { 'use strict'; var results, options; const helpers = axe._thisWillBeDeletedDoNotUse.helpers; + const fixture = document.getElementById('fixture'); beforeEach(function () { results = [ @@ -83,7 +84,7 @@ describe('helpers.processAggregate', function () { relatedNodes: [ { element: document.createElement('input'), - selector: '#dopel', + selector: ['#dopel'], source: ['<input id="dopel"/>'], xpath: ['/main/input[@id="dopel"]'], ancestry: ['html > body > main > input:nth-child(2)'], @@ -176,6 +177,26 @@ describe('helpers.processAggregate', function () { }); }); + describe('axe.configure({ noHtml: true })', () => { + afterEach(() => { + axe.reset(); + }); + + it('sets html to null on nodes', () => { + axe.configure({ noHtml: true }); + const { passes, violations } = helpers.processAggregate(results, {}); + assert.isNull(passes[0].nodes[0].html); + assert.isNull(violations[0].nodes[0].html); + }); + + it('sets html to null on relatedNodes', () => { + axe.configure({ noHtml: true }); + const { passes, violations } = helpers.processAggregate(results, {}); + assert.isNull(passes[0].nodes[0].any[0].relatedNodes[0].html); + assert.isNull(violations[0].nodes[0].any[0].relatedNodes[0].html); + }); + }); + describe('`options` argument', function () { describe('`resultTypes` option', function () { it('should reduce the unwanted result types to 1 in the `resultObject`', function () { @@ -194,6 +215,21 @@ describe('helpers.processAggregate', function () { assert.isDefined(resultObject.incomplete); assert.isDefined(resultObject.inapplicable); }); + + it('should not compute selectors of filtered nodes', () => { + const dqElm = new axe.utils.DqElement(fixture); + Object.defineProperty(dqElm, 'ancestry', { + get() { + throw new Error('Should not be called'); + } + }); + results[0].passes[1].node = dqElm; + assert.doesNotThrow(() => { + helpers.processAggregate(results, { + resultTypes: ['violations'] + }); + }); + }); }); describe('`elementRef` option', function () { @@ -280,6 +316,19 @@ describe('helpers.processAggregate', function () { resultObject.passes[0].nodes[0].any[0].relatedNodes[0].target ); }); + + it('should not call DqElement.selector', () => { + const dqElm = new axe.utils.DqElement(fixture); + Object.defineProperty(dqElm, 'selector', { + get() { + throw new Error('Should not be called'); + } + }); + results[0].passes[0].node = dqElm; + assert.doesNotThrow(() => { + helpers.processAggregate(results, options); + }); + }); }); }); @@ -331,6 +380,21 @@ describe('helpers.processAggregate', function () { resultObject.passes[0].nodes[0].any[0].relatedNodes[0].ancestry ); }); + + it('should not call DqElement.ancestry', () => { + const dqElm = new axe.utils.DqElement(fixture, options, { + selector: ['div'] // prevent axe._selectorData error + }); + Object.defineProperty(dqElm, 'ancestry', { + get() { + throw new Error('Should not be called'); + } + }); + results[0].passes[0].node = dqElm; + assert.doesNotThrow(() => { + helpers.processAggregate(results, options); + }); + }); }); describe('when not set at all', function () { @@ -381,6 +445,21 @@ describe('helpers.processAggregate', function () { resultObject.passes[0].nodes[0].any[0].relatedNodes[0].xpath ); }); + + it('should not call DqElement.xpath', () => { + const dqElm = new axe.utils.DqElement(fixture, options, { + selector: ['div'] // prevent axe._selectorData error + }); + Object.defineProperty(dqElm, 'xpath', { + get() { + throw new Error('Should not be called'); + } + }); + results[0].passes[0].node = dqElm; + assert.doesNotThrow(() => { + helpers.processAggregate(results, options); + }); + }); }); }); }); diff --git a/test/core/reporters/raw-env.js b/test/core/reporters/raw-env.js index 84550453bd..e8ca9fb7c7 100644 --- a/test/core/reporters/raw-env.js +++ b/test/core/reporters/raw-env.js @@ -26,7 +26,8 @@ describe('reporters - raw-env', function () { any: [ { result: true, - data: 'minkey' + data: 'minkey', + relatedNodes: [] } ], all: [], @@ -50,7 +51,8 @@ describe('reporters - raw-env', function () { { result: false, data: 'pillock', - impact: 'cats' + impact: 'cats', + relatedNodes: [] } ], any: [], @@ -75,7 +77,8 @@ describe('reporters - raw-env', function () { { data: 'foon', impact: 'monkeys', - result: true + result: true, + relatedNodes: [] } ], any: [], @@ -93,10 +96,13 @@ describe('reporters - raw-env', function () { passes: [ { result: 'passed', + any: [], + all: [], none: [ { data: 'clueso', - result: true + result: true, + relatedNodes: [] } ], node: createDqElement() @@ -111,8 +117,9 @@ describe('reporters - raw-env', function () { axe._cache.set('selectorData', {}); }); - after(function () { + afterEach(function () { fixture.innerHTML = ''; + sinon.restore(); }); it('should serialize DqElements (#1195)', function () { @@ -149,4 +156,18 @@ describe('reporters - raw-env', function () { } ); }); + + it('uses nodeSerializer', done => { + var rawReporter = axe.getReporter('rawEnv'); + var spy = sinon.spy(axe.utils.nodeSerializer, 'mapRawNodeResults'); + rawReporter( + runResults, + {}, + function () { + assert.isTrue(spy.called); + done(); + }, + done + ); + }); }); diff --git a/test/core/reporters/raw.js b/test/core/reporters/raw.js index 61648759af..c49be13894 100644 --- a/test/core/reporters/raw.js +++ b/test/core/reporters/raw.js @@ -26,7 +26,8 @@ describe('reporters - raw', function () { any: [ { result: true, - data: 'minkey' + data: 'minkey', + relatedNodes: [] } ], all: [], @@ -50,7 +51,8 @@ describe('reporters - raw', function () { { result: false, data: 'pillock', - impact: 'cats' + impact: 'cats', + relatedNodes: [] } ], any: [], @@ -75,7 +77,8 @@ describe('reporters - raw', function () { { data: 'foon', impact: 'monkeys', - result: true + result: true, + relatedNodes: [] } ], any: [], @@ -93,10 +96,13 @@ describe('reporters - raw', function () { passes: [ { result: 'passed', + any: [], + all: [], none: [ { data: 'clueso', - result: true + result: true, + relatedNodes: [] } ], node: createDqElement() @@ -111,8 +117,9 @@ describe('reporters - raw', function () { axe._cache.set('selectorData', {}); }); - after(function () { + afterEach(function () { fixture.innerHTML = ''; + sinon.restore(); }); it('should serialize DqElements', function (done) { @@ -138,4 +145,18 @@ describe('reporters - raw', function () { }); }); }); + + it('uses nodeSerializer', done => { + var rawReporter = axe.getReporter('raw'); + var spy = sinon.spy(axe.utils.nodeSerializer, 'mapRawNodeResults'); + rawReporter( + runResults, + {}, + function () { + assert.isTrue(spy.called); + done(); + }, + done + ); + }); }); diff --git a/test/core/utils/check-helper.js b/test/core/utils/check-helper.js index b40948859e..41e2b09306 100644 --- a/test/core/utils/check-helper.js +++ b/test/core/utils/check-helper.js @@ -1,6 +1,5 @@ describe('axe.utils.checkHelper', () => { - const { queryFixture } = axe.testUtils; - const DqElement = axe.utils.DqElement; + const { queryFixture, fixtureSetup } = axe.testUtils; function noop() {} it('should be a function', () => { @@ -67,46 +66,46 @@ describe('axe.utils.checkHelper', () => { describe('relatedNodes', () => { const fixture = document.getElementById('fixture'); - afterEach(() => { - fixture.innerHTML = ''; + const getSelector = node => node.selector; + + it('returns DqElements', () => { + fixtureSetup('<div id="t1"></div><div id="t2"></div>'); + const target = {}; + const helper = axe.utils.checkHelper(target, noop); + helper.relatedNodes(fixture.children); + assert.instanceOf(target.relatedNodes[0], axe.utils.DqElement); }); it('should accept NodeList', () => { - fixture.innerHTML = '<div id="t1"></div><div id="t2"></div>'; + fixtureSetup('<div id="t1"></div><div id="t2"></div>'); const target = {}; const helper = axe.utils.checkHelper(target, noop); helper.relatedNodes(fixture.children); - assert.lengthOf(target.relatedNodes, 2); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.instanceOf(target.relatedNodes[1], DqElement); - assert.equal(target.relatedNodes[0].element, fixture.children[0]); - assert.equal(target.relatedNodes[1].element, fixture.children[1]); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#t1'], ['#t2']]); }); it('should accept a single Node', () => { - fixture.innerHTML = '<div id="t1"></div><div id="t2"></div>'; + fixtureSetup('<div id="t1"></div><div id="t2"></div>'); const target = {}; const helper = axe.utils.checkHelper(target, noop); helper.relatedNodes(fixture.firstChild); - assert.lengthOf(target.relatedNodes, 1); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.equal(target.relatedNodes[0].element, fixture.firstChild); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#t1']]); }); it('should accept an Array', () => { - fixture.innerHTML = '<div id="t1"></div><div id="t2"></div>'; + fixtureSetup('<div id="t1"></div><div id="t2"></div>'); + const target = {}; const helper = axe.utils.checkHelper(target, noop); helper.relatedNodes(Array.prototype.slice.call(fixture.children)); - assert.lengthOf(target.relatedNodes, 2); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.instanceOf(target.relatedNodes[1], DqElement); - assert.equal(target.relatedNodes[0].element, fixture.children[0]); - assert.equal(target.relatedNodes[1].element, fixture.children[1]); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#t1'], ['#t2']]); }); it('should accept an array-like Object', () => { - fixture.innerHTML = '<div id="t1"></div><div id="t2"></div>'; + fixtureSetup('<div id="t1"></div><div id="t2"></div>'); const target = {}; const helper = axe.utils.checkHelper(target, noop); const nodes = { @@ -115,11 +114,8 @@ describe('axe.utils.checkHelper', () => { length: 2 }; helper.relatedNodes(nodes); - assert.lengthOf(target.relatedNodes, 2); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.instanceOf(target.relatedNodes[1], DqElement); - assert.equal(target.relatedNodes[0].element, fixture.children[0]); - assert.equal(target.relatedNodes[1].element, fixture.children[1]); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#t1'], ['#t2']]); }); it('should accept a VirtualNode', () => { @@ -127,27 +123,24 @@ describe('axe.utils.checkHelper', () => { const target = {}; const helper = axe.utils.checkHelper(target, noop); helper.relatedNodes(vNode); - assert.lengthOf(target.relatedNodes, 1); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.equal(target.relatedNodes[0].element.nodeName, 'A'); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#target']]); }); it('should accept an array of VirtualNodes', () => { const vNode = queryFixture(` - <div id="target"><a></a><b></b></div> + <div id="target"><a id="a"></a><b id="b"></b></div> `); const target = {}; const helper = axe.utils.checkHelper(target, noop); helper.relatedNodes(vNode.children); - assert.lengthOf(target.relatedNodes, 2); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.equal(target.relatedNodes[0].element.nodeName, 'A'); - assert.equal(target.relatedNodes[1].element.nodeName, 'B'); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#a'], ['#b']]); }); it('should filter out non-nodes', () => { const vNode = queryFixture(` - <div><a id="target"></a><b></b></div> + <div><a id="target"></a><b id="b"></b></div> `); const target = {}; const helper = axe.utils.checkHelper(target, noop); @@ -162,10 +155,8 @@ describe('axe.utils.checkHelper', () => { }) ]; helper.relatedNodes(nodes); - assert.lengthOf(target.relatedNodes, 2); - assert.instanceOf(target.relatedNodes[0], DqElement); - assert.equal(target.relatedNodes[0].element.nodeName, 'A'); - assert.equal(target.relatedNodes[1].element.nodeName, 'B'); + const selectors = target.relatedNodes.map(getSelector); + assert.deepEqual(selectors, [['#target'], ['#b']]); }); it('should noop for non-node-like objects', () => { diff --git a/test/core/utils/clone.js b/test/core/utils/clone.js index 583f9bb9fe..45cc86fe76 100644 --- a/test/core/utils/clone.js +++ b/test/core/utils/clone.js @@ -1,26 +1,26 @@ -describe('utils.clone', function () { - 'use strict'; - var clone = axe.utils.clone; +describe('utils.clone', () => { + const clone = axe.utils.clone; + const fixture = document.querySelector('#fixture'); - it('should clone an object', function () { - var obj = { + it('should clone an object', () => { + const obj = { cats: true, dogs: 2, - fish: [0, 1, 2] + fish: [0, 1, { one: 'two' }] }; - var c = clone(obj); + const c = clone(obj); obj.cats = false; obj.dogs = 1; - obj.fish[0] = 'stuff'; + obj.fish[2].one = 'three'; assert.strictEqual(c.cats, true); assert.strictEqual(c.dogs, 2); - assert.deepEqual(c.fish, [0, 1, 2]); + assert.deepEqual(c.fish, [0, 1, { one: 'two' }]); }); - it('should clone nested objects', function () { - var obj = { + it('should clone nested objects', () => { + const obj = { cats: { fred: 1, billy: 2, @@ -33,7 +33,7 @@ describe('utils.clone', function () { }, fish: [0, 1, 2] }; - var c = clone(obj); + const c = clone(obj); obj.cats.fred = 47; obj.dogs = 47; @@ -54,45 +54,107 @@ describe('utils.clone', function () { assert.deepEqual(c.fish, [0, 1, 2]); }); - it('should clone objects with methods', function () { - var obj = { - cats: function () { + it('should clone objects with methods', () => { + const obj = { + cats: () => { return 'meow'; }, - dogs: function () { + dogs: () => { return 'woof'; } }; - var c = clone(obj); + const c = clone(obj); assert.strictEqual(obj.cats, c.cats); assert.strictEqual(obj.dogs, c.dogs); - obj.cats = function () {}; - obj.dogs = function () {}; + obj.cats = () => {}; + obj.dogs = () => {}; assert.notStrictEqual(obj.cats, c.cats); assert.notStrictEqual(obj.dogs, c.dogs); }); - it('should clone prototypes', function () { + it('should clone prototypes', () => { function Cat(name) { this.name = name; } - Cat.prototype.meow = function () { + Cat.prototype.meow = () => { return 'meow'; }; - Cat.prototype.bark = function () { + Cat.prototype.bark = () => { return 'cats dont bark'; }; - var cat = new Cat('Fred'), + const cat = new Cat('Fred'), c = clone(cat); assert.deepEqual(cat.name, c.name); assert.deepEqual(Cat.prototype.bark, c.bark); assert.deepEqual(Cat.prototype.meow, c.meow); }); + + it('should clone circular objects while keeping the circular reference', () => { + const obj = { cats: true }; + obj.child = obj; + const c = clone(obj); + + obj.cats = false; + + assert.deepEqual(c, { + cats: true, + child: c + }); + assert.strictEqual(c, c.child); + }); + + it('should not return the same object when cloned twice', () => { + const obj = { cats: true }; + const c1 = clone(obj); + const c2 = clone(obj); + + assert.notStrictEqual(c1, c2); + }); + + it('should not return the same object when nested', () => { + const obj = { dogs: true }; + const obj1 = { cats: true, child: { prop: obj } }; + const obj2 = { fish: [0, 1, 2], child: { prop: obj } }; + + const c1 = clone(obj1); + const c2 = clone(obj2); + + assert.notStrictEqual(c1.child.prop, c2.child.prop); + }); + + it('should not clone HTML elements', () => { + const obj = { + cats: true, + node: document.createElement('div') + }; + const c = clone(obj); + + obj.cats = false; + + assert.equal(c.cats, true); + assert.strictEqual(c.node, obj.node); + }); + + it('should not clone HTML elements from different windows', () => { + fixture.innerHTML = '<iframe id="target"></iframe>'; + const iframe = fixture.querySelector('#target'); + + const obj = { + cats: true, + node: iframe.contentDocument + }; + const c = clone(obj); + + obj.cats = false; + + assert.equal(c.cats, true); + assert.strictEqual(c.node, obj.node); + }); }); diff --git a/test/core/utils/dq-element.js b/test/core/utils/dq-element.js index 5ea8ea61d4..3e6f036f9e 100644 --- a/test/core/utils/dq-element.js +++ b/test/core/utils/dq-element.js @@ -163,11 +163,12 @@ describe('DqElement', function () { describe('toJSON', function () { it('should only stringify selector and source', function () { var spec = { - selector: 'foo > bar > joe', + selector: ['foo > bar > joe'], source: '<joe aria-required="true">', - xpath: '/foo/bar/joe', - ancestry: 'foo > bar > joe', - nodeIndexes: [123, 456] + xpath: ['/foo/bar/joe'], + ancestry: ['foo > bar > joe'], + nodeIndexes: [123], + fromFrame: false }; var div = document.createElement('div'); @@ -266,4 +267,29 @@ describe('DqElement', function () { }); }); }); + + describe('DqElement.setRunOptions', function () { + it('sets options for DqElement', function () { + axe.setup(); + var options = { absolutePaths: true, elementRef: true }; + DqElement.setRunOptions(options); + var dqElm = new DqElement(document.body); + + const { element, selector } = dqElm.toJSON(); + assert.equal(element, document.body); + assert.equal(selector, 'html > body'); + }); + + it('is reset by axe.teardown', () => { + var options = { absolutePaths: true, elementRef: true }; + DqElement.setRunOptions(options); + axe.teardown(); + + axe.setup(); + var dqElm = new DqElement(document.body); + const { element, selector } = dqElm.toJSON(); + assert.isUndefined(element); + assert.equal(selector, 'body'); + }); + }); }); diff --git a/test/core/utils/find-by.js b/test/core/utils/find-by.js index 79911a6ff8..e6482fcec3 100644 --- a/test/core/utils/find-by.js +++ b/test/core/utils/find-by.js @@ -40,4 +40,21 @@ describe('axe.utils.findBy', function () { it('should not throw if passed falsey first parameter', function () { assert.isUndefined(axe.utils.findBy(null, 'id', 'macaque')); }); + + it('ignores any non-object elements in the array', function () { + const obj = { + id: 'monkeys', + foo: 'bar' + }; + const array = ['bananas', true, null, 123, obj]; + + assert.equal(axe.utils.findBy(array, 'id', 'monkeys'), obj); + }); + + it('only looks at owned properties', function () { + const obj1 = { id: 'monkeys', eat: 'bananas' }; + const obj2 = Object.create(obj1); + obj2.id = 'gorillas'; + assert.equal(axe.utils.findBy([obj2, obj1], 'eat', 'bananas'), obj1); + }); }); diff --git a/test/core/utils/get-shadow-selector.js b/test/core/utils/get-shadow-selector.js index bfdd361df3..9356f8cee8 100644 --- a/test/core/utils/get-shadow-selector.js +++ b/test/core/utils/get-shadow-selector.js @@ -1,77 +1,74 @@ -describe('axe.utils.getShadowSelector', function () { - var fixture = document.getElementById('fixture'); - var shadowTest = axe.testUtils.shadowSupport.v1 ? it : xit; - var getShadowSelector = axe.utils.getShadowSelector; +describe('axe.utils.getShadowSelector', () => { + const fixture = document.getElementById('fixture'); + const shadowTest = axe.testUtils.shadowSupport.v1 ? it : xit; + const getShadowSelector = axe.utils.getShadowSelector; function generator(node) { return node.nodeName.toLowerCase(); } - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; }); - it('returns generated output for light DOM nodes', function () { - var h1 = document.createElement('h1'); + it('returns generated output for light DOM nodes', () => { + const h1 = document.createElement('h1'); fixture.appendChild(h1); - var selector = getShadowSelector(generator, h1); + const selector = getShadowSelector(generator, h1); assert.equal(selector, 'h1'); }); - it('passes node and options to generator', function () { - var called = false; - var node = document.createElement('h1'); - var options = { hello: 'world' }; - function generator(arg1, arg2) { + it('passes node and options to generator', () => { + let called = false; + const node = document.createElement('h1'); + const options = { hello: 'world' }; + function generatorFn(arg1, arg2) { called = true; assert.equal(arg1, node); assert.equal(arg2, options); } - getShadowSelector(generator, node, options); + getShadowSelector(generatorFn, node, options); assert.isTrue(called); }); - it('passes am empty object if no options are provided', function () { - var called = false; - var node = document.createElement('h1'); - function generator(_, arg2) { + it('passes am empty object if no options are provided', () => { + let called = false; + const node = document.createElement('h1'); + function generatorFn(_, arg2) { called = true; assert.deepEqual(arg2, {}); } - getShadowSelector(generator, node); + getShadowSelector(generatorFn, node); assert.isTrue(called); }); - shadowTest('returns the output of the generator for light DOM', function () { + shadowTest('returns the output of the generator for light DOM', () => { fixture.innerHTML = '<div><h1>Hello world</h1></div>'; - var div = fixture.querySelector('div'); - var h1 = fixture.querySelector('h1'); - var shadowHost = div.attachShadow({ mode: 'open' }); + const div = fixture.querySelector('div'); + const h1 = fixture.querySelector('h1'); + const shadowHost = div.attachShadow({ mode: 'open' }); shadowHost.innerHTML = '<span><slot /></span>'; assert.equal(getShadowSelector(generator, h1), 'h1'); }); - shadowTest( - 'returns an array of outputs for each shadow tree host', - function () { - var node = document.createElement('section'); - fixture.appendChild(node); + shadowTest('returns an array of outputs for each shadow tree host', () => { + const node = document.createElement('section'); + fixture.appendChild(node); - var shadowRoot1 = node.attachShadow({ mode: 'open' }); - shadowRoot1.innerHTML = '<div><article><slot /></article</div>'; + const shadowRoot1 = node.attachShadow({ mode: 'open' }); + shadowRoot1.innerHTML = '<div><article><slot /></article</div>'; - var shadowRoot2 = shadowRoot1 - .querySelector('article') - .attachShadow({ mode: 'open' }); - shadowRoot2.innerHTML = '<h1>Hello world</h1>'; + const shadowRoot2 = shadowRoot1 + .querySelector('article') + .attachShadow({ mode: 'open' }); + shadowRoot2.innerHTML = '<h1>Hello world</h1>'; - var target = shadowRoot2.querySelector('h1'); - var sel = getShadowSelector(generator, target); - assert.deepEqual(sel, ['section', 'article', 'h1']); - } - ); + const target = shadowRoot2.querySelector('h1'); + const sel = getShadowSelector(generator, target); + assert.deepEqual(sel, ['section', 'article', 'h1']); + }); }); diff --git a/test/core/utils/node-serializer.js b/test/core/utils/node-serializer.js new file mode 100644 index 0000000000..5d170de025 --- /dev/null +++ b/test/core/utils/node-serializer.js @@ -0,0 +1,280 @@ +describe('nodeSerializer', () => { + const { nodeSerializer, DqElement } = axe.utils; + const fixture = document.querySelector('#fixture'); + beforeEach(() => { + axe.setup(); + }); + + afterEach(() => { + nodeSerializer.update(null); + }); + + describe('.toSpec()', () => { + it('returns DqElement.toJSON() by default', () => { + const spec = nodeSerializer.toSpec(fixture); + const dqElm = new DqElement(fixture); + assert.deepEqual(spec, dqElm.toJSON()); + }); + + it('can be replaced with nodeSerializer.update({ toSpec: fn })', () => { + nodeSerializer.update({ + toSpec(dqElm) { + const json = dqElm.toJSON(); + json.source = 'Replaced'; + return json; + } + }); + + const spec = nodeSerializer.toSpec(fixture); + const dqElm = new DqElement(fixture); + assert.deepEqual(spec, { ...dqElm.toJSON(), source: 'Replaced' }); + }); + }); + + describe('.dqElmToSpec()', () => { + it('returns DqElement.toJSON() by default', () => { + const dqElm = new DqElement(fixture); + const spec = nodeSerializer.dqElmToSpec(dqElm); + assert.deepEqual(spec, dqElm.toJSON()); + }); + + it('can be replaced with nodeSerializer.update({ toSpec: fn })', () => { + nodeSerializer.update({ + toSpec(dqElm) { + const json = dqElm.toJSON(); + json.source = 'Replaced'; + return json; + } + }); + + const dqElm = new DqElement(fixture); + const spec = nodeSerializer.dqElmToSpec(dqElm); + assert.deepEqual(spec, { ...dqElm.toJSON(), source: 'Replaced' }); + }); + + it('optionally accepts runOptions, replacing real values with dummy values', () => { + const dqElm = new DqElement(fixture); + const spec = nodeSerializer.dqElmToSpec(dqElm, { + selectors: false, + xpath: false, + ancestry: false + }); + assert.deepEqual(spec, { + ...dqElm.toJSON(), + selector: [':root'], + ancestry: [':root'], + xpath: '/' + }); + }); + + it('returns selector when falsey but not false', () => { + const dqElm = new DqElement(fixture); + const spec = nodeSerializer.dqElmToSpec(dqElm, { selectors: null }); + assert.deepEqual(spec, { + ...dqElm.toJSON(), + ancestry: [':root'], + xpath: '/' + }); + }); + + it('returns selector if fromFrame, even if runOptions.selectors is false', () => { + const dqElm = new DqElement(fixture); + dqElm.fromFrame = true; + const spec = nodeSerializer.dqElmToSpec(dqElm, { + selectors: false + }); + assert.deepEqual(spec, { + ...dqElm.toJSON(), + ancestry: [':root'], + xpath: '/' + }); + }); + + it('skips computing props turned off with runOptions', () => { + const dqElm = new DqElement(fixture); + + const throws = () => { + throw new Error('Should not be called'); + }; + Object.defineProperty(dqElm, 'selector', { get: throws }); + Object.defineProperty(dqElm, 'ancestry', { get: throws }); + Object.defineProperty(dqElm, 'xpath', { get: throws }); + + assert.doesNotThrow(() => { + nodeSerializer.dqElmToSpec(dqElm, { + selectors: false, + xpath: false, + ancestry: false + }); + }); + }); + }); + + describe('.mergeSpecs()', () => { + const nodeSpec = { + source: '<div id="fixture"></div>', + selector: ['#fixture'], + ancestry: ['html > body > #fixture'], + nodeIndexes: [3], + xpath: ['html/body/div[1]'] + }; + const frameSpec = { + source: '<iframe></iframe>', + selector: ['#frame'], + ancestry: ['html > body > #frame'], + nodeIndexes: [3], + xpath: ['html/body/iframe[1]'] + }; + + it('returns DqElement.mergeSpecs() by default', () => { + const combinedSpec = nodeSerializer.mergeSpecs(nodeSpec, frameSpec); + assert.deepEqual(combinedSpec, DqElement.mergeSpecs(nodeSpec, frameSpec)); + }); + + it('can be replaced with nodeSerializer.update({ mergeSpecs: fn })', () => { + nodeSerializer.update({ + mergeSpecs(childSpec, parentSpec) { + const spec = DqElement.mergeSpecs(childSpec, parentSpec); + spec.source = 'Replaced'; + return spec; + } + }); + const spec = nodeSerializer.mergeSpecs(nodeSpec, frameSpec); + assert.deepEqual(spec, { + ...DqElement.mergeSpecs(nodeSpec, frameSpec), + source: 'Replaced' + }); + }); + }); + + describe('.mapRawNodeResults()', () => { + it('returns undefined when passed undefined', () => { + assert.isUndefined(nodeSerializer.mapRawNodeResults(undefined)); + }); + + it('converts DqElements node to specs', () => { + const dqElm = new DqElement(fixture); + const rawNodeResults = [ + { + any: [], + all: [], + none: [ + { + id: 'nope', + data: null, + relatedNodes: [] + } + ], + node: dqElm, + result: 'failed' + } + ]; + + const serialized = nodeSerializer.mapRawNodeResults(rawNodeResults); + assert.deepEqual(serialized, [ + { + ...rawNodeResults[0], + node: dqElm.toJSON() + } + ]); + }); + + it('converts DqElements relatedNodes to specs', () => { + const dqElm = new DqElement(fixture); + const related = new DqElement(fixture.querySelector('p')); + const rawNodeResults = [ + { + any: [ + { + id: 'something', + data: null, + relatedNodes: [related] + } + ], + all: [ + { + id: 'everything', + data: null, + relatedNodes: [related] + } + ], + none: [ + { + id: 'nope', + data: null, + relatedNodes: [related] + } + ], + node: dqElm, + result: 'failed' + } + ]; + + const serialized = nodeSerializer.mapRawNodeResults(rawNodeResults); + assert.deepEqual(serialized[0].any, [ + { + ...rawNodeResults[0].any[0], + relatedNodes: [related.toJSON()] + } + ]); + assert.deepEqual(serialized[0].all, [ + { + ...rawNodeResults[0].all[0], + relatedNodes: [related.toJSON()] + } + ]); + assert.deepEqual(serialized[0].none, [ + { + ...rawNodeResults[0].none[0], + relatedNodes: [related.toJSON()] + } + ]); + }); + }); + + describe('.mapRawResults()', () => { + it('converts DqElements to specs', () => { + const dqElm = new DqElement(fixture); + const rawNodeResults = [ + { + any: [ + { + id: 'nope', + data: null, + relatedNodes: [dqElm] + } + ], + all: [], + none: [], + node: dqElm, + result: 'failed' + } + ]; + const rawResults = [ + { + id: 'test', + nodes: rawNodeResults + } + ]; + + const serialized = nodeSerializer.mapRawResults(rawResults); + assert.deepEqual(serialized, [ + { + ...rawResults[0], + nodes: [ + { + ...rawNodeResults[0], + node: dqElm.toJSON(), + any: [ + { + ...rawNodeResults[0].any[0], + relatedNodes: [dqElm.toJSON()] + } + ] + } + ] + } + ]); + }); + }); +}); diff --git a/test/core/utils/parse-sameorigin-stylesheet.js b/test/core/utils/parse-sameorigin-stylesheet.js index 9f5c2dca6a..950e67415e 100644 --- a/test/core/utils/parse-sameorigin-stylesheet.js +++ b/test/core/utils/parse-sameorigin-stylesheet.js @@ -1,8 +1,6 @@ -describe('axe.utils.parseSameOriginStylesheet', function () { - 'use strict'; - - var stylesForPage; - var styleSheets = { +describe('axe.utils.parseSameOriginStylesheet', () => { + let stylesForPage; + const styleSheets = { emptyStyleTag: { id: 'emptyStyleTag', text: '' @@ -16,43 +14,43 @@ describe('axe.utils.parseSameOriginStylesheet', function () { text: '.inline-css { font-weight:normal; }' } }; - var dynamicDoc; - var convertDataToStylesheet; + let dynamicDoc; + let convertDataToStylesheet; - beforeEach(function () { + beforeEach(() => { dynamicDoc = document.implementation.createHTMLDocument( 'Dynamic document for testing axe.utils.parseSameOriginStylesheet' ); convertDataToStylesheet = axe.utils.getStyleSheetFactory(dynamicDoc); }); - afterEach(function (done) { + afterEach(done => { dynamicDoc = undefined; convertDataToStylesheet = undefined; - axe.testUtils.removeStyleSheets(stylesForPage).then(function () { + axe.testUtils.removeStyleSheets(stylesForPage).then(() => { done(); stylesForPage = undefined; }); }); - it('returns empty results when given sheet has no cssRules', function (done) { + it('returns empty results when given sheet has no cssRules', done => { // add style that has no styles stylesForPage = [styleSheets.emptyStyleTag]; - axe.testUtils.addStyleSheets(stylesForPage).then(function () { + axe.testUtils.addStyleSheets(stylesForPage).then(() => { // get recently added sheet - var sheet = Array.from(document.styleSheets).filter(function (sheet) { - return sheet.ownerNode.id === styleSheets.emptyStyleTag.id; + const sheet = Array.from(document.styleSheets).filter(styleSheet => { + return styleSheet.ownerNode.id === styleSheets.emptyStyleTag.id; })[0]; // parse sheet - var options = { + const options = { rootNode: document, shadowId: undefined, convertDataToStylesheet: convertDataToStylesheet }; - var priority = [1, 0]; - var importedUrls = []; - var isCrossOriginRequest = false; + const priority = [1, 0]; + const importedUrls = []; + const isCrossOriginRequest = false; axe.utils .parseSameOriginStylesheet( sheet, @@ -73,24 +71,24 @@ describe('axe.utils.parseSameOriginStylesheet', function () { }); }); - it('returns @import rule specified in the stylesheet', function (done) { + it('returns @import rule specified in the stylesheet', done => { // add style that has @import style stylesForPage = [styleSheets.styleTagWithOneImport]; - axe.testUtils.addStyleSheets(stylesForPage).then(function () { + axe.testUtils.addStyleSheets(stylesForPage).then(() => { // get recently added sheet - var sheet = Array.from(document.styleSheets).filter(function (sheet) { - return sheet.ownerNode.id === styleSheets.styleTagWithOneImport.id; + const sheet = Array.from(document.styleSheets).filter(styleSheet => { + return styleSheet.ownerNode.id === styleSheets.styleTagWithOneImport.id; })[0]; // parse sheet - var options = { + const options = { rootNode: document, shadowId: undefined, convertDataToStylesheet: convertDataToStylesheet }; - var priority = [1, 0]; - var importedUrls = []; - var isCrossOriginRequest = false; + const priority = [1, 0]; + const importedUrls = []; + const isCrossOriginRequest = false; axe.utils .parseSameOriginStylesheet( sheet, @@ -102,7 +100,7 @@ describe('axe.utils.parseSameOriginStylesheet', function () { .then(function (data) { assert.isDefined(data); - var parsedImportData = data[0]; + const parsedImportData = data[0]; assert.isDefined(parsedImportData.sheet); assert.equal(parsedImportData.isCrossOrigin, isCrossOriginRequest); // as @import is a style with in @imported sheet, an additional priority is appended. @@ -119,24 +117,24 @@ describe('axe.utils.parseSameOriginStylesheet', function () { }); }); - it('returns inline style specified in the stylesheet', function (done) { + it('returns inline style specified in the stylesheet', done => { // add style that has @import style stylesForPage = [styleSheets.inlineStyle]; - axe.testUtils.addStyleSheets(stylesForPage).then(function () { + axe.testUtils.addStyleSheets(stylesForPage).then(() => { // get recently added sheet - var sheet = Array.from(document.styleSheets).filter(function (sheet) { - return sheet.ownerNode.id === styleSheets.inlineStyle.id; + const sheet = Array.from(document.styleSheets).filter(styleSheet => { + return styleSheet.ownerNode.id === styleSheets.inlineStyle.id; })[0]; // parse sheet - var options = { + const options = { rootNode: document, shadowId: undefined, convertDataToStylesheet: convertDataToStylesheet }; - var priority = [1, 0]; - var importedUrls = []; - var isCrossOriginRequest = false; + const priority = [1, 0]; + const importedUrls = []; + const isCrossOriginRequest = false; axe.utils .parseSameOriginStylesheet( sheet, diff --git a/test/core/utils/selector-cache.js b/test/core/utils/selector-cache.js index 9e6e70deeb..36d555407a 100644 --- a/test/core/utils/selector-cache.js +++ b/test/core/utils/selector-cache.js @@ -1,74 +1,97 @@ -describe('utils.selector-cache', function () { - var fixture = document.querySelector('#fixture'); - var cacheNodeSelectors = +describe('utils.selector-cache', () => { + const fixture = document.querySelector('#fixture'); + const cacheNodeSelectors = axe._thisWillBeDeletedDoNotUse.utils.cacheNodeSelectors; - var getNodesMatchingExpression = + const getNodesMatchingExpression = axe._thisWillBeDeletedDoNotUse.utils.getNodesMatchingExpression; - var convertSelector = axe.utils.convertSelector; - var shadowSupported = axe.testUtils.shadowSupport.v1; + const convertSelector = axe.utils.convertSelector; + const shadowSupported = axe.testUtils.shadowSupport.v1; - var vNode; - beforeEach(function () { + let vNode; + beforeEach(() => { fixture.innerHTML = '<div id="target" class="foo" aria-label="bar"></div>'; vNode = new axe.VirtualNode(fixture.firstChild); }); - describe('cacheNodeSelectors', function () { - it('should add the node to the global selector', function () { - var map = {}; + describe('cacheNodeSelectors', () => { + it('should add the node to the global selector', () => { + const map = {}; cacheNodeSelectors(vNode, map); assert.deepEqual(map['*'], [vNode]); }); - it('should add the node to the nodeName', function () { - var map = {}; + it('should add the node to the nodeName', () => { + const map = {}; cacheNodeSelectors(vNode, map); assert.deepEqual(map.div, [vNode]); }); - it('should add the node to all attribute selectors', function () { - var map = {}; + it('should add the node to all attribute selectors', () => { + const map = {}; cacheNodeSelectors(vNode, map); assert.deepEqual(map['[id]'], [vNode]); assert.deepEqual(map['[class]'], [vNode]); assert.deepEqual(map['[aria-label]'], [vNode]); }); - it('should add the node to the id map', function () { - var map = {}; + it('should add the node to the id map', () => { + const map = {}; cacheNodeSelectors(vNode, map); assert.deepEqual(map[' [idsMap]'].target, [vNode]); }); - it('should not add the node to selectors it does not match', function () { - var map = {}; + it('should not add the node to selectors it does not match', () => { + const map = {}; cacheNodeSelectors(vNode, map); assert.isUndefined(map['[for]']); assert.isUndefined(map.h1); }); - it('should ignore non-element nodes', function () { - var map = {}; + it('should ignore non-element nodes', () => { + const map = {}; fixture.innerHTML = 'Hello'; vNode = new axe.VirtualNode(fixture.firstChild); cacheNodeSelectors(vNode, map); assert.lengthOf(Object.keys(map), 0); }); + + describe('with javascripty attribute selectors', () => { + const terms = [ + 'prototype', + 'constructor', + '__proto__', + 'Element', + 'nodeName', + 'valueOf', + 'toString' + ]; + for (const term of terms) { + it(`works with ${term}`, () => { + fixture.innerHTML = `<div id="${term}" class="${term}" aria-label="${term}"></div>`; + vNode = new axe.VirtualNode(fixture.firstChild); + const map = {}; + cacheNodeSelectors(vNode, map); + assert.deepEqual(map['[id]'], [vNode]); + assert.deepEqual(map['[class]'], [vNode]); + assert.deepEqual(map['[aria-label]'], [vNode]); + }); + } + }); }); - describe('getNodesMatchingExpression', function () { - var tree; - var spanVNode; - var headingVNode; + describe('getNodesMatchingExpression', () => { + let tree; + let spanVNode; + let headingVNode; function createTree() { - for (var i = 0; i < fixture.children.length; i++) { - var child = fixture.children[i]; - var isShadow = child.hasAttribute('data-shadow'); - var html = child.innerHTML; + for (let i = 0; i < fixture.children.length; i++) { + const child = fixture.children[i]; + const isShadow = child.hasAttribute('data-shadow'); + const html = child.innerHTML; if (isShadow) { - var shadowRoot = child.attachShadow({ mode: 'open' }); + const shadowRoot = child.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = html; child.innerHTML = ''; } @@ -77,7 +100,7 @@ describe('utils.selector-cache', function () { return axe.utils.getFlattenedTree(fixture); } - beforeEach(function () { + beforeEach(() => { fixture.firstChild.innerHTML = '<h1><span class="bar" id="not-target" aria-labelledby="target"></span></h1>'; tree = axe.utils.getFlattenedTree(fixture.firstChild); @@ -87,14 +110,14 @@ describe('utils.selector-cache', function () { spanVNode = headingVNode.children[0]; }); - it('should return undefined if the cache is not primed', function () { + it('should return undefined if the cache is not primed', () => { tree[0]._selectorMap = null; - var expression = convertSelector('div'); + const expression = convertSelector('div'); assert.isUndefined(getNodesMatchingExpression(tree, expression)); }); - it('should return a list of matching nodes by global selector', function () { - var expression = convertSelector('*'); + it('should return a list of matching nodes by global selector', () => { + const expression = convertSelector('*'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [ vNode, headingVNode, @@ -102,24 +125,24 @@ describe('utils.selector-cache', function () { ]); }); - it('should return a list of matching nodes by nodeName', function () { - var expression = convertSelector('div'); + it('should return a list of matching nodes by nodeName', () => { + const expression = convertSelector('div'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); - it('should return a list of matching nodes by id', function () { - var expression = convertSelector('#target'); + it('should return a list of matching nodes by id', () => { + const expression = convertSelector('#target'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); (shadowSupported ? it : xit)( 'should only return nodes matching shadowId when matching by id', - function () { + () => { fixture.innerHTML = '<div id="target"></div><div data-shadow><div id="target"></div></div>'; - var tree = createTree(); - var expression = convertSelector('#target'); - var expected = [tree[0].children[0]]; + tree = createTree(); + const expression = convertSelector('#target'); + const expected = [tree[0].children[0]]; assert.deepEqual( getNodesMatchingExpression(tree, expression), expected @@ -127,83 +150,83 @@ describe('utils.selector-cache', function () { } ); - it('should return a list of matching nodes by class', function () { - var expression = convertSelector('.foo'); + it('should return a list of matching nodes by class', () => { + const expression = convertSelector('.foo'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); - it('should return a list of matching nodes by attribute', function () { - var expression = convertSelector('[aria-label]'); + it('should return a list of matching nodes by attribute', () => { + const expression = convertSelector('[aria-label]'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); - it('should return an empty array if selector does not match', function () { - var expression = convertSelector('main'); + it('should return an empty array if selector does not match', () => { + const expression = convertSelector('main'); assert.lengthOf(getNodesMatchingExpression(tree, expression), 0); }); - it('should return an empty array for complex selector that does not match', function () { - var expression = convertSelector('span.missingClass[id]'); + it('should return an empty array for complex selector that does not match', () => { + const expression = convertSelector('span.missingClass[id]'); assert.lengthOf(getNodesMatchingExpression(tree, expression), 0); }); - it('should return an empty array for a non-complex selector that does not match', function () { - var expression = convertSelector('div#not-target[id]'); + it('should return an empty array for a non-complex selector that does not match', () => { + const expression = convertSelector('div#not-target[id]'); assert.lengthOf(getNodesMatchingExpression(tree, expression), 0); }); - it('should return nodes for each expression', function () { + it('should return nodes for each expression', () => { fixture.innerHTML = '<div role="button"></div><span aria-label="other"></span>'; - var tree = createTree(); - var expression = convertSelector('[role], [aria-label]'); - var expected = [tree[0].children[0], tree[0].children[1]]; + tree = createTree(); + const expression = convertSelector('[role], [aria-label]'); + const expected = [tree[0].children[0], tree[0].children[1]]; assert.deepEqual(getNodesMatchingExpression(tree, expression), expected); }); - it('should return nodes for child combinator selector', function () { - var expression = convertSelector('div span'); + it('should return nodes for child combinator selector', () => { + const expression = convertSelector('div span'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [ spanVNode ]); }); - it('should return nodes for direct child combinator selector', function () { - var expression = convertSelector('div > h1'); + it('should return nodes for direct child combinator selector', () => { + const expression = convertSelector('div > h1'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [ headingVNode ]); }); - it('should not return nodes for direct child combinator selector that does not match', function () { - var expression = convertSelector('div > span'); + it('should not return nodes for direct child combinator selector that does not match', () => { + const expression = convertSelector('div > span'); assert.lengthOf(getNodesMatchingExpression(tree, expression), 0); }); - it('should return nodes for attribute value selector', function () { - var expression = convertSelector('[id="target"]'); + it('should return nodes for attribute value selector', () => { + const expression = convertSelector('[id="target"]'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); - it('should return undefined for combinator selector with global selector', function () { - var expression = convertSelector('body *'); + it('should return undefined for combinator selector with global selector', () => { + const expression = convertSelector('body *'); assert.isUndefined(getNodesMatchingExpression(tree, expression)); }); - it('should return nodes for multipart selectors', function () { - var expression = convertSelector('div.foo[id]'); + it('should return nodes for multipart selectors', () => { + const expression = convertSelector('div.foo[id]'); assert.deepEqual(getNodesMatchingExpression(tree, expression), [vNode]); }); - it('should remove duplicates', function () { + it('should remove duplicates', () => { fixture.innerHTML = '<div role="button" aria-label="other"></div>'; - var tree = createTree(); - var expression = convertSelector('div[role], [aria-label]'); - var expected = [tree[0].children[0]]; + tree = createTree(); + const expression = convertSelector('div[role], [aria-label]'); + const expected = [tree[0].children[0]]; assert.deepEqual(getNodesMatchingExpression(tree, expression), expected); }); - it('should sort nodes by added order', function () { + it('should sort nodes by added order', () => { fixture.innerHTML = '<div id="id0"></div>' + '<span id="id1"></span>' + @@ -217,10 +240,10 @@ describe('utils.selector-cache', function () { '<span id="id9"></span>'; tree = createTree(); - var expression = convertSelector('div, span'); - var nodes = getNodesMatchingExpression(tree, expression); - var ids = []; - for (var i = 0; i < nodes.length; i++) { + const expression = convertSelector('div, span'); + const nodes = getNodesMatchingExpression(tree, expression); + const ids = []; + for (let i = 0; i < nodes.length; i++) { ids.push(nodes[i].attr('id')); } @@ -239,31 +262,31 @@ describe('utils.selector-cache', function () { ]); }); - it('should filter nodes', function () { + it('should filter nodes', () => { fixture.innerHTML = '<div role="button" aria-label="other"></div><div></div>'; - var tree = createTree(); + tree = createTree(); function filter(node) { return node.hasAttr('role'); } - var nonFilteredNodes = getNodesMatchingExpression( + const nonFilteredNodes = getNodesMatchingExpression( tree, convertSelector('div, [aria-label]') ); - var nonFilteredExpected = [ + const nonFilteredExpected = [ tree[0], tree[0].children[0], tree[0].children[1] ]; - var filteredNodes = getNodesMatchingExpression( + const filteredNodes = getNodesMatchingExpression( tree, convertSelector('div, [aria-label]'), filter ); - var filteredExpected = [tree[0].children[0]]; + const filteredExpected = [tree[0].children[0]]; assert.deepEqual(nonFilteredNodes, nonFilteredExpected); assert.deepEqual(filteredNodes, filteredExpected); diff --git a/test/core/utils/send-command-to-frame.js b/test/core/utils/send-command-to-frame.js index 7cc1e109bf..0fe4412e19 100644 --- a/test/core/utils/send-command-to-frame.js +++ b/test/core/utils/send-command-to-frame.js @@ -1,23 +1,25 @@ -describe('axe.utils.sendCommandToFrame', function () { - 'use strict'; +describe('axe.utils.sendCommandToFrame', () => { + const fixture = document.getElementById('fixture'); + let params; + const captureError = axe.testUtils.captureError; - var fixture = document.getElementById('fixture'); - var params = { command: 'rules' }; - var captureError = axe.testUtils.captureError; + beforeEach(() => { + params = { command: 'rules' }; + }); - afterEach(function () { + afterEach(() => { fixture.innerHTML = ''; axe._tree = undefined; axe._selectorData = undefined; }); - var assertNotCalled = function () { + const assertNotCalled = () => { assert.ok(false, 'should not be called'); }; - it('should return results from frames', function (done) { - var frame = document.createElement('iframe'); - frame.addEventListener('load', function () { + it('should return results from frames', done => { + const frame = document.createElement('iframe'); + frame.addEventListener('load', () => { axe.utils.sendCommandToFrame( frame, params, @@ -26,7 +28,7 @@ describe('axe.utils.sendCommandToFrame', function () { assert.equal(res[0].id, 'html'); done(); }, done), - function () { + () => { done(new Error('sendCommandToFrame should not error')); } ); @@ -37,15 +39,15 @@ describe('axe.utils.sendCommandToFrame', function () { fixture.appendChild(frame); }); - it('adjusts skips ping with options.pingWaitTime=0', function (done) { - var frame = document.createElement('iframe'); - var params = { + it('adjusts skips ping with options.pingWaitTime=0', done => { + const frame = document.createElement('iframe'); + params = { command: 'rules', options: { pingWaitTime: 0 } }; - frame.addEventListener('load', function () { - var topics = []; + frame.addEventListener('load', () => { + const topics = []; frame.contentWindow.addEventListener('message', function (event) { try { topics.push(JSON.parse(event.data).topic); @@ -56,7 +58,7 @@ describe('axe.utils.sendCommandToFrame', function () { axe.utils.sendCommandToFrame( frame, params, - captureError(function () { + captureError(() => { try { assert.deepEqual(topics, ['axe.start']); done(); @@ -64,7 +66,7 @@ describe('axe.utils.sendCommandToFrame', function () { done(e); } }, done), - function () { + () => { done(new Error('sendCommandToFrame should not error')); } ); @@ -75,8 +77,8 @@ describe('axe.utils.sendCommandToFrame', function () { fixture.appendChild(frame); }); - it('should timeout if there is no response from frame', function (done) { - var orig = window.setTimeout; + it('should timeout if there is no response from frame', done => { + const orig = window.setTimeout; window.setTimeout = function (fn, to) { if (to === 30000) { assert.ok('timeout set'); @@ -88,8 +90,8 @@ describe('axe.utils.sendCommandToFrame', function () { return 'cats'; }; - var frame = document.createElement('iframe'); - frame.addEventListener('load', function () { + const frame = document.createElement('iframe'); + frame.addEventListener('load', () => { axe._tree = axe.utils.getFlattenedTree(document.documentElement); axe.utils.sendCommandToFrame( frame, diff --git a/test/integration/adapter.js b/test/integration/adapter.js index d082612253..4848785ee2 100644 --- a/test/integration/adapter.js +++ b/test/integration/adapter.js @@ -1,5 +1,14 @@ /*global mocha */ -var failedTests = []; +const failedTests = []; +function flattenTitles(test) { + const titles = []; + while (test.parent.title) { + titles.push(test.parent.title); + test = test.parent; + } + return titles.reverse(); +} + (function () { 'use strict'; @@ -9,14 +18,6 @@ var failedTests = []; window.mochaResults.reports = failedTests; }); runner.on('fail', function logFailure(test, err) { - var flattenTitles = function (test) { - var titles = []; - while (test.parent.title) { - titles.push(test.parent.title); - test = test.parent; - } - return titles.reverse(); - }; failedTests.push({ name: test.title, result: false, diff --git a/test/integration/api/external/index.js b/test/integration/api/external/index.js index 75cdbc8bba..f15ced95d7 100644 --- a/test/integration/api/external/index.js +++ b/test/integration/api/external/index.js @@ -1,52 +1,50 @@ // describes the API used by axe Pro -describe('external API', function () { - 'use strict'; - - afterEach(function () { +describe('external API', () => { + afterEach(() => { // setup _tree as needed, always reset axe._tree = null; }); - describe('axe.commons.text.sanitize', function () { - it('must be a function with the signature String -> String', function () { + describe('axe.commons.text.sanitize', () => { + it('must be a function with the signature String -> String', () => { assert.isString(axe.commons.text.sanitize('')); assert.isString(axe.commons.text.sanitize('not empty')); }); }); - describe('axe.utils.getBaseLang', function () { - it('must be a function with the signature String -> String', function () { + describe('axe.utils.getBaseLang', () => { + it('must be a function with the signature String -> String', () => { assert.isString(axe.utils.getBaseLang('')); assert.isString(axe.utils.getBaseLang('not empty')); }); }); - describe('axe.utils.validLangs', function () { - it('be a function with the signature * -> [String]', function () { - var langs = axe.utils.validLangs(); + describe('axe.utils.validLangs', () => { + it('be a function with the signature * -> [String]', () => { + const langs = axe.utils.validLangs(); assert.isArray(langs); langs.forEach(assert.isString); assert.isArray(axe.utils.validLangs(document)); }); }); - describe('axe.commons.dom.isVisible', function () { - it('must be a function with the signature Element -> Boolean', function () { - var el = randomNodeInTree(isElement); + describe('axe.commons.dom.isVisible', () => { + it('must be a function with the signature Element -> Boolean', () => { + const el = randomNodeInTree(isElement); assert.isBoolean(axe.commons.dom.isVisible(el)); assert.isBoolean(axe.commons.dom.isVisible(randomNodeInTree(isElement))); }); }); - describe('axe.commons.aria.implicitRole', function () { - it('must be a function with the signature Element -> String|null', function () { + describe('axe.commons.aria.implicitRole', () => { + it('must be a function with the signature Element -> String|null', () => { axe.utils.getFlattenedTree(document.documentElement); - var implicitRolesOrNull = getEntries( + const implicitRolesOrNull = getEntries( axe.commons.aria.lookupTable.role ).reduce( function (roles, entry) { - var role = entry[0]; - var val = entry[1]; + const role = entry[0]; + const val = entry[1]; if (val.implicit) { roles.push(role); } @@ -54,18 +52,18 @@ describe('external API', function () { }, [null] ); - var role = axe.commons.aria.implicitRole(randomNodeInTree()); + const role = axe.commons.aria.implicitRole(randomNodeInTree()); assert.oneOf(role, implicitRolesOrNull); }); }); - describe('axe.commons.aria.lookupTable.role', function () { - it('must be an object (dict)', function () { + describe('axe.commons.aria.lookupTable.role', () => { + it('must be an object (dict)', () => { assert.isObject(axe.commons.aria.lookupTable.role); }); - it('must have the signature String -> String {role.type}', function () { - var keys = getKeys(axe.commons.aria.lookupTable.role); - var types = getValues(axe.commons.aria.lookupTable.role).map(function ( + it('must have the signature String -> String {role.type}', () => { + const keys = getKeys(axe.commons.aria.lookupTable.role); + const types = getValues(axe.commons.aria.lookupTable.role).map(function ( role ) { return role.type; @@ -75,8 +73,8 @@ describe('external API', function () { }); }); - describe('axe.utils.getFlattenedTree', function () { - it('must be a function with the signature Element -> [vnode]', function () { + describe('axe.utils.getFlattenedTree', () => { + it('must be a function with the signature Element -> [vnode]', () => { assert.isArray(axe.utils.getFlattenedTree(document.body)); assert.lengthOf( axe.utils.getFlattenedTree(randomNodeInTree(isElement)), @@ -85,48 +83,48 @@ describe('external API', function () { }); }); - describe('axe.utils.getNodeFromTree', function () { - it('must be a function with the signature Node -> vnode', function () { + describe('axe.utils.getNodeFromTree', () => { + it('must be a function with the signature Node -> vnode', () => { axe._tree = axe.utils.getFlattenedTree(document.body); assert.oneOf( axe.utils.getNodeFromTree(randomNodeInTree()), flat(axe._tree[0]) ); }); - it('must return null for nodes not in the axe._tree', function () { + it('must return null for nodes not in the axe._tree', () => { assert.isNull(axe.utils.getNodeFromTree(randomElement())); }); }); - describe('axe.commons.dom.isOpaque', function () { - it('must be a function with the signature Element -> Boolean', function () { + describe('axe.commons.dom.isOpaque', () => { + it('must be a function with the signature Element -> Boolean', () => { assert.isBoolean(axe.commons.dom.isOpaque(randomNodeInTree(isElement))); }); }); - describe('axe.commons.text.accessibleTextVirtual', function () { - it('must be a function with the signature Element vnode -> String', function () { + describe('axe.commons.text.accessibleTextVirtual', () => { + it('must be a function with the signature Element vnode -> String', () => { axe._tree = axe.utils.getFlattenedTree(document.body); - var vnode = axe.utils.getNodeFromTree(randomNodeInTree(isElement)); + const vnode = axe.utils.getNodeFromTree(randomNodeInTree(isElement)); assert.isString(axe.commons.text.accessibleTextVirtual(vnode)); }); }); }); -var elements = [ +const elements = [ document.createElement('div'), document.createElement('button'), document.createElement('article') ]; -var inTree = []; -var walker = collectNodes(); -var next = walker.iterate().next(); +const inTree = []; +const treeWalker = collectNodes(); +let next = treeWalker.iterate().next(); while (!next.done) { if (next.value.nodeType === 1) { inTree.push(next.value); } - next = walker.iterate().next(); + next = treeWalker.iterate().next(); } function isElement(el) { @@ -136,8 +134,8 @@ function isElement(el) { function random(fromArr) { return function (filter) { filter = filter || isTrue; - var arr = fromArr.filter(filter); - var seed = Math.random(); + const arr = fromArr.filter(filter); + const seed = Math.random(); return arr[Math.floor(seed * arr.length)]; }; } @@ -152,7 +150,7 @@ function randomElement(filter) { // mimic tree: body and all element and text children function collectNodes() { - var walker = document.createTreeWalker( + const walker = document.createTreeWalker( document, NodeFilter.SHOW_ALL, function (node) { @@ -163,22 +161,22 @@ function collectNodes() { }, false ); - var next = function () { - var value = walker.nextNode(); + const nextNode = () => { + const value = walker.nextNode(); return { value: value, done: !value }; }; - walker.iterate = function () { - return { next: next }; + walker.iterate = () => { + return { next: nextNode }; }; return walker; } function flat(tree) { - var result = []; - var insert = function (node) { + const result = []; + const insert = function (node) { result.push(node); (node.children || []).forEach(insert); }; @@ -193,8 +191,8 @@ function isTrue() { } function getEntries(obj) { - var results = []; - var key; + const results = []; + let key; for (key in obj) { if (obj.hasOwnProperty(key)) { results.push([key, obj[key]]); diff --git a/test/integration/full/all-rules/all-rules.html b/test/integration/full/all-rules/all-rules.html index 05c32b4ad4..631b2d2e32 100644 --- a/test/integration/full/all-rules/all-rules.html +++ b/test/integration/full/all-rules/all-rules.html @@ -28,11 +28,11 @@ > <banner></banner> <main> - <div accesskey="B"></div> + <div accesskey="B" id="__proto__"></div> <map> <area href="#" id="pass1" alt="monkeys" /> </map> - <div aria-label="foo">Foo</div> + <div aria-label="foo" id="constructor">Foo</div> <div role="contentinfo"></div> <div role="link">Home</div> <div role="dialog" aria-label="Cookies"></div> @@ -51,7 +51,7 @@ <div role="treeitem">Item</div> </div> <audio id="caption"><track kind="captions" /></audio> - <input autocomplete="username" /> + <input autocomplete="username" id="toString" /> <p id="fail1" style="line-height: 1.5 !important">Banana error</p> <p><blink>text</blink></p> <button id="text">Name</button> @@ -68,7 +68,7 @@ <h2>Ok</h2> </tr> </table> - <img src="img.jpg" alt="" /> + <img src="img.jpg" alt="" aria-braillelabel="image" /> <video><track kind="captions" /></video> <svg xmlns="http://www.w3.org/2000/svg" diff --git a/test/integration/full/configure-options/configure-options.js b/test/integration/full/configure-options/configure-options.js index b90456c75c..43f83bb7f8 100644 --- a/test/integration/full/configure-options/configure-options.js +++ b/test/integration/full/configure-options/configure-options.js @@ -1,16 +1,14 @@ -describe('Configure Options', function () { - 'use strict'; +describe('Configure Options', () => { + const target = document.querySelector('#target'); - var target = document.querySelector('#target'); - - afterEach(function () { + afterEach(() => { axe.reset(); target.innerHTML = ''; }); - describe('Check', function () { - describe('aria-allowed-attr', function () { - it('should allow an attribute supplied in options', function (done) { + describe('Check', () => { + describe('aria-allowed-attr', () => { + it('should allow an attribute supplied in options', done => { target.setAttribute('role', 'separator'); target.setAttribute('aria-valuenow', '0'); @@ -37,7 +35,7 @@ describe('Configure Options', function () { ); }); - it('should not normalize external check options', function (done) { + it('should not normalize external check options', done => { target.setAttribute('lang', 'en'); axe.configure({ @@ -46,7 +44,7 @@ describe('Configure Options', function () { id: 'dylang', options: ['dylan'], evaluate: - 'function (node, options) {\n var lang = (node.getAttribute("lang") || "").trim().toLowerCase();\n var xmlLang = (node.getAttribute("xml:lang") || "").trim().toLowerCase();\n var invalid = [];\n (options || []).forEach(function(cc) {\n cc = cc.toLowerCase();\n if (lang && (lang === cc || lang.indexOf(cc.toLowerCase() + "-") === 0)) {\n lang = null;\n }\n if (xmlLang && (xmlLang === cc || xmlLang.indexOf(cc.toLowerCase() + "-") === 0)) {\n xmlLang = null;\n }\n });\n if (xmlLang) {\n invalid.push(\'xml:lang="\' + xmlLang + \'"\');\n }\n if (lang) {\n invalid.push(\'lang="\' + lang + \'"\');\n }\n if (invalid.length) {\n this.data(invalid);\n return true;\n }\n return false;\n }', + 'function (node, options) {\n const lang = (node.getAttribute("lang") || "").trim().toLowerCase();\n const xmlLang = (node.getAttribute("xml:lang") || "").trim().toLowerCase();\n const invalid = [];\n (options || []).forEach(function(cc) {\n cc = cc.toLowerCase();\n if (lang && (lang === cc || lang.indexOf(cc.toLowerCase() + "-") === 0)) {\n lang = null;\n }\n if (xmlLang && (xmlLang === cc || xmlLang.indexOf(cc.toLowerCase() + "-") === 0)) {\n xmlLang = null;\n }\n });\n if (xmlLang) {\n invalid.push(\'xml:lang="\' + xmlLang + \'"\');\n }\n if (lang) {\n invalid.push(\'lang="\' + lang + \'"\');\n }\n if (invalid.length) {\n this.data(invalid);\n return true;\n }\n return false;\n }', messages: { pass: 'Good language', fail: 'You mst use the DYLAN language' @@ -100,8 +98,8 @@ describe('Configure Options', function () { }); }); - describe('aria-required-attr', function () { - it('should report unique attributes when supplied from options', function (done) { + describe('aria-required-attr', () => { + it('should report unique attributes when supplied from options', done => { target.setAttribute('role', 'slider'); axe.configure({ checks: [ @@ -131,8 +129,8 @@ describe('Configure Options', function () { }); }); - describe('disableOtherRules', function () { - it('disables rules that are not in the `rules` array', function (done) { + describe('disableOtherRules', () => { + it('disables rules that are not in the `rules` array', done => { axe.configure({ disableOtherRules: true, rules: [ @@ -160,9 +158,9 @@ describe('Configure Options', function () { }); }); - describe('noHtml', function () { - var captureError = axe.testUtils.captureError; - it('prevents html property on nodes', function (done) { + describe('noHtml', () => { + const captureError = axe.testUtils.captureError; + it('prevents html property on nodes', done => { target.setAttribute('role', 'slider'); axe.configure({ noHtml: true, @@ -189,8 +187,8 @@ describe('Configure Options', function () { ); }); - it('prevents html property on nodes from iframes', function (done) { - var config = { + it('prevents html property on nodes from iframes', done => { + const config = { noHtml: true, rules: [ { @@ -202,11 +200,10 @@ describe('Configure Options', function () { ] }; - var iframe = document.createElement('iframe'); - iframe.src = '/test/mock/frames/context.html'; - iframe.onload = function () { + const iframe = document.createElement('iframe'); + iframe.src = '/test/mock/frames/noHtml-config.html'; + iframe.onload = () => { axe.configure(config); - axe.run( '#target', { @@ -229,8 +226,8 @@ describe('Configure Options', function () { target.appendChild(iframe); }); - it('prevents html property in postMesage', function (done) { - var config = { + it('prevents html property in postMesage', done => { + const config = { noHtml: true, rules: [ { @@ -242,9 +239,9 @@ describe('Configure Options', function () { ] }; - var iframe = document.createElement('iframe'); + const iframe = document.createElement('iframe'); iframe.src = '/test/mock/frames/noHtml-config.html'; - iframe.onload = function () { + iframe.onload = () => { axe.configure(config); axe.run('#target', { @@ -256,14 +253,14 @@ describe('Configure Options', function () { }; target.appendChild(iframe); - window.addEventListener('message', function (e) { - var data = JSON.parse(e.data); + window.addEventListener('message', function (evt) { + const data = JSON.parse(evt.data); if (Array.isArray(data.payload)) { try { assert.isNull(data.payload[0].nodes[0].node.source); done(); - } catch (e) { - done(e); + } catch (err) { + done(err); } } }); diff --git a/test/integration/full/contrast/blending.js b/test/integration/full/contrast/blending.js index 90d1ed7767..1a75035a56 100644 --- a/test/integration/full/contrast/blending.js +++ b/test/integration/full/contrast/blending.js @@ -1,7 +1,7 @@ -describe('color-contrast blending test', function () { - var include = []; - var resultElms = []; - var expected = [ +describe('color-contrast blending test', () => { + const include = []; + const resultElms = []; + const expected = [ // normal 'rgb(223, 112, 96)', 'rgb(255, 128, 128)', @@ -124,8 +124,8 @@ describe('color-contrast blending test', function () { 'rgb(198, 198, 198)' ]; - var fixture = document.querySelector('#fixture'); - var testGroup = document.querySelector('.test-group'); + const fixture = document.querySelector('#fixture'); + const testGroup = document.querySelector('.test-group'); [ 'multiply', 'screen', @@ -138,19 +138,19 @@ describe('color-contrast blending test', function () { 'soft-light', 'difference', 'exclusion' - ].forEach(function (blendMode) { - var nodes = testGroup.cloneNode(true); - var group = testGroup.cloneNode(); + ].forEach(blendMode => { + const nodes = testGroup.cloneNode(true); + const group = testGroup.cloneNode(); - var heading = document.createElement('h2'); + const 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); + Array.from(nodes.children).forEach((node, index) => { + const id = node.id; + const target = node.querySelector('#' + id + '-target'); + const result = node.querySelector('#' + id + '-result'); + const blendModeIndex = blendMode + (index + 1); node.id = blendModeIndex; target.id = blendModeIndex + '-target'; @@ -165,37 +165,37 @@ describe('color-contrast blending test', function () { 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'); - var result = testElm.querySelector('#' + id + '-result'); + const testElms = Array.from(document.querySelectorAll('.test-group > div')); + testElms.forEach(testElm => { + const id = testElm.id; + const target = testElm.querySelector('#' + id + '-target'); + const result = testElm.querySelector('#' + id + '-result'); include.push(target); resultElms.push(result); }); - before(function (done) { + before(done => { axe.run( { include: include }, { runOnly: ['color-contrast'] }, - function (err, res) { + (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 = [] + const 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( + results.forEach(result => { + result.nodes.forEach(node => { + const bgColor = node.any[0].data.bgColor; + const id = node.target[0].substring( 0, node.target[0].lastIndexOf('-') ); - var result = document.querySelector(id + '-result'); - result.style.backgroundColor = bgColor; + const resultNode = document.querySelector(id + '-result'); + resultNode.style.backgroundColor = bgColor; }); }); @@ -204,9 +204,9 @@ describe('color-contrast blending test', function () { ); }); - resultElms.forEach(function (elm, index) { - it('produces the correct blended color for ' + elm.id, function () { - var style = window.getComputedStyle(elm); + resultElms.forEach((elm, index) => { + it('produces the correct blended color for ' + elm.id, () => { + const style = window.getComputedStyle(elm); assert.equal(style.getPropertyValue('background-color'), expected[index]); }); }); diff --git a/test/integration/full/contrast/memory.html b/test/integration/full/contrast/memory.html new file mode 100644 index 0000000000..641a26ad64 --- /dev/null +++ b/test/integration/full/contrast/memory.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Test Page</title> + <link + rel="stylesheet" + type="text/css" + href="/node_modules/mocha/mocha.css" + /> + <script src="/node_modules/mocha/mocha.js"></script> + <script src="/node_modules/chai/chai.js"></script> + <script src="/axe.js"></script> + <script> + mocha.setup({ + timeout: 2000, + ui: 'bdd' + }); + var assert = chai.assert; + </script> + <script src="/test/integration/no-ui-reporter.js"></script> + <style> + * { + box-sizing: border-box; + } + html, + body { + padding: 0; + margin: 0; + height: 100%; + background: #fff; + color: #000; + } + </style> + </head> + <body> + <div id="fixture"> + <div style="overflow: hidden; width: 200px; height: 200px"> + <!-- browsers each have their own CSS size limit so this number (which should be greater than the limit) will be capped to that --> + <div style="width: 1e9px; height: 1e9px">Hello World</div> + </div> + </div> + <script src="memory.js"></script> + <script src="/test/integration/adapter.js"></script> + </body> +</html> diff --git a/test/integration/full/contrast/memory.js b/test/integration/full/contrast/memory.js new file mode 100644 index 0000000000..ed78d06d46 --- /dev/null +++ b/test/integration/full/contrast/memory.js @@ -0,0 +1,18 @@ +/* + Since we can't easily test for memory issue we'll assume that if this test doesn't time out then the memory issue isn't a problem +*/ +describe('color-contrast memory test', () => { + describe('violations', () => { + it('should find none', done => { + axe.run( + '#fixture', + { runOnly: { type: 'rule', values: ['color-contrast'] } }, + (err, results) => { + assert.isNull(err); + assert.lengthOf(results.violations, 0); + done(); + } + ); + }); + }); +}); diff --git a/test/integration/full/css-orientation-lock/violations.js b/test/integration/full/css-orientation-lock/violations.js index ff38cfd788..e3821dbf44 100644 --- a/test/integration/full/css-orientation-lock/violations.js +++ b/test/integration/full/css-orientation-lock/violations.js @@ -1,9 +1,7 @@ -describe('css-orientation-lock violations test', function () { - 'use strict'; +describe('css-orientation-lock violations test', () => { + const shadowSupported = axe.testUtils.shadowSupport.v1; - var shadowSupported = axe.testUtils.shadowSupport.v1; - - var styleSheets = [ + const styleSheets = [ { href: 'https://stackpath.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' }, @@ -12,26 +10,26 @@ describe('css-orientation-lock violations test', function () { } ]; - before(function (done) { + before(done => { axe.testUtils .addStyleSheets(styleSheets) - .then(function () { + .then(() => { done(); }) - .catch(function (error) { + .catch(error => { done(new Error('Could not load stylesheets for testing. ' + error)); }); }); function assertViolatedSelectors(relatedNodes, violatedSelectors) { - relatedNodes.forEach(function (node) { - var target = node.target[0]; - var className = Array.isArray(target) ? target.reverse()[0] : target; + relatedNodes.forEach(node => { + const target = node.target[0]; + const className = Array.isArray(target) ? target.reverse()[0] : target; assert.isTrue(violatedSelectors.indexOf(className) !== -1); }); } - it('returns VIOLATIONS if preload is set to TRUE', function (done) { + it('returns VIOLATIONS if preload is set to TRUE', done => { // the sheets included in the html, have styles for transform and rotate, hence the violation axe.run( { @@ -40,7 +38,7 @@ describe('css-orientation-lock violations test', function () { values: ['css-orientation-lock'] } }, - function (err, res) { + (err, res) => { try { assert.isNull(err); assert.isDefined(res); @@ -50,11 +48,11 @@ describe('css-orientation-lock violations test', function () { assert.lengthOf(res.violations, 1); // assert the node - var checkedNode = res.violations[0].nodes[0]; + const checkedNode = res.violations[0].nodes[0]; assert.isTrue(/html/i.test(checkedNode.html)); // assert the relatedNodes - var checkResult = checkedNode.all[0]; + const checkResult = checkedNode.all[0]; assert.lengthOf(checkResult.relatedNodes, 4); assertViolatedSelectors(checkResult.relatedNodes, [ '.someDiv', @@ -64,8 +62,8 @@ describe('css-orientation-lock violations test', function () { ]); done(); - } catch (err) { - done(err); + } catch (e) { + done(e); } } ); @@ -73,9 +71,9 @@ describe('css-orientation-lock violations test', function () { (shadowSupported ? it : xit)( 'returns VIOLATIONS whilst also accommodating shadowDOM styles', - function (done) { - var fixture = document.getElementById('shadow-fixture'); - var shadow = fixture.attachShadow({ mode: 'open' }); + done => { + const fixture = document.getElementById('shadow-fixture'); + const shadow = fixture.attachShadow({ mode: 'open' }); shadow.innerHTML = '<style> @media screen and (min-width: 10px) and (max-width: 2000px) and (orientation: portrait) { .shadowDiv { transform: rotate3d(0,0,1,90deg); } } .green { background-color: green; } </style>' + '<div class="green">green</div>' + @@ -88,7 +86,7 @@ describe('css-orientation-lock violations test', function () { values: ['css-orientation-lock'] } }, - function (err, res) { + (err, res) => { try { assert.isNull(err); assert.isDefined(res); @@ -98,11 +96,11 @@ describe('css-orientation-lock violations test', function () { assert.lengthOf(res.violations, 1); // assert the node - var checkedNode = res.violations[0].nodes[0]; + const checkedNode = res.violations[0].nodes[0]; assert.isTrue(/html/i.test(checkedNode.html)); // assert the relatedNodes - var checkResult = checkedNode.all[0]; + const checkResult = checkedNode.all[0]; assert.lengthOf(checkResult.relatedNodes, 5); assertViolatedSelectors(checkResult.relatedNodes, [ '.someDiv', @@ -113,8 +111,8 @@ describe('css-orientation-lock violations test', function () { ]); done(); - } catch (err) { - done(err); + } catch (e) { + done(e); } } ); diff --git a/test/integration/full/dialog/dialog.html b/test/integration/full/dialog/dialog.html index e1168db4cf..07847f14d3 100644 --- a/test/integration/full/dialog/dialog.html +++ b/test/integration/full/dialog/dialog.html @@ -22,12 +22,12 @@ <body> <div id="target"> <button id="root-button"></button> - <div style="background-color: #333"> + <div style="background-color: #333; color: #000"> <span id="root-color">Contrast failure</span> </div> <dialog> <button id="dialog-button"></button> - <div style="background-color: #333"> + <div style="background-color: #333; color: #000"> <span id="dialog-color">Contrast failure</span> </div> </dialog> diff --git a/test/integration/full/isolated-env/isolated-env.html b/test/integration/full/isolated-env/isolated-env.html index ddf770cfd6..e1d9d751e6 100644 --- a/test/integration/full/isolated-env/isolated-env.html +++ b/test/integration/full/isolated-env/isolated-env.html @@ -68,7 +68,7 @@ <h2>Ok</h2> </tr> </table> - <img src="img.jpg" alt="" /> + <img src="img.jpg" alt="" aria-braillelabel="my image" /> <video><track kind="captions" /></video> <svg xmlns="http://www.w3.org/2000/svg" diff --git a/test/integration/full/landmark-no-duplicate-banner/frames/level1.html b/test/integration/full/landmark-no-duplicate-banner/frames/level1.html index 2bc9f751fb..118a97d857 100644 --- a/test/integration/full/landmark-no-duplicate-banner/frames/level1.html +++ b/test/integration/full/landmark-no-duplicate-banner/frames/level1.html @@ -21,5 +21,20 @@ <section> <header>Header in section</header> </section> + <div role="article"> + <header>Header in role=article</header> + </div> + <div role="complementary"> + <header>Header in role=complementary</header> + </div> + <div role="main"> + <header>Header in role=main landmark</header> + </div> + <div role="navigation"> + <header>Header in role=navigation</header> + </div> + <div role="region"> + <header>Header in role=region</header> + </div> </body> </html> diff --git a/test/integration/full/landmark-no-duplicate-contentinfo/frames/level1.html b/test/integration/full/landmark-no-duplicate-contentinfo/frames/level1.html index 8d5fcbcab5..244e53f85e 100644 --- a/test/integration/full/landmark-no-duplicate-contentinfo/frames/level1.html +++ b/test/integration/full/landmark-no-duplicate-contentinfo/frames/level1.html @@ -21,5 +21,20 @@ <section> <footer>Footer in section</footer> </section> + <div role="article"> + <footer>Footer in role=article</footer> + </div> + <div role="complementary"> + <footer>Footer in role=complementary</footer> + </div> + <div role="main"> + <footer>Footer in role=main landmark</footer> + </div> + <div role="navigation"> + <footer>Footer in role=navigation</footer> + </div> + <div role="region"> + <footer>Footer in role=region</footer> + </div> </body> </html> diff --git a/test/integration/full/preload-cssom/preload-cssom.js b/test/integration/full/preload-cssom/preload-cssom.js index 151b66cfb9..24a998063c 100644 --- a/test/integration/full/preload-cssom/preload-cssom.js +++ b/test/integration/full/preload-cssom/preload-cssom.js @@ -1,4 +1,3 @@ -/* global axe */ describe('preload cssom integration test', function () { 'use strict'; @@ -7,8 +6,8 @@ describe('preload cssom integration test', function () { // time to resolve tests that we want to throw this.timeout(15000); - var shadowSupported = axe.testUtils.shadowSupport.v1; - var styleSheets = { + const shadowSupported = axe.testUtils.shadowSupport.v1; + const styleSheets = { crossOriginLinkHref: { id: 'crossOriginLinkHref', href: 'https://stackpath.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' @@ -43,11 +42,11 @@ describe('preload cssom integration test', function () { text: '@import "cyclic-cross-origin-import-1.css";' } }; - var stylesForPage; - var nestedFrame; + let stylesForPage; + let nestedFrame; - before(function (done) { - axe.testUtils.awaitNestedLoad(function () { + before(done => { + axe.testUtils.awaitNestedLoad(() => { nestedFrame = document.getElementById('frame1').contentDocument; done(); }); @@ -56,10 +55,10 @@ describe('preload cssom integration test', function () { function attachStylesheets(options, callback) { axe.testUtils .addStyleSheets(options.styles, options.root) - .then(function () { + .then(() => { callback(); }) - .catch(function (error) { + .catch(error => { callback(new Error('Could not load stylesheets for testing. ' + error)); }); } @@ -70,37 +69,37 @@ describe('preload cssom integration test', function () { } axe.testUtils .removeStyleSheets(stylesForPage) - .then(function () { + .then(() => { done(); stylesForPage = undefined; }) - .catch(function (err) { + .catch(err => { done(err); stylesForPage = undefined; }); } function getPreloadCssom(root) { - var treeRoot = axe.utils.getFlattenedTree(root ? root : document); + const treeRoot = axe.utils.getFlattenedTree(root ? root : document); return axe.utils.preloadCssom({ treeRoot: treeRoot }); } function commonTestsForRootNodeAndNestedFrame(root) { - it('returns cross-origin stylesheet', function (done) { + it('returns cross-origin stylesheet', done => { stylesForPage = [styleSheets.crossOriginLinkHref]; attachStylesheets( { root: root, styles: stylesForPage }, - function (err) { + err => { if (err) { done(err); } getPreloadCssom(root) - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); - var sheetData = sheets[0].sheet; + const sheetData = sheets[0].sheet; axe.testUtils.assertStylesheet( sheetData, ':root', @@ -109,7 +108,7 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }, @@ -117,23 +116,23 @@ describe('preload cssom integration test', function () { ); }); - it('returns no stylesheets when cross-origin stylesheets are of media=print', function (done) { + it('returns no stylesheets when cross-origin stylesheets are of media=print', done => { stylesForPage = [styleSheets.crossOriginLinkHrefMediaPrint]; attachStylesheets( { root: root, styles: stylesForPage }, - function (err) { + err => { if (err) { done(err); } getPreloadCssom(root) - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 0); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }, @@ -141,7 +140,7 @@ describe('preload cssom integration test', function () { ); }); - it('throws if cross-origin stylesheet fail to load', function (done) { + it('throws if cross-origin stylesheet fail to load', done => { stylesForPage = [ { id: 'nonExistingStylesheet', @@ -153,16 +152,16 @@ describe('preload cssom integration test', function () { root: root, styles: stylesForPage }, - function (err) { + err => { if (err) { done(err); } getPreloadCssom(root) - .then(function () { + .then(() => { done(new Error('Expected getPreload to reject.')); }) - .catch(function (err) { - assert.isDefined(err); + .catch(e => { + assert.isDefined(e); done(); }); }, @@ -171,33 +170,33 @@ describe('preload cssom integration test', function () { }); } - describe('tests for root (document)', function () { - var shadowFixture; + describe('tests for root (document)', () => { + let shadowFixture; - beforeEach(function () { - var shadowNode = document.createElement('div'); + beforeEach(() => { + const shadowNode = document.createElement('div'); shadowNode.id = 'shadow-fixture'; document.body.appendChild(shadowNode); shadowFixture = document.getElementById('shadow-fixture'); }); - afterEach(function (done) { + afterEach(done => { if (shadowFixture) { document.body.removeChild(shadowFixture); } detachStylesheets(done); }); - it('returns stylesheets defined via <style> tag', function (done) { + it('returns stylesheets defined via <style> tag', done => { stylesForPage = [styleSheets.styleTag]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); - var sheetData = sheets[0].sheet; + const sheetData = sheets[0].sheet; axe.testUtils.assertStylesheet( sheetData, '.inline-css-test', @@ -205,22 +204,22 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); }); - it('returns stylesheets with in same-origin', function (done) { + it('returns stylesheets with in same-origin', done => { stylesForPage = [styleSheets.styleTagWithOneImport]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); - var nonCrossOriginSheets = sheets.filter(function (s) { + const nonCrossOriginSheets = sheets.filter(s => { return !s.isCrossOrigin; }); assert.lengthOf(nonCrossOriginSheets, 1); @@ -231,7 +230,7 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); @@ -239,8 +238,8 @@ describe('preload cssom integration test', function () { (shadowSupported ? it : xit)( 'returns styles from shadow DOM (handles @import in <style>)', - function (done) { - var shadow = shadowFixture.attachShadow({ mode: 'open' }); + done => { + const shadow = shadowFixture.attachShadow({ mode: 'open' }); shadow.innerHTML = '<style>' + // stylesheet -> 1 @@ -250,13 +249,13 @@ describe('preload cssom integration test', function () { '</style>' + '<div class="initialism">Some text</div>'; getPreloadCssom(shadowFixture) - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 2); - var nonCrossOriginSheetsWithInShadowDOM = sheets - .filter(function (s) { + const nonCrossOriginSheetsWithInShadowDOM = sheets + .filter(s => { return !s.isCrossOrigin; }) - .filter(function (s) { + .filter(s => { return s.shadowId; }); axe.testUtils.assertStylesheet( @@ -268,7 +267,7 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); } @@ -276,8 +275,8 @@ describe('preload cssom integration test', function () { (shadowSupported ? it : xit)( 'returns styles from base document and shadow DOM with right priority', - function (done) { - var shadow = shadowFixture.attachShadow({ mode: 'open' }); + done => { + const shadow = shadowFixture.attachShadow({ mode: 'open' }); shadow.innerHTML = '<style>' + // stylesheet -> 1 -> inside shadow DOM @@ -287,15 +286,15 @@ describe('preload cssom integration test', function () { // sheet appended to root document stylesForPage = [styleSheets.styleTag]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom(shadowFixture) - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 2); - var shadowDomStyle = sheets.filter(function (s) { + const shadowDomStyle = sheets.filter(s => { return s.shadowId; })[0]; axe.testUtils.assertStylesheet( @@ -304,7 +303,7 @@ describe('preload cssom integration test', function () { '.style-from-base-css { font-size: 100%; }' ); - var rootDocumentStyle = sheets.filter(function (s) { + const rootDocumentStyle = sheets.filter(s => { return !s.shadowId; })[0]; assert.isAbove( @@ -313,25 +312,25 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); } ); - it('returns styles from various @import(ed) styles from an @import(ed) stylesheet', function (done) { + it('returns styles from various @import(ed) styles from an @import(ed) stylesheet', done => { stylesForPage = [ styleSheets.styleTagWithMultipleImports // this imports 2 other stylesheets ]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 2); - var nonCrossOriginSheets = sheets.filter(function (s) { + const nonCrossOriginSheets = sheets.filter(s => { return !s.isCrossOrigin; }); assert.lengthOf(nonCrossOriginSheets, 2); @@ -347,22 +346,22 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); }); - it('returns style from nested @import (3 levels deep)', function (done) { + it('returns style from nested @import (3 levels deep)', done => { stylesForPage = [styleSheets.styleTagWithNestedImports]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); - var nonCrossOriginSheets = sheets.filter(function (s) { + const nonCrossOriginSheets = sheets.filter(s => { return !s.isCrossOrigin; }); assert.lengthOf(nonCrossOriginSheets, 1); @@ -373,20 +372,20 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); }); - it('returns style from cyclic @import (exits recursion successfully)', function (done) { + it('returns style from cyclic @import (exits recursion successfully)', done => { stylesForPage = [styleSheets.styleTagWithCyclicImports]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); axe.testUtils.assertStylesheet( sheets[0].sheet, @@ -395,20 +394,20 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); }); - it('returns style from cyclic @import which only imports one cross-origin stylesheet', function (done) { + it('returns style from cyclic @import which only imports one cross-origin stylesheet', done => { stylesForPage = [styleSheets.styleTagWithCyclicCrossOriginImports]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreloadCssom() - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 1); axe.testUtils.assertStylesheet( sheets[0].sheet, @@ -417,7 +416,7 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); @@ -426,17 +425,17 @@ describe('preload cssom integration test', function () { commonTestsForRootNodeAndNestedFrame(); }); - describe('tests for nested document', function () { - afterEach(function (done) { + describe('tests for nested document', () => { + afterEach(done => { detachStylesheets(done); }); - it('returns styles defined using <style> tag', function (done) { + it('returns styles defined using <style> tag', done => { getPreloadCssom(nestedFrame) - .then(function (sheets) { + .then(sheets => { assert.lengthOf(sheets, 2); - var nonCrossOriginSheet = sheets.filter(function (s) { + const nonCrossOriginSheet = sheets.filter(s => { return !s.isCrossOrigin; })[0].sheet; axe.testUtils.assertStylesheet( @@ -446,7 +445,7 @@ describe('preload cssom integration test', function () { ); done(); }) - .catch(function () { + .catch(() => { done(new Error('Expected getPreload to resolve.')); }); }); diff --git a/test/integration/full/preload/preload.js b/test/integration/full/preload/preload.js index f554250c23..0a3fbb61ed 100644 --- a/test/integration/full/preload/preload.js +++ b/test/integration/full/preload/preload.js @@ -1,9 +1,5 @@ -/* global axe, Promise */ - -describe('axe.utils.preload integration test', function () { - 'use strict'; - - var styleSheets = { +describe('axe.utils.preload integration test', () => { + const styleSheets = { crossOriginLinkHref: { id: 'crossOriginLinkHref', href: 'https://unpkg.com/gutenberg-css@0.4' @@ -22,15 +18,15 @@ describe('axe.utils.preload integration test', function () { text: '.inline-css-test{font-size:inherit;}' } }; - var stylesForPage; + let stylesForPage; function attachStylesheets(options, callback) { axe.testUtils .addStyleSheets(options.styles, document) - .then(function () { + .then(() => { callback(); }) - .catch(function (error) { + .catch(error => { callback(new Error('Could not load stylesheets for testing. ' + error)); }); } @@ -41,11 +37,11 @@ describe('axe.utils.preload integration test', function () { } axe.testUtils .removeStyleSheets(stylesForPage) - .then(function () { + .then(() => { done(); stylesForPage = undefined; }) - .catch(function (err) { + .catch(err => { done(err); stylesForPage = undefined; }); @@ -62,22 +58,22 @@ describe('axe.utils.preload integration test', function () { }); } - afterEach(function (done) { + afterEach(done => { axe._tree = undefined; detachStylesheets(done); }); - it('returns preloaded assets defined via <style> tag', function (done) { + it('returns preloaded assets defined via <style> tag', done => { stylesForPage = [styleSheets.styleTag]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreload() - .then(function (preloadedAssets) { + .then(preloadedAssets => { assert.property(preloadedAssets, 'cssom'); assert.lengthOf(preloadedAssets.cssom, 1); - var sheetData = preloadedAssets.cssom[0].sheet; + const sheetData = preloadedAssets.cssom[0].sheet; axe.testUtils.assertStylesheet( sheetData, '.inline-css-test', @@ -89,14 +85,14 @@ describe('axe.utils.preload integration test', function () { }); }); - it('returns NO preloaded CSSOM assets when requested stylesheets are of media=print', function (done) { + it('returns NO preloaded CSSOM assets when requested stylesheets are of media=print', done => { stylesForPage = [styleSheets.crossOriginLinkHrefMediaPrint]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreload() - .then(function (preloadedAssets) { + .then(preloadedAssets => { assert.property(preloadedAssets, 'cssom'); assert.lengthOf(preloadedAssets.cssom, 0); done(); @@ -105,66 +101,66 @@ describe('axe.utils.preload integration test', function () { }); }); - it.skip('returns NO preloaded CSSOM assets when requested stylesheet does not exist`', function (done) { + it.skip('returns NO preloaded CSSOM assets when requested stylesheet does not exist`', done => { stylesForPage = [styleSheets.crossOriginDoesNotExist]; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreload() - .then(function () { + .then(() => { done(new Error('Not expecting to complete the promise')); }) - .catch(function (err) { - assert.isNotNull(err); - assert.isTrue(!err.message.includes('Preload assets timed out')); + .catch(e => { + assert.isNotNull(e); + assert.isTrue(!e.message.includes('Preload assets timed out')); done(); }) .catch(done); }); }); - it.skip('rejects preload function when timed out before fetching assets', function (done) { + it.skip('rejects preload function when timed out before fetching assets', done => { stylesForPage = [styleSheets.crossOriginLinkHref]; - var origPreloadCssom = axe.utils.preloadCssom; - axe.utils.preloadCssom = function () { - return new Promise(function (res) { - setTimeout(function () { + const origPreloadCssom = axe.utils.preloadCssom; + axe.utils.preloadCssom = () => { + return new Promise(res => { + setTimeout(() => { res(true); }, 2000); }); }; - attachStylesheets({ styles: stylesForPage }, function (err) { + attachStylesheets({ styles: stylesForPage }, err => { if (err) { done(err); } getPreload(1) - .then(function () { + .then(() => { done(new Error('Not expecting to complete the promise')); }) - .catch(function (err) { - assert.isNotNull(err); - assert.isTrue(err.message.includes('Preload assets timed out')); + .catch(e => { + assert.isNotNull(e); + assert.isTrue(e.message.includes('Preload assets timed out')); axe.utils.preloadCssom = origPreloadCssom; done(); }) - .catch(function (e) { + .catch(e => { axe.utils.preloadCssom = origPreloadCssom; done(e); }); }); }); - describe('verify preloaded assets via axe.run against custom rules', function () { + describe('verify preloaded assets via axe.run against custom rules', () => { function customCheckEvalFn(node, options, virtualNode, context) { // populate the data here which is asserted in tests this.data(context); return true; } - beforeEach(function (done) { + beforeEach(done => { /** * Load custom rule & check * -> one check is preload dependent @@ -209,11 +205,11 @@ describe('axe.utils.preload integration test', function () { attachStylesheets({ styles: stylesForPage }, done); }); - after(function (done) { + after(done => { detachStylesheets(done); }); - it("returns preloaded assets to the check's evaluate fn for the rule which has `preload:true`", function (done) { + it("returns preloaded assets to the check's evaluate fn for the rule which has `preload:true`", done => { axe.run( { runOnly: { @@ -222,28 +218,28 @@ describe('axe.utils.preload integration test', function () { }, preload: true }, - function (err, res) { + (err, res) => { assert.isNull(err); assert.isDefined(res); assert.property(res, 'passes'); assert.lengthOf(res.passes, 1); - var checkData = res.passes[0].nodes[0].any[0].data; + const checkData = res.passes[0].nodes[0].any[0].data; assert.property(checkData, 'cssom'); - var cssom = checkData.cssom; + const cssom = checkData.cssom; // ignores all media='print' styleSheets assert.lengthOf(cssom, 2); // there should be no external sheet returned - var crossOriginSheet = cssom.filter(function (s) { + const crossOriginSheet = cssom.filter(s => { return s.isCrossOrigin; }); assert.lengthOf(crossOriginSheet, 1); // verify content of stylesheet - var inlineStylesheet = cssom.filter(function (s) { + const inlineStylesheet = cssom.filter(s => { return s.sheet.cssRules.length === 1 && !s.isCrossOrigin; })[0].sheet; axe.testUtils.assertStylesheet( @@ -257,7 +253,7 @@ describe('axe.utils.preload integration test', function () { ); }); - it("returns NO preloaded assets to the check which does not require preload'", function (done) { + it("returns NO preloaded assets to the check which does not require preload'", done => { axe.run( { runOnly: { @@ -266,13 +262,13 @@ describe('axe.utils.preload integration test', function () { }, preload: true }, - function (err, res) { + (err, res) => { assert.isNull(err); assert.isDefined(res); assert.property(res, 'passes'); assert.lengthOf(res.passes, 1); - var checkData = res.passes[0].nodes[0].any[0]; + const checkData = res.passes[0].nodes[0].any[0]; assert.notProperty(checkData, 'cssom'); done(); } diff --git a/test/integration/full/serializer/custom-source-serializer.js b/test/integration/full/serializer/custom-source-serializer.js new file mode 100644 index 0000000000..d151847f3b --- /dev/null +++ b/test/integration/full/serializer/custom-source-serializer.js @@ -0,0 +1,12 @@ +axe.utils.nodeSerializer.update({ + toSpec(dqElm) { + const result = dqElm.toJSON(); + result.source = dqElm.element.id; + return result; + }, + mergeSpecs(childSpec, parentSpec) { + const result = axe.utils.DqElement.mergeSpecs(childSpec, parentSpec); + result.source = `${parentSpec.source} > ${childSpec.source}`; + return result; + } +}); diff --git a/test/integration/full/serializer/frames/level1.html b/test/integration/full/serializer/frames/level1.html new file mode 100644 index 0000000000..9e73f2e891 --- /dev/null +++ b/test/integration/full/serializer/frames/level1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html id="level1" lang="INVALID"> + <head> + <meta charset="utf8" /> + <script src="/axe.js"></script> + <script src="../custom-source-serializer.js"></script> + </head> + <body> + <iframe id="frame2-a" src="level2-a.html"></iframe> + <iframe id="frame2-b" src="level2-b.html"></iframe> + </body> +</html> diff --git a/test/integration/full/serializer/frames/level2-a.html b/test/integration/full/serializer/frames/level2-a.html new file mode 100644 index 0000000000..b7f39033ea --- /dev/null +++ b/test/integration/full/serializer/frames/level2-a.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html id="level2-a" lang="INVALID"> + <head> + <meta charset="utf8" /> + <script src="/axe.js"></script> + <script src="../custom-source-serializer.js"></script> + </head> + <body></body> +</html> diff --git a/test/integration/full/serializer/frames/level2-b.html b/test/integration/full/serializer/frames/level2-b.html new file mode 100644 index 0000000000..49cca1d099 --- /dev/null +++ b/test/integration/full/serializer/frames/level2-b.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html id="level2-b" xml:lang="INVALID"> + <head> + <meta charset="utf8" /> + <script src="/axe.js"></script> + <script src="../custom-source-serializer.js"></script> + </head> + <body></body> +</html> diff --git a/test/integration/full/serializer/serializer.html b/test/integration/full/serializer/serializer.html new file mode 100644 index 0000000000..95f0a73250 --- /dev/null +++ b/test/integration/full/serializer/serializer.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html id="level0" lang="INVALID"> + <head> + <title>Serializer Test</title> + <link + rel="stylesheet" + type="text/css" + href="/node_modules/mocha/mocha.css" + /> + <script src="/node_modules/mocha/mocha.js"></script> + <script src="/node_modules/chai/chai.js"></script> + <script src="/axe.js"></script> + <script src="custom-source-serializer.js"></script> + <script> + mocha.setup({ + timeout: 10000, + ui: 'bdd' + }); + var assert = chai.assert; + </script> + </head> + <body> + <iframe id="frame1" src="frames/level1.html"></iframe> + <main id="mocha"></main> + <script src="/test/testutils.js"></script> + <script src="serializer.js"></script> + <script src="/test/integration/adapter.js"></script> + </body> +</html> diff --git a/test/integration/full/serializer/serializer.js b/test/integration/full/serializer/serializer.js new file mode 100644 index 0000000000..3ae2766403 --- /dev/null +++ b/test/integration/full/serializer/serializer.js @@ -0,0 +1,46 @@ +describe('serializer', () => { + const { awaitNestedLoad, runPartialRecursive } = axe.testUtils; + + beforeEach(async () => { + await new Promise(res => awaitNestedLoad(res)); + }); + + const runOptions = { runOnly: 'html-lang-valid' }; + const expectedCustomNodeSources = [ + 'level0', + 'frame1 > level1', + 'frame1 > frame2-a > level2-a', + 'frame1 > frame2-b > level2-b' + ]; + + it('applies serializer hooks with axe.runPartial/finishRun', async () => { + const partialResults = await Promise.all( + runPartialRecursive(document, runOptions) + ); + const results = await axe.finishRun(partialResults, runOptions); + const nodesHtml = results.violations[0].nodes.map(n => n.html); + assert.deepStrictEqual(nodesHtml, expectedCustomNodeSources); + }); + + it('applies serializer hooks with axe.run', async () => { + const results = await axe.run(document, runOptions); + const nodesHtml = results.violations[0].nodes.map(n => n.html); + assert.deepStrictEqual(nodesHtml, expectedCustomNodeSources); + }); + + it('still supports axe.run with options.elementRef', async () => { + const results = await axe.run(document, { + ...runOptions, + elementRef: true + }); + const nodeElements = results.violations[0].nodes.map(n => n.element); + + assert.deepStrictEqual(nodeElements, [ + document.querySelector('html'), + // as usual, elementRef only works for the top frame + undefined, + undefined, + undefined + ]); + }); +}); diff --git a/test/integration/full/target-size/target-size.html b/test/integration/full/target-size/target-size.html index 02a36e9a25..9358f9ab81 100644 --- a/test/integration/full/target-size/target-size.html +++ b/test/integration/full/target-size/target-size.html @@ -177,6 +177,9 @@ .ml-n6 { margin-left: -36px; } + .ml-n5 { + margin-left: -30px; + } .ml-n4 { margin-left: -24px; } @@ -350,7 +353,7 @@ </div> </section> - <p>Example E1 and E2 pass, the two outside elements of E3 and E4 fail.</p> + <p>Example E1 - E3 pass, E4 fails.</p> <section class="sample"> <div> <span class="passed h2 w2"></span> @@ -363,18 +366,18 @@ <span class="passed h2 w2"></span> </div> <div> - <span class="failed h2 w2"></span> + <span class="passed h2 w2"></span> <span class="passed h2 w4 ml1 mr1"></span> - <span class="failed h2 w2"></span> + <span class="passed h2 w2"></span> </div> <div> <span class="failed h2 w2"></span> - <span class="passed h2 w4"></span> + <span class="failed h2 w4"></span> <span class="failed h2 w2"></span> </div> </section> - <p>Example F1 and F2 pass, the inside element of F3 and F4 fail.</p> + <p>Example F1 - F3 pass, F4 fails.</p> <section class="sample"> <div> <span class="passed h2 w4"></span> @@ -388,13 +391,13 @@ </div> <div> <span class="passed h2 w4"></span> - <span class="failed h2 w2 ml1 mr1"></span> + <span class="passed h2 w2 ml1 mr1"></span> <span class="passed h2 w4"></span> </div> <div> - <span class="passed h2 w4"></span> + <span class="failed h2 w4"></span> <span class="failed h2 w2"></span> - <span class="passed h2 w4"></span> + <span class="failed h2 w4"></span> </div> </section> @@ -440,7 +443,7 @@ </span> </div> <div> - <span class="passed h6 w10"> + <span class="failed h6 w10"> <span class="passed h4 w4 mt1 ml1"></span> <span class="passed h4 w4 mt1"></span> </span> @@ -456,7 +459,7 @@ </div> <div> <span class="passed h4 w6 ml4"> - <span class="passed h2 w6 mt1 ml-n4"></span> + <span class="passed h2 w6 mt1 ml-n5"></span> </span> </div> <div> @@ -480,7 +483,7 @@ </div> <div> <span class="passed h4 w6 ml4"> - <span class="passed h2 w6 ml-n4"></span> + <span class="passed h2 w6 ml-n5"></span> </span> </div> <div> @@ -495,26 +498,26 @@ </div> </section> - <p>Example K1 - K3 pass, the middle element of K4 fails.</p> + <p>Example K1 and K2 pass, K3 and K4 fail.</p> <section class="sample"> <div> <span class="passed h4 w6 ml4"> - <span class="passed h2 w5 mt1 ml-n4"></span> + <span class="passed h2 w5 mt1 ml-n5"></span> </span> </div> <div> <span class="passed h4 w6 ml4"> - <span class="passed h2 w6 mt1 ml-n4"></span> + <span class="passed h2 w6 mt1 ml-n5"></span> </span> </div> <div> <span class="failed h4 w6 ml4"> - <span class="passed h2 w7 mt1 ml-n4"></span> + <span class="failed h2 w7 mt1 ml-n4"></span> </span> </div> <div> <span class="failed h4 w6 ml4"> - <span class="passed h2 w8 mt1 ml-n4"></span> + <span class="failed h2 w8 mt1 ml-n5"></span> </span> </div> </section> @@ -587,6 +590,7 @@ </span> </div> <div> + <!-- this should technically fail the target-size rule if we were implementing the size based on the largest inscribed square --> <span class="passed h6 w10 rnd-50"> <span class="failed h2 w2 mt2 ml4"></span> </span> @@ -608,7 +612,7 @@ <span class="passed h4 w4 mt1 ml-n1"></span> </div> <div> - <span class="passed h6 w6 ml2"> + <span class="failed h6 w6 ml2"> <span class="passed h4 w4 mt1 ml-n2"></span> </span> <span class="passed h4 w4 mt1 ml-n1"></span> @@ -639,6 +643,7 @@ </span> </div> <div> + <!-- this should technically fail the target-size rule if we were implementing the size based on the largest inscribed square --> <span class="passed h6 w10 rnd-0-100"> <span class="passed h4 w4 mt1 ml1"></span> </span> diff --git a/test/integration/full/test-webdriver.js b/test/integration/full/test-webdriver.js index 01aacf2c53..6e673b17d4 100644 --- a/test/integration/full/test-webdriver.js +++ b/test/integration/full/test-webdriver.js @@ -1,5 +1,3 @@ -/*global window, Promise */ - const globby = require('globby'); const chrome = require('selenium-webdriver/chrome'); const firefox = require('selenium-webdriver/firefox'); @@ -9,12 +7,12 @@ const args = process.argv.slice(2); // allow running certain browsers through command line args // (only one browser supported, run multiple times for more browsers) -let browser = 'chrome'; -args.forEach(function (arg) { +let browserArg = 'chrome'; +args.forEach(arg => { // pattern: browsers=Chrome const parts = arg.split('='); if (parts[0] === 'browser') { - browser = parts[1].toLowerCase(); + browserArg = parts[1].toLowerCase(); } }); @@ -24,9 +22,9 @@ args.forEach(function (arg) { function collectTestResults(driver) { // inject a script that waits half a second return driver - .executeAsyncScript(function () { + .executeAsyncScript(() => { const callback = arguments[arguments.length - 1]; - setTimeout(function () { + setTimeout(() => { if (!window.mocha) { callback('mocha-missing;' + window.location.href); } @@ -34,7 +32,7 @@ function collectTestResults(driver) { callback(window.mochaResults); }, 500); }) - .then(function (result) { + .then(result => { // If there are no results, listen a little longer if (typeof result === 'string' && result.includes('mocha-missing')) { throw new Error( @@ -64,14 +62,14 @@ function runTestUrls(driver, isMobile, urls, errors) { driver .get(url) // Get results - .then(function () { + .then(() => { return Promise.all([ driver.getCapabilities(), collectTestResults(driver) ]); }) // And process them - .then(function (promiseResults) { + .then(promiseResults => { const capabilities = promiseResults[0]; const result = promiseResults[1]; const browserName = @@ -80,7 +78,7 @@ function runTestUrls(driver, isMobile, urls, errors) { console.log(url + ' [' + browserName + ']'); // Remember the errors - (result.reports || []).forEach(function (err) { + (result.reports || []).forEach(err => { console.log(err.message); err.url = url; err.browser = browserName; @@ -101,7 +99,7 @@ function runTestUrls(driver, isMobile, urls, errors) { ); console.log(); }) - .then(function () { + .then(() => { // Start the next job, if any if (urls.length > 0) { return runTestUrls(driver, isMobile, urls, errors); @@ -160,7 +158,7 @@ function start(options) { 'test/integration/full/**/*.{html,xhtml}', '!**/frames/**/*.{html,xhtml}' ]) - .map(function (url) { + .map(url => { return 'http://localhost:9876/' + url; }); @@ -190,9 +188,9 @@ function start(options) { // Test all pages runTestUrls(driver, isMobile, testUrls) - .then(function (testErrors) { + .then(testErrors => { // log each error and abort - testErrors.forEach(function (err) { + testErrors.forEach(err => { console.log(); console.log('URL: ' + err.url); console.log('Browser: ' + err.browser); @@ -206,10 +204,10 @@ function start(options) { // catch any potential problems }) - .catch(function (err) { + .catch(err => { console.log(err); process.exit(1); }); } -start({ browser: browser }); +start({ browser: browserArg }); diff --git a/test/integration/rules/aria-allowed-attr/failures.html b/test/integration/rules/aria-allowed-attr/failures.html index cef9d931ff..cdb251a248 100644 --- a/test/integration/rules/aria-allowed-attr/failures.html +++ b/test/integration/rules/aria-allowed-attr/failures.html @@ -2,57 +2,10 @@ <div role="link" aria-selected="true" id="fail2"></div> <div role="row" aria-colcount="value" id="fail3"></div> <div role="row" aria-rowcount="value" id="fail4"></div> -<div role="caption" aria-label="value" id="fail5"></div> -<div role="caption" aria-labelledby="value" id="fail6"></div> -<div role="code" aria-label="value" id="fail7"></div> -<div role="code" aria-labelledby="value" id="fail8"></div> -<div role="deletion" aria-label="value" id="fail9"></div> -<div role="deletion" aria-labelledby="value" id="fail10"></div> -<div role="emphasis" aria-label="value" id="fail11"></div> -<div role="emphasis" aria-labelledby="value" id="fail12"></div> -<div role="insertion" aria-label="value" id="fail13"></div> -<div role="insertion" aria-labelledby="value" id="fail14"></div> -<div role="paragraph" aria-label="value" id="fail15"></div> -<div role="paragraph" aria-labelledby="value" id="fail16"></div> -<div role="strong" aria-label="value" id="fail17"></div> -<div role="strong" aria-labelledby="value" id="fail18"></div> -<div role="subscript" aria-label="value" id="fail19"></div> -<div role="subscript" aria-labelledby="value" id="fail20"></div> -<div role="superscript" aria-label="value" id="fail21"></div> -<div role="superscript" aria-labelledby="value" id="fail22"></div> -<div aria-label="value" id="fail23"></div> -<div aria-labelledby="value" id="fail24"></div> -<!-- technically presentation and none roles do not allow aria-label and aria-labelledby, but since those are global attributes the presentation role conflict will not resolve the roles to none or presentation --> -<span aria-label="value" id="fail25"></span> -<strong aria-label="value" id="fail26"></strong> -<kbd aria-label="value" id="fail27"></kbd> -<abbr aria-label="value" id="fail28"></abbr> -<custom-elm aria-label="value" id="fail29"></custom-elm> + <audio src="/test/assets/moon-speech.mp3" controls aria-orientation="horizontal" - id="fail30" + id="fail5" ></audio> -<div role="mark" aria-label="value" id="fail31"></div> -<div role="mark" aria-labelledby="value" id="fail32"></div> -<div role="suggestion" aria-label="value" id="fail33"></div> -<div role="suggestion" aria-labelledby="value" id="fail34"></div> - -<div role="table"> - <div role="row" aria-expanded="false" id="fail35"></div> - <div role="row" aria-posinset="1" id="fail36"></div> - <div role="row" aria-setsize="10" id="fail37"></div> - <div role="row" aria-level="1" id="fail38"></div> -</div> - -<div role="grid"> - <div role="row" aria-expanded="false" id="fail39"></div> - <div role="row" aria-posinset="1" id="fail40"></div> - <div role="row" aria-setsize="10" id="fail41"></div> - <div role="row" aria-level="1" id="fail42"></div> -</div> - -<input type="checkbox" aria-checked="mixed" id="fail43" /> -<input type="checkbox" aria-checked="true" id="fail44" /> -<input type="checkbox" aria-checked="false" checked id="fail45" /> diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json index 895d49aac7..cedecf0810 100644 --- a/test/integration/rules/aria-allowed-attr/failures.json +++ b/test/integration/rules/aria-allowed-attr/failures.json @@ -1,51 +1,5 @@ { "description": "aria-allowed-attr failing tests", "rule": "aria-allowed-attr", - "violations": [ - ["#fail1"], - ["#fail2"], - ["#fail3"], - ["#fail4"], - ["#fail5"], - ["#fail6"], - ["#fail7"], - ["#fail8"], - ["#fail9"], - ["#fail10"], - ["#fail11"], - ["#fail12"], - ["#fail13"], - ["#fail14"], - ["#fail15"], - ["#fail16"], - ["#fail17"], - ["#fail18"], - ["#fail19"], - ["#fail20"], - ["#fail21"], - ["#fail22"], - ["#fail23"], - ["#fail24"], - ["#fail25"], - ["#fail26"], - ["#fail27"], - ["#fail28"], - ["#fail29"], - ["#fail30"], - ["#fail31"], - ["#fail32"], - ["#fail33"], - ["#fail34"], - ["#fail35"], - ["#fail36"], - ["#fail37"], - ["#fail38"], - ["#fail39"], - ["#fail40"], - ["#fail41"], - ["#fail42"], - ["#fail43"], - ["#fail44"], - ["#fail45"] - ] + "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]] } diff --git a/test/integration/rules/aria-allowed-attr/incomplete.html b/test/integration/rules/aria-allowed-attr/incomplete.html index e187dba1c6..b98bc3380b 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.html +++ b/test/integration/rules/aria-allowed-attr/incomplete.html @@ -1,4 +1 @@ -<div id="incomplete0" aria-label="foo">Foo</div> -<div id="incomplete1" aria-labelledby="missing">Foo</div> -<my-custom-elm id="incomplete2" aria-expanded="true">Foo</my-custom-elm> -<div id="incomplete3" aria-label="foo" role="code">Foo</div> +<my-custom-elm id="incomplete1" aria-expanded="true">Foo</my-custom-elm> diff --git a/test/integration/rules/aria-allowed-attr/incomplete.json b/test/integration/rules/aria-allowed-attr/incomplete.json index fa37187123..5a2b2b4fc3 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.json +++ b/test/integration/rules/aria-allowed-attr/incomplete.json @@ -1,10 +1,5 @@ { "description": "aria-allowed-attr incomplete tests", "rule": "aria-allowed-attr", - "incomplete": [ - ["#incomplete0"], - ["#incomplete1"], - ["#incomplete2"], - ["#incomplete3"] - ] + "incomplete": [["#incomplete1"]] } diff --git a/test/integration/rules/aria-allowed-attr/passes.html b/test/integration/rules/aria-allowed-attr/passes.html index 5449480b8a..dfef5d3ed5 100644 --- a/test/integration/rules/aria-allowed-attr/passes.html +++ b/test/integration/rules/aria-allowed-attr/passes.html @@ -1901,6 +1901,7 @@ <div role="switch" id="pass62" + aria-expanded="value" aria-checked="value" aria-atomic="value" aria-braillelabel="value" @@ -2092,8 +2093,9 @@ <button id="pass73" aria-roledescription="attachment button"></button> <input type="checkbox" - aria-roledescription="cuisine type checkbox" id="pass74" + aria-roledescription="cuisine type checkbox" + aria-expanded="false" /> <span role="radio" id="pass75" aria-checked="false">I am RED!</span> @@ -2121,26 +2123,6 @@ <svg aria-label="value" id="pass89"></svg> <video aria-label="value" id="pass90" controls></video> -<table role="treegrid"> - <thead> - <tr> - <th>Col 1</th> - </tr> - </thead> - <tbody> - <tr - id="treegrid" - role="row" - aria-level="1" - aria-posinset="1" - aria-setsize="1" - aria-expanded="true" - > - <td role="gridcell">Treegrids are awesome</td> - </tr> - </tbody> -</table> - <div id="pass91" role="comment" @@ -2151,5 +2133,30 @@ ok </div> -<input type="checkbox" aria-checked="false" id="pass92" /> -<input type="checkbox" aria-checked="true" checked id="pass93" /> +<!-- Prohibited attributes fail in aria-prohibited-attr --> +<div role="caption" aria-label="value" id="pass92"></div> +<div role="caption" aria-labelledby="value" id="pass93"></div> +<div role="paragraph" aria-labelledby="value" id="pass94"></div> +<div role="strong" aria-label="value" id="pass95"></div> + +<!-- Conditional ARIA attributes tested under aria-conditional-attr: --> +<div role="table"> + <div role="row" aria-expanded="false" id="pass96"></div> +</div> +<table role="treegrid"> + <tr + id="pass97" + role="row" + aria-level="1" + aria-posinset="1" + aria-setsize="1" + aria-expanded="true" + > + <td role="gridcell">Treegrids are awesome</td> + </tr> +</table> + +<input type="checkbox" aria-checked="false" id="pass98" /> +<input type="checkbox" aria-checked="mixed" id="pass99" /> + +<search aria-expanded="true" id="pass100"></search> diff --git a/test/integration/rules/aria-allowed-attr/passes.json b/test/integration/rules/aria-allowed-attr/passes.json index 84f487f8b1..aa13ff6f71 100644 --- a/test/integration/rules/aria-allowed-attr/passes.json +++ b/test/integration/rules/aria-allowed-attr/passes.json @@ -93,9 +93,15 @@ ["#pass88"], ["#pass89"], ["#pass90"], - ["#treegrid"], ["#pass91"], ["#pass92"], - ["#pass93"] + ["#pass93"], + ["#pass94"], + ["#pass95"], + ["#pass96"], + ["#pass97"], + ["#pass98"], + ["#pass99"], + ["#pass100"] ] } 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 1760747d16..3a2b99efad 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -9,6 +9,7 @@ <li id="pass-li-role-doc-biblioentry" role="doc-biblioentry"></li> </ul> <aside id="pass-aside-doc-example" role="doc-example"></aside> +<aside id="pass-aside-doc-glossary" role="doc-glossary"></aside> <div id="pass-div-has-any-role" role="contentinfo"></div> <div id="pass-div-valid-role" role="link">ok</div> <ol id="pass-ol-valid-role" role="directory"></ol> @@ -246,6 +247,15 @@ <h1 id="pass-h1-valid-role" role="none"></h1> <div id="fail-dpub-4" role="doc-noteref">ok</div> <!-- images --> <div id="fail-dpub-5" role="doc-cover">ok</div> +<img + src="#" + role="meter" + aria-valuenow="0" + aria-valuemin="0" + aria-valuemax="100" + alt="test" + id="pass-img-valid-role-meter" +/> <img role="doc-cover" aria-label="foo" id="pass-img-valid-role-aria-label" /> <img role="menuitem" title="bar" id="pass-img-valid-role-title" /> <div id="image-baz">hazaar</div> @@ -291,3 +301,5 @@ <h1 role="text" id="fail-text-1">ok</h1> /> <area shape="circle" coords="15,15,10" role="checkbox" id="fail-imgmap-2" /> </map> + +<search role="form" id="pass-search-elm"></search> 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 3b6b6663b3..e8b7e83a4a 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.json +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.json @@ -11,6 +11,7 @@ ["#pass-section-role-doc-bib"], ["#pass-li-role-doc-biblioentry"], ["#pass-aside-doc-example"], + ["#pass-aside-doc-glossary"], ["#pass-div-valid-role"], ["#pass-ol-valid-role"], ["#pass-nav-role-doc-index"], @@ -82,10 +83,12 @@ ["#pass-img-valid-role-title"], ["#pass-img-valid-role-aria-labelledby"], ["#pass-img-valid-role-radio"], + ["#pass-img-valid-role-meter"], ["#pass-imgmap-1"], ["#pass-imgmap-2"], ["#pass-navnone-1"], - ["#pass-navnone-2"] + ["#pass-navnone-2"], + ["#pass-search-elm"] ], "violations": [ ["#fail-dd-no-role"], diff --git a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html new file mode 100644 index 0000000000..c5aa2eb572 --- /dev/null +++ b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html @@ -0,0 +1,27 @@ +<button id="pass1" aria-braillelabel="hello">Hello</button> +<button id="pass2" aria-braillelabel=""></button> +<button id="incomplete1" aria-braillelabel="hello"></button> + +<aside + id="pass3" + aria-roledescription="table of contents" + aria-brailleroledescription="" +></aside> + +<aside + id="pass4" + aria-roledescription="table of contents" + aria-brailleroledescription="table of contents" +></aside> + +<aside + id="pass5" + aria-roledescription="" + aria-brailleroledescription="" +></aside> + +<aside + id="incomplete2" + aria-roledescription="" + aria-brailleroledescription="table of contents" +></aside> diff --git a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json new file mode 100644 index 0000000000..d1982970f2 --- /dev/null +++ b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json @@ -0,0 +1,7 @@ +{ + "description": "aria-braille-equivalent tests", + "rule": "aria-braille-equivalent", + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]], + "incomplete": [["#incomplete1"], ["#incomplete2"]], + "violation": [] +} diff --git a/test/integration/rules/aria-conditional-attr/aria-conditional-attr.html b/test/integration/rules/aria-conditional-attr/aria-conditional-attr.html new file mode 100644 index 0000000000..3831f26ac0 --- /dev/null +++ b/test/integration/rules/aria-conditional-attr/aria-conditional-attr.html @@ -0,0 +1,32 @@ +<input type="checkbox" aria-checked="false" id="pass1" /> +<input type="checkbox" aria-checked="true" checked id="pass2" /> +<table role="treegrid"> + <tr + id="pass3" + role="row" + aria-level="1" + aria-posinset="1" + aria-setsize="1" + aria-expanded="true" + > + <td role="gridcell">Treegrids are awesome</td> + </tr> +</table> + +<div role="table"> + <div role="row" aria-expanded="false" id="fail1"></div> + <div role="row" aria-posinset="1" id="fail2"></div> + <div role="row" aria-setsize="10" id="fail3"></div> + <div role="row" aria-level="1" id="fail4"></div> +</div> + +<div role="grid"> + <div role="row" aria-expanded="false" id="fail5"></div> + <div role="row" aria-posinset="1" id="fail6"></div> + <div role="row" aria-setsize="10" id="fail7"></div> + <div role="row" aria-level="1" id="fail8"></div> +</div> + +<input type="checkbox" aria-checked="mixed" id="fail9" /> +<input type="checkbox" aria-checked="true" id="fail10" /> +<input type="checkbox" aria-checked="false" checked id="fail11" /> diff --git a/test/integration/rules/aria-conditional-attr/aria-conditional-attr.json b/test/integration/rules/aria-conditional-attr/aria-conditional-attr.json new file mode 100644 index 0000000000..9dbaadcf94 --- /dev/null +++ b/test/integration/rules/aria-conditional-attr/aria-conditional-attr.json @@ -0,0 +1,18 @@ +{ + "description": "aria-conditional-attr tests", + "rule": "aria-conditional-attr", + "passes": [["#pass1"], ["#pass2"], ["#pass3"]], + "violations": [ + ["#fail1"], + ["#fail2"], + ["#fail3"], + ["#fail4"], + ["#fail5"], + ["#fail6"], + ["#fail7"], + ["#fail8"], + ["#fail9"], + ["#fail10"], + ["#fail11"] + ] +} diff --git a/test/integration/rules/aria-deprecated-role/aria-deprecated-role.html b/test/integration/rules/aria-deprecated-role/aria-deprecated-role.html new file mode 100644 index 0000000000..1e48b79d63 --- /dev/null +++ b/test/integration/rules/aria-deprecated-role/aria-deprecated-role.html @@ -0,0 +1,19 @@ +<div role="alert" id="pass1">ok</div> +<div role="alertdialog" id="pass2">ok</div> +<div role="application" id="pass3">ok</div> +<!-- fallback roles --> +<div role="button alert" id="pass4">fail</div> +<!-- abstract roles --> +<div role="command" id="pass5">fail</div> +<div role="composite" id="pass6">fail</div> +<!-- invalid roles --> +<div role="lol" id="pass7">fail</div> + +<!-- deprecated roles--> +<div role="doc-biblioentry" id="fail1">fail</div> +<div role="doc-endnote" id="fail2">fail</div> +<div role="directory" id="fail3">ok</div> + +<!-- inapplicable --> +<div id="inapplicable1">no role attribute</div> +<div id="inapplicable2" role>role not defined</div> diff --git a/test/integration/rules/aria-deprecated-role/aria-deprecated-role.json b/test/integration/rules/aria-deprecated-role/aria-deprecated-role.json new file mode 100644 index 0000000000..0aa7ee8665 --- /dev/null +++ b/test/integration/rules/aria-deprecated-role/aria-deprecated-role.json @@ -0,0 +1,14 @@ +{ + "description": "aria-deprecated-role tests", + "rule": "aria-deprecated-role", + "violations": [["#fail1"], ["#fail2"], ["#fail3"]], + "passes": [ + ["#pass1"], + ["#pass2"], + ["#pass3"], + ["#pass4"], + ["#pass5"], + ["#pass6"], + ["#pass7"] + ] +} diff --git a/test/integration/rules/aria-dialog-name/aria-dialog-name.html b/test/integration/rules/aria-dialog-name/aria-dialog-name.html index bcfdce3ef0..2052a0501a 100644 --- a/test/integration/rules/aria-dialog-name/aria-dialog-name.html +++ b/test/integration/rules/aria-dialog-name/aria-dialog-name.html @@ -19,6 +19,9 @@ <h2 id="alertdialog-heading">Cookies</h2> <div id="pass9" role="alertdialog" aria-labelledby="alertdialog-div-heading"> <h2 id="alertdialog-div-heading">Cookies</h2> </div> +<div id="pass10" role="dialog" aria-labelledby="pass10-heading"> + <header id="pass10-heading">Named from author element</header> +</div> <!-- FAIL --> <div id="fail1" role="dialog"></div> diff --git a/test/integration/rules/aria-dialog-name/aria-dialog-name.json b/test/integration/rules/aria-dialog-name/aria-dialog-name.json index ebda19ffdc..e2171ce49a 100644 --- a/test/integration/rules/aria-dialog-name/aria-dialog-name.json +++ b/test/integration/rules/aria-dialog-name/aria-dialog-name.json @@ -10,7 +10,8 @@ ["#pass6"], ["#pass7"], ["#pass8"], - ["#pass9"] + ["#pass9"], + ["#pass10"] ], "violations": [ ["#fail1"], diff --git a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html new file mode 100644 index 0000000000..ec354260a3 --- /dev/null +++ b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html @@ -0,0 +1,41 @@ +<div role="checkbox" aria-required="true" id="pass1">ok</div> +<div id="pass2" aria-label=" ">Foo</div> +<div id="pass3" aria-labelledby=" ">Foo</div> +<div role="alert" aria-selected="true" id="pass4"></div> +<div role="row" aria-colcount="value" id="pass5"></div> + +<div role="caption" aria-label="value" id="fail1"></div> +<div role="caption" aria-labelledby="value" id="fail2"></div> +<div role="code" aria-label="value" id="fail3"></div> +<div role="code" aria-labelledby="value" id="fail4"></div> +<div role="deletion" aria-label="value" id="fail5"></div> +<div role="deletion" aria-labelledby="value" id="fail6"></div> +<div role="emphasis" aria-label="value" id="fail7"></div> +<div role="emphasis" aria-labelledby="value" id="fail8"></div> +<div role="insertion" aria-label="value" id="fail9"></div> +<div role="insertion" aria-labelledby="value" id="fail10"></div> +<div role="paragraph" aria-label="value" id="fail11"></div> +<div role="paragraph" aria-labelledby="value" id="fail12"></div> +<div role="strong" aria-label="value" id="fail13"></div> +<div role="strong" aria-labelledby="value" id="fail14"></div> +<div role="subscript" aria-label="value" id="fail15"></div> +<div role="subscript" aria-labelledby="value" id="fail16"></div> +<div role="superscript" aria-label="value" id="fail17"></div> +<div role="superscript" aria-labelledby="value" id="fail18"></div> +<div aria-label="value" id="fail19"></div> +<div aria-labelledby="value" id="fail20"></div> +<!- aria-label(ledby) is prohibited on none / presentation. Axe-core considers this to trigger presentation role +conflict, which was true in ARIA 1.1. This changed in ARIA 1.2 but so far has only been implemented in Chomium. --> +<span aria-label="value" id="fail21"></span> +<strong aria-label="value" id="fail22"></strong> +<kbd aria-label="value" id="fail23"></kbd> +<abbr aria-label="value" id="fail24"></abbr> +<custom-elm aria-label="value" id="fail25"></custom-elm> +<div role="mark" aria-label="value" id="fail26"></div> +<div role="mark" aria-labelledby="value" id="fail27"></div> +<div role="suggestion" aria-label="value" id="fail28"></div> +<div role="suggestion" aria-labelledby="value" id="fail29"></div> + +<div id="incomplete1" aria-label="foo">Foo</div> +<div id="incomplete2" aria-labelledby="missing">Foo</div> +<div id="incomplete3" aria-label="foo" role="code">Foo</div> diff --git a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json new file mode 100644 index 0000000000..f6f9e7717b --- /dev/null +++ b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json @@ -0,0 +1,37 @@ +{ + "description": "aria-prohibited-attr tests", + "rule": "aria-prohibited-attr", + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]], + "incomplete": [["#incomplete1"], ["#incomplete2"], ["#incomplete3"]], + "violations": [ + ["#fail1"], + ["#fail2"], + ["#fail3"], + ["#fail4"], + ["#fail5"], + ["#fail6"], + ["#fail7"], + ["#fail8"], + ["#fail9"], + ["#fail10"], + ["#fail11"], + ["#fail12"], + ["#fail13"], + ["#fail14"], + ["#fail15"], + ["#fail16"], + ["#fail17"], + ["#fail18"], + ["#fail19"], + ["#fail20"], + ["#fail21"], + ["#fail22"], + ["#fail23"], + ["#fail24"], + ["#fail25"], + ["#fail26"], + ["#fail27"], + ["#fail28"], + ["#fail29"] + ] +} 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 410434d972..def03ca181 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.html +++ b/test/integration/rules/aria-required-children/aria-required-children.html @@ -27,6 +27,9 @@ <div role="menu" id="incomplete10"> <!-- --> <span role="menuitem" hidden></span> + <span role="none"> + <span role="menuitem" aria-hidden="true">hidden</span> + </span> </div> <div role="menubar" id="incomplete11"></div> <div role="list" id="fail4"> @@ -143,3 +146,12 @@ <div role="menuitem">menu item</div> <div aria-hidden="true">shouldn't be flagged but is</div> </div> + +<div role="list" id="pass19"> + <div role="presentation"> + <div style="display: none">ignore</div> + <div style="visibility: hidden" aria-hidden="true">ignore</div> + <li>item 1</li> + <li>item 2</li> + </div> +</div> 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 23f2822f0a..21f2ed2f3a 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.json +++ b/test/integration/rules/aria-required-children/aria-required-children.json @@ -33,7 +33,8 @@ ["#pass15"], ["#pass16"], ["#pass17"], - ["#pass18"] + ["#pass18"], + ["#pass19"] ], "incomplete": [ ["#incomplete1"], diff --git a/test/integration/rules/aria-roles/aria-roles.html b/test/integration/rules/aria-roles/aria-roles.html index 0416d2dbbe..612b0dba15 100644 --- a/test/integration/rules/aria-roles/aria-roles.html +++ b/test/integration/rules/aria-roles/aria-roles.html @@ -132,9 +132,9 @@ <div role="lol" id="fail13">fail</div> <!-- unsupported roles --> <!-- deprecated roles--> - <div role="doc-biblioentry" id="fail14">fail</div> - <div role="doc-endnote" id="fail15">fail</div> - <div role="directory" id="fail16">ok</div> + <div role="doc-biblioentry" id="pass118">fail</div> + <div role="doc-endnote" id="pass119">fail</div> + <div role="directory" id="pass120">ok</div> </div> <!-- inapplicable --> diff --git a/test/integration/rules/aria-roles/aria-roles.json b/test/integration/rules/aria-roles/aria-roles.json index e444afd603..082a3d30d1 100644 --- a/test/integration/rules/aria-roles/aria-roles.json +++ b/test/integration/rules/aria-roles/aria-roles.json @@ -14,10 +14,7 @@ ["#fail10"], ["#fail11"], ["#fail12"], - ["#fail13"], - ["#fail14"], - ["#fail15"], - ["#fail16"] + ["#fail13"] ], "passes": [ ["#pass1"], @@ -132,6 +129,9 @@ ["#pass114"], ["#pass115"], ["#pass116"], - ["#pass117"] + ["#pass117"], + ["#pass118"], + ["#pass119"], + ["#pass120"] ] } diff --git a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html index b5a9c00f4d..42e60bae4c 100644 --- a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html +++ b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html @@ -39,57 +39,56 @@ <h2>Violations</h2> hi </div> <div id="violation35-ref"></div> - <div aria-valuetext="" id="violation36"></div> <!-- don't allow empty aria-valuetext --> - <div aria-level="0" id="violation37">hi</div> - <div aria-level="-1" id="violation38">hi</div> - <div aria-level="-22" id="violation39">hi</div> + <div aria-level="0" id="violation36">hi</div> + <div aria-level="-1" id="violation37">hi</div> + <div aria-level="-22" id="violation38">hi</div> - <div aria-posinset="0" id="violation40">hi</div> - <div aria-posinset="-1" id="violation41">hi</div> - <div aria-posinset="-22" id="violation42">hi</div> + <div aria-posinset="0" id="violation39">hi</div> + <div aria-posinset="-1" id="violation40">hi</div> + <div aria-posinset="-22" id="violation41">hi</div> - <div aria-setsize="-22" id="violation43">hi</div> + <div aria-setsize="-22" id="violation42">hi</div> <div> <input type="text" - id="violation44" + id="violation43" aria-invalid="true" - aria-errormessage="violation44-ref" + aria-errormessage="violation43-ref" /> - <div id="violation44-ref" hidden>Error message 1</div> + <div id="violation43-ref" hidden>Error message 1</div> </div> <div> <input type="text" - id="violation45" + id="violation44" aria-invalid="true" - aria-errormessage="violation45-ref" + aria-errormessage="violation44-ref" /> - <div id="violation45-ref" style="display: none">Error message 1</div> + <div id="violation44-ref" style="display: none">Error message 1</div> </div> <div> <input type="text" - id="violation46" + id="violation45" aria-invalid="true" - aria-errormessage="violation46-ref" + aria-errormessage="violation45-ref" /> - <div id="violation46-ref" style="visibility: hidden">Error message 1</div> + <div id="violation45-ref" style="visibility: hidden">Error message 1</div> </div> <div> <input type="text" - id="violation47" + id="violation46" aria-invalid="true" - aria-errormessage="violation47-ref" + aria-errormessage="violation46-ref" /> - <div id="violation47-ref" aria-hidden="true">Error message 1</div> + <div id="violation46-ref" aria-hidden="true">Error message 1</div> </div> </div> <h2>Possible False Positives</h2> @@ -343,6 +342,12 @@ <h2>Possible False Positives</h2> aria-errormessage="violation44-ref" /> </div> + <div + id="pass191" + aria-braillelabel="" + aria-brailleroledescription="" + aria-valuetext="" + ></div> <div id="ref">Hi</div> <div id="ref2">Hi2</div> diff --git a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json index b552427d1f..8fb15e1190 100644 --- a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json +++ b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json @@ -44,8 +44,7 @@ ["#violation43"], ["#violation44"], ["#violation45"], - ["#violation46"], - ["#violation47"] + ["#violation46"] ], "passes": [ ["#pass1"], @@ -226,7 +225,8 @@ ["#pass187"], ["#pass188"], ["#pass189"], - ["#pass190"] + ["#pass190"], + ["#pass191"] ], "incomplete": [ ["#incomplete1"], diff --git a/test/integration/rules/color-contrast/color-contrast.html b/test/integration/rules/color-contrast/color-contrast.html index dd3a114312..5a6cc1e690 100644 --- a/test/integration/rules/color-contrast/color-contrast.html +++ b/test/integration/rules/color-contrast/color-contrast.html @@ -448,3 +448,14 @@ <div id="ignore12" style="background: #000; color: #333">hello</div> </div> </div> + +<div + id="pass23" + style=" + color: #000; + background: #737373; + text-shadow: color(display-p3 1 1 1 / 1) 0 0 3px; + " +> + Hello world +</div> diff --git a/test/integration/rules/color-contrast/color-contrast.json b/test/integration/rules/color-contrast/color-contrast.json index 0cd24e9ad7..a5a8aa0355 100644 --- a/test/integration/rules/color-contrast/color-contrast.json +++ b/test/integration/rules/color-contrast/color-contrast.json @@ -36,7 +36,8 @@ ["#pass19"], ["#pass20"], ["#pass21"], - ["#pass22"] + ["#pass22"], + ["#pass23"] ], "incomplete": [ ["#canttell1"], diff --git a/test/integration/rules/color-contrast/text-shadows.html b/test/integration/rules/color-contrast/text-shadows.html index ef8cc3f585..d6d0eb531b 100644 --- a/test/integration/rules/color-contrast/text-shadows.html +++ b/test/integration/rules/color-contrast/text-shadows.html @@ -2,7 +2,7 @@ section { display: inline-block; padding: 18px; - font-size: 36px; + font-size: 32px; } section > div { display: flex; @@ -67,6 +67,10 @@ color: #444; background-color: #ccc; } + .s7 { + color: #838383; + background-color: #bbb; + } .f1 { font-size: 12px; @@ -315,6 +319,77 @@ .l5 i:nth-child(4) { text-shadow: 0 0 0.5em #fff, 0 0 0.5em #fff, 0 0 0.5em #fff, 0 0 0.5em #fff; } + + .x1 i:nth-child(1) { + text-shadow: 0 -1px 0 #000, 1px 0 0 #000, 0 1px 0 #000; + } + .x1 i:nth-child(2) { + text-shadow: 0 -1px 0 #000, 1px 0 0 #000, 0 1px 0 #000, -1px 0 0 #000; + } + .x1 i:nth-child(3) { + text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, 1px 1px 0 #000, + -1px 1px 0 #000; + } + .x1 i:nth-child(4) { + text-shadow: -1px -1px 0 #000, 0 -1px 0 #000, 1px -1px 0 #000, 1px 0 0 #000, + 1px 1px 0 #000, 0 1px 0 #000, -1px 0 0 #000; + } + .x2 i:nth-child(1) { + text-shadow: 0 -1px 0 #444, 1px 0 0 #444, 0 1px 0 #444; + } + .x2 i:nth-child(2) { + text-shadow: 0 -1px 0 #444, 1px 0 0 #444, 0 1px 0 #444, -1px 0 0 #444; + } + .x2 i:nth-child(3) { + text-shadow: -1px -1px 0 #444, 1px -1px 0 #444, 1px 1px 0 #444, + -1px 1px 0 #444; + } + .x2 i:nth-child(4) { + text-shadow: -1px -1px 0 #444, 0 -1px 0 #444, 1px -1px 0 #444, 1px 0 0 #444, + 1px 1px 0 #444, 0 1px 0 #444, -1px 0 0 #444; + } + .x3 i:nth-child(1) { + text-shadow: 0 -1px 0 #888, 1px 0 0 #888, 0 1px 0 #888; + } + .x3 i:nth-child(2) { + text-shadow: 0 -1px 0 #888, 1px 0 0 #888, 0 1px 0 #888, -1px 0 0 #888; + } + .x3 i:nth-child(3) { + text-shadow: -1px -1px 0 #888, 1px -1px 0 #888, 1px 1px 0 #888, + -1px 1px 0 #888; + } + .x3 i:nth-child(4) { + text-shadow: -1px -1px 0 #888, 0 -1px 0 #888, 1px -1px 0 #888, 1px 0 0 #888, + 1px 1px 0 #888, 0 1px 0 #888, -1px 0 0 #888; + } + .x4 i:nth-child(1) { + text-shadow: 0 -1px 0 #aaa, 1px 0 0 #aaa, 0 1px 0 #aaa; + } + .x4 i:nth-child(2) { + text-shadow: 0 -1px 0 #aaa, 1px 0 0 #aaa, 0 1px 0 #aaa, -1px 0 0 #aaa; + } + .x4 i:nth-child(3) { + text-shadow: -1px -1px 0 #aaa, 1px -1px 0 #aaa, 1px 1px 0 #aaa, + -1px 1px 0 #aaa; + } + .x4 i:nth-child(4) { + text-shadow: -1px -1px 0 #aaa, 0 -1px 0 #aaa, 1px -1px 0 #aaa, 1px 0 0 #aaa, + 1px 1px 0 #aaa, 0 1px 0 #aaa, -1px 0 0 #aaa; + } + .x5 i:nth-child(1) { + text-shadow: 0 -1px 0 #fff, 1px 0 0 #fff, 0 1px 0 #fff; + } + .x5 i:nth-child(2) { + text-shadow: 0 -1px 0 #fff, 1px 0 0 #fff, 0 1px 0 #fff, -1px 0 0 #fff; + } + .x5 i:nth-child(3) { + text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, 1px 1px 0 #fff, + -1px 1px 0 #fff; + } + .x5 i:nth-child(4) { + text-shadow: -1px -1px 0 #fff, 0 -1px 0 #fff, 1px -1px 0 #fff, 1px 0 0 #fff, + 1px 1px 0 #fff, 0 1px 0 #fff, -1px 0 0 #fff; + } </style> <section class="s1" id="b1"> @@ -750,6 +825,72 @@ </div> </section> +<section class="s4" id="b14"> + <div class="f1 x2"> + <i id="b14-f1-i1">b14-f1-i1</i> + <i id="b14-f1-i2">b14-f1-i2</i> + <i id="b14-f1-i3">b14-f1-i3</i> + <i id="b14-f1-i4">b14-f1-i4</i> + </div> + <div class="f2 x2"> + <i id="b14-f2-i1">b14-f2-i1</i> + <i id="b14-f2-i2">b14-f2-i2</i> + <i id="b14-f2-i3">b14-f2-i3</i> + <i id="b14-f2-i4">b14-f2-i4</i> + </div> + <div class="f3 x2"> + <i id="b14-f3-i1">b14-f3-i1</i> + <i id="b14-f3-i2">b14-f3-i2</i> + <i id="b14-f3-i3">b14-f3-i3</i> + <i id="b14-f3-i4">b14-f3-i4</i> + </div> + <div class="f4 x2"> + <i id="b14-f4-i1">b14-f4-i1</i> + <i id="b14-f4-i2">b14-f4-i2</i> + <i id="b14-f4-i3">b14-f4-i3</i> + <i id="b14-f4-i4">b14-f4-i4</i> + </div> + <div class="f5 x2"> + <i id="b14-f5-i1">b14-f5-i1</i> + <i id="b14-f5-i2">b14-f5-i2</i> + <i id="b14-f5-i3">b14-f5-i3</i> + <i id="b14-f5-i4">b14-f5-i4</i> + </div> +</section> + +<section class="s7" id="b15"> + <div class="x1"> + <i id="b15-x1-i1">b15-x1-i1</i> + <i id="b15-x1-i2">b15-x1-i2</i> + <i id="b15-x1-i3">b15-x1-i3</i> + <i id="b15-x1-i4">b15-x1-i4</i> + </div> + <div class="x2"> + <i id="b15-x2-i1">b15-x2-i1</i> + <i id="b15-x2-i2">b15-x2-i2</i> + <i id="b15-x2-i3">b15-x2-i3</i> + <i id="b15-x2-i4">b15-x2-i4</i> + </div> + <div class="x3"> + <i id="b15-x3-i1">b15-x3-i1</i> + <i id="b15-x3-i2">b15-x3-i2</i> + <i id="b15-x3-i3">b15-x3-i3</i> + <i id="b15-x3-i4">b15-x3-i4</i> + </div> + <div class="x4"> + <i id="b15-x4-i1">b15-x4-i1</i> + <i id="b15-x4-i2">b15-x4-i2</i> + <i id="b15-x4-i3">b15-x4-i3</i> + <i id="b15-x4-i4">b15-x4-i4</i> + </div> + <div class="x5"> + <i id="b15-x5-i1">b15-x5-i1</i> + <i id="b15-x5-i2">b15-x5-i2</i> + <i id="b15-x5-i3">b15-x5-i3</i> + <i id="b15-x5-i4">b15-x5-i4</i> + </div> +</section> + <script src="/axe.js"></script> <script> // Script added for testing purposes, does not run in automated tests diff --git a/test/integration/rules/color-contrast/text-shadows.json b/test/integration/rules/color-contrast/text-shadows.json index 81674e45e4..cab0d0a6c1 100644 --- a/test/integration/rules/color-contrast/text-shadows.json +++ b/test/integration/rules/color-contrast/text-shadows.json @@ -129,7 +129,29 @@ ["#b13-f2-i3"], ["#b13-f2-i4"], ["#b13-f3-i3"], - ["#b13-f3-i4"] + ["#b13-f3-i4"], + + ["#b14-f1-i2"], + ["#b14-f1-i3"], + ["#b14-f1-i4"], + ["#b14-f2-i2"], + ["#b14-f2-i3"], + ["#b14-f2-i4"], + ["#b14-f3-i2"], + ["#b14-f3-i3"], + ["#b14-f3-i4"], + ["#b14-f4-i2"], + ["#b14-f5-i2"], + + ["#b15-x2-i2"], + ["#b15-x2-i3"], + ["#b15-x2-i4"], + ["#b15-x3-i2"], + ["#b15-x3-i3"], + ["#b15-x3-i4"], + ["#b15-x4-i2"], + ["#b15-x4-i3"], + ["#b15-x4-i4"] ], "passes": [ ["#b1-r1-i1"], @@ -290,6 +312,30 @@ ["#b13-f5-i1"], ["#b13-f5-i2"], ["#b13-f5-i3"], - ["#b13-f5-i4"] + ["#b13-f5-i4"], + + ["#b14-f4-i3"], + ["#b14-f4-i4"], + ["#b14-f5-i3"], + ["#b14-f5-i4"], + + ["#b15-x1-i2"], + ["#b15-x1-i3"], + ["#b15-x1-i4"], + ["#b15-x5-i2"], + ["#b15-x5-i3"], + ["#b15-x5-i4"] + ], + "incomplete": [ + ["#b14-f1-i1"], + ["#b14-f2-i1"], + ["#b14-f3-i1"], + ["#b14-f4-i1"], + ["#b14-f5-i1"], + ["#b15-x1-i1"], + ["#b15-x2-i1"], + ["#b15-x3-i1"], + ["#b15-x4-i1"], + ["#b15-x5-i1"] ] } diff --git a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html index a2e6bf29b9..68dad18989 100644 --- a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html +++ b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html @@ -8,11 +8,11 @@ <select id="ignore4"></select> <div tabindex="0" id="ignore5"></div> -<span id="fail1" class="fail1"></span> -<button id="fail1"></button> +<span id="incomplete1" class="incomplete1"></span> +<button id="incomplete1"></button> <span id="pass1"></span> <button id="pass2"></button> -<div aria-labelledby="fail1 pass1 pass2"></div> +<div aria-labelledby="incomplete1 pass1 pass2"></div> <input id="ignore6" type="hidden" /> <button id="ignore7" disabled></button> diff --git a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json index 2d16afd673..ffb1ae4d94 100644 --- a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json +++ b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json @@ -1,6 +1,7 @@ { "description": "duplicate-id-aria test", "rule": "duplicate-id-aria", - "violations": [[".fail1"]], + "violations": [], + "incomplete": [[".incomplete1"]], "passes": [["#pass1"], ["#pass2"]] } diff --git a/test/integration/rules/label/label.html b/test/integration/rules/label/label.html index a9a5d99b3d..d83f720578 100644 --- a/test/integration/rules/label/label.html +++ b/test/integration/rules/label/label.html @@ -5,67 +5,85 @@ <input type="submit" id="na4" /> <input type="reset" id="na5" /> <input type="text" id="fail1" /> - <textarea id="fail3"></textarea> + <textarea id="fail2"></textarea> <input type="text" aria-label="label" id="pass1" /> - <textarea aria-label="label" id="pass3"></textarea> - <input type="text" aria-labelledby="label" id="pass4" /> - <textarea aria-labelledby="label" id="pass6"></textarea> + <textarea aria-label="label" id="pass2"></textarea> + <input type="text" aria-labelledby="label" id="pass3" /> + <textarea aria-labelledby="label" id="pass4"></textarea> <div id="label">Label</div> - <label>Label <input type="text" id="pass7" /></label> - <label>Label <textarea id="pass9"></textarea></label> + <label>Label <input type="text" id="pass5" /></label> + <label>Label <textarea id="pass6"></textarea></label> - <label><input type="text" id="fail4" /></label> - <label><textarea id="fail6"></textarea></label> - <label for="fail7"></label><input type="text" id="fail7" /> - <label for="fail9"></label><textarea id="fail9"></textarea> + <label><input type="text" id="fail3" /></label> + <label><textarea id="fail4"></textarea></label> + <label for="fail5"></label><input type="text" id="fail5" /> + <label for="fail6"></label><textarea id="fail6"></textarea> - <label for="fail11" style="display: none">Text</label - ><input type="text" id="fail11" /> <label for="pass10">Label</label - ><input type="text" id="pass10" /> <label for="pass12">Label</label - ><textarea id="pass12"></textarea> + <label for="fail7" style="display: none">Text</label + ><input type="text" id="fail7" /> <label for="pass7">Label</label + ><input type="text" id="pass7" /><label for="pass8">Label</label + ><textarea id="pass8"></textarea> - <input id="pass13" title="Label" /> - <textarea id="pass15" title="Label"></textarea> + <input id="pass9" title="Label" /> + <textarea id="pass10" title="Label"></textarea> <label - ><label><input type="text" id="fail22" /></label + ><label><input type="text" id="fail8" /></label ></label> <div> - <label> - <select id="fail24"> - <option selected="selected">Chosen</option> - <option>Not Selected</option> - </select> - </label> - <input aria-labelledby="fail24" type="text" id="fail25" /> + <select id="pass11-label"> + <option selected="selected">Chosen</option> + <option>Not Selected</option> + </select> + <input aria-labelledby="pass11-label" type="text" id="pass11" /> </div> <div> - <label for="fail27"> - <select id="fail26"> + <label for="pass12"> + <select> <option selected="selected">Chosen</option> <option>Not Selected</option> </select> </label> - <input type="text" id="fail27" /> + <input type="text" id="pass12" /> </div> <div> - <label for="pass-gh1176" style="display: none"> Hello world </label> - <input id="pass-gh1176" title="Hi" /> + <label for="pass13" style="display: none"> Hello world </label> + <input id="pass13" title="Hi" /> </div> - <input id="pass16" role="none" disabled /> - <input id="pass17" role="presentation" disabled /> + <input id="pass14" role="none" disabled /> + <input id="pass15" role="presentation" disabled /> <div id="hiddenlabel" aria-hidden="true">Hidden label</div> - <input type="text" id="pass18" aria-labelledby="hiddenlabel" /> + <input type="text" id="pass16" aria-labelledby="hiddenlabel" /> + + <label> <input id="fail9" value="val" /></label> + <label for="fail10"> <input id="fail10" value="val" /></label> + <div id="lbl-f13"> + <input id="fail11" value="val" aria-labelledby="lbl-f13" /> + </div> + <label><textarea id="fail12"> value</textarea></label> + + <table> + <tr> + <th id="pass17-label">Named from author elm</th> + <td><input id="pass17" aria-labelledby="pass17-label" /></td> + </tr> + </table> + + <div id="fail13-label"> + <span role="progressbar" aria-valuenow="23">23%</span> + </div> + <input type="text" id="fail13" aria-labelledby="fail13-label" /> - <label> <input id="fail28" value="val" /></label> - <label for="fail29"> <input id="fail29" value="val" /></label> - <div id="lbl-f30"> - <input id="fail30" value="val" aria-labelledby="lbl-f30" /> + <div id="fail14-label"> + <select> + <option selected></option> + <option>Option 2</option> + </select> </div> - <label><textarea id="fail31"> value</textarea></label> + <input type="text" id="fail14" aria-labelledby="fail14-label" /> </form> diff --git a/test/integration/rules/label/label.json b/test/integration/rules/label/label.json index abccc763b6..1f99651df3 100644 --- a/test/integration/rules/label/label.json +++ b/test/integration/rules/label/label.json @@ -3,34 +3,37 @@ "rule": "label", "violations": [ ["#fail1"], + ["#fail2"], ["#fail3"], ["#fail4"], + ["#fail5"], ["#fail6"], ["#fail7"], + ["#fail8"], ["#fail9"], + ["#fail10"], ["#fail11"], - ["#fail22"], - ["#fail25"], - ["#fail27"], - ["#fail28"], - ["#fail29"], - ["#fail30"], - ["#fail31"] + ["#fail12"], + ["#fail13"], + ["#fail14"] ], "passes": [ ["#pass1"], + ["#pass2"], ["#pass3"], ["#pass4"], + ["#pass5"], ["#pass6"], ["#pass7"], + ["#pass8"], ["#pass9"], ["#pass10"], + ["#pass11"], ["#pass12"], ["#pass13"], + ["#pass14"], ["#pass15"], ["#pass16"], - ["#pass17"], - ["#pass-gh1176"], - ["#pass18"] + ["#pass17"] ] } diff --git a/test/integration/rules/runner.js b/test/integration/rules/runner.js index 134be306cc..0110f03cd8 100644 --- a/test/integration/rules/runner.js +++ b/test/integration/rules/runner.js @@ -1,9 +1,9 @@ -(function () { +(() => { // this line is replaced with the test object in preprocessor.js - var test = {}; /*tests*/ + const testObj = {}; /*tests*/ - var ruleId = test.rule; - var testName = test.description || ruleId + ' test'; + const ruleId = testObj.rule; + const testName = testObj.description || ruleId + ' test'; function flattenResult(results) { return { @@ -14,62 +14,63 @@ } function waitForFrames(context, cb) { + let loaded = 0; + const frames = context.querySelectorAll('iframe'); + function loadListener() { loaded++; if (loaded === length) { cb(); } } - var loaded = 0; - var frames = context.querySelectorAll('iframe'); if (!frames.length) { return cb(); } - for (var index = 0, length = frames.length; index < length; index++) { + for (let index = 0, length = frames.length; index < length; index++) { frames[index].addEventListener('load', loadListener); } } - var fixture = document.getElementById('fixture'); - var rule = axe.utils.getRule(ruleId); + const fixture = document.getElementById('fixture'); + const rule = axe.utils.getRule(ruleId); if (!rule) { return; } // don't run rules that are deprecated and disabled - var deprecated = rule.tags.indexOf('deprecated') !== -1; - (deprecated && !rule.enabled ? describe.skip : describe)(ruleId, function () { - describe(testName, function () { + const deprecated = rule.tags.indexOf('deprecated') !== -1; + (deprecated && !rule.enabled ? describe.skip : describe)(ruleId, () => { + describe(testName, () => { function runTest(test, collection) { if (!test[collection]) { return; } - describe(collection, function () { - var nodes; - before(function () { + describe(collection, () => { + let nodes; + before(() => { if (typeof results[collection] === 'object') { nodes = results[collection].nodes; } }); - test[collection].forEach(function (selector) { - it('should find ' + JSON.stringify(selector), function () { + test[collection].forEach(selector => { + it('should find ' + JSON.stringify(selector), () => { if (!nodes) { assert(false, 'there are no ' + collection); return; } - var matches = nodes.filter(function (node) { - for (var i = 0; i < selector.length; i++) { + const matches = nodes.filter(node => { + for (let i = 0; i < selector.length; i++) { if (node.target[i] !== selector[i]) { return false; } } return node.target.length === selector.length; }); - matches.forEach(function (node) { + matches.forEach(node => { // remove each node we find nodes.splice(nodes.indexOf(node), 1); }); @@ -87,24 +88,28 @@ }); }); - it('should not return other results', function () { + it('should not return other results', () => { if (typeof nodes !== 'undefined') { - var targets = nodes.map(function (node) { + const targets = nodes.map(node => { return node.target; }); // check that all nodes are removed assert.equal(JSON.stringify(targets), '[]'); } else { - assert(false, 'there are no ' + collection); + assert.lengthOf( + test[collection], + 0, + 'there are no ' + collection + ); } }); }); } - var results; - before(function (done) { - fixture.innerHTML = test.content; - waitForFrames(fixture, function () { + let results; + before(done => { + fixture.innerHTML = testObj.content; + waitForFrames(fixture, () => { axe.run( fixture, { @@ -116,16 +121,16 @@ performanceTimer: false, runOnly: { type: 'rule', values: [ruleId] } }, - function (err, r) { + (err, r) => { // assert that there are no errors - if error exists a stack trace is logged. - var errStack = err && err.stack ? err.stack : ''; + const errStack = err && err.stack ? err.stack : ''; assert.isNull(err, 'Error should be null. ' + errStack); // assert that result is defined assert.isDefined(r, 'Results are defined.'); // assert that result has certain keys assert.hasAnyKeys(r, ['incomplete', 'violations', 'passes']); // assert incomplete(s) does not have error - r.incomplete.forEach(function (incomplete) { + r.incomplete.forEach(incomplete => { assert.isUndefined(incomplete.error); }); // flatten results @@ -135,10 +140,10 @@ ); }); }); - runTest(test, 'passes'); - runTest(test, 'violations'); - if (test.incomplete) { - runTest(test, 'incomplete'); + runTest(testObj, 'passes'); + runTest(testObj, 'violations'); + if (testObj.incomplete) { + runTest(testObj, 'incomplete'); } }); }); diff --git a/test/integration/virtual-rules/aria-allowed-attr.js b/test/integration/virtual-rules/aria-allowed-attr.js index 9c89aa9e74..4f9b67a2f4 100644 --- a/test/integration/virtual-rules/aria-allowed-attr.js +++ b/test/integration/virtual-rules/aria-allowed-attr.js @@ -120,21 +120,6 @@ describe('aria-allowed-attr virtual-rule', function () { assert.lengthOf(results.incomplete, 0); }); - it('fails when aria-checked is inconsistent with native checkbox state', () => { - var results = axe.runVirtualRule('aria-allowed-attr', { - nodeName: 'input', - checked: true, - attributes: { - type: 'checkbox', - 'aria-checked': 'false' - } - }); - - assert.lengthOf(results.passes, 0); - 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', diff --git a/test/integration/virtual-rules/aria-braille-equivalent.js b/test/integration/virtual-rules/aria-braille-equivalent.js new file mode 100644 index 0000000000..cb20f39553 --- /dev/null +++ b/test/integration/virtual-rules/aria-braille-equivalent.js @@ -0,0 +1,74 @@ +describe('aria-braille-equivalent virtual-rule', () => { + afterEach(() => { + axe.reset(); + }); + + it('passes when aria-braillelabel is not empty', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'img', + attributes: { + alt: 'Hello world', + 'aria-braillelabel': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('incompletes when accessible text is empty but braille label is not', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'img', + attributes: { + alt: '', + 'aria-braillelabel': 'hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('passes when roledescription and brailleroledescription are not empty', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'div', + attributes: { + 'aria-roledescription': 'Hello world', + 'aria-brailleroledescription': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('incompletes when roledescription is empty but brailleroledescription is not', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'div', + attributes: { + 'aria-roledescription': '', + 'aria-brailleroledescription': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('incompletes if the subtree fails to compute with aria-braillelabel', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'button', + attributes: { + 'aria-braillelabel': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); +}); diff --git a/test/integration/virtual-rules/aria-conditional-attr.js b/test/integration/virtual-rules/aria-conditional-attr.js new file mode 100644 index 0000000000..9aa9ffd109 --- /dev/null +++ b/test/integration/virtual-rules/aria-conditional-attr.js @@ -0,0 +1,111 @@ +describe('aria-conditional-attr virtual-rule', function () { + it('passes when aria-checked is consistent with native checkbox state', () => { + const results = axe.runVirtualRule('aria-conditional-attr', { + nodeName: 'input', + checked: true, + attributes: { + type: 'checkbox', + 'aria-checked': 'true' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('fails when aria-checked is inconsistent with native checkbox state', () => { + const results = axe.runVirtualRule('aria-conditional-attr', { + nodeName: 'input', + checked: true, + attributes: { + type: 'checkbox', + 'aria-checked': 'false' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('passes conditional row attributes on detached rows', () => { + const results = axe.runVirtualRule('aria-conditional-attr', { + nodeName: 'tr', + attributes: { + role: 'row', + 'aria-level': '1', + 'aria-posinset': '1', + 'aria-setsize': '1', + 'aria-expanded': 'true' + } + }); + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('fails conditional row attributes on table', () => { + const table = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + const tr = new axe.SerialVirtualNode({ + nodeName: 'tr', + attributes: { + role: 'row', + 'aria-level': '1', + 'aria-posinset': '1', + 'aria-setsize': '1', + 'aria-expanded': 'true' + } + }); + const td = new axe.SerialVirtualNode({ nodeName: 'td' }); + table.children = [tr]; + tr.children = [td]; + td.parent = tr; + tr.parent = table; + table.parent = null; + + const results = axe.runVirtualRule('aria-conditional-attr', tr); + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + + const { invalidAttrs } = results.violations[0].nodes[0].all[0].data; + invalidAttrs.sort(); + assert.deepEqual(invalidAttrs, [ + 'aria-expanded', + 'aria-level', + 'aria-posinset', + 'aria-setsize' + ]); + }); + + it('passes conditional row attributes on treegrid', () => { + const table = new axe.SerialVirtualNode({ + nodeName: 'table', + attributes: { role: 'treegrid' } + }); + const tr = new axe.SerialVirtualNode({ + nodeName: 'tr', + attributes: { + role: 'row', + 'aria-level': '1', + 'aria-posinset': '1', + 'aria-setsize': '1', + 'aria-expanded': 'true' + } + }); + const td = new axe.SerialVirtualNode({ nodeName: 'td' }); + table.children = [tr]; + tr.children = [td]; + td.parent = tr; + tr.parent = table; + table.parent = null; + + const results = axe.runVirtualRule('aria-conditional-attr', tr); + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/virtual-rules/aria-prohibited-attr.js b/test/integration/virtual-rules/aria-prohibited-attr.js new file mode 100644 index 0000000000..ccacf57b33 --- /dev/null +++ b/test/integration/virtual-rules/aria-prohibited-attr.js @@ -0,0 +1,61 @@ +describe('aria-prohibited-attr virtual-rule', () => { + it('should pass for required attributes', () => { + const results = axe.runVirtualRule('aria-prohibited-attr', { + nodeName: 'div', + attributes: { + role: 'checkbox', + 'aria-checked': true + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for allowed attributes', () => { + const results = axe.runVirtualRule('aria-prohibited-attr', { + nodeName: 'div', + attributes: { + role: 'radio', + 'aria-required': true, + 'aria-checked': true + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for invalid attributes', () => { + const results = axe.runVirtualRule('aria-prohibited-attr', { + nodeName: 'div', + attributes: { + role: 'dialog', + 'aria-cats': true + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail for prohibited attributes', () => { + const vNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + role: 'code', + 'aria-label': 'foo' + } + }); + vNode.children = []; + + const results = axe.runVirtualRule('aria-prohibited-attr', vNode); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/virtual-rules/aria-valid-attr-value.js b/test/integration/virtual-rules/aria-valid-attr-value.js index 2029f09c1f..b530ed27b1 100644 --- a/test/integration/virtual-rules/aria-valid-attr-value.js +++ b/test/integration/virtual-rules/aria-valid-attr-value.js @@ -23,7 +23,6 @@ describe('aria-valid-attr-value virtual-rule', function () { attributes: { role: 'slider', 'aria-valuemin': true, - 'aria-valuetext': '', 'aria-expanded': 'grid', 'aria-haspopup': 'Range', 'aria-valuenow': false @@ -34,7 +33,7 @@ describe('aria-valid-attr-value virtual-rule', function () { assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); - assert.lengthOf(results.violations[0].nodes[0].all[0].data, 5); + assert.lengthOf(results.violations[0].nodes[0].all[0].data, 4); }); it('should only mark invalid values', function () { diff --git a/test/node/jsdom.js b/test/node/jsdom.js index 5d65ceb99e..e7bddb4191 100644 --- a/test/node/jsdom.js +++ b/test/node/jsdom.js @@ -1,8 +1,8 @@ -var axe = require('../../'); -var jsdom = require('jsdom'); -var assert = require('assert'); +const axe = require('../../'); +const jsdom = require('jsdom'); +const assert = require('assert'); -var domStr = +const domStr = '<!DOCTYPE html>' + '<html lang="en">' + '<head>' + @@ -17,9 +17,9 @@ var domStr = '</body>' + '</html>'; -describe('jsdom axe-core', function () { - it('should run without setting globals', function () { - var dom = new jsdom.JSDOM(domStr); +describe('jsdom axe-core', () => { + it('should run without setting globals', () => { + const dom = new jsdom.JSDOM(domStr); return axe .run(dom.window.document.documentElement, { @@ -30,8 +30,8 @@ describe('jsdom axe-core', function () { }); }); - it('should unset globals so it can run with a new set of globals', function () { - var dom = new jsdom.JSDOM(domStr); + it('should unset globals so it can run with a new set of globals', () => { + let dom = new jsdom.JSDOM(domStr); return axe .run(dom.window.document.documentElement, { @@ -40,45 +40,45 @@ describe('jsdom axe-core', function () { .then(function (results) { assert.notStrictEqual(results.violations.length, 0); - var dom = new jsdom.JSDOM(domStr); + dom = new jsdom.JSDOM(domStr); return axe .run(dom.window.document.documentElement, { rules: { 'color-contrast': { enabled: false } } }) - .then(function (results) { - assert.notStrictEqual(results.violations.length, 0); + .then(function (res) { + assert.notStrictEqual(res.violations.length, 0); }); }); }); - describe('audit', function () { - var audit = axe._audit; + describe('audit', () => { + const audit = axe._audit; - it('should have an empty allowedOrigins', function () { + it('should have an empty allowedOrigins', () => { // JSDOM does not have window.location, so there is no default origin assert.strictEqual(audit.allowedOrigins.length, 0); }); }); - describe('isCurrentPageLink', function () { + describe('isCurrentPageLink', () => { // 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 () { + afterEach(() => { 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'); + it('should return true if url starts with #', () => { + const dom = new jsdom.JSDOM(domStr); + const anchor = dom.window.document.getElementById('hash-link'); axe.configure({ checks: [ { id: 'check-current-page-link', - evaluate: function () { + evaluate: () => { return axe.commons.dom.isCurrentPageLink(anchor) === true; } } @@ -100,15 +100,15 @@ describe('jsdom axe-core', function () { }); }); - 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'); + it('should return null for absolute link when url is not set', () => { + const dom = new jsdom.JSDOM(domStr); + const anchor = dom.window.document.getElementById('skip'); axe.configure({ checks: [ { id: 'check-current-page-link', - evaluate: function () { + evaluate: () => { return axe.commons.dom.isCurrentPageLink(anchor) === null; } } @@ -130,15 +130,15 @@ describe('jsdom axe-core', function () { }); }); - 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'); + it('should return true for absolute link when url is set', () => { + const dom = new jsdom.JSDOM(domStr, { url: 'https://page.com' }); + const anchor = dom.window.document.getElementById('skip'); axe.configure({ checks: [ { id: 'check-current-page-link', - evaluate: function () { + evaluate: () => { return axe.commons.dom.isCurrentPageLink(anchor) === true; } } @@ -160,4 +160,37 @@ describe('jsdom axe-core', function () { }); }); }); + + describe('axe.setup()', () => { + afterEach(() => { + axe.teardown(); + }); + + it('sets up the tree', function () { + const { document } = new jsdom.JSDOM(domStr).window; + const tree = axe.setup(document.body); + assert.equal(tree, axe._tree[0]); + assert.equal(tree.actualNode, document.body); + }); + + it('can use commons after axe.setup()', () => { + const { document } = new jsdom.JSDOM(domStr).window; + axe.setup(document); + + const skipLink = document.querySelector('#skip'); + assert.equal(axe.commons.aria.getRole(skipLink), 'link'); + assert.equal(axe.commons.text.accessibleText(skipLink), 'Skip Link'); + }); + + it('is cleaned up with axe.teardown()', () => { + const { document } = new jsdom.JSDOM(domStr).window; + axe.setup(document); + axe.teardown(); + const skipLink = document.querySelector('#skip'); + + assert.throws(() => { + assert.equal(axe.commons.aria.getRole(skipLink), 'link'); + }); + }); + }); }); diff --git a/test/testutils.js b/test/testutils.js index 843441bbf4..cd0e16fe1b 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -1,678 +1,711 @@ -/* global axe, checks */ +/*eslint no-unused-vars: 0*/ + +// these are global to the entire test suite so need to be declared at the global +// level (old architecture that should not be relied on in any new code) +var checks; +var commons; -// Let the user know they need to disable their axe/attest extension before running the tests. -if (window.__AXE_EXTENSION__) { - throw new Error( - 'You must disable your axe/attest browser extension in order to run the test suite.' - ); -} +(() => { + // Let the user know they need to disable their axe/attest extension before running the tests. + if (window.__AXE_EXTENSION__) { + throw new Error( + 'You must disable your axe/attest browser extension in order to run the test suite.' + ); + } -/*eslint indent: 0*/ -var testUtils = {}; + const testUtils = (axe.testUtils = {}); -/*eslint no-unused-vars: 0*/ -var checks, commons; -var originalChecks = (checks = axe._audit.checks); -var originalAudit = axe._audit; -var originalRules = axe._audit.rules; -var originalCommons = (commons = axe.commons); - -// add fixture to the body if it's not already -var fixture = document.getElementById('fixture'); -if (!fixture) { - fixture = document.createElement('div'); - fixture.setAttribute('id', 'fixture'); - document.body.insertBefore(fixture, document.body.firstChild); -} - -// determine which checks are used only in the `none` array of rules -var noneChecks = []; - -function verifyIsNoneCheck(check) { - var index = noneChecks.indexOf(check); - if (index !== -1) { - noneChecks.splice(index, 1); + const originalChecks = (checks = axe._audit.checks); + const originalAudit = axe._audit; + const originalRules = axe._audit.rules; + const originalCommons = (commons = axe.commons); + + // Global chai configuration + if (window.chai) { + window.chai.config.truncateThreshold = 0; + } + + // add fixture to the body if it's not already + let fixture = document.getElementById('fixture'); + if (!fixture) { + fixture = document.createElement('div'); + fixture.setAttribute('id', 'fixture'); + document.body.insertBefore(fixture, document.body.firstChild); } -} -axe._audit.rules.forEach(function (rule) { - rule.none.forEach(function (check) { - check = check.id || check; - if (noneChecks.indexOf(check) === -1) { - noneChecks.push(check); + // determine which checks are used only in the `none` array of rules + const noneChecks = []; + + function verifyIsNoneCheck(check) { + const index = noneChecks.indexOf(check); + if (index !== -1) { + noneChecks.splice(index, 1); } + } + + axe._audit.rules.forEach(function (rule) { + rule.none.forEach(function (check) { + check = check.id || check; + if (noneChecks.indexOf(check) === -1) { + noneChecks.push(check); + } + }); }); -}); - -axe._audit.rules.forEach(function (rule) { - rule.any.forEach(verifyIsNoneCheck); - rule.all.forEach(verifyIsNoneCheck); -}); - -/** - * Create a check context for mocking/resetting data and relatedNodes in tests - * - * @return Object - */ -testUtils.MockCheckContext = function () { - 'use strict'; - return { - _relatedNodes: [], - _data: null, - // When using this.async() in a check, assign a function to _onAsync - // to catch the response. - _onAsync: null, - async: function () { - var self = this; - return function (result) { - // throws if _onAsync isn't set - self._onAsync(result, self); - }; - }, - data: function (d) { - this._data = d; - }, - relatedNodes: function (nodes) { - this._relatedNodes = Array.isArray(nodes) ? nodes : [nodes]; - }, - reset: function () { - this._data = null; - this._relatedNodes = []; - this._onAsync = null; + + axe._audit.rules.forEach(function (rule) { + rule.any.forEach(verifyIsNoneCheck); + rule.all.forEach(verifyIsNoneCheck); + }); + + /** + * Create a check context for mocking/resetting data and relatedNodes in tests + * + * @return Object + */ + testUtils.MockCheckContext = function () { + 'use strict'; + return { + _relatedNodes: [], + _data: null, + // When using this.async() in a check, assign a function to _onAsync + // to catch the response. + _onAsync: null, + async: function () { + const self = this; + return function (result) { + // throws if _onAsync isn't set + self._onAsync(result, self); + }; + }, + data: function (d) { + this._data = d; + }, + relatedNodes: function (nodes) { + this._relatedNodes = Array.isArray(nodes) ? nodes : [nodes]; + }, + reset: function () { + this._data = null; + this._relatedNodes = []; + this._onAsync = null; + } + }; + }; + + /** + * Provide an API for determining Shadow DOM v0 and v1 support in tests. + * + * @param HTMLDocumentElement The document of the current context + * @return Object + */ + testUtils.shadowSupport = (function (document) { + 'use strict'; + const v0 = + document.body && typeof document.body.createShadowRoot === 'function', + v1 = document.body && typeof document.body.attachShadow === 'function'; + + return { + v0: v0 === true, + v1: v1 === true, + undefined: + document.body && + typeof document.body.attachShadow === 'undefined' && + typeof document.body.createShadowRoot === 'undefined' + }; + })(document); + + /** + * Return the fixture element + * @return HTMLElement + */ + testUtils.getFixture = function () { + 'use strict'; + return fixture; + }; + + /** + * Method for injecting content into a fixture + * @param {String|Node} content Stuff to go into the fixture (html or DOM node) + * @return HTMLElement + */ + testUtils.injectIntoFixture = function (content) { + 'use strict'; + if (typeof content !== 'undefined') { + fixture.innerHTML = ''; + } + + if (typeof content === 'string') { + fixture.innerHTML = content; + } else if (content instanceof Node) { + fixture.appendChild(content); + } else if (Array.isArray(content)) { + content.forEach(function (node) { + fixture.appendChild(node); + }); } + + return fixture; }; -}; - -/** - * Provide an API for determining Shadow DOM v0 and v1 support in tests. - * - * @param HTMLDocumentElement The document of the current context - * @return Object - */ -testUtils.shadowSupport = (function (document) { - 'use strict'; - var v0 = - document.body && typeof document.body.createShadowRoot === 'function', - v1 = document.body && typeof document.body.attachShadow === 'function'; - - return { - v0: v0 === true, - v1: v1 === true, - undefined: - document.body && - typeof document.body.attachShadow === 'undefined' && - typeof document.body.createShadowRoot === 'undefined' + + /** + * Method for injecting content into a fixture and caching + * the flattened DOM tree (light and Shadow DOM together) + * + * @param {String|Node} content Stuff to go into the fixture (html or DOM node) + * @return HTMLElement + */ + testUtils.fixtureSetup = function (content) { + 'use strict'; + testUtils.injectIntoFixture(content); + axe.teardown(); + return axe.setup(fixture); }; -})(document); - -/** - * Return the fixture element - * @return HTMLElement - */ -testUtils.getFixture = function () { - 'use strict'; - return fixture; -}; - -/** - * Method for injecting content into a fixture - * @param {String|Node} content Stuff to go into the fixture (html or DOM node) - * @return HTMLElement - */ -testUtils.injectIntoFixture = function (content) { - 'use strict'; - if (typeof content !== 'undefined') { - fixture.innerHTML = ''; - } - if (typeof content === 'string') { - fixture.innerHTML = content; - } else if (content instanceof Node) { - fixture.appendChild(content); - } else if (Array.isArray(content)) { - content.forEach(function (node) { - fixture.appendChild(node); - }); - } + /** + * Create check arguments + * + * @param Node|String Stuff to go into the fixture (html or node) + * @param Object Options argument for the check (optional, default: {}) + * @param String Target for the check, CSS selector (default: '#target') + * @return Array + */ + testUtils.checkSetup = function (content, options, target) { + 'use strict'; + // Normalize the params + if (typeof options !== 'object') { + target = options; + options = {}; + } + // Normalize target, allow it to be the inserted node or '#target' + target = target || (content instanceof Node ? content : '#target'); + const rootNode = testUtils.fixtureSetup(content); + + let node; + if (typeof target === 'string') { + node = axe.utils.querySelectorAll(rootNode, target)[0]; + } else if (target instanceof Node) { + node = axe.utils.getNodeFromTree(target); + } else { + node = target; + } + return [node.actualNode, options, node]; + }; - return fixture; -}; - -/** - * Method for injecting content into a fixture and caching - * the flattened DOM tree (light and Shadow DOM together) - * - * @param {String|Node} content Stuff to go into the fixture (html or DOM node) - * @return HTMLElement - */ -testUtils.fixtureSetup = function (content) { - 'use strict'; - testUtils.injectIntoFixture(content); - axe.teardown(); - return axe.setup(fixture); -}; - -/** - * Create check arguments - * - * @param Node|String Stuff to go into the fixture (html or node) - * @param Object Options argument for the check (optional, default: {}) - * @param String Target for the check, CSS selector (default: '#target') - * @return Array - */ -testUtils.checkSetup = function (content, options, target) { - 'use strict'; - // Normalize the params - if (typeof options !== 'object') { - target = options; - options = {}; - } - // Normalize target, allow it to be the inserted node or '#target' - target = target || (content instanceof Node ? content : '#target'); - var rootNode = testUtils.fixtureSetup(content); - - var node; - if (typeof target === 'string') { - node = axe.utils.querySelectorAll(rootNode, target)[0]; - } else if (target instanceof Node) { - node = axe.utils.getNodeFromTree(target); - } else { - node = target; - } - return [node.actualNode, options, node]; -}; - -/** - * Create a shadow DOM tree on #shadow and setup axe for it, returning #target - * - * @param Node|String Stuff to go into the fixture (html string or DOM Node) - * @param Node|String Stuff to go into the shadow boundary (html or node) - * @param String Target selector for the check, can be inside or outside of Shadow DOM (optional, default: '#target') - * @return VirtualNode - */ -testUtils.queryShadowFixture = function ( - content, - shadowContent, - targetSelector -) { - 'use strict'; - - // Normalize target, allow it to be the provided string or use '#target' to query composed tree - if (typeof targetSelector !== 'string') { - targetSelector = '#target'; - } + /** + * Create a shadow DOM tree on #shadow and setup axe for it, returning #target + * + * @param Node|String Stuff to go into the fixture (html string or DOM Node) + * @param Node|String Stuff to go into the shadow boundary (html or node) + * @param String Target selector for the check, can be inside or outside of Shadow DOM (optional, default: '#target') + * @return VirtualNode + */ + testUtils.queryShadowFixture = function ( + content, + shadowContent, + targetSelector + ) { + 'use strict'; - var fixture = testUtils.injectIntoFixture(content); - var targetCandidate = fixture.querySelector(targetSelector); - var container = targetCandidate; - if (!targetCandidate) { - // check if content specifies a shadow container - container = fixture.querySelector('#shadow'); - if (!container) { - container = fixture.firstChild; + // Normalize target, allow it to be the provided string or use '#target' to query composed tree + if (typeof targetSelector !== 'string') { + targetSelector = '#target'; } - } - // attach a shadowRoot with the content provided - var shadowRoot = container.attachShadow({ mode: 'open' }); - if (typeof shadowContent === 'string') { - shadowRoot.innerHTML = shadowContent; - } else if (content instanceof Node) { - shadowRoot.appendChild(shadowContent); - } - if (!targetCandidate) { - targetCandidate = shadowRoot.querySelector(targetSelector); - } - if (!targetSelector && !targetCandidate) { - throw 'shadowCheckSetup requires at least one fragment to have #target, or a provided targetSelector'; - } + const fixtureNode = testUtils.injectIntoFixture(content); + let targetCandidate = fixtureNode.querySelector(targetSelector); + let container = targetCandidate; + if (!targetCandidate) { + // check if content specifies a shadow container + container = fixtureNode.querySelector('#shadow'); + if (!container) { + container = fixtureNode.firstChild; + } + } + // attach a shadowRoot with the content provided + const shadowRoot = container.attachShadow({ mode: 'open' }); + if (typeof shadowContent === 'string') { + shadowRoot.innerHTML = shadowContent; + } else if (content instanceof Node) { + shadowRoot.appendChild(shadowContent); + } - // query the composed tree AFTER shadowDOM has been attached - axe.setup(fixture); - return axe.utils.getNodeFromTree(targetCandidate); -}; - -/** - * Create check arguments with Shadow DOM. Target can be inside or outside of Shadow DOM, queried by - * adding `id="target"` to a fragment. Or specify a custom selector as the `targetSelector` argument. - * - * @param Node|String Stuff to go into the fixture (html string or DOM Node) - * @param Node|String Stuff to go into the shadow boundary (html or node) - * @param Object Options argument for the check (optional, default: {}) - * @param String Target selector for the check, can be inside or outside of Shadow DOM (optional, default: '#target') - * @return Array - */ -testUtils.shadowCheckSetup = function ( - content, - shadowContent, - options, - targetSelector -) { - // Normalize the object params - if (typeof options !== 'object') { - options = {}; - } - var node = testUtils.queryShadowFixture( + if (!targetCandidate) { + targetCandidate = shadowRoot.querySelector(targetSelector); + } + if (!targetSelector && !targetCandidate) { + throw 'shadowCheckSetup requires at least one fragment to have #target, or a provided targetSelector'; + } + + // query the composed tree AFTER shadowDOM has been attached + axe.setup(fixtureNode); + return axe.utils.getNodeFromTree(targetCandidate); + }; + + /** + * Create check arguments with Shadow DOM. Target can be inside or outside of Shadow DOM, queried by + * adding `id="target"` to a fragment. Or specify a custom selector as the `targetSelector` argument. + * + * @param Node|String Stuff to go into the fixture (html string or DOM Node) + * @param Node|String Stuff to go into the shadow boundary (html or node) + * @param Object Options argument for the check (optional, default: {}) + * @param String Target selector for the check, can be inside or outside of Shadow DOM (optional, default: '#target') + * @return Array + */ + testUtils.shadowCheckSetup = function ( content, shadowContent, + options, targetSelector - ); - return [node.actualNode, options, node]; -}; - -/** - * Setup axe._tree flat tree - * @param Node Stuff to go in the flat tree - * @returns vNode[] - */ -testUtils.flatTreeSetup = function (content) { - axe._tree = axe.utils.getFlattenedTree(content); - return axe._tree; -}; - -/** - * Wait for all nested frames to be loaded - * - * @param Object Window to wait for (optional) - * @param function Callback, called once resolved - * @param function Callback, called once rejected - */ -testUtils.awaitNestedLoad = function awaitNestedLoad(win, cb, errCb) { - 'use strict'; - if (typeof win === 'function') { - errCb = cb; - cb = win; - win = window; - } - var document = win.document; - var q = axe.utils.queue(); + ) { + // Normalize the object params + if (typeof options !== 'object') { + options = {}; + } + const node = testUtils.queryShadowFixture( + content, + shadowContent, + targetSelector + ); + return [node.actualNode, options, node]; + }; - // Wait for page load - q.defer(function (resolve) { - if (document.readyState === 'complete') { - resolve(); - } else { - win.addEventListener('load', resolve); + /** + * Setup axe._tree flat tree + * @param Node Stuff to go in the flat tree + * @returns vNode[] + */ + testUtils.flatTreeSetup = function (content) { + axe._tree = axe.utils.getFlattenedTree(content); + return axe._tree; + }; + + /** + * Wait for all nested frames to be loaded + * + * @param Object Window to wait for (optional) + * @param function Callback, called once resolved + * @param function Callback, called once rejected + */ + testUtils.awaitNestedLoad = function awaitNestedLoad(win, cb, errCb) { + 'use strict'; + if (typeof win === 'function') { + errCb = cb; + cb = win; + win = window; } - }); + const document = win.document; + const q = axe.utils.queue(); - // Wait for all frames to be loaded - Array.from(document.querySelectorAll('iframe')).forEach(function (frame) { + // Wait for page load q.defer(function (resolve) { - return awaitNestedLoad(frame.contentWindow, resolve); + if (document.readyState === 'complete') { + resolve(); + } else { + win.addEventListener('load', resolve); + } }); - }); - // Complete (don't pass the args on to the callback) - q.then(function () { - cb(); - }); + // Wait for all frames to be loaded + Array.from(document.querySelectorAll('iframe')).forEach(function (frame) { + q.defer(function (resolve) { + return awaitNestedLoad(frame.contentWindow, resolve); + }); + }); - if (errCb) { - q.catch(errCb); - } -}; - -/** - * Add a given stylesheet dynamically to the document - * - * @param {Object} data composite object containing properties to create stylesheet - * @property {String} data.href relative or absolute url for stylesheet to be loaded - * @property {Boolean} data.mediaPrint boolean to represent if the constructed sheet is for print media - * @property {String} data.text text contents to be written to the stylesheet - * @property {String} data.id id reference to link or style to be added to document - * @param {Object} rootNode document/fragment to which to append style - * @returns {Object} axe.utils.queue - */ -testUtils.addStyleSheet = function addStyleSheet(data, rootNode) { - var doc = rootNode ? rootNode : document; - var q = axe.utils.queue(); - if (data.href) { - q.defer(function (resolve, reject) { - var link = doc.createElement('link'); - link.rel = 'stylesheet'; - link.href = data.href; - if (data.id) { - link.id = data.id; - } - if (data.mediaPrint) { - link.media = 'print'; - } - link.onload = function () { + // Complete (don't pass the args on to the callback) + q.then(function () { + cb(); + }); + + if (errCb) { + q.catch(errCb); + } + }; + + /** + * Add a given stylesheet dynamically to the document + * + * @param {Object} data composite object containing properties to create stylesheet + * @property {String} data.href relative or absolute url for stylesheet to be loaded + * @property {Boolean} data.mediaPrint boolean to represent if the constructed sheet is for print media + * @property {String} data.text text contents to be written to the stylesheet + * @property {String} data.id id reference to link or style to be added to document + * @param {Object} rootNode document/fragment to which to append style + * @returns {Object} axe.utils.queue + */ + testUtils.addStyleSheet = function addStyleSheet(data, rootNode) { + const doc = rootNode ? rootNode : document; + const q = axe.utils.queue(); + if (data.href) { + q.defer(function (resolve, reject) { + const link = doc.createElement('link'); + link.rel = 'stylesheet'; + link.href = data.href; + if (data.id) { + link.id = data.id; + } + if (data.mediaPrint) { + link.media = 'print'; + } + link.onload = function () { + setTimeout(function () { + resolve(); + }); + }; + link.onerror = function () { + reject(); + }; + doc.head.appendChild(link); + }); + } else { + q.defer(function (resolve) { + const style = doc.createElement('style'); + if (data.id) { + style.id = data.id; + } + style.type = 'text/css'; + style.appendChild(doc.createTextNode(data.text)); + doc.head.appendChild(style); setTimeout(function () { resolve(); - }); - }; - link.onerror = function () { - reject(); - }; - doc.head.appendChild(link); + }, 100); // -> note: gives firefox to load (document.stylesheets), other browsers are fine. + }); + } + return q; + }; + + /** + * Add a list of stylesheets + * + * @param {Object} sheets array of sheets data object + * @returns {Object} axe.utils.queue + */ + testUtils.addStyleSheets = function addStyleSheets(sheets, rootNode) { + const q = axe.utils.queue(); + sheets.forEach(function (data) { + q.defer(axe.testUtils.addStyleSheet(data, rootNode)); }); - } else { - q.defer(function (resolve) { - var style = doc.createElement('style'); - if (data.id) { - style.id = data.id; - } - style.type = 'text/css'; - style.appendChild(doc.createTextNode(data.text)); - doc.head.appendChild(style); - setTimeout(function () { + return q; + }; + + /** + * Remove a list of stylesheets from the document + * @param {Array<Object>} sheets array of sheets data object + * @returns {Object} axe.utils.queue + */ + testUtils.removeStyleSheets = function removeStyleSheets(sheets) { + const q = axe.utils.queue(); + sheets.forEach(function (data) { + q.defer(function (resolve, reject) { + const node = document.getElementById(data.id); + if (!node || !node.parentNode) { + reject(); + } + node.parentNode.removeChild(node); resolve(); - }, 100); // -> note: gives firefox to load (document.stylesheets), other browsers are fine. - }); - } - return q; -}; - -/** - * Add a list of stylesheets - * - * @param {Object} sheets array of sheets data object - * @returns {Object} axe.utils.queue - */ -testUtils.addStyleSheets = function addStyleSheets(sheets, rootNode) { - var q = axe.utils.queue(); - sheets.forEach(function (data) { - q.defer(axe.testUtils.addStyleSheet(data, rootNode)); - }); - return q; -}; - -/** - * Remove a list of stylesheets from the document - * @param {Array<Object>} sheets array of sheets data object - * @returns {Object} axe.utils.queue - */ -testUtils.removeStyleSheets = function removeStyleSheets(sheets) { - var q = axe.utils.queue(); - sheets.forEach(function (data) { - q.defer(function (resolve, reject) { - var node = document.getElementById(data.id); - if (!node || !node.parentNode) { - reject(); - } - node.parentNode.removeChild(node); - resolve(); + }); }); - }); - return q; -}; - -/** - * Assert a given stylesheet against selectorText and cssText - * - * @param {Object} sheet CSS Stylesheet - * @param {String} selectorText CSS Selector - * @param {String} cssText CSS Values - * @param {Boolean} includes (Optional) flag to check if existence of selectorText within cssText - */ -testUtils.assertStylesheet = function assertStylesheet( - sheet, - selectorText, - cssText, - includes -) { - assert.isDefined(sheet); - assert.property(sheet, 'cssRules'); - if (includes) { - assert.isTrue(cssText.includes(selectorText)); - } else { - assert.equal(sheet.cssRules[0].selectorText, selectorText); - - // compare the selector properties - var styleEl = document.createElement('style'); - styleEl.type = 'text/css'; - styleEl.innerHTML = cssText; - document.body.appendChild(styleEl); - - var testSheet = document.styleSheets[document.styleSheets.length - 1]; - var sheetRule = sheet.cssRules[0]; - var testRule = testSheet.cssRules[0]; - - try { - for (var i = 0; i < testRule.style.length; i++) { - var property = testRule.style[i]; - assert.equal(sheetRule.style[property], testRule.style[property]); + return q; + }; + + /** + * Assert a given stylesheet against selectorText and cssText + * + * @param {Object} sheet CSS Stylesheet + * @param {String} selectorText CSS Selector + * @param {String} cssText CSS Values + * @param {Boolean} includes (Optional) flag to check if existence of selectorText within cssText + */ + testUtils.assertStylesheet = function assertStylesheet( + sheet, + selectorText, + cssText, + includes + ) { + assert.isDefined(sheet); + assert.property(sheet, 'cssRules'); + if (includes) { + assert.isTrue(cssText.includes(selectorText)); + } else { + assert.equal(sheet.cssRules[0].selectorText, selectorText); + + // compare the selector properties + const styleEl = document.createElement('style'); + styleEl.type = 'text/css'; + styleEl.innerHTML = cssText; + document.body.appendChild(styleEl); + + const testSheet = document.styleSheets[document.styleSheets.length - 1]; + const sheetRule = sheet.cssRules[0]; + const testRule = testSheet.cssRules[0]; + + try { + for (let i = 0; i < testRule.style.length; i++) { + const property = testRule.style[i]; + assert.equal(sheetRule.style[property], testRule.style[property]); + } + } finally { + styleEl.parentNode.removeChild(styleEl); } - } finally { - styleEl.parentNode.removeChild(styleEl); } - } -}; - -/** - * Injecting content into a fixture and return queried element within fixture - * - * @param {String|Node} html - content to go into the fixture (html or DOM node) - * @param {String} [query=#target] - the CSS selector query to find target DOM node - * @return {VirtualNode} - */ -testUtils.queryFixture = function queryFixture(html, query) { - query = query || '#target'; - var rootNode = testUtils.fixtureSetup(html); - var vNode = axe.utils.querySelectorAll(rootNode, query)[0]; - assert.exists( - vNode, - 'Node does not exist in query `' + - query + - '`. This is usually fixed by adding the default target (`id="target"`) to your html parameter. If you do not intend on querying the fixture for #target, consider using `axe.testUtils.fixtureSetup()` instead.' - ); - return vNode; -}; - -/** - * Return the checks evaluate method and apply default options - * @param {string} checkId - ID of the check - * @param {} testOptions - Options for the test - * @returns {evaluateWrapper} evaluateWrapper - Check evaluation wrapper - */ -testUtils.getCheckEvaluate = function getCheckEvaluate(checkId, testOptions) { - var check = checks[checkId]; - testOptions = testOptions || {}; + }; /** - * Wraps a check for evaluation using .call() - * @param {HTMLElement} node - * @param {*} options - * @param {VirtualNode} virtualNode - * @param {Context} context + * Injecting content into a fixture and return queried element within fixture + * + * @param {String|Node} html - content to go into the fixture (html or DOM node) + * @param {String} [query=#target] - the CSS selector query to find target DOM node + * @return {VirtualNode} */ - var evaluateWrapper = function (node, options, virtualNode, context) { - var opts = check.getOptions(options); - - var result = check.evaluate.call(this, node, opts, virtualNode, context); - - // ensure that every result has a corresponding message - if (testOptions.verifyMessage !== false) { - var messages = axe._audit.data.checks[checkId].messages; - var messageKey = this._data && this._data.messageKey; - - // see how the check is used to know where to find the message - // e.g. a check used only in the `none` array of a rule will look at - // the messageKey of a passing result in the `fail` messages - var keyResult = result; - var isNoneCheck = noneChecks.indexOf(checkId) !== -1; - if (isNoneCheck) { - keyResult = result === true ? false : result === false ? true : result; - } + testUtils.queryFixture = function queryFixture(html, query) { + query = query || '#target'; + const rootNode = testUtils.fixtureSetup(html); + const vNode = axe.utils.querySelectorAll(rootNode, query)[0]; + assert.exists( + vNode, + 'Node does not exist in query `' + + query + + '`. This is usually fixed by adding the default target (`id="target"`) to your html parameter. If you do not intend on querying the fixture for #target, consider using `axe.testUtils.fixtureSetup()` instead.' + ); + return vNode; + }; - var key = - keyResult === true - ? 'pass' - : keyResult === false - ? 'fail' - : 'incomplete'; - var noneCheckMessage = isNoneCheck - ? '. Note that since this check is only used in the "none" array of all rules, the messages use the inverse of the result (e.g. a result of false uses the "pass" messages)' - : ''; - - assert.exists( - messages[key], - 'Missing "' + - key + - '" message for check result of ' + - result + - noneCheckMessage + /** + * Return the checks evaluate method and apply default options + * @param {string} checkId - ID of the check + * @param {} testOptions - Options for the test + * @returns {evaluateWrapper} evaluateWrapper - Check evaluation wrapper + */ + testUtils.getCheckEvaluate = function getCheckEvaluate(checkId, testOptions) { + const check = checks[checkId]; + testOptions = testOptions || {}; + + /** + * Wraps a check for evaluation using .call() + * @param {HTMLElement} node + * @param {*} options + * @param {VirtualNode} virtualNode + * @param {Context} context + */ + const evaluateWrapper = function (node, options, virtualNode, context) { + const opts = check.getOptions(options); + + const result = check.evaluate.call( + this, + node, + opts, + virtualNode, + context ); - if (messageKey) { + + // ensure that every result has a corresponding message + if (testOptions.verifyMessage !== false) { + const messages = axe._audit.data.checks[checkId].messages; + const messageKey = this._data && this._data.messageKey; + + // see how the check is used to know where to find the message + // e.g. a check used only in the `none` array of a rule will look at + // the messageKey of a passing result in the `fail` messages + let keyResult = result; + const isNoneCheck = noneChecks.indexOf(checkId) !== -1; + if (isNoneCheck) { + keyResult = + result === true ? false : result === false ? true : result; + } + + const key = + keyResult === true + ? 'pass' + : keyResult === false + ? 'fail' + : 'incomplete'; + const noneCheckMessage = isNoneCheck + ? '. Note that since this check is only used in the "none" array of all rules, the messages use the inverse of the result (e.g. a result of false uses the "pass" messages)' + : ''; + assert.exists( - messages[key][messageKey], - 'Missing ' + + messages[key], + 'Missing "' + key + - ' message key "' + - messageKey + - '" for check result of ' + + '" message for check result of ' + result + noneCheckMessage ); - - var message = axe.utils.processMessage( - messages[key][messageKey], - this._data - ); - assert.isTrue( - message.indexOf('${') === -1, - 'Data object missing properties for ' + - key + - ' message key "' + - messageKey + - '": "' + - message + - '"' - ); - } else { - var message = axe.utils.processMessage(messages[key], this._data); - assert.isTrue( - message.indexOf('${') === -1, - 'Data object missing properties for ' + - key + - ' message: "' + - message + - '"' - ); + if (messageKey) { + assert.exists( + messages[key][messageKey], + 'Missing ' + + key + + ' message key "' + + messageKey + + '" for check result of ' + + result + + noneCheckMessage + ); + + const message = axe.utils.processMessage( + messages[key][messageKey], + this._data + ); + assert.isTrue( + message.indexOf('${') === -1, + 'Data object missing properties for ' + + key + + ' message key "' + + messageKey + + '": "' + + message + + '"' + ); + } else { + const message = axe.utils.processMessage(messages[key], this._data); + assert.isTrue( + message.indexOf('${') === -1, + 'Data object missing properties for ' + + key + + ' message: "' + + message + + '"' + ); + } } - } - return result; + return result; + }; + return evaluateWrapper; }; - return evaluateWrapper; -}; - -axe.testUtils = testUtils; - -if (typeof beforeEach !== 'undefined' && typeof afterEach !== 'undefined') { - // prevent setting read-only properties - // @see https://github.com/dequelabs/axe-core/issues/3837 - const readonlyRect = new DOMRectReadOnly(); - const proto = Object.getPrototypeOf(readonlyRect); - ['left', 'right', 'top', 'bottom'].forEach(prop => { - Object.defineProperty(proto, prop, { - set(value) { - throw new TypeError(`setting getter-only property "${prop}"`); + + if (typeof beforeEach !== 'undefined' && typeof afterEach !== 'undefined') { + // prevent setting read-only properties + // @see https://github.com/dequelabs/axe-core/issues/3837 + const readonlyRect = new DOMRectReadOnly(); + const proto = Object.getPrototypeOf(readonlyRect); + ['left', 'right', 'top', 'bottom'].forEach(prop => { + Object.defineProperty(proto, prop, { + set(value) { + throw new TypeError(`setting getter-only property "${prop}"`); + } + }); + }); + + beforeEach(function () { + // reset from axe._load overriding + checks = originalChecks; + axe._audit = originalAudit; + axe._audit.rules = originalRules; + commons = axe.commons = originalCommons; + }); + + afterEach(function () { + axe.teardown(); + fixture.innerHTML = ''; + + // remove all attributes from fixture (otherwise a leftover + // style attribute would cause avoid-inline-spacing integration + // test to fail with [#fixture] being included in the results) + const attrs = fixture.attributes; + for (let i = 0; i < attrs.length; i++) { + const attrName = attrs[i].name; + if (attrName !== 'id') { + fixture.removeAttribute(attrs[i].name); + } } + + // reset html and body styles + document.body.removeAttribute('style'); + document.documentElement.removeAttribute('style'); }); - }); + } - beforeEach(function () { - // reset from axe._load overriding - checks = originalChecks; - axe._audit = originalAudit; - axe._audit.rules = originalRules; - commons = axe.commons = originalCommons; - }); + testUtils.captureError = function captureError(cb, errorHandler) { + return function () { + try { + cb.apply(null, arguments); + } catch (e) { + errorHandler(e); + } + }; + }; - afterEach(function () { - axe.teardown(); - fixture.innerHTML = ''; - - // remove all attributes from fixture (otherwise a leftover - // style attribute would cause avoid-inline-spacing integration - // test to fail with [#fixture] being included in the results) - var attrs = fixture.attributes; - for (var i = 0; i < attrs.length; i++) { - var attrName = attrs[i].name; - if (attrName !== 'id') { - fixture.removeAttribute(attrs[i].name); + testUtils.runPartialRecursive = function runPartialRecursive( + context, + options, + win + ) { + options = options || {}; + win = win || window; + const axe = win.axe; + const frameContexts = axe.utils.getFrameContexts(context); + let promiseResults = [axe.runPartial(context, options)]; + + frameContexts.forEach(function (c) { + const frame = testUtils.shadowQuerySelector( + c.frameSelector, + win.document + ); + const frameWin = frame.contentWindow; + const frameResults = testUtils.runPartialRecursive( + c.frameContext, + options, + frameWin + ); + promiseResults = promiseResults.concat(frameResults); + }); + return promiseResults; + }; + + testUtils.shadowQuerySelector = function shadowQuerySelector( + axeSelector, + doc + ) { + let elm; + doc = doc || document; + axeSelector = Array.isArray(axeSelector) ? axeSelector : [axeSelector]; + axeSelector.forEach(function (selectorStr) { + elm = doc && doc.querySelector(selectorStr); + doc = elm && elm.shadowRoot; + }); + return elm; + }; + + testUtils.createNestedShadowDom = function createFixtureShadowTree( + fixtureNode, + ...htmlCodes + ) { + if (htmlCodes.length <= 1) { + throw new Error( + 'createNestedShadowDom must contain at least two HTML snippets' + ); + } + let htmlCode; + while ((htmlCode = htmlCodes.shift())) { + appendHtml(fixtureNode, htmlCode); + if (htmlCodes.length) { + const query = fixtureNode.querySelectorAll('#shadowHost, .shadowHost'); + fixtureNode = query[query.length - 1]; + fixtureNode = fixtureNode.attachShadow({ mode: 'open' }); } } + return fixtureNode.querySelector('#target'); + }; - // reset body styles - document.body.removeAttribute('style'); - }); -} - -testUtils.captureError = function captureError(cb, errorHandler) { - return function () { - try { - cb.apply(null, arguments); - } catch (e) { - errorHandler(e); - } + /** + * Enables test code like html` <img /> ` to get code highlighting + * @param {array} strings + * @param {...any} values + * @returns {string} + */ + testUtils.html = (strings, ...values) => { + return strings.reduce((total, string, i) => { + return total + string + (values[i] ?? ''); + }, ''); }; -}; - -testUtils.runPartialRecursive = function runPartialRecursive( - context, - options, - win -) { - options = options || {}; - win = win || window; - var axe = win.axe; - var frameContexts = axe.utils.getFrameContexts(context); - var promiseResults = [axe.runPartial(context, options)]; - - frameContexts.forEach(function (c) { - var frame = testUtils.shadowQuerySelector(c.frameSelector, win.document); - var frameWin = frame.contentWindow; - var frameResults = testUtils.runPartialRecursive( - c.frameContext, - options, - frameWin - ); - promiseResults = promiseResults.concat(frameResults); - }); - return promiseResults; -}; - -testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { - var elm; - doc = doc || document; - axeSelector = Array.isArray(axeSelector) ? axeSelector : [axeSelector]; - axeSelector.forEach(function (selectorStr) { - elm = doc && doc.querySelector(selectorStr); - doc = elm && elm.shadowRoot; - }); - return elm; -}; - -testUtils.createNestedShadowDom = function createFixtureShadowTree( - fixture, - ...htmlCodes -) { - if (htmlCodes.length <= 1) { - throw new Error( - 'createNestedShadowDom must contain at least two HTML snippets' - ); - } - let htmlCode; - while ((htmlCode = htmlCodes.shift())) { - appendHtml(fixture, htmlCode); - if (htmlCodes.length) { - const query = fixture.querySelectorAll('#shadowHost, .shadowHost'); - fixture = query[query.length - 1]; - fixture = fixture.attachShadow({ mode: 'open' }); + + function appendHtml(fixtureNode, htmlCode) { + const tmp = document.createElement('div'); + tmp.innerHTML = htmlCode; + // Append to avoid clobbering other shadow trees with innerHTML + for (let child of tmp.children) { + fixtureNode.appendChild(child.cloneNode(true)); } } - return fixture.querySelector('#target'); -}; - -function appendHtml(fixture, htmlCode) { - const tmp = document.createElement('div'); - tmp.innerHTML = htmlCode; - // Append to avoid clobbering other shadow trees with innerHTML - for (const child of tmp.children) { - fixture.appendChild(child.cloneNode(true)); - } -} +})(); diff --git a/typings/axe-core/axe-core-tests.ts b/typings/axe-core/axe-core-tests.ts index 309dc9e75a..bff4ec46da 100644 --- a/typings/axe-core/axe-core-tests.ts +++ b/typings/axe-core/axe-core-tests.ts @@ -221,9 +221,15 @@ var spec: axe.Spec = { checks: [ { id: 'custom-check', - evaluate: function () { + evaluate: function (node) { + this.relatedNodes([node]); + this.data('some data'); return true; }, + after: function (results) { + const id = results[0].id; + return results; + }, metadata: { impact: 'minor', messages: { @@ -235,6 +241,13 @@ var spec: axe.Spec = { } } } + }, + { + id: 'async-check', + evaluate: function (node) { + const done = this.async(); + done(true); + } } ], standards: { @@ -264,11 +277,15 @@ var spec: axe.Spec = { { id: 'custom-rule', any: ['custom-check'], + matches: function (node) { + return node.tagName === 'BODY'; + }, + tags: ['a'], + actIds: ['b'], metadata: { description: 'custom rule', help: 'different help', - helpUrl: 'https://example.com', - tags: ['custom'] + helpUrl: 'https://example.com' } } ] @@ -318,6 +335,10 @@ axe.configure({ incomplete: { foo: 'bar' } + }, + bar: { + pass: 'pass', + fail: 'fail' } } } @@ -327,9 +348,10 @@ axe.configure({ let fooReporter = ( results: axe.RawResult[], options: axe.RunOptions, - cb: (out: 'foo') => void + resolve: (out: 'foo') => void, + reject: (err: Error) => void ) => { - cb('foo'); + reject && resolve('foo'); }; axe.addReporter<'foo'>('foo', fooReporter, true); @@ -360,3 +382,14 @@ var pluginSrc: axe.AxePlugin = { }; axe.registerPlugin(pluginSrc); axe.cleanup(); + +// Utils +const dqElement = new axe.utils.DqElement(document.body); +const element = axe.utils.shadowSelect(dqElement.selector[0]); +const uuid = axe.utils.uuid() as string; + +// Commons +axe.commons.aria.getRoleType('img'); +axe.commons.dom.isFocusable(document.body); +axe.commons.dom.isNativelyFocusable(document.body); +axe.commons.text.accessibleText(document.body);