Skip to content

Commit

Permalink
feat(autocomplete-matches): use virtualNode only lookups (#1604)
Browse files Browse the repository at this point in the history
* feat(autocomplete-matches): use virtualNode only lookups

* change name to refer to element, make getter

* change api names
  • Loading branch information
straker committed Jun 5, 2019
1 parent 0088e94 commit b32d4fe
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 104 deletions.
2 changes: 1 addition & 1 deletion lib/checks/forms/autocomplete-appropriate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Select and textarea is always allowed
if (virtualNode.elementNodeName !== 'input') {
if (virtualNode.props.nodeName !== 'input') {
return true;
}

Expand Down
22 changes: 14 additions & 8 deletions lib/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@ class VirtualNode {
this._isHidden = null; // will be populated by axe.utils.isHidden
this._cache = {};

// abstract Node and Element APIs so we can run axe in DOM-less
// environments. these are static properties in the assumption
// that axe does not change any of them while it runs.
this.elementNodeType = node.nodeType;
this.elementNodeName = node.nodeName.toLowerCase();
this.elementId = node.id;

if (axe._cache.get('nodeMap')) {
axe._cache.get('nodeMap').set(node, this);
}
}

// abstract Node properties so we can run axe in DOM-less environments.
// add to the prototype so memory is shared across all virtual nodes
get props() {
const { nodeType, nodeName, id, type } = this.actualNode;

return {
nodeType,
nodeName: nodeName.toLowerCase(),
id,
type
};
}

/**
* Determine if the actualNode has the given class name.
* @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass
Expand Down Expand Up @@ -66,7 +72,7 @@ class VirtualNode {
/**
* Determine if the element has the given attribute.
* @param {String} attrName The name of the attribute
* @return {Bool} True if the element has the attribute, false otherwise.
* @return {Boolean} True if the element has the attribute, false otherwise.
*/
hasAttr(attrName) {
if (typeof this.actualNode.hasAttribute !== 'function') {
Expand Down
6 changes: 3 additions & 3 deletions lib/core/utils/qsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ var matchExpressions = function() {};

function matchesTag(vNode, exp) {
return (
vNode.elementNodeType === 1 &&
(exp.tag === '*' || vNode.elementNodeName === exp.tag)
vNode.props.nodeType === 1 &&
(exp.tag === '*' || vNode.props.nodeName === exp.tag)
);
}

Expand All @@ -26,7 +26,7 @@ function matchesAttributes(vNode, exp) {
}

function matchesId(vNode, exp) {
return !exp.id || vNode.elementId === exp.id;
return !exp.id || vNode.props.id === exp.id;
}

function matchesPseudos(target, exp) {
Expand Down
24 changes: 14 additions & 10 deletions lib/rules/autocomplete-matches.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
const { text, aria, dom } = axe.commons;

const autocomplete = node.getAttribute('autocomplete');
const autocomplete = virtualNode.attr('autocomplete');
if (!autocomplete || text.sanitize(autocomplete) === '') {
return false;
}

const nodeName = node.nodeName.toUpperCase();
if (['TEXTAREA', 'INPUT', 'SELECT'].includes(nodeName) === false) {
const nodeName = virtualNode.props.nodeName;
if (['textarea', 'input', 'select'].includes(nodeName) === false) {
return false;
}

// The element is an `input` element a `type` of `hidden`, `button`, `submit` or `reset`
const excludedInputTypes = ['submit', 'reset', 'button', 'hidden'];
if (nodeName === 'INPUT' && excludedInputTypes.includes(node.type)) {
if (
nodeName === 'input' &&
excludedInputTypes.includes(virtualNode.props.type)
) {
return false;
}

// The element has a `disabled` or `aria-disabled="true"` attribute
const ariaDisabled = node.getAttribute('aria-disabled') || 'false';
if (node.disabled || ariaDisabled.toLowerCase() === 'true') {
const ariaDisabled = virtualNode.attr('aria-disabled') || 'false';
if (virtualNode.hasAttr('disabled') || ariaDisabled.toLowerCase() === 'true') {
return false;
}

// The element has `tabindex="-1"` and has a [[semantic role]] that is
// not a [widget](https://www.w3.org/TR/wai-aria-1.1/#widget_roles)
const role = node.getAttribute('role');
const tabIndex = node.getAttribute('tabindex');
const role = virtualNode.attr('role');
const tabIndex = virtualNode.attr('tabindex');
if (tabIndex === '-1' && role) {
const roleDef = aria.lookupTable.role[role];
if (roleDef === undefined || roleDef.type !== 'widget') {
Expand All @@ -36,8 +39,9 @@ if (tabIndex === '-1' && role) {
// The element is **not** visible on the page or exposed to assistive technologies
if (
tabIndex === '-1' &&
!dom.isVisible(node, false) &&
!dom.isVisible(node, true)
virtualNode.actualNode &&
!dom.isVisible(virtualNode.actualNode, false) &&
!dom.isVisible(virtualNode.actualNode, true)
) {
return false;
}
Expand Down
13 changes: 8 additions & 5 deletions test/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ describe('VirtualNode', function() {
assert.equal(vNode.actualNode, node);
});

it('should abstract Node and Element APIs', function() {
it('should abstract Node properties', function() {
node = document.createElement('input');
node.id = 'monkeys';
var vNode = new VirtualNode(node);

assert.equal(vNode.elementNodeType, 1);
assert.equal(vNode.elementNodeName, 'div');
assert.equal(vNode.elementId, 'monkeys');
assert.isDefined(vNode.props);
assert.equal(vNode.props.nodeType, 1);
assert.equal(vNode.props.nodeName, 'input');
assert.equal(vNode.props.id, 'monkeys');
assert.equal(vNode.props.type, 'text');
});

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

assert.equal(vNode.elementNodeName, 'foobar');
assert.equal(vNode.props.nodeName, 'foobar');
});

describe('hasClass', function() {
Expand Down
132 changes: 55 additions & 77 deletions test/rule-matches/autocomplete-matches.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
describe('autocomplete-matches', function() {
'use strict';
var fixture = document.getElementById('fixture');
var queryFixture = axe.testUtils.queryFixture;
var rule = axe._audit.rules.find(function(rule) {
return rule.id === 'autocomplete-valid';
});
Expand All @@ -14,105 +15,88 @@ describe('autocomplete-matches', function() {
});

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

it('returns true for select elements', function() {
var elm = document.createElement('select');
elm.setAttribute('autocomplete', 'foo');
fixture.appendChild(elm);
assert.isTrue(rule.matches(elm));
var vNode = queryFixture('<select id="target" autocomplete="foo">');
assert.isTrue(rule.matches(null, vNode));
});

it('returns true for textarea elements', function() {
var elm = document.createElement('textarea');
elm.setAttribute('autocomplete', 'foo');
fixture.appendChild(elm);
assert.isTrue(rule.matches(elm));
var vNode = queryFixture('<textarea id="target" autocomplete="foo">');
assert.isTrue(rule.matches(null, vNode));
});

it('returns false for buttons elements', function() {
var elm = document.createElement('button');
elm.setAttribute('autocomplete', 'foo');
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture('<button id="target" autocomplete="foo">');
assert.isFalse(rule.matches(null, vNode));
});

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

it('returns false for input buttons', function() {
['reset', 'submit', 'button'].forEach(function(type) {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', 'foo');
elm.type = type;
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture(
'<input id="target" type="' + type + '" autocomplete="foo">'
);
assert.isFalse(rule.matches(null, vNode));
});
});

it('returns false for elements with an empty autocomplete', function() {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', ' ');
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture('<input id="target" autocomplete=" ">');
assert.isFalse(rule.matches(null, vNode));
});

it('returns false for intput[type=hidden]', function() {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', 'foo');
elm.type = 'hidden';
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture(
'<input id="target" type="hidden" autocomplete="foo">'
);
assert.isFalse(rule.matches(null, vNode));
});

it('returns false for disabled fields', function() {
['input', 'select', 'textarea'].forEach(function(tagName) {
var elm = document.createElement(tagName);
elm.setAttribute('autocomplete', 'foo');
elm.disabled = true;
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture(
'<' + tagName + ' id="target" disabled autocomplete="foo">'
);
assert.isFalse(rule.matches(null, vNode));
});
});

it('returns false for aria-disabled=true fields', function() {
['input', 'select', 'textarea'].forEach(function(tagName) {
var elm = document.createElement(tagName);
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('aria-disabled', 'true');
fixture.appendChild(elm);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture(
'<' + tagName + ' id="target" aria-disabled="true" autocomplete="foo">'
);
assert.isFalse(rule.matches(null, vNode));
});
});

it('returns true for aria-disabled=false fields', function() {
['input', 'select', 'textarea'].forEach(function(tagName) {
var elm = document.createElement(tagName);
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('aria-disabled', 'false');
fixture.appendChild(elm);
assert.isTrue(rule.matches(elm));
var vNode = queryFixture(
'<' + tagName + ' id="target" aria-disabled="false" autocomplete="foo">'
);
assert.isTrue(rule.matches(null, vNode));
});
});

it('returns false for non-widget roles with tabindex=-1', function() {
var nonWidgetRoles = ['application', 'fakerole', 'main'];
nonWidgetRoles.forEach(function(role) {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('role', role);
elm.setAttribute('tabindex', '-1');
fixture.appendChild(elm);
var vNode = queryFixture(
'<input id="target" role="' +
role +
'" tabindex="-1" autocomplete="foo">'
);
assert.isFalse(
rule.matches(elm),
rule.matches(null, vNode),
'Expect role=' + role + ' to be ignored when it has tabindex=-1'
);
});
Expand All @@ -121,36 +105,30 @@ describe('autocomplete-matches', function() {
it('returns true for form fields with a widget role with tabindex=-1', function() {
var nonWidgetRoles = ['button', 'menuitem', 'slider'];
nonWidgetRoles.forEach(function(role) {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('role', role);
elm.setAttribute('tabindex', '-1');
fixture.appendChild(elm);
assert.isTrue(rule.matches(elm));
var vNode = queryFixture(
'<input id="target" role="' +
role +
'" tabindex="-1" autocomplete="foo">'
);
assert.isTrue(rule.matches(null, vNode));
});
});

it('returns true for form fields with tabindex=-1', function() {
['input', 'select', 'textarea'].forEach(function(tagName) {
var elm = document.createElement(tagName);
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('tabindex', -1);
fixture.appendChild(elm);
assert.isTrue(rule.matches(elm));
var vNode = queryFixture(
'<' + tagName + ' id="target" tabindex="-1" autocomplete="foo">'
);
assert.isTrue(rule.matches(null, vNode));
});
});

it('returns false for off screen and hidden form fields with tabindex=-1', function() {
var elm = document.createElement('input');
elm.setAttribute('autocomplete', 'foo');
elm.setAttribute('tabindex', -1);
elm.setAttribute('style', 'position:absolute; top:-9999em');

var parent = document.createElement('div');
parent.appendChild(elm);
parent.setAttribute('aria-hidden', 'true');

fixture.appendChild(parent);
assert.isFalse(rule.matches(elm));
var vNode = queryFixture(
'<div aria-hidden="true">' +
'<input id="target" tabindex="-1" style="position:absolute; top:-9999em" autocomplete="foo">' +
'</div>'
);
assert.isFalse(rule.matches(null, vNode));
});
});

0 comments on commit b32d4fe

Please sign in to comment.