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(autocomplete-matches): use virtualNode only lookups #1604

Merged
merged 3 commits into from
Jun 5, 2019
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
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));
});
});