Skip to content

Commit b32d4fe

Browse files
authored
feat(autocomplete-matches): use virtualNode only lookups (#1604)
* feat(autocomplete-matches): use virtualNode only lookups * change name to refer to element, make getter * change api names
1 parent 0088e94 commit b32d4fe

File tree

6 files changed

+95
-104
lines changed

6 files changed

+95
-104
lines changed

lib/checks/forms/autocomplete-appropriate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Select and textarea is always allowed
2-
if (virtualNode.elementNodeName !== 'input') {
2+
if (virtualNode.props.nodeName !== 'input') {
33
return true;
44
}
55

lib/core/base/virtual-node.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,24 @@ class VirtualNode {
1717
this._isHidden = null; // will be populated by axe.utils.isHidden
1818
this._cache = {};
1919

20-
// abstract Node and Element APIs so we can run axe in DOM-less
21-
// environments. these are static properties in the assumption
22-
// that axe does not change any of them while it runs.
23-
this.elementNodeType = node.nodeType;
24-
this.elementNodeName = node.nodeName.toLowerCase();
25-
this.elementId = node.id;
26-
2720
if (axe._cache.get('nodeMap')) {
2821
axe._cache.get('nodeMap').set(node, this);
2922
}
3023
}
3124

25+
// abstract Node properties so we can run axe in DOM-less environments.
26+
// add to the prototype so memory is shared across all virtual nodes
27+
get props() {
28+
const { nodeType, nodeName, id, type } = this.actualNode;
29+
30+
return {
31+
nodeType,
32+
nodeName: nodeName.toLowerCase(),
33+
id,
34+
type
35+
};
36+
}
37+
3238
/**
3339
* Determine if the actualNode has the given class name.
3440
* @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass
@@ -66,7 +72,7 @@ class VirtualNode {
6672
/**
6773
* Determine if the element has the given attribute.
6874
* @param {String} attrName The name of the attribute
69-
* @return {Bool} True if the element has the attribute, false otherwise.
75+
* @return {Boolean} True if the element has the attribute, false otherwise.
7076
*/
7177
hasAttr(attrName) {
7278
if (typeof this.actualNode.hasAttribute !== 'function') {

lib/core/utils/qsa.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ var matchExpressions = function() {};
66

77
function matchesTag(vNode, exp) {
88
return (
9-
vNode.elementNodeType === 1 &&
10-
(exp.tag === '*' || vNode.elementNodeName === exp.tag)
9+
vNode.props.nodeType === 1 &&
10+
(exp.tag === '*' || vNode.props.nodeName === exp.tag)
1111
);
1212
}
1313

@@ -26,7 +26,7 @@ function matchesAttributes(vNode, exp) {
2626
}
2727

2828
function matchesId(vNode, exp) {
29-
return !exp.id || vNode.elementId === exp.id;
29+
return !exp.id || vNode.props.id === exp.id;
3030
}
3131

3232
function matchesPseudos(target, exp) {

lib/rules/autocomplete-matches.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
const { text, aria, dom } = axe.commons;
22

3-
const autocomplete = node.getAttribute('autocomplete');
3+
const autocomplete = virtualNode.attr('autocomplete');
44
if (!autocomplete || text.sanitize(autocomplete) === '') {
55
return false;
66
}
77

8-
const nodeName = node.nodeName.toUpperCase();
9-
if (['TEXTAREA', 'INPUT', 'SELECT'].includes(nodeName) === false) {
8+
const nodeName = virtualNode.props.nodeName;
9+
if (['textarea', 'input', 'select'].includes(nodeName) === false) {
1010
return false;
1111
}
1212

1313
// The element is an `input` element a `type` of `hidden`, `button`, `submit` or `reset`
1414
const excludedInputTypes = ['submit', 'reset', 'button', 'hidden'];
15-
if (nodeName === 'INPUT' && excludedInputTypes.includes(node.type)) {
15+
if (
16+
nodeName === 'input' &&
17+
excludedInputTypes.includes(virtualNode.props.type)
18+
) {
1619
return false;
1720
}
1821

1922
// The element has a `disabled` or `aria-disabled="true"` attribute
20-
const ariaDisabled = node.getAttribute('aria-disabled') || 'false';
21-
if (node.disabled || ariaDisabled.toLowerCase() === 'true') {
23+
const ariaDisabled = virtualNode.attr('aria-disabled') || 'false';
24+
if (virtualNode.hasAttr('disabled') || ariaDisabled.toLowerCase() === 'true') {
2225
return false;
2326
}
2427

2528
// The element has `tabindex="-1"` and has a [[semantic role]] that is
2629
// not a [widget](https://www.w3.org/TR/wai-aria-1.1/#widget_roles)
27-
const role = node.getAttribute('role');
28-
const tabIndex = node.getAttribute('tabindex');
30+
const role = virtualNode.attr('role');
31+
const tabIndex = virtualNode.attr('tabindex');
2932
if (tabIndex === '-1' && role) {
3033
const roleDef = aria.lookupTable.role[role];
3134
if (roleDef === undefined || roleDef.type !== 'widget') {
@@ -36,8 +39,9 @@ if (tabIndex === '-1' && role) {
3639
// The element is **not** visible on the page or exposed to assistive technologies
3740
if (
3841
tabIndex === '-1' &&
39-
!dom.isVisible(node, false) &&
40-
!dom.isVisible(node, true)
42+
virtualNode.actualNode &&
43+
!dom.isVisible(virtualNode.actualNode, false) &&
44+
!dom.isVisible(virtualNode.actualNode, true)
4145
) {
4246
return false;
4347
}

test/core/base/virtual-node.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ describe('VirtualNode', function() {
2424
assert.equal(vNode.actualNode, node);
2525
});
2626

27-
it('should abstract Node and Element APIs', function() {
27+
it('should abstract Node properties', function() {
28+
node = document.createElement('input');
2829
node.id = 'monkeys';
2930
var vNode = new VirtualNode(node);
3031

31-
assert.equal(vNode.elementNodeType, 1);
32-
assert.equal(vNode.elementNodeName, 'div');
33-
assert.equal(vNode.elementId, 'monkeys');
32+
assert.isDefined(vNode.props);
33+
assert.equal(vNode.props.nodeType, 1);
34+
assert.equal(vNode.props.nodeName, 'input');
35+
assert.equal(vNode.props.id, 'monkeys');
36+
assert.equal(vNode.props.type, 'text');
3437
});
3538

3639
it('should lowercase nodeName', function() {
@@ -39,7 +42,7 @@ describe('VirtualNode', function() {
3942
};
4043
var vNode = new VirtualNode(node);
4144

42-
assert.equal(vNode.elementNodeName, 'foobar');
45+
assert.equal(vNode.props.nodeName, 'foobar');
4346
});
4447

4548
describe('hasClass', function() {
Lines changed: 55 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
describe('autocomplete-matches', function() {
22
'use strict';
33
var fixture = document.getElementById('fixture');
4+
var queryFixture = axe.testUtils.queryFixture;
45
var rule = axe._audit.rules.find(function(rule) {
56
return rule.id === 'autocomplete-valid';
67
});
@@ -14,105 +15,88 @@ describe('autocomplete-matches', function() {
1415
});
1516

1617
it('returns true for input elements', function() {
17-
var elm = document.createElement('input');
18-
elm.setAttribute('autocomplete', 'foo');
19-
fixture.appendChild(elm);
20-
assert.isTrue(rule.matches(elm));
18+
var vNode = queryFixture('<input id="target" autocomplete="foo">');
19+
assert.isTrue(rule.matches(null, vNode));
2120
});
2221

2322
it('returns true for select elements', function() {
24-
var elm = document.createElement('select');
25-
elm.setAttribute('autocomplete', 'foo');
26-
fixture.appendChild(elm);
27-
assert.isTrue(rule.matches(elm));
23+
var vNode = queryFixture('<select id="target" autocomplete="foo">');
24+
assert.isTrue(rule.matches(null, vNode));
2825
});
2926

3027
it('returns true for textarea elements', function() {
31-
var elm = document.createElement('textarea');
32-
elm.setAttribute('autocomplete', 'foo');
33-
fixture.appendChild(elm);
34-
assert.isTrue(rule.matches(elm));
28+
var vNode = queryFixture('<textarea id="target" autocomplete="foo">');
29+
assert.isTrue(rule.matches(null, vNode));
3530
});
3631

3732
it('returns false for buttons elements', function() {
38-
var elm = document.createElement('button');
39-
elm.setAttribute('autocomplete', 'foo');
40-
fixture.appendChild(elm);
41-
assert.isFalse(rule.matches(elm));
33+
var vNode = queryFixture('<button id="target" autocomplete="foo">');
34+
assert.isFalse(rule.matches(null, vNode));
4235
});
4336

4437
it('should return false for non-form field elements', function() {
45-
var elm = document.createElement('div');
46-
elm.setAttribute('autocomplete', 'foo');
47-
fixture.appendChild(elm);
48-
assert.isFalse(rule.matches(elm));
38+
var vNode = queryFixture('<div id="target" autocomplete="foo">');
39+
assert.isFalse(rule.matches(null, vNode));
4940
});
5041

5142
it('returns false for input buttons', function() {
5243
['reset', 'submit', 'button'].forEach(function(type) {
53-
var elm = document.createElement('input');
54-
elm.setAttribute('autocomplete', 'foo');
55-
elm.type = type;
56-
fixture.appendChild(elm);
57-
assert.isFalse(rule.matches(elm));
44+
var vNode = queryFixture(
45+
'<input id="target" type="' + type + '" autocomplete="foo">'
46+
);
47+
assert.isFalse(rule.matches(null, vNode));
5848
});
5949
});
6050

6151
it('returns false for elements with an empty autocomplete', function() {
62-
var elm = document.createElement('input');
63-
elm.setAttribute('autocomplete', ' ');
64-
fixture.appendChild(elm);
65-
assert.isFalse(rule.matches(elm));
52+
var vNode = queryFixture('<input id="target" autocomplete=" ">');
53+
assert.isFalse(rule.matches(null, vNode));
6654
});
6755

6856
it('returns false for intput[type=hidden]', function() {
69-
var elm = document.createElement('input');
70-
elm.setAttribute('autocomplete', 'foo');
71-
elm.type = 'hidden';
72-
fixture.appendChild(elm);
73-
assert.isFalse(rule.matches(elm));
57+
var vNode = queryFixture(
58+
'<input id="target" type="hidden" autocomplete="foo">'
59+
);
60+
assert.isFalse(rule.matches(null, vNode));
7461
});
7562

7663
it('returns false for disabled fields', function() {
7764
['input', 'select', 'textarea'].forEach(function(tagName) {
78-
var elm = document.createElement(tagName);
79-
elm.setAttribute('autocomplete', 'foo');
80-
elm.disabled = true;
81-
fixture.appendChild(elm);
82-
assert.isFalse(rule.matches(elm));
65+
var vNode = queryFixture(
66+
'<' + tagName + ' id="target" disabled autocomplete="foo">'
67+
);
68+
assert.isFalse(rule.matches(null, vNode));
8369
});
8470
});
8571

8672
it('returns false for aria-disabled=true fields', function() {
8773
['input', 'select', 'textarea'].forEach(function(tagName) {
88-
var elm = document.createElement(tagName);
89-
elm.setAttribute('autocomplete', 'foo');
90-
elm.setAttribute('aria-disabled', 'true');
91-
fixture.appendChild(elm);
92-
assert.isFalse(rule.matches(elm));
74+
var vNode = queryFixture(
75+
'<' + tagName + ' id="target" aria-disabled="true" autocomplete="foo">'
76+
);
77+
assert.isFalse(rule.matches(null, vNode));
9378
});
9479
});
9580

9681
it('returns true for aria-disabled=false fields', function() {
9782
['input', 'select', 'textarea'].forEach(function(tagName) {
98-
var elm = document.createElement(tagName);
99-
elm.setAttribute('autocomplete', 'foo');
100-
elm.setAttribute('aria-disabled', 'false');
101-
fixture.appendChild(elm);
102-
assert.isTrue(rule.matches(elm));
83+
var vNode = queryFixture(
84+
'<' + tagName + ' id="target" aria-disabled="false" autocomplete="foo">'
85+
);
86+
assert.isTrue(rule.matches(null, vNode));
10387
});
10488
});
10589

10690
it('returns false for non-widget roles with tabindex=-1', function() {
10791
var nonWidgetRoles = ['application', 'fakerole', 'main'];
10892
nonWidgetRoles.forEach(function(role) {
109-
var elm = document.createElement('input');
110-
elm.setAttribute('autocomplete', 'foo');
111-
elm.setAttribute('role', role);
112-
elm.setAttribute('tabindex', '-1');
113-
fixture.appendChild(elm);
93+
var vNode = queryFixture(
94+
'<input id="target" role="' +
95+
role +
96+
'" tabindex="-1" autocomplete="foo">'
97+
);
11498
assert.isFalse(
115-
rule.matches(elm),
99+
rule.matches(null, vNode),
116100
'Expect role=' + role + ' to be ignored when it has tabindex=-1'
117101
);
118102
});
@@ -121,36 +105,30 @@ describe('autocomplete-matches', function() {
121105
it('returns true for form fields with a widget role with tabindex=-1', function() {
122106
var nonWidgetRoles = ['button', 'menuitem', 'slider'];
123107
nonWidgetRoles.forEach(function(role) {
124-
var elm = document.createElement('input');
125-
elm.setAttribute('autocomplete', 'foo');
126-
elm.setAttribute('role', role);
127-
elm.setAttribute('tabindex', '-1');
128-
fixture.appendChild(elm);
129-
assert.isTrue(rule.matches(elm));
108+
var vNode = queryFixture(
109+
'<input id="target" role="' +
110+
role +
111+
'" tabindex="-1" autocomplete="foo">'
112+
);
113+
assert.isTrue(rule.matches(null, vNode));
130114
});
131115
});
132116

133117
it('returns true for form fields with tabindex=-1', function() {
134118
['input', 'select', 'textarea'].forEach(function(tagName) {
135-
var elm = document.createElement(tagName);
136-
elm.setAttribute('autocomplete', 'foo');
137-
elm.setAttribute('tabindex', -1);
138-
fixture.appendChild(elm);
139-
assert.isTrue(rule.matches(elm));
119+
var vNode = queryFixture(
120+
'<' + tagName + ' id="target" tabindex="-1" autocomplete="foo">'
121+
);
122+
assert.isTrue(rule.matches(null, vNode));
140123
});
141124
});
142125

143126
it('returns false for off screen and hidden form fields with tabindex=-1', function() {
144-
var elm = document.createElement('input');
145-
elm.setAttribute('autocomplete', 'foo');
146-
elm.setAttribute('tabindex', -1);
147-
elm.setAttribute('style', 'position:absolute; top:-9999em');
148-
149-
var parent = document.createElement('div');
150-
parent.appendChild(elm);
151-
parent.setAttribute('aria-hidden', 'true');
152-
153-
fixture.appendChild(parent);
154-
assert.isFalse(rule.matches(elm));
127+
var vNode = queryFixture(
128+
'<div aria-hidden="true">' +
129+
'<input id="target" tabindex="-1" style="position:absolute; top:-9999em" autocomplete="foo">' +
130+
'</div>'
131+
);
132+
assert.isFalse(rule.matches(null, vNode));
155133
});
156134
});

0 commit comments

Comments
 (0)