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

feat(page-no-duplicate-banner/contentinfo): deprecate options.nativeScopeFilter, take into ancestors with sectioning roles #4105

Merged
merged 2 commits into from
Jul 26, 2023
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
6 changes: 6 additions & 0 deletions lib/checks/generic/page-no-duplicate-evaluate.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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>
Original file line number Diff line number Diff line change
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>