From 0e73be039eed78fda20e9bf28ee7f6f7c0d62fd8 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 30 May 2023 11:39:20 -0400 Subject: [PATCH 01/54] perf(get-element-stack): improve getElementStack performance on large pages (#4026) * perf(get-element-stack): improve getElementStack performance on large pages * use memoize instead of cache in getScroll --- lib/core/utils/get-scroll.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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); From d0a49d33560b0c926c9c21836d5c4b29bdfb023c Mon Sep 17 00:00:00 2001 From: Dan Bjorge Date: Tue, 13 Jun 2023 18:58:46 -0400 Subject: [PATCH 02/54] test: fix implicit test dependencies on local dev environment (#4053) * test: refactor get-background-color tests with color asserts * test: normalize background colors in get-background-color hooks * test: refactor and improve assert output in assertRectsEqual * test: fix integration test server ipv6 compat * test: normalize background colors in get-foreground-color hooks * test: fix light-mode color assumption in dialog integration test * resolve PR comments --- package.json | 2 +- test/commons/color/get-background-color.js | 322 ++++++------------ test/commons/color/get-foreground-color.js | 6 + .../dom/get-visible-child-text-rects.js | 26 +- test/integration/full/dialog/dialog.html | 4 +- test/testutils.js | 8 +- 6 files changed, 130 insertions(+), 238 deletions(-) diff --git a/package.json b/package.json index 3caafd20e6..9b5575fb9a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index fb76e89328..e20abb6d43 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 = '
'; 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,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, 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); }); it('ignores text-shadows thinner than shadowOutlineEmMax', function () { @@ -1091,10 +993,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 +1007,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 +1021,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 +1037,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 +1053,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 +1068,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 +1083,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/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/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 @@
-
+
Contrast failure
-
+
Contrast failure
diff --git a/test/testutils.js b/test/testutils.js index 843441bbf4..d0b7af2b8b 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -17,6 +17,11 @@ var originalAudit = axe._audit; var originalRules = axe._audit.rules; var 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 var fixture = document.getElementById('fixture'); if (!fixture) { @@ -597,8 +602,9 @@ if (typeof beforeEach !== 'undefined' && typeof afterEach !== 'undefined') { } } - // reset body styles + // reset html and body styles document.body.removeAttribute('style'); + document.documentElement.removeAttribute('style'); }); } From 173f29da9558a1fd0510609aacc9e4deebdf74b4 Mon Sep 17 00:00:00 2001 From: Scott O'Hara Date: Fri, 16 Jun 2023 10:25:21 -0400 Subject: [PATCH 03/54] fix(aria-allowed-role): add meter to allowed roles for named img (#4055) * Update: add meter to allowed roles for named img closes #4054 * fix spacing * add meter img test * Update test/integration/rules/aria-allowed-role/aria-allowed-role.html * Update aria-allowed-role.json --- lib/standards/html-elms.js | 1 + test/integration/rules/aria-allowed-role/aria-allowed-role.html | 1 + test/integration/rules/aria-allowed-role/aria-allowed-role.json | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 7e99be92a9..2ccb779c7c 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -358,6 +358,7 @@ const htmlElms = { 'menuitem', 'menuitemcheckbox', 'menuitemradio', + 'meter', 'option', 'progressbar', 'radio', 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..24e4f8ba48 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -246,6 +246,7 @@

ok
ok
+test
hazaar
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..916ecbb857 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.json +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.json @@ -82,6 +82,7 @@ ["#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"], From 8d135dd58ccd72393b981464f66a01e770d9cf95 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 22 Jun 2023 11:30:32 +0200 Subject: [PATCH 04/54] fix: avoid problems from element IDs that exist on object prototype (#4060) * fix: avoid problems from element IDs that exist on object prototype * :robot: Automated formatting fixes * Fixed tests --- lib/commons/aria/get-accessible-refs.js | 25 +++++++++-------- lib/core/utils/find-by.js | 8 +++++- lib/core/utils/selector-cache.js | 13 ++++++--- lib/standards/html-elms.js | 2 +- test/commons/aria/get-accessible-refs.js | 28 +++++++++++++++++++ test/core/utils/find-by.js | 17 +++++++++++ test/core/utils/selector-cache.js | 23 +++++++++++++++ .../integration/full/all-rules/all-rules.html | 6 ++-- .../aria-allowed-role/aria-allowed-role.html | 10 ++++++- 9 files changed, 111 insertions(+), 21 deletions(-) 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/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/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/standards/html-elms.js b/lib/standards/html-elms.js index 2ccb779c7c..1b75a8bbb8 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -358,7 +358,7 @@ const htmlElms = { 'menuitem', 'menuitemcheckbox', 'menuitemradio', - 'meter', + 'meter', 'option', 'progressbar', 'radio', 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 = `
`; + + 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 = '
'; 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/selector-cache.js b/test/core/utils/selector-cache.js index 9e6e70deeb..7a06c5288e 100644 --- a/test/core/utils/selector-cache.js +++ b/test/core/utils/selector-cache.js @@ -55,6 +55,29 @@ describe('utils.selector-cache', function () { assert.lengthOf(Object.keys(map), 0); }); + + describe('with javascripty attribute selectors', function () { + const terms = [ + 'prototype', + 'constructor', + '__proto__', + 'Element', + 'nodeName', + 'valueOf', + 'toString' + ]; + for (const term of terms) { + it(`works with ${term}`, function () { + fixture.innerHTML = `
`; + const 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 () { diff --git a/test/integration/full/all-rules/all-rules.html b/test/integration/full/all-rules/all-rules.html index 05c32b4ad4..ec5a5af5c1 100644 --- a/test/integration/full/all-rules/all-rules.html +++ b/test/integration/full/all-rules/all-rules.html @@ -28,11 +28,11 @@ >
-
+
monkeys -
Foo
+
Foo
Home
@@ -51,7 +51,7 @@
Item
- +

Banana error

text

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 24e4f8ba48..46f11eb1b2 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -246,7 +246,15 @@

ok
ok
-test +test
hazaar
From 16c5cfa66615537b2131a5a381fbed9a5336d853 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 22 Jun 2023 11:50:15 +0200 Subject: [PATCH 05/54] fix: avoid memory issues by doing better cleanup (#4059) * fix: ensure better cleanup after t * :robot: Automated formatting fixes * Fix tests * Resolve feedback --- build/tasks/esbuild.js | 5 +++- lib/checks/navigation/region-evaluate.js | 2 +- lib/commons/color/incomplete-data.js | 11 ++++++--- lib/commons/standards/implicit-html-roles.js | 20 ++++++++++------ lib/core/base/virtual-node/virtual-node.js | 7 +----- lib/core/utils/frame-messenger/message-id.js | 1 + .../utils/frame-messenger/post-message.js | 7 +++--- lib/core/utils/get-selector.js | 5 +--- lib/core/utils/is-xhtml.js | 8 ++++--- lib/core/utils/query-selector-all-filter.js | 24 +++++++++++-------- test/checks/navigation/region.js | 18 ++++++++++++++ 11 files changed, 69 insertions(+), 39 deletions(-) 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/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/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/standards/implicit-html-roles.js b/lib/commons/standards/implicit-html-roles.js index 0a7b2da142..cc3c8edb74 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; }, 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/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-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} 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/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/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/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( + '
This is random content.
' + + '

Introduction

' + ); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + it('should return false when img content is outside the region', function () { var checkArgs = checkSetup( '

Introduction

' From ab4879c1684c30794d4ac8cec2baff8bd45d2afd Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 27 Jun 2023 12:38:47 +0200 Subject: [PATCH 06/54] chore(rules): avoid passing unnecessary info into afters (#4049) --- lib/core/base/rule.js | 4 ++-- test/core/base/rule.js | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index 96123ddbb8..2ed0429294 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -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 => { diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 6be3e3b76b..2fc7cfda87 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -1483,11 +1483,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; } From 733c45e6a40a9f8ff6e75f7db864edff0b404ca2 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 29 Jun 2023 12:20:59 +0200 Subject: [PATCH 07/54] feat: deprecate & disable duplicate-id / duplicate-id-active (#4071) * feat: deprecate & disable duplicate-id / duplicate-id-active * Use wcag2a-obsolete tag --- doc/API.md | 35 +++++++++++++++--------------- doc/rule-descriptions.md | 4 ++-- lib/rules/duplicate-id-active.json | 3 ++- lib/rules/duplicate-id.json | 3 ++- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/doc/API.md b/doc/API.md index 9355005820..02194573d2 100644 --- a/doc/API.md +++ b/doc/API.md @@ -76,23 +76,24 @@ 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 | +| `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: diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 684fa0bfe6..100c036441 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -37,9 +37,7 @@ | [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) | @@ -153,3 +151,5 @@ Deprecated rules are disabled by default and will be removed in the next major r | :----------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------- | | [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) | +| [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-obsolete, wcag411, deprecated | 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-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | diff --git a/lib/rules/duplicate-id-active.json b/lib/rules/duplicate-id-active.json index 6edfbc5277..94f62e122b 100644 --- a/lib/rules/duplicate-id-active.json +++ b/lib/rules/duplicate-id-active.json @@ -3,7 +3,8 @@ "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.json b/lib/rules/duplicate-id.json index 5068a41a0f..f24644cdf5 100644 --- a/lib/rules/duplicate-id.json +++ b/lib/rules/duplicate-id.json @@ -3,7 +3,8 @@ "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", From de3da897e56179d94ef8a0dc1a667b5663c489d1 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 29 Jun 2023 17:33:07 +0200 Subject: [PATCH 08/54] feat: add EN.301.549 tags to rules (#4063) * feat: add EN.301.549 tags to rules * Add proper tag validation * Docs * Update tag validation * Change to "EN-301-549" --- build/tasks/validate.js | 169 ++++++++++++++++++++- doc/API.md | 38 ++--- doc/rule-descriptions.md | 154 +++++++++---------- lib/rules/area-alt.json | 7 +- lib/rules/aria-allowed-attr.json | 2 +- lib/rules/aria-command-name.json | 11 +- lib/rules/aria-hidden-body.json | 2 +- lib/rules/aria-hidden-focus.json | 10 +- lib/rules/aria-input-field-name.json | 11 +- lib/rules/aria-meter-name.json | 2 +- lib/rules/aria-progressbar-name.json | 2 +- lib/rules/aria-required-attr.json | 2 +- lib/rules/aria-required-children.json | 2 +- lib/rules/aria-required-parent.json | 2 +- lib/rules/aria-roledescription.json | 9 +- lib/rules/aria-roles.json | 2 +- lib/rules/aria-toggle-field-name.json | 11 +- lib/rules/aria-tooltip-name.json | 2 +- lib/rules/aria-valid-attr-value.json | 2 +- lib/rules/aria-valid-attr.json | 2 +- lib/rules/audio-caption.json | 2 + lib/rules/autocomplete-valid.json | 9 +- lib/rules/avoid-inline-spacing.json | 9 +- lib/rules/blink.json | 4 +- lib/rules/button-name.json | 6 +- lib/rules/bypass.json | 4 +- lib/rules/color-contrast.json | 11 +- lib/rules/css-orientation-lock.json | 9 +- lib/rules/definition-list.json | 2 +- lib/rules/dlitem.json | 2 +- lib/rules/document-title.json | 6 +- lib/rules/duplicate-id-aria.json | 2 +- lib/rules/form-field-multiple-labels.json | 10 +- lib/rules/frame-focusable-content.json | 10 +- lib/rules/frame-tested.json | 2 +- lib/rules/frame-title-unique.json | 10 +- lib/rules/frame-title.json | 4 +- lib/rules/hidden-content.json | 2 +- lib/rules/html-has-lang.json | 11 +- lib/rules/html-lang-valid.json | 11 +- lib/rules/html-xml-lang-mismatch.json | 9 +- lib/rules/image-alt.json | 6 +- lib/rules/input-button-name.json | 6 +- lib/rules/input-image-alt.json | 7 +- lib/rules/label-content-name-mismatch.json | 9 +- lib/rules/label.json | 6 +- lib/rules/link-in-text-block.json | 10 +- lib/rules/link-name.json | 9 +- lib/rules/list.json | 2 +- lib/rules/listitem.json | 2 +- lib/rules/marquee.json | 10 +- lib/rules/meta-refresh.json | 10 +- lib/rules/meta-viewport.json | 9 +- lib/rules/nested-interactive.json | 10 +- lib/rules/no-autoplay-audio.json | 11 +- lib/rules/object-alt.json | 4 +- lib/rules/p-as-heading.json | 9 +- lib/rules/role-img-alt.json | 6 +- lib/rules/scrollable-region-focusable.json | 10 +- lib/rules/select-name.json | 6 +- lib/rules/server-side-image-map.json | 4 +- lib/rules/svg-img-alt.json | 6 +- lib/rules/table-fake-caption.json | 4 +- lib/rules/target-size.json | 2 +- lib/rules/td-has-header.json | 4 +- lib/rules/td-headers-attr.json | 4 +- lib/rules/th-has-data-cells.json | 4 +- lib/rules/valid-lang.json | 11 +- lib/rules/video-caption.json | 4 +- 69 files changed, 584 insertions(+), 178 deletions(-) diff --git a/build/tasks/validate.js b/build/tasks/validate.js index c36dfddb53..2409bc9591 100644 --- a/build/tasks/validate.js +++ b/build/tasks/validate.js @@ -198,12 +198,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 +301,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 02194573d2..3d017858cc 100644 --- a/doc/API.md +++ b/doc/API.md @@ -76,24 +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 | -| `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 | -| `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: diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 100c036441..e425250416 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -12,69 +12,69 @@ ## 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-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) | -| [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.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, 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.7/aria-allowed-attr?application=RuleDescription) | Ensures ARIA attributes are allowed for an element's role | Serious, 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-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, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT | 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, EN-301-549, EN-9.4.1.2 | 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, 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.7/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Moderate, 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.7/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.7/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-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, 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.7/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.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, 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.7/aria-roles?application=RuleDescription) | Ensures all elements with a role attribute use a valid value | Minor, Serious, 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.7/aria-toggle-field-name?application=RuleDescription) | Ensures every ARIA toggle field has an accessible name | Moderate, 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.7/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.7/aria-valid-attr-value?application=RuleDescription) | Ensures all ARIA attributes have valid values | Serious, 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.7/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.7/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.7/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.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, 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.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, 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.7/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.7/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.7/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.7/duplicate-id-aria?application=RuleDescription) | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411, EN-301-549, EN-9.4.1.1 | 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, EN-301-549, EN-9.3.3.2 | 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, 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.7/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.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, 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.7/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.7/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.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, 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.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, 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.7/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.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, 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.7/label?application=RuleDescription) | Ensures every form element has a label | Minor, 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.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, EN-301-549, EN-9.1.4.1 | 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, 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.7/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.7/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.7/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.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, 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.7/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.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, 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.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, 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.7/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.7/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.7/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.7/select-name?application=RuleDescription) | Ensures select element has an accessible name | Minor, 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.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, EN-301-549, EN-9.2.1.1 | 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, 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.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, 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.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, 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.7/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.7/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.7/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.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, 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 @@ -82,7 +82,7 @@ These rules are disabled by default, until WCAG 2.2 is more widely adopted and r | Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | | :----------------------------------------------------------------------------------------------- | :------------------------------------------------- | :------ | :--------------------------------------------- | :------------------------- | :-------- | -| [target-size](https://dequeuniversity.com/rules/axe/4.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.7/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 @@ -97,7 +97,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | [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 | | +| [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, best-practice, review-item | 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 | | @@ -133,23 +133,23 @@ Rules that check for conformance to WCAG AAA success criteria that can be fully 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.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, 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.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, best-practice, experimental, review-item | 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, 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.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, EN-301-549, EN-9.1.3.1, 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, EN-301-549, EN-9.1.3.1 | 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, 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) | -| [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-obsolete, wcag411, deprecated | 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-obsolete, wcag411, deprecated | failure | [3ea0c8](https://act-rules.github.io/rules/3ea0c8) | +| 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, EN-301-549, EN-9.4.1.2, 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, 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.7/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.7/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/lib/rules/area-alt.json b/lib/rules/area-alt.json index afe0ddbe3c..5dfbe8c190 100644 --- a/lib/rules/area-alt.json +++ b/lib/rules/area-alt.json @@ -9,9 +9,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..fbf35f78d5 100644 --- a/lib/rules/aria-allowed-attr.json +++ b/lib/rules/aria-allowed-attr.json @@ -1,7 +1,7 @@ { "id": "aria-allowed-attr", "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", diff --git a/lib/rules/aria-command-name.json b/lib/rules/aria-command-name.json index 636da2914f..85f30009ab 100644 --- a/lib/rules/aria-command-name.json +++ b/lib/rules/aria-command-name.json @@ -2,7 +2,16 @@ "id": "aria-command-name", "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-hidden-body.json b/lib/rules/aria-hidden-body.json index f44a1922ab..694d612b83 100644 --- a/lib/rules/aria-hidden-body.json +++ b/lib/rules/aria-hidden-body.json @@ -3,7 +3,7 @@ "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" diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index 0115f19496..c6e72df2fe 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -3,7 +3,15 @@ "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..3f752fb7b5 100644 --- a/lib/rules/aria-input-field-name.json +++ b/lib/rules/aria-input-field-name.json @@ -2,7 +2,16 @@ "id": "aria-input-field-name", "selector": "[role=\"combobox\"], [role=\"listbox\"], [role=\"searchbox\"], [role=\"slider\"], [role=\"spinbutton\"], [role=\"textbox\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "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..c3d0340fc1 100644 --- a/lib/rules/aria-meter-name.json +++ b/lib/rules/aria-meter-name.json @@ -2,7 +2,7 @@ "id": "aria-meter-name", "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..84c90f0a6a 100644 --- a/lib/rules/aria-progressbar-name.json +++ b/lib/rules/aria-progressbar-name.json @@ -2,7 +2,7 @@ "id": "aria-progressbar-name", "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-required-attr.json b/lib/rules/aria-required-attr.json index 50f02d52ab..b8ccbfeb5f 100644 --- a/lib/rules/aria-required-attr.json +++ b/lib/rules/aria-required-attr.json @@ -1,7 +1,7 @@ { "id": "aria-required-attr", "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..880bb89ff1 100644 --- a/lib/rules/aria-required-children.json +++ b/lib/rules/aria-required-children.json @@ -2,7 +2,7 @@ "id": "aria-required-children", "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..5f4cedc17a 100644 --- a/lib/rules/aria-required-parent.json +++ b/lib/rules/aria-required-parent.json @@ -2,7 +2,7 @@ "id": "aria-required-parent", "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..61c5ebaac6 100644 --- a/lib/rules/aria-roledescription.json +++ b/lib/rules/aria-roledescription.json @@ -1,7 +1,14 @@ { "id": "aria-roledescription", "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..fa5f9be888 100644 --- a/lib/rules/aria-roles.json +++ b/lib/rules/aria-roles.json @@ -2,7 +2,7 @@ "id": "aria-roles", "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", diff --git a/lib/rules/aria-toggle-field-name.json b/lib/rules/aria-toggle-field-name.json index b50cf20869..bf848d3191 100644 --- a/lib/rules/aria-toggle-field-name.json +++ b/lib/rules/aria-toggle-field-name.json @@ -2,7 +2,16 @@ "id": "aria-toggle-field-name", "selector": "[role=\"checkbox\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"], [role=\"radio\"], [role=\"switch\"], [role=\"option\"]", "matches": "no-naming-method-matches", - "tags": ["cat.aria", "wcag2a", "wcag412", "ACT", "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..05f6d0d78e 100644 --- a/lib/rules/aria-tooltip-name.json +++ b/lib/rules/aria-tooltip-name.json @@ -2,7 +2,7 @@ "id": "aria-tooltip-name", "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-valid-attr-value.json b/lib/rules/aria-valid-attr-value.json index 0a6b03d0b6..ebb730f5bf 100644 --- a/lib/rules/aria-valid-attr-value.json +++ b/lib/rules/aria-valid-attr-value.json @@ -1,7 +1,7 @@ { "id": "aria-valid-attr-value", "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..749ecc6991 100644 --- a/lib/rules/aria-valid-attr.json +++ b/lib/rules/aria-valid-attr.json @@ -1,7 +1,7 @@ { "id": "aria-valid-attr", "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..288d103637 100644 --- a/lib/rules/audio-caption.json +++ b/lib/rules/audio-caption.json @@ -7,6 +7,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..778889aecb 100644 --- a/lib/rules/autocomplete-valid.json +++ b/lib/rules/autocomplete-valid.json @@ -1,7 +1,14 @@ { "id": "autocomplete-valid", "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..ac3f471b77 100644 --- a/lib/rules/avoid-inline-spacing.json +++ b/lib/rules/avoid-inline-spacing.json @@ -2,7 +2,14 @@ "id": "avoid-inline-spacing", "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..7a796d25c7 100644 --- a/lib/rules/blink.json +++ b/lib/rules/blink.json @@ -9,7 +9,9 @@ "section508", "section508.22.j", "TTv5", - "TT2.b" + "TT2.b", + "EN-301-549", + "EN-9.2.2.2" ], "metadata": { "description": "Ensures elements are not used", diff --git a/lib/rules/button-name.json b/lib/rules/button-name.json index 6ea4703975..3ae41a092f 100644 --- a/lib/rules/button-name.json +++ b/lib/rules/button-name.json @@ -8,9 +8,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..bb7348be72 100644 --- a/lib/rules/bypass.json +++ b/lib/rules/bypass.json @@ -11,7 +11,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.json b/lib/rules/color-contrast.json index 5abd09598d..78d2498793 100644 --- a/lib/rules/color-contrast.json +++ b/lib/rules/color-contrast.json @@ -2,7 +2,16 @@ "id": "color-contrast", "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..f035d21c76 100644 --- a/lib/rules/css-orientation-lock.json +++ b/lib/rules/css-orientation-lock.json @@ -1,7 +1,14 @@ { "id": "css-orientation-lock", "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..1aee5b60b3 100644 --- a/lib/rules/definition-list.json +++ b/lib/rules/definition-list.json @@ -2,7 +2,7 @@ "id": "definition-list", "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
elements are structured correctly", "help": "
elements must only directly contain properly-ordered
and
groups, '; + ''; 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
- `); - - const axe = require('axe-core'); + `).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/lib/core/public/run.js b/lib/core/public/run.js index b5bdbd40e9..29443d3fa8 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,10 +36,10 @@ 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); } catch (e) { @@ -55,7 +55,7 @@ export default function run(...args) { createReport(rawResults, options, respond); } catch (err) { axe._running = false; - cleanup(); + teardown(); callback(err); reject(err); } @@ -66,7 +66,6 @@ export default function run(...args) { axe.utils.performanceTimer.end(); } axe._running = false; - resetGlobals(); callback(err); reject(err); } @@ -97,7 +96,6 @@ function createReport(rawResults, options, respond) { } 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/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/node/jsdom.js b/test/node/jsdom.js index 267ea43e31..e7bddb4191 100644 --- a/test/node/jsdom.js +++ b/test/node/jsdom.js @@ -160,4 +160,37 @@ describe('jsdom axe-core', () => { }); }); }); + + 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'); + }); + }); + }); }); From 080cc1b5f5ed048ab435c312dec291d1b4eb4393 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 3 Aug 2023 12:51:36 +0200 Subject: [PATCH 37/54] fix: ensure reporter errors can propagate (#4111) * feat: allow reporters to error * fix(reporter): reject handle for consistent error processing --- axe.d.ts | 3 ++- lib/core/public/finish-run.js | 4 ++-- lib/core/public/run.js | 27 +++++++++++++++--------- test/core/public/finish-run.js | 34 ++++++++++++++++++++++++++++++ test/core/public/run.js | 22 +++++++++++++++++++ typings/axe-core/axe-core-tests.ts | 5 +++-- 6 files changed, 80 insertions(+), 15 deletions(-) diff --git a/axe.d.ts b/axe.d.ts index 3b232dfb96..403e504873 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -371,7 +371,8 @@ declare namespace axe { type AxeReporter = ( rawResults: RawResult[], option: RunOptions, - callback: (report: T) => void + resolve: (report: T) => void, + reject: (error: Error) => void ) => void; interface VirtualNode { diff --git a/lib/core/public/finish-run.js b/lib/core/public/finish-run.js index bc3a194f1c..a82f08ff1f 100644 --- a/lib/core/public/finish-run.js +++ b/lib/core/public/finish-run.js @@ -52,8 +52,8 @@ function getMergedFrameSpecs({ } 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/run.js b/lib/core/public/run.js index 29443d3fa8..74983c7e86 100644 --- a/lib/core/public/run.js +++ b/lib/core/public/run.js @@ -41,23 +41,29 @@ export default function run(...args) { axe._running = false; 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; - teardown(); - callback(err); - reject(err); + wrappedReject(err); } } @@ -82,14 +88,15 @@ 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); } diff --git a/test/core/public/finish-run.js b/test/core/public/finish-run.js index 1e329e279c..1199b2153d 100644 --- a/test/core/public/finish-run.js +++ b/test/core/public/finish-run.js @@ -212,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 = '

Hello world

'; + 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 = '

Hello world

'; + 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/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/typings/axe-core/axe-core-tests.ts b/typings/axe-core/axe-core-tests.ts index d968b5c30c..bff4ec46da 100644 --- a/typings/axe-core/axe-core-tests.ts +++ b/typings/axe-core/axe-core-tests.ts @@ -348,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); From 4deb0a0876d574c3d7d586b27ae07d4f5be586db Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:41:01 -0600 Subject: [PATCH 38/54] fix(color-contrast): ignore zero width characters (#4103) * fix(color-contrast): ignore zero width characters * fix * replace affirm font with purpose built zero width 0 char font * Update lib/commons/text/is-icon-ligature.js Co-authored-by: Dan Bjorge --------- Co-authored-by: Dan Bjorge --- lib/commons/text/is-icon-ligature.js | 16 ++-- test/assets/ZeroWidth0Char.woff | Bin 0 -> 976 bytes test/commons/text/is-icon-ligature.js | 118 +++++++++++++++----------- 3 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 test/assets/ZeroWidth0Char.woff diff --git a/lib/commons/text/is-icon-ligature.js b/lib/commons/text/is-icon-ligature.js index 7924b0bbc9..428117f0de 100644 --- a/lib/commons/text/is-icon-ligature.js +++ b/lib/commons/text/is-icon-ligature.js @@ -93,11 +93,7 @@ export default 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 @@ export default 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 @@ export default 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) { diff --git a/test/assets/ZeroWidth0Char.woff b/test/assets/ZeroWidth0Char.woff new file mode 100644 index 0000000000000000000000000000000000000000..3c26a6e1ed621aaf8817a01caf6836c6b069aedf GIT binary patch literal 976 zcmXT-cXRU(3GruOV7|b>#Q+4XM;I7EG$wF%b5j7SV*$!61LE}yCIxr7xrO)w#kK(X zG9bM0$kJK<>US!a%I>e+Pq6#hm1Xl!OF^M=GJ~Hh=tM7B9JD zW;}zdgj`MaYLBeBIS&CfLDh%-$Ui7>l!3X2XGH@igHaIUPM{nE zLjcfth?>Mdhb$T!r#3dO?9Sw8W_IRpNvk=KaN-1uNNPfAS_7jSn~K1MXR{7nIFJB@ zYd+j>Qsi>(WfCxAVhD=jVg=gM+P^>RP=LhI^Su`*EoE)*D{ANCQoD1+abi%H*Ncdj zU8NjqM?5@o_V8VDe{tjc3+bPFww>GcWY0W({O{e*e{&gj9OY*@7tinC#wt@Iv8|w4 z|FEvjgOnR>%I|o$7pl+WEnjhbjmWmc*OM=YZgvoV-=_QSugRT5-1~TMzu-DuAZ@D% zRIxv)Psmy(P|>-I*F@!4MDhXahNKpWmzrNr6j1agvab){r2{=Ef$lf$@{X!5T3(R)w=nEeiV%{{JuV z>A&j=FE%$bHi^j@4y%40uz-I&i literal 0 HcmV?d00001 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('
Normal text
'); + it('should return false for normal text', () => { + const target = queryFixture('
Normal text
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for emoji', function () { - var target = queryFixture('
🌎
'); + it('should return false for emoji', () => { + const target = queryFixture('
🌎
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for non-bmp unicode', function () { - var target = queryFixture('
'); + it('should return false for non-bmp unicode', () => { + const target = queryFixture('
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for whitespace strings', function () { - var target = queryFixture('
'); + it('should return false for whitespace strings', () => { + const target = queryFixture('
'); assert.isFalse(isIconLigature(target.children[0])); }); (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (fi)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
figure
' ); 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( '
ffugative
' ); 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( '
flu shot
' ); 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( '
ffigure
' ); 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( '
fflu shot
' ); 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( '
delete
' ); 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( '
fflu shot
' ); 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( '
f
' ); assert.isTrue(isIconLigature(target.children[0])); } ); + (fontApiSupport ? it : it.skip)( + 'should return true for a font that has zero width characters', + () => { + const target = queryFixture( + '
0
' + ); + 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( '
!==
' ); 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( '
keyboard_arrow_left
' ); 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( '
delete
' ); @@ -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( '
__non-icon text__
' ); @@ -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( '
delete
' ); @@ -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( '
figure
' ); 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( '
delete
' ); From fcf76e04d8534dfed75caf1f2c4a74ef4faa29ae Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 8 Aug 2023 14:35:10 +0200 Subject: [PATCH 39/54] fix(aria-allowed-attr): pass aria-expanded on checkbox & switch (#4110) * update role allowances for aria-expanded All roles reviewed to largely remove (but in a few cases add) `aria-expanded` as a supported attribute. This matches both work done in ARIA 1.2 to correct for roles that should not have allowed the attribute, while also incorporating some ARIA 1.2 updates where some roles had support for the attribute added. closes #3339 * fix(aria-allowed-attr): allow aria-expanded on more roles * Integration tests * Update lib/standards/aria-roles.js Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> * Tweak comments --------- Co-authored-by: Scott O'Hara Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> --- lib/standards/aria-roles.js | 47 ++++++++++++++++++- .../rules/aria-allowed-attr/passes.html | 4 +- 2 files changed, 48 insertions(+), 3 deletions(-) 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/test/integration/rules/aria-allowed-attr/passes.html b/test/integration/rules/aria-allowed-attr/passes.html index 1812f63cdc..dfef5d3ed5 100644 --- a/test/integration/rules/aria-allowed-attr/passes.html +++ b/test/integration/rules/aria-allowed-attr/passes.html @@ -1901,6 +1901,7 @@
I am RED! From 49eaa0e1663724f70b2571cc7393e306bf0c7321 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Tue, 8 Aug 2023 08:47:57 -0600 Subject: [PATCH 40/54] fix(target-size): update to match new spacing requirements (#4117) * fix(target-size): update to match new spacing requirements * working * tests * finalize? * tests * revert playground * :robot: Automated formatting fixes * Apply suggestions from code review Co-authored-by: Wilco Fiers * :robot: Automated formatting fixes * udpate tests * dont half minOffset but double return from getOffset * Apply suggestions from code review Co-authored-by: Wilco Fiers * :robot: Automated formatting fixes * fix tests * fix test --------- Co-authored-by: straker Co-authored-by: Wilco Fiers --- lib/checks/mobile/target-offset-evaluate.js | 4 +- lib/checks/mobile/target-offset.json | 8 +- lib/commons/dom/get-target-size.js | 66 +++++++ lib/commons/dom/index.js | 1 + lib/commons/math/get-offset.js | 168 +++++------------- lib/commons/math/split-rects.js | 30 +++- locales/_template.json | 8 +- test/checks/mobile/target-offset.js | 78 ++++---- test/commons/dom/get-target-size.js | 47 +++++ test/commons/math/get-offset.js | 143 ++++++--------- test/commons/math/split-rects.js | 129 +++++++------- .../full/target-size/target-size.html | 23 +-- 12 files changed, 359 insertions(+), 346 deletions(-) create mode 100644 lib/commons/dom/get-target-size.js create mode 100644 test/commons/dom/get-target-size.js 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/commons/dom/get-target-size.js b/lib/commons/dom/get-target-size.js new file mode 100644 index 0000000000..f16c245227 --- /dev/null +++ b/lib/commons/dom/get-target-size.js @@ -0,0 +1,66 @@ +import findNearbyElms from './find-nearby-elms'; +import { splitRects, hasVisualOverlap } from '../math'; +import memoize from '../../core/utils/memoize'; + +const roundingMargin = 0.05; + +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 nodeRect = vNode.boundingClientRect; + const overlappingVNodes = findNearbyElms(vNode).filter(vNeighbor => { + return ( + vNeighbor.getComputedStylePropertyValue('pointer-events') !== 'none' && + hasVisualOverlap(vNode, vNeighbor) + ); + }); + + if (!overlappingVNodes.length) { + return nodeRect; + } + + return getLargestUnobscuredArea(vNode, overlappingVNodes, minSize); +} + +// Find areas of the target that are not obscured +function getLargestUnobscuredArea(vNode, obscuredNodes, minSize) { + const nodeRect = vNode.boundingClientRect; + if (obscuredNodes.length === 0) { + return null; + } + const obscuringRects = obscuredNodes.map( + ({ boundingClientRect: rect }) => rect + ); + const unobscuredRects = splitRects(nodeRect, obscuringRects); + if (!unobscuredRects.length) { + return null; + } + + // Of the unobscured inner rects, work out the largest + return getLargestRect(unobscuredRects, 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; + }); +} + +function rectHasMinimumSize(minSize, { width, height }) { + return ( + width + roundingMargin >= minSize && height + roundingMargin >= minSize + ); +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 9e5f16a741..fbac8213f0 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -17,6 +17,7 @@ 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 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/math/get-offset.js b/lib/commons/math/get-offset.js index 408a3e7cef..65cba9b0e3 100644 --- a/lib/commons/math/get-offset.js +++ b/lib/commons/math/get-offset.js @@ -1,91 +1,61 @@ +import { getTargetSize } from '../dom'; + /** * Get the offset between node A and node B * @method getOffset * @memberof axe.commons.math * @param {VirtualNode} vNodeA * @param {VirtualNode} vNodeB + * @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); -} - -/** - * 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; -} +export default function getOffset(vNodeA, vNodeB, minRadiusNeighbour = 12) { + const rectA = getTargetSize(vNodeA); + const rectB = getTargetSize(vNodeB); -/** - * 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; - } - adjacentRect = ownRect; + // one of the rects is fully obscured + if (rectA === null || rectB === null) { + return 0; } - 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 centerA = { + x: rectA.x + rectA.width / 2, + y: rectA.y + rectA.height / 2 + }; + const centerB = { + x: rectB.x + rectB.width / 2, + y: rectB.y + rectB.height / 2 + }; + const sideB = getClosestPoint(centerA, rectB); - 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.min( + // subtract the radius of the circle from the distance + pointDistance(centerA, centerB) - minRadiusNeighbour, + pointDistance(centerA, sideB) + ); +} + +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 +65,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/split-rects.js b/lib/commons/math/split-rects.js index 0273445f3a..c7d45bafda 100644 --- a/lib/commons/math/split-rects.js +++ b/lib/commons/math/split-rects.js @@ -5,7 +5,7 @@ * @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]; @@ -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/locales/_template.json b/locales/_template.json index 44cc68bef4..06f198aa04 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -862,11 +862,11 @@ "fail": "${data} on 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/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( 'x' ); - 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( 'x' + @@ -31,14 +29,14 @@ describe('target-offset tests', function () { '">x' ); - 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( 'x' + @@ -47,14 +45,14 @@ describe('target-offset tests', function () { '">x' ); - 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( 'x' + @@ -63,15 +61,15 @@ describe('target-offset tests', function () { '">x' ); - 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( 'x' + @@ -80,13 +78,13 @@ describe('target-offset tests', function () { '">x
' ); - 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( 'x' + @@ -95,13 +93,13 @@ describe('target-offset tests', function () { '">x' ); - 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( 'x' + @@ -112,11 +110,11 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - 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( 'x' + @@ -135,19 +133,19 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - 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( 'x' + @@ -158,12 +156,12 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - 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/commons/dom/get-target-size.js b/test/commons/dom/get-target-size.js new file mode 100644 index 0000000000..ddb876f7aa --- /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(''); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, vNode.actualNode.getBoundingClientRect()); + }); + + it('returns target size when obscured', () => { + const vNode = queryFixture(` + +
+ `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 20, 40)); + }); + + it('ignores elements with "pointer-events: none"', () => { + const vNode = queryFixture(` + +
+ `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 30, 40)); + }); + + it("ignores elements that don't overlap the target", () => { + const vNode = queryFixture(` + +
+ `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 30, 40)); + }); + + it('returns the largest unobscured area', () => { + const vNode = queryFixture(` + +
+
+ `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 10, 20, 35)); + }); +}); diff --git a/test/commons/math/get-offset.js b/test/commons/math/get-offset.js index 9214d6a861..cdbd1a5682 100644 --- a/test/commons/math/get-offset.js +++ b/test/commons/math/get-offset.js @@ -1,104 +1,65 @@ -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( - ' ' + - ' ' - ); - 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( - ' ' + - ' ' - ); - 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(` + + + `); + 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( - ' ' + - ' ' - ); - 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 vertical distance for elements horizontally aligned', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - 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 edge of square when one is undersized', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 45, round); }); - it('returns corner to corner distance for diagonal elms', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - 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 center to corner of square when at a diagonal', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 63.6, round); }); - it('returns the distance to the edge when elements overlap on an edge', function () { - var fixture = fixtureSetup( - '' + - ' ' + - '' - ); - 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 nodeA is overlapped by nodeB', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + 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( - '' + - ' ' + - '' - ); - 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('returns 0 if nodeB is overlapped by nodeA', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[3]; + const nodeB = fixture.children[1]; + assert.equal(getOffset(nodeA, nodeB), 0); }); - it('returns smallest diagonal if elmA fully covers elmB', function () { - var fixture = fixtureSetup( - '' + - ' ' + - '' - ); - 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('subtracts minNeighbourRadius from center-to-center calculations', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB, 30), 20, round); }); }); 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/integration/full/target-size/target-size.html b/test/integration/full/target-size/target-size.html index 02a36e9a25..d6a1b2f873 100644 --- a/test/integration/full/target-size/target-size.html +++ b/test/integration/full/target-size/target-size.html @@ -350,7 +350,7 @@
-

Example E1 and E2 pass, the two outside elements of E3 and E4 fail.

+

Example E1 - E3 pass, E4 fails.

@@ -363,18 +363,18 @@
- + - +
- +
-

Example F1 and F2 pass, the inside element of F3 and F4 fail.

+

Example F1 - F3 pass, F4 fails.

@@ -388,13 +388,13 @@
- +
- + - +
@@ -440,7 +440,7 @@
- + @@ -461,7 +461,8 @@
- + +
@@ -608,7 +609,7 @@
- + From 53c7ee4d38a81378e318a60ca5e1543306be21b4 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 8 Aug 2023 17:19:39 +0200 Subject: [PATCH 41/54] chore(aria-braille-equivalent): Update to be reviewOnFail (#4123) * chore(aria-braille-equivalent): Update to be reviewOnFail * Virtual tests --- doc/rule-descriptions.md | 2 +- lib/rules/aria-braille-equivalent.json | 1 + .../aria-braille-equivalent.html | 4 ++-- .../aria-braille-equivalent.json | 3 ++- .../virtual-rules/aria-braille-equivalent.js | 12 ++++++------ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 0978c3f916..710f464ccb 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -16,7 +16,7 @@ | :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [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, 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.7/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.7/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 | failure, needs review | | +| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.7/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.7/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.7/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.7/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) | diff --git a/lib/rules/aria-braille-equivalent.json b/lib/rules/aria-braille-equivalent.json index a93f859c94..a406e59fb1 100644 --- a/lib/rules/aria-braille-equivalent.json +++ b/lib/rules/aria-braille-equivalent.json @@ -1,5 +1,6 @@ { "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"], diff --git a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html index 55b9874d70..c5aa2eb572 100644 --- a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html +++ b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.html @@ -1,6 +1,6 @@ - + diff --git a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json index 725223482e..d1982970f2 100644 --- a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json +++ b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json @@ -2,5 +2,6 @@ "description": "aria-braille-equivalent tests", "rule": "aria-braille-equivalent", "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]], - "violations": [["#fail1"], ["#fail2"]] + "incomplete": [["#incomplete1"], ["#incomplete2"]], + "violation": [] } diff --git a/test/integration/virtual-rules/aria-braille-equivalent.js b/test/integration/virtual-rules/aria-braille-equivalent.js index 481697f0e9..cb20f39553 100644 --- a/test/integration/virtual-rules/aria-braille-equivalent.js +++ b/test/integration/virtual-rules/aria-braille-equivalent.js @@ -17,7 +17,7 @@ describe('aria-braille-equivalent virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('fails when accessible text is empty but braille label is not', () => { + it('incompletes when accessible text is empty but braille label is not', () => { const results = axe.runVirtualRule('aria-braille-equivalent', { nodeName: 'img', attributes: { @@ -27,8 +27,8 @@ describe('aria-braille-equivalent virtual-rule', () => { }); assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); - assert.lengthOf(results.incomplete, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); }); it('passes when roledescription and brailleroledescription are not empty', () => { @@ -45,7 +45,7 @@ describe('aria-braille-equivalent virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('fails when roledescription is empty but brailleroledescription is not', () => { + it('incompletes when roledescription is empty but brailleroledescription is not', () => { const results = axe.runVirtualRule('aria-braille-equivalent', { nodeName: 'div', attributes: { @@ -55,8 +55,8 @@ describe('aria-braille-equivalent virtual-rule', () => { }); assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); - assert.lengthOf(results.incomplete, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); }); it('incompletes if the subtree fails to compute with aria-braillelabel', () => { From fbe99bf87a3ebd7d6bc4b4eca7a58bbff28a5b23 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 8 Aug 2023 18:12:02 +0200 Subject: [PATCH 42/54] fix(access-name): get name from header elements (#4097) * fix(dialog-name): get name from header elements * Never get content from elements with a value * Fix failing test * Grrr, prettier * More tests * Update test/testutils.js --- lib/commons/text/form-control-value.js | 2 +- lib/commons/text/subtree-text.js | 12 +- lib/commons/text/unsupported.js | 10 +- test/commons/text/accessible-text.js | 3042 ++++++++++------- test/commons/text/form-control-value.js | 453 +-- test/commons/text/subtree-text.js | 82 +- .../aria-dialog-name/aria-dialog-name.html | 3 + .../aria-dialog-name/aria-dialog-name.json | 3 +- test/integration/rules/label/label.html | 92 +- test/integration/rules/label/label.json | 23 +- test/testutils.js | 12 + 11 files changed, 2255 insertions(+), 1479 deletions(-) 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/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/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: + // + //
+ accessibleNameFromFieldValue: ['progressbar'] }; - -export default unsupported; diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js index 3fd2ca785e..fd0552f0cd 100644 --- a/test/commons/text/accessible-text.js +++ b/test/commons/text/accessible-text.js @@ -1,6 +1,6 @@ describe('text.accessibleTextVirtual', () => { const fixture = document.getElementById('fixture'); - const shadowSupport = axe.testUtils.shadowSupport; + const { html, shadowSupport } = axe.testUtils; afterEach(() => { fixture.innerHTML = ''; @@ -9,26 +9,32 @@ describe('text.accessibleTextVirtual', () => { it('is called through accessibleText with a DOM node', () => { const accessibleText = axe.commons.text.accessibleText; - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = fixture.querySelector('input'); assert.equal(accessibleText(target), ''); }); it('should match the first example from the ARIA spec', () => { - fixture.innerHTML = - '
    ' + - ' ' + - ' ' + - '
'; + fixture.innerHTML = html` +
    + + +
+ `; axe.testUtils.flatTreeSetup(fixture); const rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0]; @@ -39,29 +45,35 @@ describe('text.accessibleTextVirtual', () => { }); it('should match the second example from the ARIA spec', () => { - fixture.innerHTML = - '
' + - ' Meeting alarms' + - ' ' + - '
' + - '
' + - ' ' + - ' ' + - ' ' + - '
'; + fixture.innerHTML = html` +
+ Meeting alarms + + +
+ +
+ + + +
+ `; axe.testUtils.flatTreeSetup(fixture); 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' @@ -69,12 +81,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use aria-labelledby if present', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -85,12 +107,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use recusive aria-labelledby properly', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -101,12 +133,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should include hidden text referred to with aria-labelledby', () => { - fixture.innerHTML = - '' + - '' + - ''; + fixture.innerHTML = html` + + + ' + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -117,8 +152,9 @@ describe('text.accessibleTextVirtual', () => { }); it('should allow setting the initial includeHidden value', () => { - fixture.innerHTML = - ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0]; @@ -138,12 +174,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use aria-label if present with no labelledby', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -151,13 +191,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use alt on imgs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - 'Alt text goes here' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ Alt text goes here +
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -168,13 +211,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use alt on image inputs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - '' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ +
This is a label
+ + ' + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -185,13 +231,16 @@ describe('text.accessibleTextVirtual', () => { }); it('should use not use alt on text inputs with no ARIA', () => { - fixture.innerHTML = - '
This is of everything
' + - '' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+ +
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; @@ -199,12 +248,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should use HTML label if no ARIA information', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; @@ -212,12 +264,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle last ditch title attribute', () => { - fixture.innerHTML = - '
This is of
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -228,12 +290,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle totally empty elements', () => { - fixture.innerHTML = - '
This is of
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -244,20 +316,28 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle author name-from roles properly', () => { - fixture.innerHTML = - '
This is ' + - ' ' + - ' of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const 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 + // 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' @@ -265,9 +345,11 @@ describe('text.accessibleTextVirtual', () => { }); it('should only show each node once when label is before input', () => { - fixture.innerHTML = - '
' + - '
'; + fixture.innerHTML = html` +
+ +
+ `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( @@ -277,10 +359,12 @@ describe('text.accessibleTextVirtual', () => { }); it('should only show each node once when label follows input', () => { - fixture.innerHTML = - '
' + - '
' + - ''; + fixture.innerHTML = html` +
+ +
+ + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal( @@ -290,12 +374,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should handle nested inputs in normal context', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; @@ -306,18 +400,28 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle nested inputs properly in labelledby context', () => { - // 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 = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); 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' @@ -325,12 +429,15 @@ describe('text.accessibleTextVirtual', () => { }); it('should use ignore hidden inputs', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is of + everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; @@ -341,18 +448,22 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle inputs with no type as if they were text inputs', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const 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" + // 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' @@ -360,39 +471,49 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle nested selects properly in labelledby context', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const 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" + // 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', () => { - fixture.innerHTML = - '
This is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html` +
+ This is + + of everything +
+
This is a label
+ + + `; axe.testUtils.flatTreeSetup(fixture); const 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" + // 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' @@ -400,13 +521,21 @@ describe('text.accessibleTextVirtual', () => { }); it('should use handle ARIA labels properly in labelledby context', () => { - fixture.innerHTML = - '
This span' + - ' is of everything
' + - '
This is a label
' + - '' + - ''; + fixture.innerHTML = html`
+ This + span + is + + of everything +
+
This is a label
+ + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; @@ -417,73 +546,82 @@ describe('text.accessibleTextVirtual', () => { }); it('should come up empty if input is labeled only by select options', () => { - fixture.innerHTML = - '' + - ''; + fixture.innerHTML = html` + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "Chosen" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // 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)", () => { - fixture.innerHTML = - '' + - '' + - ''; + fixture.innerHTML = html` + + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // 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', () => { - fixture.innerHTML = - ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); 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', () => { - fixture.innerHTML = - '' + - ''; + fixture.innerHTML = html` + + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; - // Chrome 70: "" - // Firefox 62: "" - // Safari 12.0: "Chosen" - assert.equal(axe.commons.text.accessibleTextVirtual(target), ''); + // 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', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` + + `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -491,7 +629,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on anchors', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -499,7 +637,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on buttons', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; @@ -507,21 +645,21 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=presentation on summary', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello'); }); it('shoud properly fall back to title', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); 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', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -529,7 +667,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=none on buttons', () => { - fixture.innerHTML = ''; + fixture.innerHTML = html` `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; @@ -537,7 +675,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should give text even for role=none on summary', () => { - fixture.innerHTML = 'Hello'; + fixture.innerHTML = html` Hello `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; @@ -545,7 +683,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should not add extra spaces around phrasing elements', () => { - fixture.innerHTML = 'HelloWorld'; + fixture.innerHTML = html` HelloWorld `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -553,7 +691,12 @@ describe('text.accessibleTextVirtual', () => { }); it('should add spaces around non-phrasing elements', () => { - fixture.innerHTML = 'Hello
World
'; + fixture.innerHTML = html` + Hello +
World
+ `; axe.testUtils.flatTreeSetup(fixture); const target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; @@ -570,7 +713,7 @@ describe('text.accessibleTextVirtual', () => { }); it('should use