diff --git a/lib/checks/navigation/region-after.js b/lib/checks/navigation/region-after.js
deleted file mode 100644
index c60b163cf3..0000000000
--- a/lib/checks/navigation/region-after.js
+++ /dev/null
@@ -1 +0,0 @@
-return [results[0]];
diff --git a/lib/checks/navigation/region.js b/lib/checks/navigation/region.js
index 0e886ec29b..2a829f5d7e 100644
--- a/lib/checks/navigation/region.js
+++ b/lib/checks/navigation/region.js
@@ -2,6 +2,11 @@ const { dom, aria } = axe.commons;
const landmarkRoles = aria.getRolesByType('landmark');
const implicitAriaLiveRoles = ['alert', 'log', 'status'];
+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)), [])
@@ -52,11 +57,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 {
@@ -67,7 +79,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);
diff --git a/lib/checks/navigation/region.json b/lib/checks/navigation/region.json
index bd3a69b2cc..98b5612cf8 100644
--- a/lib/checks/navigation/region.json
+++ b/lib/checks/navigation/region.json
@@ -1,7 +1,6 @@
{
"id": "region",
"evaluate": "region.js",
- "after": "region-after.js",
"metadata": {
"impact": "moderate",
"messages": {
diff --git a/lib/rules/region.json b/lib/rules/region.json
index e43f17d59f..dea8ed46e5 100644
--- a/lib/rules/region.json
+++ b/lib/rules/region.json
@@ -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",
diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js
index caf5896f58..6f1b5f7f64 100644
--- a/test/checks/navigation/region.js
+++ b/test/checks/navigation/region.js
@@ -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();
@@ -12,94 +13,81 @@ 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(
- '
'
+ ''
);
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
- assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return false when img content is outside the region', function() {
var checkArgs = checkSetup(
- 'Introduction
'
+ 'Introduction
'
);
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(
- ''
+ 'Introduction
'
);
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(
- ''
+ ''
);
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(
- ''
+ 'Click Here
Introduction
'
);
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(
- ''
+ 'Click HereIntroduction
'
);
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(
- ''
+ 'Click HereIntroduction
'
);
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(
- 'This is random content.
Introduction
'
+ 'This is random content.
Introduction
'
);
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(
- ''
+ 'Click HereIntroduction
'
);
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(
- ''
+ 'Content
'
);
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
@@ -107,57 +95,66 @@ describe('region', function() {
it('should considered aria labelled elements as content', function() {
var checkArgs = checkSetup(
- ''
+ 'Content
'
);
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(
+ 'Content '
+ );
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ });
+
+ it('should allow native main elements', function() {
+ var checkArgs = checkSetup(
+ 'Content '
+ );
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ });
+
+ it('should allow native aside elements', function() {
+ var checkArgs = checkSetup(
+ 'Content '
+ );
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ });
+
+ it('should allow native footer elements', function() {
var checkArgs = checkSetup(
- ''
+ 'Content '
);
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(
- 'Content
'
+ 'ContentContent
'
);
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(
- ''
+ 'Text
'
);
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(
- ''
+ 'Content
'
);
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 with aria label as landmarks', function() {
@@ -176,22 +173,18 @@ describe('region', function() {
it('treats forms without aria label as not a landmarks', function() {
var checkArgs = checkSetup(
- ''
+ 'Content
'
);
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(
- ''
+ 'Content
'
);
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() {
@@ -204,12 +197,10 @@ describe('region', function() {
it('treats forms with empty titles not as landmarks', function() {
var checkArgs = checkSetup(
- ''
+ 'Content
'
);
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() {
@@ -236,7 +227,7 @@ describe('region', function() {
it('does not allow content in aria-live=off', function() {
var checkArgs = checkSetup(
- ''
+ 'Content
'
);
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
});
@@ -284,14 +275,30 @@ describe('region', function() {
assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
});
+ it('returns the outermost element as the error', function() {
+ var checkArgs = checkSetup(
+ 'Introduction
'
+ );
+
+ 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() {
@@ -302,21 +309,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 = 'skiplinkContent
';
+ div.innerHTML =
+ 'skiplinkContent
';
var shadow = div.querySelector('div').attachShadow({ mode: 'open' });
shadow.innerHTML = '
';
- 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
+ )
+ );
}
);
diff --git a/test/integration/full/region/region-fail.js b/test/integration/full/region/region-fail.js
index 20f2c4723d..21e6871159 100644
--- a/test/integration/full/region/region-fail.js
+++ b/test/integration/full/region/region-fail.js
@@ -17,22 +17,8 @@ describe('region fail test', function() {
assert.lengthOf(results.violations[0].nodes, 1);
});
- it('should find html', function() {
- assert.deepEqual(results.violations[0].nodes[0].target, ['html']);
- });
-
- it('should have all text content as related nodes', function() {
- var wrapper = document.querySelector('#wrapper');
- assert.equal(
- results.violations[0].nodes[0].any[0].relatedNodes.length,
- wrapper.querySelectorAll('h1, li, p, a').length
- );
- });
- });
-
- describe('passes', function() {
- it('should find none', function() {
- assert.lengthOf(results.passes, 0);
+ it('should find wrapper', function() {
+ assert.deepEqual(results.violations[0].nodes[0].target, ['#wrapper']);
});
});
});
diff --git a/test/integration/full/region/region-pass.js b/test/integration/full/region/region-pass.js
index b1a1325fa3..a6406bb1f5 100644
--- a/test/integration/full/region/region-pass.js
+++ b/test/integration/full/region/region-pass.js
@@ -13,12 +13,10 @@ describe('region pass test', function() {
});
describe('passes', function() {
- it('should find one', function() {
- assert.lengthOf(results.passes[0].nodes, 1);
- });
-
- it('should find html', function() {
- assert.deepEqual(results.passes[0].nodes[0].target, ['html']);
+ it('should pass nodes', function() {
+ // it seems CircleCI and localhost have different number of DOM nodes,
+ // so as long as everything passes and nothing fails, the rule is working
+ assert.isTrue(results.passes[0].nodes.length > 0);
});
});