Skip to content

Commit 54a8116

Browse files
authored
fix(label): avoid passing labels because of an input[value] (#3688)
* fix(label): avoid passing labels because of an input[value] * correct label examples * Add textarea integration test * Fix failing test * Use sanitize * Add addition test for explicit-evaluate
1 parent 95cf6e7 commit 54a8116

File tree

9 files changed

+312
-223
lines changed

9 files changed

+312
-223
lines changed

lib/checks/label/explicit-evaluate.js

+32-23
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
11
import { getRootNode, isVisibleOnScreen } from '../../commons/dom';
2-
import { accessibleText } from '../../commons/text';
2+
import { accessibleText, sanitize } from '../../commons/text';
33
import { escapeSelector } from '../../core/utils';
44

55
function explicitEvaluate(node, options, virtualNode) {
6-
if (virtualNode.attr('id')) {
7-
if (!virtualNode.actualNode) {
8-
return undefined;
9-
}
6+
if (!virtualNode.attr('id')) {
7+
return false;
8+
}
9+
if (!virtualNode.actualNode) {
10+
return undefined;
11+
}
1012

11-
const root = getRootNode(virtualNode.actualNode);
12-
const id = escapeSelector(virtualNode.attr('id'));
13-
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`));
13+
const root = getRootNode(virtualNode.actualNode);
14+
const id = escapeSelector(virtualNode.attr('id'));
15+
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`));
16+
this.relatedNodes(labels);
1417

15-
if (labels.length) {
16-
try {
17-
return labels.some(label => {
18-
// defer to hidden-explicit-label check for better messaging
19-
if (!isVisibleOnScreen(label)) {
20-
return true;
21-
} else {
22-
return !!accessibleText(label);
23-
}
24-
});
25-
} catch (e) {
26-
return undefined;
27-
}
28-
}
18+
if (!labels.length) {
19+
return false;
2920
}
3021

31-
return false;
22+
try {
23+
return labels.some(label => {
24+
// defer to hidden-explicit-label check for better messaging
25+
if (!isVisibleOnScreen(label)) {
26+
return true;
27+
} else {
28+
const explicitLabel = sanitize(
29+
accessibleText(label, {
30+
inControlContext: true,
31+
startNode: virtualNode
32+
})
33+
);
34+
this.data({ explicitLabel });
35+
return !!explicitLabel;
36+
}
37+
});
38+
} catch (e) {
39+
return undefined;
40+
}
3241
}
3342

3443
export default explicitEvaluate;

lib/checks/label/implicit-evaluate.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { closest } from '../../core/utils';
2-
import { accessibleTextVirtual } from '../../commons/text';
2+
import { accessibleTextVirtual, sanitize } from '../../commons/text';
33

