Skip to content

Commit

Permalink
feat(page-no-duplicate-banner/contentinfo): deprecate options.nativeS…
Browse files Browse the repository at this point in the history
…copeFilter, take into ancestors with sectioning roles (#4105)

* fix(page-no-duplicate-banner/contentinfo): take into account elements which have ancestors with sectioning roles

* integration tests
  • Loading branch information
straker committed Jul 26, 2023
1 parent e031d68 commit c6e07be
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 55 deletions.
6 changes: 6 additions & 0 deletions lib/checks/generic/page-no-duplicate-evaluate.js
@@ -1,6 +1,7 @@
import cache from '../../core/base/cache';
import { querySelectorAllFilter } from '../../core/utils';
import { isVisibleToScreenReaders, findUpVirtual } from '../../commons/dom';
import { getRole } from '../../commons/aria';

function pageNoDuplicateEvaluate(node, options, virtualNode) {
if (!options || !options.selector || typeof options.selector !== 'string') {
Expand All @@ -21,6 +22,7 @@ function pageNoDuplicateEvaluate(node, options, virtualNode) {
isVisibleToScreenReaders(elm)
);

// @deprecated options.nativeScopeFilter
// Filter elements that, within certain contexts, don't map their role.
// e.g. a <footer> inside a <main> is not a banner, but in the <body> context it is
if (typeof options.nativeScopeFilter === 'string') {
Expand All @@ -32,6 +34,10 @@ function pageNoDuplicateEvaluate(node, options, virtualNode) {
});
}

if (typeof options.role === 'string') {
elms = elms.filter(elm => getRole(elm) === options.role);
}

this.relatedNodes(
elms.filter(elm => elm !== virtualNode).map(elm => elm.actualNode)
);
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/keyboard/page-no-duplicate-banner.json
Expand Up @@ -4,7 +4,7 @@
"after": "page-no-duplicate-after",
"options": {
"selector": "header:not([role]), [role=banner]",
"nativeScopeFilter": "article, aside, main, nav, section"
"role": "banner"
},
"metadata": {
"impact": "moderate",
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/keyboard/page-no-duplicate-contentinfo.json
Expand Up @@ -4,7 +4,7 @@
"after": "page-no-duplicate-after",
"options": {
"selector": "footer:not([role]), [role=contentinfo]",
"nativeScopeFilter": "article, aside, main, nav, section"
"role": "contentinfo"
},
"metadata": {
"impact": "moderate",
Expand Down
179 changes: 126 additions & 53 deletions test/checks/keyboard/page-no-duplicate.js
@@ -1,29 +1,26 @@
describe('page-no-duplicate', function () {
'use strict';
describe('page-no-duplicate', () => {
const fixture = document.getElementById('fixture');
const checkContext = new axe.testUtils.MockCheckContext();
const checkSetup = axe.testUtils.checkSetup;
const shadowSupported = axe.testUtils.shadowSupport.v1;

var fixture = document.getElementById('fixture');
var checkContext = new axe.testUtils.MockCheckContext();
var checkSetup = axe.testUtils.checkSetup;
var shadowSupported = axe.testUtils.shadowSupport.v1;
const check = checks['page-no-duplicate-main'];

var check = checks['page-no-duplicate-main'];

afterEach(function () {
fixture.innerHTML = '';
afterEach(() => {
checkContext.reset();
});

describe('options.selector', function () {
it('throws if there is no selector', function () {
assert.throws(function () {
var params = checkSetup('<div id="target"></div>', undefined);
describe('options.selector', () => {
it('throws if there is no selector', () => {
assert.throws(() => {
const params = checkSetup('<div id="target"></div>', undefined);
assert.isFalse(check.evaluate.apply(checkContext, params));
});
});

it('should return false if there is more than one element matching the selector', function () {
var options = { selector: 'main' };
var params = checkSetup(
it('should return false if there is more than one element matching the selector', () => {
const options = { selector: 'main' };
const params = checkSetup(
'<div><main id="target"></main><main id="dup"></main></div>',
options
);
Expand All @@ -35,33 +32,33 @@ describe('page-no-duplicate', function () {
);
});

it('should return true if there is only one element matching the selector', function () {
var options = { selector: 'main' };
var params = checkSetup('<div role="main" id="target"></div>', options);
it('should return true if there is only one element matching the selector', () => {
const options = { selector: 'main' };
const params = checkSetup('<div role="main" id="target"></div>', options);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should return true if there are no element matching the selector', function () {
var options = { selector: 'footer' };
var params = checkSetup(
it('should return true if there are no element matching the selector', () => {
const options = { selector: 'footer' };
const params = checkSetup(
'<div><main id="target"></main><main></main></div>',
options
);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should return true if there is more than one element matching the selector but only one is visible', function () {
var options = { selector: 'main' };
var params = checkSetup(
it('should return true if there is more than one element matching the selector but only one is visible', () => {
const options = { selector: 'main' };
const params = checkSetup(
'<div><main id="target"></main><main id="dup" style="display:none;"></main></div>',
options
);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should return true if there is more than one element matching the selector but only one is visible to screenreaders', function () {
var options = { selector: 'main' };
var params = checkSetup(
it('should return true if there is more than one element matching the selector but only one is visible to screenreaders', () => {
const options = { selector: 'main' };
const params = checkSetup(
'<div><main id="target" aria-hidden="true"></main><main id="dup"></main></div>',
options
);
Expand All @@ -70,17 +67,17 @@ describe('page-no-duplicate', function () {

(shadowSupported ? it : xit)(
'should return false if there is a second matching element inside the shadow dom',
function () {
var options = { selector: 'main' };
var div = document.createElement('div');
() => {
const options = { selector: 'main' };
const div = document.createElement('div');
div.innerHTML = '<div id="shadow"></div><main id="target"></main>';

var shadow = div
const shadow = div
.querySelector('#shadow')
.attachShadow({ mode: 'open' });
shadow.innerHTML = '<main></main>';
axe.testUtils.fixtureSetup(div);
var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];
const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];

assert.isFalse(
check.evaluate.call(checkContext, vNode.actualNode, options, vNode)
Expand All @@ -93,18 +90,18 @@ describe('page-no-duplicate', function () {

(shadowSupported ? it : xit)(
'should return true if there is a second matching element inside the shadow dom but only one is visible to screenreaders',
function () {
var options = { selector: 'main' };
var div = document.createElement('div');
() => {
const options = { selector: 'main' };
const div = document.createElement('div');
div.innerHTML =
'<div id="shadow"></div><main id="target" aria-hidden="true"></main>';

var shadow = div
const shadow = div
.querySelector('#shadow')
.attachShadow({ mode: 'open' });
shadow.innerHTML = '<main></main>';
axe.testUtils.fixtureSetup(div);
var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];
const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];

assert.isTrue(
check.evaluate.call(checkContext, vNode.actualNode, options, vNode)
Expand All @@ -116,13 +113,13 @@ describe('page-no-duplicate', function () {
);
});

describe('option.nativeScopeFilter', function () {
it('should ignore element contained in a nativeScopeFilter match', function () {
var options = {
describe('option.nativeScopeFilter', () => {
it('should ignore element contained in a nativeScopeFilter match', () => {
const options = {
selector: 'footer',
nativeScopeFilter: 'main'
};
var params = checkSetup(
const params = checkSetup(
'<div><footer id="target"></footer>' +
'<main><footer></footer></main>' +
'</div>',
Expand All @@ -131,12 +128,12 @@ describe('page-no-duplicate', function () {
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should not ignore element contained in a nativeScopeFilter match with their roles redefined', function () {
var options = {
it('should not ignore element contained in a nativeScopeFilter match with their roles redefined', () => {
const options = {
selector: 'footer, [role="contentinfo"]',
nativeScopeFilter: 'main'
};
var params = checkSetup(
const params = checkSetup(
'<div><footer id="target"></footer>' +
'<main><div role="contentinfo"></div></main>' +
'</div>',
Expand All @@ -145,12 +142,12 @@ describe('page-no-duplicate', function () {
assert.isFalse(check.evaluate.apply(checkContext, params));
});

it('should pass when there are two elements and the first is contained within a nativeSccopeFilter', function () {
var options = {
it('should pass when there are two elements and the first is contained within a nativeSccopeFilter', () => {
const options = {
selector: 'footer, [role="contentinfo"]',
nativeScopeFilter: 'article'
};
var params = checkSetup(
const params = checkSetup(
'<article>' +
'<footer id="target">Article footer</footer>' +
'</article>' +
Expand All @@ -162,19 +159,95 @@ describe('page-no-duplicate', function () {

(shadowSupported ? it : xit)(
'elements if its ancestor is outside the shadow DOM tree',
function () {
var options = {
() => {
const options = {
selector: 'footer',
nativeScopeFilter: 'header'
};

var div = document.createElement('div');
const div = document.createElement('div');
div.innerHTML =
'<header id="shadow"></header><footer id="target"></footer>';
div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML =
'<footer></footer>';
axe.testUtils.fixtureSetup(div);
var vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];
const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];

assert.isTrue(
check.evaluate.call(checkContext, vNode.actualNode, options, vNode)
);
}
);
});

describe('options.role', () => {
it('should pass when element does not match the role', () => {
const options = {
selector: 'footer',
role: 'contentinfo'
};
const params = checkSetup(
`<div>
<footer id="target"></footer>
<div role="main">
<footer></footer>
</div>
</div>`,
options
);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should fail when element matches the role', () => {
const options = {
selector: 'footer',
role: 'contentinfo'
};
const params = checkSetup(
`<div>
<footer id="target"></footer>
<div>
<footer id="fail"></footer>
</div>
</div>`,
options
);
assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, [
fixture.querySelector('#fail')
]);
});

it('should pass when there are two elements and the first does not match the role', () => {
const options = {
selector: 'footer, [role="contentinfo"]',
role: 'contentinfo'
};
const params = checkSetup(
`<article>
<footer id="target">Article footer</footer>
</article>
<footer>Body footer</footer>`,
options
);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

(shadowSupported ? it : xit)(
"should pass if element's ancestor is outside the shadow DOM tree",
() => {
const options = {
selector: 'footer',
role: 'contentinfo'
};

const div = document.createElement('div');
div.innerHTML =
'<article id="shadow"></article><footer id="target"></footer>';
div.querySelector('#shadow').attachShadow({ mode: 'open' }).innerHTML =
'<footer></footer>';
axe.testUtils.fixtureSetup(div);
const vNode = axe.utils.querySelectorAll(axe._tree, '#target')[0];

assert.isTrue(
check.evaluate.call(checkContext, vNode.actualNode, options, vNode)
Expand Down
Expand Up @@ -21,5 +21,20 @@
<section>
<header>Header in section</header>
</section>
<div role="article">
<header>Header in role=article</header>
</div>
<div role="complementary">
<header>Header in role=complementary</header>
</div>
<div role="main">
<header>Header in role=main landmark</header>
</div>
<div role="navigation">
<header>Header in role=navigation</header>
</div>
<div role="region">
<header>Header in role=region</header>
</div>
</body>
</html>
Expand Up @@ -21,5 +21,20 @@
<section>
<footer>Footer in section</footer>
</section>
<div role="article">
<footer>Footer in role=article</footer>
</div>
<div role="complementary">
<footer>Footer in role=complementary</footer>
</div>
<div role="main">
<footer>Footer in role=main landmark</footer>
</div>
<div role="navigation">
<footer>Footer in role=navigation</footer>
</div>
<div role="region">
<footer>Footer in role=region</footer>
</div>
</body>
</html>

0 comments on commit c6e07be

Please sign in to comment.