Skip to content

Commit

Permalink
fix(label): avoid passing labels because of an input[value] (#3688)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
WilcoFiers committed Oct 4, 2022
1 parent 95cf6e7 commit 54a8116
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 223 deletions.
55 changes: 32 additions & 23 deletions lib/checks/label/explicit-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { getRootNode, isVisibleOnScreen } from '../../commons/dom';
import { accessibleText } from '../../commons/text';
import { accessibleText, sanitize } from '../../commons/text';
import { escapeSelector } from '../../core/utils';

function explicitEvaluate(node, options, virtualNode) {
if (virtualNode.attr('id')) {
if (!virtualNode.actualNode) {
return undefined;
}
if (!virtualNode.attr('id')) {
return false;
}
if (!virtualNode.actualNode) {
return undefined;
}

const root = getRootNode(virtualNode.actualNode);
const id = escapeSelector(virtualNode.attr('id'));
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`));
const root = getRootNode(virtualNode.actualNode);
const id = escapeSelector(virtualNode.attr('id'));
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`));
this.relatedNodes(labels);

if (labels.length) {
try {
return labels.some(label => {
// defer to hidden-explicit-label check for better messaging
if (!isVisibleOnScreen(label)) {
return true;
} else {
return !!accessibleText(label);
}
});
} catch (e) {
return undefined;
}
}
if (!labels.length) {
return false;
}

return false;
try {
return labels.some(label => {
// defer to hidden-explicit-label check for better messaging
if (!isVisibleOnScreen(label)) {
return true;
} else {
const explicitLabel = sanitize(
accessibleText(label, {
inControlContext: true,
startNode: virtualNode
})
);
this.data({ explicitLabel });
return !!explicitLabel;
}
});
} catch (e) {
return undefined;
}
}

export default explicitEvaluate;
14 changes: 12 additions & 2 deletions lib/checks/label/implicit-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { closest } from '../../core/utils';
import { accessibleTextVirtual } from '../../commons/text';
import { accessibleTextVirtual, sanitize } from '../../commons/text';