44
function implicitEvaluate(node, options, virtualNode) {
55
try {
66
const label = closest(virtualNode, 'label');
77
if (label) {
8-
return !!accessibleTextVirtual(label, { inControlContext: true });
8+
const implicitLabel = sanitize(
9+
accessibleTextVirtual(label, {
10+
inControlContext: true,
11+
startNode: virtualNode
12+
})
13+
);
14+
if (label.actualNode) {
15+
this.relatedNodes([label.actualNode]);
16+
}
17+
this.data({ implicitLabel });
18+
return !!implicitLabel;
919
}
1020
return false;
1121
} catch (e) {

test/checks/label/explicit.js

+116-85
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,167 @@
1-
describe('explicit-label', function() {
2-
'use strict';
3-
4-
var fixture = document.getElementById('fixture');
5-
var fixtureSetup = axe.testUtils.fixtureSetup;
6-
var queryFixture = axe.testUtils.queryFixture;
7-
var shadowSupport = axe.testUtils.shadowSupport;
8-
9-
afterEach(function() {
10-
fixture.innerHTML = '';
1+
describe('explicit-label', () => {
2+
const fixtureSetup = axe.testUtils.fixtureSetup;
3+
const checkSetup = axe.testUtils.checkSetup;
4+
const checkEvaluate = axe.testUtils.getCheckEvaluate('explicit-label');
5+
const checkContext = axe.testUtils.MockCheckContext();
6+
7+
afterEach(() => {
8+
checkContext.reset();
119
});
1210

13-
it('should return false if an empty label is present', function() {
14-
var vNode = queryFixture(
11+
it('returns false if an empty label is present', () => {
12+
const params = checkSetup(
1513
'<label for="target"></label><input type="text" id="target">'
1614
);
17-
assert.isFalse(
18-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
15+
assert.isFalse(checkEvaluate.apply(checkContext, params));
16+
});
17+
18+
it('returns false if the label is empty except for the target value', () => {
19+
const params = checkSetup(
20+
'<label for="target"> <input type="text" id="target" value="snacks"> </label>'
1921
);
22+
assert.isFalse(checkEvaluate.apply(checkContext, params));
2023
});
2124

22-
it('should return true if a non-empty label is present', function() {
23-
var vNode = queryFixture(
24-
'<label for="target">Text</label><input type="text" id="target">'
25+
it('returns false if an empty label is present that uses aria-labelledby', () => {
26+
const params = checkSetup(
27+
'<input type="text" id="target">' +
28+
'<label for="target" aria-labelledby="lbl"></label>' +
29+
'<span id="lbl">aria label</span>'
2530
);
26-
assert.isTrue(
27-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
31+
assert.isFalse(checkEvaluate.apply(checkContext, params));
32+
});
33+
34+
it('returns true if a non-empty label is present', () => {
35+
const params = checkSetup(
36+
'<label for="target">Text</label><input type="text" id="target">'
2837
);
38+
assert.isTrue(checkEvaluate.apply(checkContext, params));
2939
});
3040

31-
it('should return true if an invisible non-empty label is present, to defer to hidden-explicit-label', function() {
32-
var vNode = queryFixture(
41+
it('returns true if an invisible non-empty label is present, to defer to hidden-explicit-label', () => {
42+
const params = checkSetup(
3343
'<label for="target" style="display: none;">Text</label><input type="text" id="target">'
3444
);
35-
assert.isTrue(
36-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
37-
);
45+
assert.isTrue(checkEvaluate.apply(checkContext, params));
3846
});
3947

40-
it('should return false if a label is not present', function() {
41-
var vNode = queryFixture('<input type="text" id="target" />');
42-
assert.isFalse(
43-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
44-
);
48+
it('returns false if a label is not present', () => {
49+
const params = checkSetup('<input type="text" id="target" />');
50+
assert.isFalse(checkEvaluate.apply(checkContext, params));
4551
});
4652

47-
it('should work for multiple labels', function() {
48-
var vNode = queryFixture(
53+
it('should work for multiple labels', () => {
54+
const params = checkSetup(
4955
'<label for="target"></label><label for="target">Text</label><input type="text" id="target">'
5056
);
51-
assert.isTrue(
52-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
53-
);
57+
assert.isTrue(checkEvaluate.apply(checkContext, params));
5458
});
5559

56-
(shadowSupport.v1 ? it : xit)(
57-
'should return true if input and label are in the same shadow root',
58-
function() {
59-
var root = document.createElement('div');
60-
var shadow = root.attachShadow({ mode: 'open' });
60+
describe('.data', () => {
61+
it('is null if there is no label', () => {
62+
const params = checkSetup('<input type="text" id="target" />');
63+
checkEvaluate.apply(checkContext, params);
64+
assert.isNull(checkContext._data);
65+
});
66+
67+
it('includes the `explicitLabel` text of the first non-empty label', () => {
68+
const params = checkSetup(
69+
'<label for="target"> </label>' +
70+
'<label for="target"> text </label>' +
71+
'<label for="target"> more text </label>' +
72+
'<input type="text" id="target" />'
73+
);
74+
checkEvaluate.apply(checkContext, params);
75+
assert.deepEqual(checkContext._data, { explicitLabel: 'text' });
76+
});
77+
78+
it('is empty { explicitLabel: "" } if the label is empty', () => {
79+
const params = checkSetup(
80+
'<label for="target"> </label>' +
81+
'<label for="target"></label>' +
82+
'<input type="text" id="target" />'
83+
);
84+
checkEvaluate.apply(checkContext, params);
85+
assert.deepEqual(checkContext._data, { explicitLabel: '' });
86+
});
87+
});
88+
89+
describe('related nodes', () => {
90+
it('is empty when there are no labels', () => {
91+
const params = checkSetup('<input type="text" id="target" />');
92+
checkEvaluate.apply(checkContext, params);
93+
assert.isEmpty(checkContext._relatedNodes);
94+
});
95+
96+
it('includes each associated label', () => {
97+
const params = checkSetup(
98+
'<label for="target" id="lbl1"></label>' +
99+
'<label for="target" id="lbl2"></label>' +
100+
'<input type="text" id="target" />'
101+
);
102+
checkEvaluate.apply(checkContext, params);
103+
const ids = checkContext._relatedNodes.map(node => '#' + node.id);
104+
assert.deepEqual(ids, ['#lbl1', '#lbl2']);
105+
});
106+
});
107+
108+
describe('with shadow DOM', () => {
109+
it('returns true if input and label are in the same shadow root', () => {
110+
const root = document.createElement('div');
111+
const shadow = root.attachShadow({ mode: 'open' });
61112
shadow.innerHTML =
62113
'<label for="target">American band</label><input id="target">';
63114
fixtureSetup(root);
64115

65-
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
66-
assert.isTrue(
67-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
68-
);
69-
}
70-
);
116+
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
117+
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode));
118+
});
71119

72-
(shadowSupport.v1 ? it : xit)(
73-
'should return true if label content is slotted',
74-
function() {
75-
var root = document.createElement('div');
120+
it('returns true if label content is slotted', () => {
121+
const root = document.createElement('div');
76122
root.innerHTML = 'American band';
77-
var shadow = root.attachShadow({ mode: 'open' });
123+
const shadow = root.attachShadow({ mode: 'open' });
78124
shadow.innerHTML =
79125
'<label for="target"><slot></slot></label><input id="target">';
80126
fixtureSetup(root);
81127

82-
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
83-
assert.isTrue(
84-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
85-
);
86-
}
87-
);
128+
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
129+
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode));
130+
});
88131

89-
(shadowSupport.v1 ? it : xit)(
90-
'should return false if input is inside shadow DOM and the label is not',
91-
function() {
92-
var root = document.createElement('div');
132+
it('returns false if input is inside shadow DOM and the label is not', () => {
133+
const root = document.createElement('div');
93134
root.innerHTML = '<label for="target">American band</label>';
94-
var shadow = root.attachShadow({ mode: 'open' });
135+
const shadow = root.attachShadow({ mode: 'open' });
95136
shadow.innerHTML = '<slot></slot><input id="target">';
96137
fixtureSetup(root);
97138

98-
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
99-
assert.isFalse(
100-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
101-
);
102-
}
103-
);
139+
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
140+
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode));
141+
});
104142

105-
(shadowSupport.v1 ? it : xit)(
106-
'should return false if label is inside shadow DOM and the input is not',
107-
function() {
108-
var root = document.createElement('div');
143+
it('returns false if label is inside shadow DOM and the input is not', () => {
144+
const root = document.createElement('div');
109145
root.innerHTML = '<input id="target">';
110-
var shadow = root.attachShadow({ mode: 'open' });
146+
const shadow = root.attachShadow({ mode: 'open' });
111147
shadow.innerHTML =
112148
'<label for="target">American band</label><slot></slot>';
113149
fixtureSetup(root);
114150

115-
var vNode = axe.utils.getNodeFromTree(root.querySelector('#target'));
116-
assert.isFalse(
117-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
118-
);
119-
}
120-
);
151+
const vNode = axe.utils.getNodeFromTree(root.querySelector('#target'));
152+
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode));
153+
});
154+
});
121155

122-
describe('SerialVirtualNode', function() {
123-
it('should return undefined', function() {
124-
var virtualNode = new axe.SerialVirtualNode({
156+
describe('SerialVirtualNode', () => {
157+
it('returns undefined', () => {
158+
const virtualNode = new axe.SerialVirtualNode({
125159
nodeName: 'input',
126160
attributes: {
127161
type: 'text'
128162
}
129163
});
130-
131-
assert.isFalse(
132-
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, virtualNode)
133-
);
164+
assert.isFalse(checkEvaluate.call(checkContext, null, {}, virtualNode));
134165
});
135166
});
136167
});

0 commit comments

Comments
 (0)