Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(region): return outermost regionless node instead of html #1980

Merged
merged 9 commits into from
Jan 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion lib/checks/navigation/region-after.js

This file was deleted.

37 changes: 33 additions & 4 deletions lib/checks/navigation/region.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const { dom, aria } = axe.commons;
const landmarkRoles = aria.getRolesByType('landmark');

let regionlessNodes = axe._cache.get('regionlessNodes');
if (regionlessNodes) {
return !regionlessNodes.includes(virtualNode);
}

// Create a list of nodeNames that have a landmark as an implicit role
const implicitLandmarks = landmarkRoles
.reduce((arr, role) => arr.concat(aria.implicitNodes(role)), [])
Expand Down Expand Up @@ -47,11 +52,18 @@ function findRegionlessElms(virtualNode) {
dom.getElementByReference(virtualNode.actualNode, 'href')) ||
!dom.isVisible(node, true)
) {
// Mark each parent node as having region descendant
let vNode = virtualNode;
while (vNode) {
vNode._hasRegionDescendant = true;
vNode = vNode.parent;
}

return [];

// Return the node is a content element
} else if (dom.hasContent(node, /* noRecursion: */ true)) {
return [node];
return [virtualNode];

// Recursively look at all child elements
} else {
Expand All @@ -62,7 +74,24 @@ function findRegionlessElms(virtualNode) {
}
}

var regionlessNodes = findRegionlessElms(virtualNode);
this.relatedNodes(regionlessNodes);
regionlessNodes = findRegionlessElms(axe._tree[0])
// Find first parent marked as having region descendant (or body) and
// return the node right before it as the "outer" element
.map(vNode => {
while (
vNode.parent &&
!vNode.parent._hasRegionDescendant &&
vNode.parent.actualNode !== document.body
) {
vNode = vNode.parent;
}

return vNode;
})
// Remove duplicate containers
.filter((vNode, index, array) => {
return array.indexOf(vNode) === index;
});
axe._cache.set('regionlessNodes', regionlessNodes);