function implicitEvaluate(node, options, virtualNode) {
try {
const label = closest(virtualNode, 'label');
if (label) {
return !!accessibleTextVirtual(label, { inControlContext: true });
const implicitLabel = sanitize(
accessibleTextVirtual(label, {
inControlContext: true,
startNode: virtualNode
})
);
if (label.actualNode) {
this.relatedNodes([label.actualNode]);
}
this.data({ implicitLabel });
return !!implicitLabel;
}
return false;
} catch (e) {
Expand Down
201 changes: 116 additions & 85 deletions test/checks/label/explicit.js
Original file line number Diff line number Diff line change
@@ -1,136 +1,167 @@
describe('explicit-label', function() {
'use strict';

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;
var queryFixture = axe.testUtils.queryFixture;
var shadowSupport = axe.testUtils.shadowSupport;

afterEach(function() {
fixture.innerHTML = '';
describe('explicit-label', () => {
const fixtureSetup = axe.testUtils.fixtureSetup;
const checkSetup = axe.testUtils.checkSetup;
const checkEvaluate = axe.testUtils.getCheckEvaluate('explicit-label');
const checkContext = axe.testUtils.MockCheckContext();

afterEach(() => {
checkContext.reset();
});

it('should return false if an empty label is present', function() {
var vNode = queryFixture(
it('returns false if an empty label is present', () => {
const params = checkSetup(
'<label for="target"></label><input type="text" id="target">'
);
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('returns false if the label is empty except for the target value', () => {
const params = checkSetup(
'<label for="target"> <input type="text" id="target" value="snacks"> </label>'
);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should return true if a non-empty label is present', function() {
var vNode = queryFixture(
'<label for="target">Text</label><input type="text" id="target">'
it('returns false if an empty label is present that uses aria-labelledby', () => {
const params = checkSetup(
'<input type="text" id="target">' +
'<label for="target" aria-labelledby="lbl"></label>' +
'<span id="lbl">aria label</span>'
);
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('returns true if a non-empty label is present', () => {
const params = checkSetup(
'<label for="target">Text</label><input type="text" id="target">'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

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

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

it('should work for multiple labels', function() {
var vNode = queryFixture(
it('should work for multiple labels', () => {
const params = checkSetup(
'<label for="target"></label><label for="target">Text</label><input type="text" id="target">'
);
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

(shadowSupport.v1 ? it : xit)(
'should return true if input and label are in the same shadow root',
function() {
var root = document.createElement('div');
var shadow = root.attachShadow({ mode: 'open' });
describe('.data', () => {
it('is null if there is no label', () => {
const params = checkSetup('<input type="text" id="target" />');
checkEvaluate.apply(checkContext, params);
assert.isNull(checkContext._data);
});

it('includes the `explicitLabel` text of the first non-empty label', () => {
const params = checkSetup(
'<label for="target"> </label>' +
'<label for="target"> text </label>' +
'<label for="target"> more text </label>' +
'<input type="text" id="target" />'
);
checkEvaluate.apply(checkContext, params);
assert.deepEqual(checkContext._data, { explicitLabel: 'text' });
});

it('is empty { explicitLabel: "" } if the label is empty', () => {
const params = checkSetup(
'<label for="target"> </label>' +
'<label for="target"></label>' +
'<input type="text" id="target" />'
);
checkEvaluate.apply(checkContext, params);
assert.deepEqual(checkContext._data, { explicitLabel: '' });
});
});

describe('related nodes', () => {
it('is empty when there are no labels', () => {
const params = checkSetup('<input type="text" id="target" />');
checkEvaluate.apply(checkContext, params);
assert.isEmpty(checkContext._relatedNodes);
});

it('includes each associated label', () => {
const params = checkSetup(
'<label for="target" id="lbl1"></label>' +
'<label for="target" id="lbl2"></label>' +
'<input type="text" id="target" />'
);
checkEvaluate.apply(checkContext, params);
const ids = checkContext._relatedNodes.map(node => '#' + node.id);
assert.deepEqual(ids, ['#lbl1', '#lbl2']);
});
});

describe('with shadow DOM', () => {
it('returns true if input and label are in the same shadow root', () => {
const root = document.createElement('div');
const shadow = root.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<label for="target">American band</label><input id="target">';
fixtureSetup(root);

var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode));
});

(shadowSupport.v1 ? it : xit)(
'should return true if label content is slotted',
function() {
var root = document.createElement('div');
it('returns true if label content is slotted', () => {
const root = document.createElement('div');
root.innerHTML = 'American band';
var shadow = root.attachShadow({ mode: 'open' });
const shadow = root.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<label for="target"><slot></slot></label><input id="target">';
fixtureSetup(root);

var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode));
});

(shadowSupport.v1 ? it : xit)(
'should return false if input is inside shadow DOM and the label is not',
function() {
var root = document.createElement('div');
it('returns false if input is inside shadow DOM and the label is not', () => {
const root = document.createElement('div');
root.innerHTML = '<label for="target">American band</label>';
var shadow = root.attachShadow({ mode: 'open' });
const shadow = root.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot><input id="target">';
fixtureSetup(root);

var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode));
});

(shadowSupport.v1 ? it : xit)(
'should return false if label is inside shadow DOM and the input is not',
function() {
var root = document.createElement('div');
it('returns false if label is inside shadow DOM and the input is not', () => {
const root = document.createElement('div');
root.innerHTML = '<input id="target">';
var shadow = root.attachShadow({ mode: 'open' });
const shadow = root.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<label for="target">American band</label><slot></slot>';
fixtureSetup(root);

var vNode = axe.utils.getNodeFromTree(root.querySelector('#target'));
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);
const vNode = axe.utils.getNodeFromTree(root.querySelector('#target'));
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode));
});
});

describe('SerialVirtualNode', function() {
it('should return undefined', function() {
var virtualNode = new axe.SerialVirtualNode({
describe('SerialVirtualNode', () => {
it('returns undefined', () => {
const virtualNode = new axe.SerialVirtualNode({
nodeName: 'input',
attributes: {
type: 'text'
}
});

assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, virtualNode)
);
assert.isFalse(checkEvaluate.call(checkContext, null, {}, virtualNode));
});
});
});
Loading

0 comments on commit 54a8116

Please sign in to comment.