From 867681b5440e27a0cf469a5008f644544e89a386 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 28 Jun 2022 16:19:19 +0200 Subject: [PATCH 1/7] fix(valid-lang): ignore lang on elements with no text --- lib/commons/dom/has-content-virtual.js | 2 +- lib/commons/dom/has-lang-text.js | 24 ++++++ lib/commons/dom/index.js | 1 + lib/rules/has-lang-text-matches.js | 7 ++ lib/rules/valid-lang.json | 4 +- test/commons/dom/has-lang-text.js | 83 +++++++++++++++++++ .../rules/valid-lang/valid-lang.html | 9 ++ .../rules/valid-lang/valid-lang.json | 3 +- 8 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 lib/commons/dom/has-lang-text.js create mode 100644 lib/rules/has-lang-text-matches.js create mode 100644 test/commons/dom/has-lang-text.js diff --git a/lib/commons/dom/has-content-virtual.js b/lib/commons/dom/has-content-virtual.js index 8dfc7e74c7..a6459cbfb2 100644 --- a/lib/commons/dom/has-content-virtual.js +++ b/lib/commons/dom/has-content-virtual.js @@ -14,7 +14,7 @@ const hiddenTextElms = [ 'NOSCRIPT' ]; -function hasChildTextNodes(elm) { +export function hasChildTextNodes(elm) { if (!hiddenTextElms.includes(elm.actualNode.nodeName.toUpperCase())) { return elm.children.some( ({ actualNode }) => diff --git a/lib/commons/dom/has-lang-text.js b/lib/commons/dom/has-lang-text.js new file mode 100644 index 0000000000..464197bfdf --- /dev/null +++ b/lib/commons/dom/has-lang-text.js @@ -0,0 +1,24 @@ +import { hasChildTextNodes } from './has-content-virtual'; +import isVisualContent from './is-visual-content'; +import isVisible from './is-visible'; + +/** + * Check that a node has text, or an accessible name which language is defined by the + * nearest ancestor's lang attribute. + * @param {VirtualNode} virtualNode + * @return boolean + */ +export default function hasLangText(virtualNode) { + if (hasChildTextNodes(virtualNode)) { + return true; + } + if (virtualNode.props.nodeType === 1 && isVisualContent(virtualNode.actualNode)) { + // See: https://github.com/dequelabs/axe-core/issues/3281 + return !!axe.commons.text.accessibleTextVirtual(virtualNode); + } + return virtualNode.children.some(child => ( + !child.attr('lang') && // non-empty lang + hasLangText(child) && // has text + isVisible(child, true) // Not hidden for AT + )); +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 5acd13c076..6e60cec766 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -17,6 +17,7 @@ export { default as getTextElementStack } from './get-text-element-stack'; export { default as getViewportSize } from './get-viewport-size'; export { default as hasContentVirtual } from './has-content-virtual'; export { default as hasContent } from './has-content'; +export { default as hasLangText } from './has-lang-text'; export { default as idrefs } from './idrefs'; export { default as insertedIntoFocusOrder } from './inserted-into-focus-order'; export { default as isCurrentPageLink } from './is-current-page-link'; diff --git a/lib/rules/has-lang-text-matches.js b/lib/rules/has-lang-text-matches.js new file mode 100644 index 0000000000..8262485390 --- /dev/null +++ b/lib/rules/has-lang-text-matches.js @@ -0,0 +1,7 @@ +import { hasLangText } from '../commons/dom' + +function hasLangTextMatches(node, virtualNode) { + return hasLangText(virtualNode); +} + +export default hasLangTextMatches; diff --git a/lib/rules/valid-lang.json b/lib/rules/valid-lang.json index 81df179ef8..fcc5301848 100644 --- a/lib/rules/valid-lang.json +++ b/lib/rules/valid-lang.json @@ -1,7 +1,7 @@ { "id": "valid-lang", - "selector": "[lang], [xml\\:lang]", - "matches": "not-html-matches", + "selector": "[lang]:not(html), [xml\\:lang]:not(html)", + "matches": "has-lang-text-matches", "tags": ["cat.language", "wcag2aa", "wcag312", "ACT"], "actIds": ["de46e4"], "metadata": { diff --git a/test/commons/dom/has-lang-text.js b/test/commons/dom/has-lang-text.js new file mode 100644 index 0000000000..c293fe73f0 --- /dev/null +++ b/test/commons/dom/has-lang-text.js @@ -0,0 +1,83 @@ +describe('dom.hasLangText', function() { + 'use strict'; + var hasLangText = axe.commons.dom.hasLangText; + var fixture = document.getElementById('fixture'); + var tree; + + it('returns true when the element has a non-empty text node as its content', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('returns true when the element has nested text node as its content', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('returns false when the element has nested text is hidden', function () { + fixture.innerHTML = '
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isFalse(hasLangText(target, { skipNestedLang: true })); + }); + + it('returns false when the element has an empty text node as its content', function () { + fixture.innerHTML = '
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isFalse(hasLangText(target, { skipNestedLang: true })); + }); + + it('returns false if all text is in a child with a lang attribute', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isFalse(hasLangText(target, { skipNestedLang: true })); + }); + + it('does not skip if lang is on the starting node', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('ignores empty lang attributes', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('ignores null lang attributes', function () { + fixture.innerHTML = '
text
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('true for non-text content with an accessible name', function () { + fixture.innerHTML = '
foo
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isTrue(hasLangText(target, { skipNestedLang: true })); + }); + + it('false for non-text content without accessible name', function () { + fixture.innerHTML = '
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isFalse(hasLangText(target, { skipNestedLang: true })); + }); + + it('returns false for non-text content with a lang attr', function () { + fixture.innerHTML = '
foo
'; + tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(tree, '#target')[0]; + assert.isFalse(hasLangText(target, { skipNestedLang: true })); + }); +}); \ No newline at end of file diff --git a/test/integration/rules/valid-lang/valid-lang.html b/test/integration/rules/valid-lang/valid-lang.html index 0dd98d03bc..36fde532c9 100644 --- a/test/integration/rules/valid-lang/valid-lang.html +++ b/test/integration/rules/valid-lang/valid-lang.html @@ -9,3 +9,12 @@

Not English

Mix

English

+ +

+

+

+ English +

+

+ +

diff --git a/test/integration/rules/valid-lang/valid-lang.json b/test/integration/rules/valid-lang/valid-lang.json index b8034e1631..cae7d0f244 100644 --- a/test/integration/rules/valid-lang/valid-lang.json +++ b/test/integration/rules/valid-lang/valid-lang.json @@ -8,6 +8,7 @@ ["#pass3"], ["#pass4"], ["#pass5"], - ["#pass6"] + ["#pass6"], + ["#pass7"] ] } From 819e0ff05e4d05b5e5e0b70e2c6cd9ab3f1a278c Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 28 Jun 2022 16:26:45 +0200 Subject: [PATCH 2/7] Delete broken / unnecessary test --- test/rule-matches/not-html-matches.js | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 test/rule-matches/not-html-matches.js diff --git a/test/rule-matches/not-html-matches.js b/test/rule-matches/not-html-matches.js deleted file mode 100644 index c850c94a9d..0000000000 --- a/test/rule-matches/not-html-matches.js +++ /dev/null @@ -1,20 +0,0 @@ -describe('not-html-matches', function() { - 'use strict'; - - var fixture = document.getElementById('fixture'); - var rule; - - beforeEach(function() { - rule = axe.utils.getRule('valid-lang'); - }); - - it('returns true when element is not the html element', function() { - var vNode = axe.setup(fixture); - assert.isTrue(rule.matches(null, vNode)); - }); - - it('returns false when element is the html element', function() { - var vNode = axe.setup(document.documentElement); - assert.isFalse(rule.matches(null, vNode)); - }); -}); From 497621cd3b0d11f2003a44f5a024499269eee6af Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Wed, 29 Jun 2022 12:04:13 +0200 Subject: [PATCH 3/7] Fix failing tests --- lib/commons/dom/has-content-virtual.js | 35 +++++----- lib/commons/dom/has-lang-text.js | 4 +- lib/commons/dom/is-visual-content.js | 50 +++++++------- test/commons/dom/is-visual-content.js | 91 ++++++++++++++------------ 4 files changed, 100 insertions(+), 80 deletions(-) diff --git a/lib/commons/dom/has-content-virtual.js b/lib/commons/dom/has-content-virtual.js index a6459cbfb2..9d28a05802 100644 --- a/lib/commons/dom/has-content-virtual.js +++ b/lib/commons/dom/has-content-virtual.js @@ -2,25 +2,30 @@ import isVisualContent from './is-visual-content'; import labelVirtual from '../aria/label-virtual'; const hiddenTextElms = [ - 'HEAD', - 'TITLE', - 'TEMPLATE', - 'SCRIPT', - 'STYLE', - 'IFRAME', - 'OBJECT', - 'VIDEO', - 'AUDIO', - 'NOSCRIPT' + 'head', + 'title', + 'template', + 'script', + 'style', + 'iframe', + 'object', + 'video', + 'audio', + 'noscript' ]; +/** + * Test if the element has child nodes that are non-empty text nodes + * @param {VirtualNode} elm + * @returns boolean + */ export function hasChildTextNodes(elm) { - if (!hiddenTextElms.includes(elm.actualNode.nodeName.toUpperCase())) { - return elm.children.some( - ({ actualNode }) => - actualNode.nodeType === 3 && actualNode.nodeValue.trim() - ); + if (hiddenTextElms.includes(elm.props.nodeName)) { + return false } + return elm.children.some(({ props }) => { + return props.nodeType === 3 && props.nodeValue.trim() + }) } /** diff --git a/lib/commons/dom/has-lang-text.js b/lib/commons/dom/has-lang-text.js index 464197bfdf..30538f07c2 100644 --- a/lib/commons/dom/has-lang-text.js +++ b/lib/commons/dom/has-lang-text.js @@ -9,10 +9,10 @@ import isVisible from './is-visible'; * @return boolean */ export default function hasLangText(virtualNode) { - if (hasChildTextNodes(virtualNode)) { + if (typeof virtualNode.children === 'undefined' || hasChildTextNodes(virtualNode)) { return true; } - if (virtualNode.props.nodeType === 1 && isVisualContent(virtualNode.actualNode)) { + if (virtualNode.props.nodeType === 1 && isVisualContent(virtualNode)) { // See: https://github.com/dequelabs/axe-core/issues/3281 return !!axe.commons.text.accessibleTextVirtual(virtualNode); } diff --git a/lib/commons/dom/is-visual-content.js b/lib/commons/dom/is-visual-content.js index ef8b7c0aa8..87e377ada2 100644 --- a/lib/commons/dom/is-visual-content.js +++ b/lib/commons/dom/is-visual-content.js @@ -1,8 +1,13 @@ +import { getNodeFromTree } from '../../core/utils'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; + const visualRoles = [ 'checkbox', 'img', + 'meter', + 'progressbar', + 'scrollbar', 'radio', - 'range', 'slider', 'spinbutton', 'textbox' @@ -13,34 +18,35 @@ const visualRoles = [ * @method isVisualContent * @memberof axe.commons.dom * @instance - * @param {Element} element The element to check + * @param {Element|VirtualNode} element The element to check * @return {Boolean} */ -function isVisualContent(element) { - /*eslint indent: 0*/ - const role = element.getAttribute('role'); +function isVisualContent(el) { + const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); + const role = axe.commons.aria.getExplicitRole(vNode); + console.log({ role }) if (role) { return visualRoles.indexOf(role) !== -1; } - switch (element.nodeName.toUpperCase()) { - case 'IMG': - case 'IFRAME': - case 'OBJECT': - case 'VIDEO': - case 'AUDIO': - case 'CANVAS': - case 'SVG': - case 'MATH': - case 'BUTTON': - case 'SELECT': - case 'TEXTAREA': - case 'KEYGEN': - case 'PROGRESS': - case 'METER': + switch (vNode.props.nodeName) { + case 'img': + case 'iframe': + case 'object': + case 'video': + case 'audio': + case 'canvas': + case 'svg': + case 'math': + case 'button': + case 'select': + case 'textarea': + case 'keygen': + case 'progress': + case 'meter': return true; - case 'INPUT': - return element.type !== 'hidden'; + case 'input': + return vNode.props.type !== 'hidden'; default: return false; } diff --git a/test/commons/dom/is-visual-content.js b/test/commons/dom/is-visual-content.js index 1c6d1014f2..701429712b 100644 --- a/test/commons/dom/is-visual-content.js +++ b/test/commons/dom/is-visual-content.js @@ -2,110 +2,119 @@ describe('dom.isVisualContent', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; + var isVisualContent = axe.commons.dom.isVisualContent afterEach(function() { - document.getElementById('fixture').innerHTML = ''; + fixture.innerHTML = ''; }); describe('isVisualContent', function() { it('should return true for img', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for iframe', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for object', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for video', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for audio', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for canvas', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for svg', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for math', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for button', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for select', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for textarea', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for keygen', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for meter', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for non-hidden input', function() { - fixture.innerHTML = ''; - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isTrue(isVisualContent(virtualNode)); }); it('should return true for elements with a visual aria role', function() { - fixture.innerHTML = + var virtualNode = queryFixture('
' + '' + '' + '' + - '' + + '' + + '' + + '' + '' + '' + - ''; - - for (var i = 0; i < fixture.children.length; i++) { - assert.isTrue(axe.commons.dom.isVisualContent(fixture.children[i])); + '' + + '
' + ); + + for (var i = 0; i < virtualNode.children.length; i++) { + assert.isTrue( + isVisualContent(virtualNode.children[i]), + 'for role ' + virtualNode.children[i].attr('role') + ); } }); it('should return false for hidden input', function() { - fixture.innerHTML = ''; - assert.isFalse(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture(''); + assert.isFalse(isVisualContent(virtualNode)); }); it('should return false for p', function() { - fixture.innerHTML = '

Paragraph!

'; - assert.isFalse(axe.commons.dom.isVisualContent(fixture.children[0])); + var virtualNode = queryFixture('

Paragraph!

'); + assert.isFalse(isVisualContent(virtualNode)); }); }); }); From f9c82be2e54fa7f0b776a3d5fa0f3ba02ae60d66 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Wed, 29 Jun 2022 15:33:49 +0200 Subject: [PATCH 4/7] fix broken test --- test/integration/full/isolated-env/isolated-env.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/full/isolated-env/isolated-env.html b/test/integration/full/isolated-env/isolated-env.html index defb21a83c..b229f66bfd 100644 --- a/test/integration/full/isolated-env/isolated-env.html +++ b/test/integration/full/isolated-env/isolated-env.html @@ -79,7 +79,7 @@

Ok

I am a circle -
+
English
  • Hello
  • From 2437511eb90ff0eb96444f17a11fcdba539bd34f Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 30 Jun 2022 10:57:48 +0200 Subject: [PATCH 5/7] Address feedback --- lib/commons/dom/is-visual-content.js | 1 - test/commons/dom/has-lang-text.js | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/commons/dom/is-visual-content.js b/lib/commons/dom/is-visual-content.js index 87e377ada2..3efef3c870 100644 --- a/lib/commons/dom/is-visual-content.js +++ b/lib/commons/dom/is-visual-content.js @@ -24,7 +24,6 @@ const visualRoles = [ function isVisualContent(el) { const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); const role = axe.commons.aria.getExplicitRole(vNode); - console.log({ role }) if (role) { return visualRoles.indexOf(role) !== -1; } diff --git a/test/commons/dom/has-lang-text.js b/test/commons/dom/has-lang-text.js index c293fe73f0..2ed94fdfd1 100644 --- a/test/commons/dom/has-lang-text.js +++ b/test/commons/dom/has-lang-text.js @@ -8,76 +8,76 @@ describe('dom.hasLangText', function() { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('returns true when the element has nested text node as its content', function () { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('returns false when the element has nested text is hidden', function () { fixture.innerHTML = '
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isFalse(hasLangText(target, { skipNestedLang: true })); + assert.isFalse(hasLangText(target)); }); it('returns false when the element has an empty text node as its content', function () { fixture.innerHTML = '
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isFalse(hasLangText(target, { skipNestedLang: true })); + assert.isFalse(hasLangText(target)); }); it('returns false if all text is in a child with a lang attribute', function () { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isFalse(hasLangText(target, { skipNestedLang: true })); + assert.isFalse(hasLangText(target)); }); it('does not skip if lang is on the starting node', function () { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('ignores empty lang attributes', function () { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('ignores null lang attributes', function () { fixture.innerHTML = '
    text
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('true for non-text content with an accessible name', function () { fixture.innerHTML = '
    foo
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isTrue(hasLangText(target, { skipNestedLang: true })); + assert.isTrue(hasLangText(target)); }); it('false for non-text content without accessible name', function () { fixture.innerHTML = '
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isFalse(hasLangText(target, { skipNestedLang: true })); + assert.isFalse(hasLangText(target)); }); it('returns false for non-text content with a lang attr', function () { fixture.innerHTML = '
    foo
    '; tree = axe.utils.getFlattenedTree(fixture); var target = axe.utils.querySelectorAll(tree, '#target')[0]; - assert.isFalse(hasLangText(target, { skipNestedLang: true })); + assert.isFalse(hasLangText(target)); }); -}); \ No newline at end of file +}); From 8007fb2db2ae3e483c93ec34f9e210d993f00330 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 1 Jul 2022 12:45:18 +0200 Subject: [PATCH 6/7] Disable flakey IE test --- test/commons/color/get-background-color.js | 39 ++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index b4fa859961..9fc20d7a3e 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -4,6 +4,7 @@ describe('color.getBackgroundColor', function() { var fixture = document.getElementById('fixture'); var shadowSupported = axe.testUtils.shadowSupport.v1; + var isIE11 = axe.testUtils.isIE11; var origBodyBg; var origHtmlBg; @@ -480,11 +481,11 @@ describe('color.getBackgroundColor', function() { assert.equal(actual.alpha, expected.alpha); }); - it('handles nested inline elements in the middle of a text', function () { + it('handles nested inline elements in the middle of a text', function() { fixture.innerHTML = '
    ' + '
    ' + - ' Text '+ + ' Text ' + ' ' + ' ' + '
    '; @@ -499,7 +500,7 @@ describe('color.getBackgroundColor', function() { assert.equal(actual.alpha, 1); }); - it('should return null for inline elements with position:absolute', function () { + it('should return null for inline elements with position:absolute', function() { fixture.innerHTML = '
    ' + '
    ' + @@ -725,21 +726,25 @@ describe('color.getBackgroundColor', function() { assert.isNull(actual); }); - it('should return background color for inline elements that do not fit the viewport', function() { - var html = ''; - for (var i = 0; i < 300; i++) { - html += 'foo
    '; - } - fixture.innerHTML = '' + html + ''; - axe.testUtils.flatTreeSetup(fixture); - var actual = axe.commons.color.getBackgroundColor(fixture, []); - var expected = new axe.commons.color.Color(255, 255, 255, 1); + // Test is flakey in IE11, timing out regularly + (isIE11 ? xit : it)( + 'should return background color for inline elements that do not fit the viewport', + function() { + var html = ''; + for (var i = 0; i < 300; i++) { + html += 'foo
    '; + } + fixture.innerHTML = '' + html + ''; + 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); - }); + 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); + } + ); it('should return the body bgColor when content does not overlap', function() { fixture.innerHTML = From 9f5df5fe25aadddcb93c354c3046dcb6fcb3c1b6 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Mon, 4 Jul 2022 15:01:35 +0200 Subject: [PATCH 7/7] Make hasLangText part of valid-lang check --- lib/checks/language/valid-lang-evaluate.js | 16 +++- lib/rules/has-lang-text-matches.js | 7 -- lib/rules/valid-lang.json | 1 - test/checks/language/valid-lang.js | 25 +++-- .../rules/valid-lang/valid-lang.html | 10 +- .../rules/valid-lang/valid-lang.json | 6 +- .../virtual-rules/html-lang-valid.js | 95 +++++++++++++++++++ 7 files changed, 132 insertions(+), 28 deletions(-) delete mode 100644 lib/rules/has-lang-text-matches.js create mode 100644 test/integration/virtual-rules/html-lang-valid.js diff --git a/lib/checks/language/valid-lang-evaluate.js b/lib/checks/language/valid-lang-evaluate.js index 6a2460b440..b59970281d 100644 --- a/lib/checks/language/valid-lang-evaluate.js +++ b/lib/checks/language/valid-lang-evaluate.js @@ -1,5 +1,6 @@ import { isValidLang, getBaseLang } from '../../core/utils'; import { sanitize } from '../../commons/text'; +import { hasLangText } from '../../commons/dom' function validLangEvaluate(node, options, virtualNode) { const invalid = []; @@ -25,12 +26,17 @@ function validLangEvaluate(node, options, virtualNode) { } }); - if (invalid.length) { - this.data(invalid); - return true; + if (!invalid.length) { + return false; } - - return false; + if ( // Except for `html`, ignore elements with no text + virtualNode.props.nodeName !== 'html' && + !hasLangText(virtualNode) + ) { + return false; + } + this.data(invalid); + return true; } export default validLangEvaluate; diff --git a/lib/rules/has-lang-text-matches.js b/lib/rules/has-lang-text-matches.js deleted file mode 100644 index 8262485390..0000000000 --- a/lib/rules/has-lang-text-matches.js +++ /dev/null @@ -1,7 +0,0 @@ -import { hasLangText } from '../commons/dom' - -function hasLangTextMatches(node, virtualNode) { - return hasLangText(virtualNode); -} - -export default hasLangTextMatches; diff --git a/lib/rules/valid-lang.json b/lib/rules/valid-lang.json index fcc5301848..382b15f949 100644 --- a/lib/rules/valid-lang.json +++ b/lib/rules/valid-lang.json @@ -1,7 +1,6 @@ { "id": "valid-lang", "selector": "[lang]:not(html), [xml\\:lang]:not(html)", - "matches": "has-lang-text-matches", "tags": ["cat.language", "wcag2aa", "wcag312", "ACT"], "actIds": ["de46e4"], "metadata": { diff --git a/test/checks/language/valid-lang.js b/test/checks/language/valid-lang.js index f4f553e77b..5ca9a56a04 100644 --- a/test/checks/language/valid-lang.js +++ b/test/checks/language/valid-lang.js @@ -12,7 +12,7 @@ describe('valid-lang', function() { }); it('should return false if a lang attribute is present in options', function() { - var params = checkSetup('
    ', { + var params = checkSetup('
    text
    ', { value: ['blah', 'blah', 'woohoo'] }); @@ -20,7 +20,7 @@ describe('valid-lang', function() { }); it('should lowercase options and attribute first', function() { - var params = checkSetup('
    ', { + var params = checkSetup('
    text
    ', { value: ['blah', 'blah', 'wOohoo'] }); @@ -28,39 +28,39 @@ describe('valid-lang', function() { }); it('should return true if a lang attribute is not present in options', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isTrue(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, ['lang="FOO"']); }); it('should return false (and not throw) when given no present in options', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); it('should return true if the language is badly formatted', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isTrue(validLangEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, ['lang="en_US"']); }); it('should return false if it matches a substring proceeded by -', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); it('should work with xml:lang', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isFalse(validLangEvaluate.apply(checkContext, params)); }); it('should accept options.attributes', function() { - var params = checkSetup('
    ', { + var params = checkSetup('
    text
    ', { attributes: ['custom-lang'] }); @@ -69,8 +69,15 @@ describe('valid-lang', function() { }); it('should return true if lang value is just whitespace', function() { - var params = checkSetup('
    '); + var params = checkSetup('
    text
    '); assert.isTrue(validLangEvaluate.apply(checkContext, params)); }); + + it('should return false if a lang attribute element has no content', function() { + var params = checkSetup('
    '); + + assert.isFalse(validLangEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, null); + }); }); diff --git a/test/integration/rules/valid-lang/valid-lang.html b/test/integration/rules/valid-lang/valid-lang.html index 36fde532c9..77f59ec10f 100644 --- a/test/integration/rules/valid-lang/valid-lang.html +++ b/test/integration/rules/valid-lang/valid-lang.html @@ -10,11 +10,11 @@

    Mix

    English

    -

    -

    -

    - English +

    +

    +

    + English

    -

    +

    diff --git a/test/integration/rules/valid-lang/valid-lang.json b/test/integration/rules/valid-lang/valid-lang.json index cae7d0f244..6fa8341ffd 100644 --- a/test/integration/rules/valid-lang/valid-lang.json +++ b/test/integration/rules/valid-lang/valid-lang.json @@ -9,6 +9,10 @@ ["#pass4"], ["#pass5"], ["#pass6"], - ["#pass7"] + ["#pass7"], + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] } diff --git a/test/integration/virtual-rules/html-lang-valid.js b/test/integration/virtual-rules/html-lang-valid.js new file mode 100644 index 0000000000..d89d410ca6 --- /dev/null +++ b/test/integration/virtual-rules/html-lang-valid.js @@ -0,0 +1,95 @@ +describe('html-lang-valid virtual-rule', function() { + it('is inapplicable without lang or xml:lang', function() { + // Error caught by html-has-lang instead + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: {} + }); + + assert.lengthOf(results.inapplicable, 1); + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass with a valid lang', function() { + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: { + lang: 'en' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass with a valid xml:lang', function() { + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: { + 'xml:lang': 'en' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for both lang and xml:lang', function() { + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: { + lang: 'en', + 'xml:lang': 'en' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail with an invalid lang', function() { + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: { + lang: 'invalid' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail with an invalid xml:lang', function() { + var results = axe.runVirtualRule('html-lang-valid', { + nodeName: 'html', + attributes: { + 'xml:lang': 'invalid' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail with an invalid lang, and explicitly no children', function() { + var html = new axe.SerialVirtualNode({ + nodeName: 'html', + attributes: { + lang: 'invalid' + } + }); + html.children = []; + + var results = axe.runVirtualRule('html-lang-valid', html); + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); +});