return regionlessNodes.length === 0;
return !regionlessNodes.includes(virtualNode);
1 change: 0 additions & 1 deletion lib/checks/navigation/region.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"id": "region",
"evaluate": "region.js",
"after": "region-after.js",
"metadata": {
"impact": "moderate",
"messages": {
Expand Down
3 changes: 1 addition & 2 deletions lib/rules/region.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"id": "region",
"selector": "html",
"pageLevel": true,
"selector": "body *",
"tags": ["cat.keyboard", "best-practice"],
"metadata": {
"description": "Ensures all page content is contained by landmarks",
Expand Down
144 changes: 79 additions & 65 deletions test/checks/navigation/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ describe('region', function() {
var fixture = document.getElementById('fixture');
var shadowSupport = axe.testUtils.shadowSupport;
var checkSetup = axe.testUtils.checkSetup;
var fixtureSetup = axe.testUtils.fixtureSetup;

var checkContext = new axe.testUtils.MockCheckContext();

Expand All @@ -12,152 +13,148 @@ describe('region', function() {
checkContext.reset();
});

it('should return true when all content is inside the region', function() {
it('should return true when content is inside the region', function() {
var checkArgs = checkSetup(
'<div id="target"><div role="main"><a href="a.html#mainheader">Click Here</a><div><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'
'<div role="main"><a id="target" href="a.html#mainheader">Click Here</a><div><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
straker marked this conversation as resolved.
Show resolved Hide resolved
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return false when img content is outside the region', function() {
var checkArgs = checkSetup(
'<div id="target"><img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
'<img id="target" src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});

it('should return true when textless text content is outside the region', function() {
var checkArgs = checkSetup(
'<div id="target"><p></p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
'<p id="target"></p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return true when wrapper content is outside the region', function() {
var checkArgs = checkSetup(
'<div id="target"><div><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div></div>'
'<div id="target"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return true when invisible content is outside the region', function() {
var checkArgs = checkSetup(
'<div id="target"><p style="display: none">Click Here</p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
'<p id="target" style="display: none">Click Here</p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return true when there is a skiplink', function() {
var checkArgs = checkSetup(
'<div id="target"><a href="#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
'<a id="target" href="#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return true when there is an Angular skiplink', function() {
var checkArgs = checkSetup(
'<div id="target"><a href="/#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div></div>'
'<a id="target" href="/#mainheader">Click Here</a><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});

it('should return false when there is a non-region element', function() {
var checkArgs = checkSetup(
'<div id="target"><div>This is random content.</div><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'
'<div id="target">This is random content.</div><div role="main"><h1 id="mainheader">Introduction</h1></div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});

it('should return the first item when after is called', function() {
assert.equal(checks.region.after([2, 3, 1]), 2);
});

it('should return false when there is a non-skiplink', function() {
var checkArgs = checkSetup(
'<div id="target"><a href="something.html#mainheader">Click Here</a><div role="main"><h1 id="mainheader">Introduction</h1></div></div>'
'<a id="target" href="something.html#mainheader">Click Here</a><div role="main"><h1 id="mainheader">Introduction</h1></div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});

it('should return true if the non-region element is a script', function() {
var checkArgs = checkSetup(
'<div id="target"><script>axe.run()</script><div role="main">Content</div></div>'
'<script id="target">axe.run()</script><div role="main">Content</div>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});

it('should considered aria labelled elements as content', function() {
var checkArgs = checkSetup(
'<div id="target"><div aria-label="axe-core logo" role="img"></div><div role="main">Content</div></div>'
'<div id="target" aria-label="axe-core logo" role="img"></div><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.deepEqual(checkContext._relatedNodes, [
fixture.querySelector('div[aria-label]')
]);
});

it('should allow native landmark elements', function() {
it('should allow native header elements', function() {
var checkArgs = checkSetup(
'<header id="target">branding</header><main>Content </main><aside>stuff</aside><footer>copyright</footer>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});

it('should allow native main elements', function() {
var checkArgs = checkSetup(
'<header>branding</header><main id="target">Content </main><aside>stuff</aside><footer>copyright</footer>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});

it('should allow native aside elements', function() {
var checkArgs = checkSetup(
'<header>branding</header><main>Content </main><aside id="target">stuff</aside><footer>copyright</footer>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});

it('should allow native footer elements', function() {
var checkArgs = checkSetup(
'<div id="target"><header>branding</header><main>Content </main><aside>stuff</aside><footer>copyright</footer></div>'
'<header>branding</header><main>Content </main><aside>stuff</aside><footer id="target">copyright</footer>'
);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 0);
});

it('ignores native landmark elements with an overwriting role', function() {
var checkArgs = checkSetup(
'<div id="target"><header role="heading" level="1"></header><main role="none">Content</main></div>'
'<main id="target" role="none">Content</main><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
assert.deepEqual(checkContext._relatedNodes, [
fixture.querySelector('main')
]);
});

it('returns false for content outside of form tags with accessible names', function() {
var checkArgs = checkSetup(
'<div id="target"><p>Text</p><form aria-label="form"></form></div>'
'<p id="target">Text</p><form aria-label="form"></form>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('p')]);
});

it('ignores unlabeled forms as they are not landmarks', function() {
var checkArgs = checkSetup(
'<div id="target"><p>Text</p><form><fieldset>foo</fieldset></form></div>'
'<form id="target"><fieldset>foo</fieldset></form><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 2);
assert.deepEqual(checkContext._relatedNodes, [
fixture.querySelector('p'),
fixture.querySelector('fieldset')
]);
});

it('treats <forms> with aria label as landmarks', function() {
Expand All @@ -176,22 +173,18 @@ describe('region', function() {

it('treats forms without aria label as not a landmarks', function() {
var checkArgs = checkSetup(
'<form id="target"><p>This is random content.</p></form>'
'<form id="target"><p>This is random content.</p></form><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('p')]);
});

it('treats forms with an empty aria label as not a landmarks', function() {
var checkArgs = checkSetup(
'<form id="target" aria-label=" "><p>This is random content.</p></form>'
'<form id="target" aria-label=" "><p>This is random content.</p></form><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('p')]);
});

it('treats forms with non empty titles as landmarks', function() {
Expand All @@ -204,12 +197,10 @@ describe('region', function() {

it('treats forms with empty titles not as landmarks', function() {
var checkArgs = checkSetup(
'<form id="target" title=""><p>This is random content.</p></form>'
'<form id="target" title=""><p>This is random content.</p></form><div role="main">Content</div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('p')]);
});

it('treats ARIA forms with no label or title as landmarks', function() {
Expand All @@ -236,7 +227,7 @@ describe('region', function() {

it('does not allow content in aria-live=off', function() {
var checkArgs = checkSetup(
'<div aria-live="off" id="target"><p>This is random content.</p></div>'
'<div aria-live="off" id="target"><p>This is random content.</p></div><div role="main">Content</div>'
);
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
});
Expand All @@ -249,14 +240,30 @@ describe('region', function() {
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});

it('returns the outermost element as the error', function() {
var checkArgs = checkSetup(
'<div id="target"><p>This is random content.</p></div><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
});

(shadowSupport.v1 ? it : xit)('should test Shadow tree content', function() {
var div = document.createElement('div');
var shadow = div.attachShadow({ mode: 'open' });
shadow.innerHTML = 'Some text';
var checkArgs = checkSetup(div);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.deepEqual(checkContext._relatedNodes, [div]);
fixtureSetup(div);
var virutalNode = axe._tree[0];

// fixture is the outermost element
assert.isFalse(
checks.region.evaluate.call(
checkContext,
virutalNode.actualNode,
null,
virutalNode
)
);
});

(shadowSupport.v1 ? it : xit)('should test slotted content', function() {
Expand All @@ -267,21 +274,28 @@ describe('region', function() {
var checkArgs = checkSetup(div);

assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 0);
});

(shadowSupport.v1 ? it : xit)(
'should ignore skiplink targets inside shadow trees',
function() {
var div = document.createElement('div');
div.innerHTML = '<a href="#foo">skiplink</a><div>Content</div>';
div.innerHTML =
'<a id="target" href="#foo">skiplink</a><div>Content</div>';

var shadow = div.querySelector('div').attachShadow({ mode: 'open' });
shadow.innerHTML = '<div role="main" id=#foo"><slot></slot></div>';
var checkArgs = checkSetup(div);

assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.deepEqual(checkContext._relatedNodes, [div.querySelector('a')]);
fixtureSetup(div);
var virutalNode = axe.utils.getNodeFromTree(div.querySelector('#target'));

assert.isFalse(
checks.region.evaluate.call(
checkContext,
virutalNode.actualNode,
null,
virutalNode
)
);
}
);

Expand Down
Loading