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 2 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
11 changes: 10 additions & 1 deletion lib/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,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 All @@ -76,6 +76,15 @@ class VirtualNode {
return this.actualNode.hasAttribute(attrName);
}

/**
* Get the input type. This is more complicated when you don't have an
* actualNode to reference.
* @return {String|undefined}
*/
get elementType() {
straker marked this conversation as resolved.
Show resolved Hide resolved
return this.actualNode.type;
straker marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Determine if the element is focusable and cache the result.
* @return {Boolean} True if the element is focusable, false otherwise.
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.elementNodeName;
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.elementType)
) {
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
4 changes: 3 additions & 1 deletion test/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ describe('VirtualNode', function() {
});

it('should abstract Node and Element APIs', 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.elementNodeName, 'input');
assert.equal(vNode.elementId, 'monkeys');
assert.equal(vNode.elementType, 'text');
});

it('should lowercase nodeName', 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));
});
});