From 8e03e2cabe628b036a48575ec84569e7e0e333ae Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 4 Oct 2022 22:33:58 +0200 Subject: [PATCH] fix(object-alt): ignore unloaded objects (#3680) * fix(object-alt): ignore unloaded objects * fix failing test --- lib/rules/object-alt.json | 4 +- lib/rules/object-is-loaded-matches.js | 22 +++++ .../full/isolated-env/isolated-env.html | 5 +- .../rules/object-alt/object-alt.html | 89 +++++++++++++++---- test/integration/virtual-rules/object-alt.js | 66 +++++++++----- test/rule-matches/object-is-loaded-matches.js | 56 ++++++++++++ 6 files changed, 200 insertions(+), 42 deletions(-) create mode 100644 lib/rules/object-is-loaded-matches.js create mode 100644 test/rule-matches/object-is-loaded-matches.js diff --git a/lib/rules/object-alt.json b/lib/rules/object-alt.json index 53054cd366..191cd41c52 100644 --- a/lib/rules/object-alt.json +++ b/lib/rules/object-alt.json @@ -1,7 +1,7 @@ { "id": "object-alt", - "selector": "object", - "matches": "no-explicit-name-required-matches", + "selector": "object[data]", + "matches": "object-is-loaded-matches", "tags": [ "cat.text-alternatives", "wcag2a", diff --git a/lib/rules/object-is-loaded-matches.js b/lib/rules/object-is-loaded-matches.js new file mode 100644 index 0000000000..6b96fd2b4a --- /dev/null +++ b/lib/rules/object-is-loaded-matches.js @@ -0,0 +1,22 @@ +import noExplicitNameRequired from './no-explicit-name-required-matches'; + +export default (node, vNode) => + [noExplicitNameRequired, objectHasLoaded].every(fn => fn(node, vNode)); + +/** + * Test if an object loaded content; assume yes if we can't prove otherwise + * + * @param {Element} node + * @param {VirtualNode} vNode + * @returns {boolean} + */ +function objectHasLoaded(node) { + if (!node?.ownerDocument?.createRange) { + return true; // Assume it did + } + // There's no ready + const range = node.ownerDocument.createRange(); + range.setStart(node, 0); + range.setEnd(node, node.childNodes.length); + return range.getClientRects().length === 0; +} diff --git a/test/integration/full/isolated-env/isolated-env.html b/test/integration/full/isolated-env/isolated-env.html index e7bf4b9591..a38d802c7e 100644 --- a/test/integration/full/isolated-env/isolated-env.html +++ b/test/integration/full/isolated-env/isolated-env.html @@ -80,7 +80,10 @@

Ok

English
- +
  • Hello
  • diff --git a/test/integration/rules/object-alt/object-alt.html b/test/integration/rules/object-alt/object-alt.html index 0945e5c45b..5c0bb9ed92 100644 --- a/test/integration/rules/object-alt/object-alt.html +++ b/test/integration/rules/object-alt/object-alt.html @@ -1,18 +1,75 @@ - - -this object has text - - - + + +this object has text + + + - -
    -This object has text. - - - - + + +
    +
    + + This object has text. + + + + + + + - - - + +Fallback content + diff --git a/test/integration/virtual-rules/object-alt.js b/test/integration/virtual-rules/object-alt.js index d641a8e9ac..7aaf0e991f 100644 --- a/test/integration/virtual-rules/object-alt.js +++ b/test/integration/virtual-rules/object-alt.js @@ -1,9 +1,20 @@ -describe('object-alt virtual-rule', function() { - it('should pass for aria-label', function() { +describe('object-alt virtual-rule', function () { + const data = `data:text/html,Object%20content`; + + it('is inapplicable when the object has no data attribute', function () { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: {} + }); + assert.lengthOf(results.inapplicable, 1); + }); + + it('should pass for aria-label', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', attributes: { - 'aria-label': 'foobar' + 'aria-label': 'foobar', + data } }); @@ -12,11 +23,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for aria-labelledby', function() { + it('should incomplete for aria-labelledby', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', attributes: { - 'aria-labelledby': 'foobar' + 'aria-labelledby': 'foobar', + data } }); @@ -25,11 +37,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 1); }); - it('should pass for title', function() { + it('should pass for title', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', attributes: { - title: 'foobar' + title: 'foobar', + data } }); @@ -38,11 +51,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=presentation', function() { + it('should pass for role=presentation', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', attributes: { - role: 'presentation' + role: 'presentation', + data } }); @@ -51,11 +65,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=none', function() { + it('should pass for role=none', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', attributes: { - role: 'none' + role: 'none', + data } }); @@ -64,9 +79,10 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail for visible text content', function() { + it('should fail for visible text content', function () { var node = new axe.SerialVirtualNode({ - nodeName: 'object' + nodeName: 'object', + attributes: { data } }); var child = new axe.SerialVirtualNode({ nodeName: '#text', @@ -82,10 +98,10 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail when alt and children are missing', function() { + it('should fail when alt and children are missing', function () { var results = axe.runVirtualRule('object-alt', { nodeName: 'object', - attributes: {} + attributes: { data } }); assert.lengthOf(results.passes, 0); @@ -93,9 +109,10 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail children contain no visible text', function() { + it('should fail children contain no visible text', function () { var node = new axe.SerialVirtualNode({ - nodeName: 'object' + nodeName: 'object', + attributes: { data } }); node.children = []; @@ -106,11 +123,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail when alt contains only whitespace', function() { + it('should fail when alt contains only whitespace', function () { var node = new axe.SerialVirtualNode({ nodeName: 'object', attributes: { - alt: ' \t \n ' + alt: ' \t \n ', + data } }); node.children = []; @@ -122,11 +140,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail when aria-label is empty', function() { + it('should fail when aria-label is empty', function () { var node = new axe.SerialVirtualNode({ nodeName: 'object', attributes: { - 'aria-label': '' + 'aria-label': '', + data } }); node.children = []; @@ -138,11 +157,12 @@ describe('object-alt virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail when title is empty', function() { + it('should fail when title is empty', function () { var node = new axe.SerialVirtualNode({ nodeName: 'object', attributes: { - title: '' + title: '', + data } }); node.children = []; diff --git a/test/rule-matches/object-is-loaded-matches.js b/test/rule-matches/object-is-loaded-matches.js new file mode 100644 index 0000000000..0beb013739 --- /dev/null +++ b/test/rule-matches/object-is-loaded-matches.js @@ -0,0 +1,56 @@ +describe('object-is-loaded-matches', () => { + let rule, fixture; + const data = `data:text/html,Object%20content`; + + // Give enough time for the browser to load / decide it cannot load objects + async function delayedQueryFixture(html, delay = 50) { + fixture.innerHTML = html; + await new Promise(r => setTimeout(r, delay)); + const tree = axe.setup(); + return axe.utils.querySelectorAll(tree, '#target')[0]; + } + + before(() => { + fixture = document.querySelector('#fixture'); + rule = axe.utils.getRule('object-alt'); + }); + + afterEach(() => { + fixture.innerHTML = ''; + }); + + it(`returns true objects with hidden fallback content`, async () => { + const vNode = await delayedQueryFixture( + ` + Fallback content + ` + ); + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it(`returns false if the object shows any content`, async () => { + const vNode = await delayedQueryFixture( + ` + Fallback content + ` + ); + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it(`returns true if the object shows no content`, async () => { + const vNode = await delayedQueryFixture( + `` + ); + // Ideally, this should be false, don't know it can be done + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it(`returns false if the object has a role that doesn't require a name`, async () => { + const vNode = await delayedQueryFixture( + ` + Fallback content + ` + ); + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); +});