accessibility-developer-tools
Version:
This is a library of accessibility-related testing and utility code.
629 lines (551 loc) • 24.2 kB
JavaScript
// Copyright 2012 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
module("Zero Area", {
setup: function () {
var fixture = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(fixture);
this.fixture_ = fixture;
}
});
test("Large element has non-zero area.", function () {
this.fixture_.style.display = "block";
this.fixture_.style.width = "500px";
this.fixture_.style.height = "500px";
equal(axs.utils.elementHasZeroArea(this.fixture_), false);
});
test("Small element has non-zero area.", function () {
this.fixture_.style.display = "block";
this.fixture_.style.width = "1px";
this.fixture_.style.height = "1px";
equal(axs.utils.elementHasZeroArea(this.fixture_), false);
});
test("Empty element has zero area.", function () {
equal(axs.utils.elementHasZeroArea(this.fixture_), true);
});
test("Inline element has non-zero area.", function () {
this.fixture_.style.display = "inline";
this.fixture_.appendChild(document.createTextNode('Size!'));
equal(axs.utils.elementHasZeroArea(this.fixture_), false);
});
module("Transparency", {
setup: function () {
var fixture = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(fixture);
this.fixture_ = fixture;
}
});
test("Transparent elements are transparent.", function () {
this.fixture_.style.opacity = 0;
equal(axs.utils.elementIsTransparent(this.fixture_), true);
});
test("Hidden, but opaque elements are not transparent.", function () {
this.fixture_.style.display = 'none';
this.fixture_.style.opacity = 1;
equal(axs.utils.elementIsTransparent(this.fixture_), false);
});
test("Non-transparent elements are non-transparent.", function () {
for (var i = 0.001; i <= 1; i += 0.001) {
this.fixture_.style.opacity = i;
equal(axs.utils.elementIsTransparent(this.fixture_), false);
}
});
module("Control labels", {
setup: function () {
}
});
test("Input type=submit has a label.", function() {
var element = document.createElement("input");
element.type = "submit";
equal(axs.utils.hasLabel(element), true);
});
test("A placeholder does not count as a label.", function() {
var element0 = document.createElement("textarea");
element0.placeholder = "Your life story";
equal(axs.utils.hasLabel(element0), false);
var element1 = document.createElement("input");
element1.placeholder = "First name";
equal(axs.utils.hasLabel(element1), false);
var element2 = document.createElement("input");
element2.type = "url";
element2.placeholder = "Homepage";
equal(axs.utils.hasLabel(element2), false);
});
test('axs.utils.hasLabel() does not crash for element with numeric id attribute', function() {
var element = document.createElement('input');
element.setAttribute('id', '123_user');
try {
equal(axs.utils.hasLabel(element), false);
} catch(e) {
ok(false, 'Threw exception: ' + e);
}
});
module("getQuerySelectorText", {
setup: function () {
this.fixture_ = document.getElementById('qunit-fixture');
}
});
test("returns the selector text for a nested object with a class attribute", function() {
var targetNode = document.createElement('em');
targetNode.setAttribute('class', 'foo');
var targetParentNode = document.createElement('p');
targetParentNode.appendChild(targetNode);
this.fixture_.appendChild(targetParentNode);
equal(axs.utils.getQuerySelectorText(targetNode), "#qunit-fixture > P > .foo");
});
test("nth-of-type does not refer to a selector but a tagName", function() {
var html = '<ul><li>One</li><li class="thing">Two</li><li class="thing">Three</li></ul>';
this.fixture_.innerHTML = html;
var lis = document.querySelectorAll('li');
var lastLi = lis[lis.length - 1];
var selector = axs.utils.getQuerySelectorText(lastLi);
equal(lastLi, document.querySelector(selector),
'selector "' + selector + '" does not match element');
});
test('special characters in IDs are properly escaped', function() {
var div = document.createElement('div');
div.id = 'some.id.with.special.chars';
this.fixture_.appendChild(div);
var selector = axs.utils.getQuerySelectorText(div);
equal(div, document.querySelector(selector),
'selector "' + selector + '" does not match element');
});
module("getIdReferrers", {
setup: function () {
this.fixture_ = document.getElementById('qunit-fixture');
}
});
test("returns the aria owners for a given element", function() {
var owned = document.createElement("div");
var ownerCount = 5;
owned.id = "theOwned";
this.fixture_.appendChild(owned);
for(var i = 0; i < ownerCount; i++) {
var owner = document.createElement("div");
owner.setAttribute("aria-owns", "theOwned");
owner.setAttribute("class", "owner");
this.fixture_.appendChild(owner);
}
var expected = this.fixture_.querySelectorAll(".owner");
var actual = axs.utils.getAriaIdReferrers(owned, "aria-owns");
equal(expected.length, ownerCount); // sanity check the test itself
equal(actual.length, ownerCount);
var allFound = Array.prototype.every.call(expected, function(element) {
return (Array.prototype.indexOf.call(actual, element) >= 0);
});
equal(allFound, true);
});
test("returns the elements this element labels", function() {
var label = document.createElement("div");
var labelledCount = 2;
label.id = "theLabel";
this.fixture_.appendChild(label);
for(var i = 0; i < labelledCount; i++) {
var labelled = document.createElement("div");
labelled.setAttribute("aria-labelledby", "theLabel notPresentInDom");
labelled.setAttribute("class", "labelled");
this.fixture_.appendChild(labelled);
}
var expected = this.fixture_.querySelectorAll(".labelled");
var actual = axs.utils.getAriaIdReferrers(label, "aria-labelledby");
equal(expected.length, labelledCount); // sanity check the test itself
equal(actual.length, labelledCount);
var allFound = Array.prototype.every.call(expected, function(element) {
return (Array.prototype.indexOf.call(actual, element) >= 0);
});
equal(allFound, true);
});
module("getAriaPropertiesByValueType", {
setup: function () {
}
});
test("Returns idref and idref_list types.", function() {
var expected = ["activedescendant", "controls", "describedby", "flowto", "labelledby", "owns"];
var actual = axs.utils.getAriaPropertiesByValueType(["idref", "idref_list"]);
actual = Object.keys(actual);
actual.sort();
deepEqual(actual, expected);
});
test("Returns idref types.", function() {
var expected = ["activedescendant"];
var actual = axs.utils.getAriaPropertiesByValueType(["idref"]);
actual = Object.keys(actual);
actual.sort();
deepEqual(actual, expected);
});
module("getSelectorForAriaProperties", {
setup: function () {
}
});
test("Returns a selector to match all aria properties.", function() {
var expected = "[aria-activedescendant],[aria-atomic],[aria-autocomplete],[aria-busy],[aria-checked],[aria-controls],";
expected += "[aria-describedby],[aria-disabled],[aria-dropeffect],[aria-expanded],[aria-flowto],[aria-grabbed],";
expected += "[aria-haspopup],[aria-hidden],[aria-invalid],[aria-label],[aria-labelledby],[aria-level],[aria-live],";
expected += "[aria-multiline],[aria-multiselectable],[aria-orientation],[aria-owns],[aria-posinset],[aria-pressed],";
expected += "[aria-readonly],[aria-relevant],[aria-required],[aria-selected],[aria-setsize],[aria-sort],[aria-valuemax],";
expected += "[aria-valuemin],[aria-valuenow],[aria-valuetext]";
var actual = axs.utils.getSelectorForAriaProperties(axs.constants.ARIA_PROPERTIES);
deepEqual(actual, expected);
});
test("Returns a selector to match all aria idref properties.", function() {
var expected = "[aria-activedescendant]";
var actual = axs.utils.getSelectorForAriaProperties(axs.utils.getAriaPropertiesByValueType(["idref"]));
deepEqual(actual, expected);
});
module("getRoles", {
setup: function () {}
});
test("getRoles on element with valid role.", function() {
for (var role in axs.constants.ARIA_ROLES) {
if (axs.constants.ARIA_ROLES.hasOwnProperty(role) && !axs.constants.ARIA_ROLES[role].abstract) {
var appliedRole = { name: role, valid: true, details: axs.constants.ARIA_ROLES[role] };
var expected = {
valid: true,
applied: appliedRole,
roles: [appliedRole]
};
var element = document.createElement('div');
element.setAttribute('role', role);
var actual = axs.utils.getRoles(element);
deepEqual(actual, expected);
}
}
});
test("getRoles on element with no role.", function() {
var expected = null;
var element = document.createElement('input');
element.setAttribute('type', 'checkbox');
var actual = axs.utils.getRoles(element);
strictEqual(actual, expected);
});
test("getRoles on element with empty role.", function() {
var expected = null;
var element = document.createElement('div');
element.setAttribute('role', '');
var actual = axs.utils.getRoles(element);
strictEqual(actual, expected);
});
test("getRoles on element with implicit role and options.implicit.", function() {
var appliedRole = { name: 'checkbox', valid: true, details: axs.constants.ARIA_ROLES['checkbox'] };
var expected = {
valid: true,
applied: appliedRole,
roles: [appliedRole]
};
var element = document.createElement('input');
element.setAttribute('type', 'checkbox');
var actual = axs.utils.getRoles(element, { implicit: true });
deepEqual(actual, expected);
});
test("getRoles on element with no role and options.implicit.", function() {
var expected = null;
var element = document.createElement('div');
element.setAttribute('type', 'checkbox'); // invalid but let's put the pressure on
var actual = axs.utils.getRoles(element, { implicit: true });
strictEqual(actual, expected);
});
test("getRoles on element with abstract role.", function() {
for (var role in axs.constants.ARIA_ROLES) {
if (axs.constants.ARIA_ROLES.hasOwnProperty(role) && axs.constants.ARIA_ROLES[role].abstract) {
var expected = {
valid: false,
roles: [{ name: role, valid: false }]
};
var element = document.createElement('div');
element.setAttribute('role', role);
var actual = axs.utils.getRoles(element);
deepEqual(actual, expected);
}
}
});
(function() {
/**
* Creates a 'role detail' object which can be used for comparison in the assertions below.
* @param {!string} role A potential ARIA role.
* @return The 'role detail' object.
*/
function createExpectedRoleObject(role) {
var valid = (axs.constants.ARIA_ROLES.hasOwnProperty(role) && !axs.constants.ARIA_ROLES[role].abstract);
var result = { name: role, valid: valid };
if (valid) {
result.details = axs.constants.ARIA_ROLES[role];
}
return result;
}
/**
* Helper for multiple role tests.
* @param {!Array<string>} roles Strings to set in the 'role' attribute of the element under test.
* @param {!number} validIdx The index of the expected applied (valid) ARIA role in the array above
* or a negative number if there are no valid roles.
* @return {Function} A test function for qunit.
*/
function multipleRoleTestHelper(roles, validIdx) {
return function() {
var expectedRoles = roles.map(createExpectedRoleObject);
var expected = {
roles: expectedRoles
};
if (validIdx >= 0) {
expected.valid = true;
expected.applied = expectedRoles[validIdx];
}
else {
expected.valid = false;
}
var element = document.createElement('div');
element.setAttribute('role', roles.join(' '));
var actual = axs.utils.getRoles(element);
deepEqual(actual, expected);
};
}
test("getRoles on element with multiple valid roles.", multipleRoleTestHelper(['checkbox', 'button', 'radio'], 0));
test("getRoles on element with invalid and valid roles.", multipleRoleTestHelper(['foo', 'button', 'bar'], 1));
test("getRoles on element with multiple invalid roles.", multipleRoleTestHelper(['foo', 'fubar', 'bar'], -1));
}());
module("isValidNumber");
test("with integer.", function() {
var actual = axs.utils.isValidNumber("10");
strictEqual(actual.value, 10, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with leading and trailing whitespace.", function() {
var actual = axs.utils.isValidNumber(" 10 ");
strictEqual(actual.value, 10, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with zero.", function() {
var actual = axs.utils.isValidNumber("0");
strictEqual(actual.value, 0, "Zero should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with leading zero integer.", function() {
var actual = axs.utils.isValidNumber("09");
strictEqual(actual.value, 9, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with multiple leading zero integer.", function() {
var actual = axs.utils.isValidNumber("000000009");
strictEqual(actual.value, 9, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with leading zero positivie integer.", function() {
var actual = axs.utils.isValidNumber("+09");
strictEqual(actual.value, 9, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with leading zero negative integer.", function() {
var actual = axs.utils.isValidNumber("-09");
strictEqual(actual.value, -9, "Integer should be parsed");
ok(actual.valid, "Integer should be valid");
});
test("with string starting with number.", function() {
var actual = axs.utils.isValidNumber("10 foo");
equal(actual.valid, false, "String that starts with a number is not valid");
ok(actual.reason, "There should be a reason");
});
test("with true.", function() {
var actual = axs.utils.isValidNumber("true");
equal(actual.valid, false, "boolean is not valid");
ok(actual.reason, "There should be a reason");
});
test("with true, leading and trailing space.", function() {
var actual = axs.utils.isValidNumber(" true ");
equal(actual.valid, false, "boolean is not valid");
ok(actual.reason, "There should be a reason");
});
test("with false.", function() {
var actual = axs.utils.isValidNumber("false");
equal(actual.valid, false, "boolean is not valid");
ok(actual.reason, "There should be a reason");
});
test("with hexadecimal.", function() {
var actual = axs.utils.isValidNumber("0xF");
equal(actual.valid, false, "Hexadecimal is not valid");
ok(actual.reason, "There should be a reason");
});
test("with float.", function() {
var actual = axs.utils.isValidNumber("0.5");
strictEqual(actual.value, 0.5, "Float should be parsed");
ok(actual.valid, "Float should be valid");
});
test("with float with no integer part.", function() {
var actual = axs.utils.isValidNumber(".5");
strictEqual(actual.value, .5, "Float should be parsed");
ok(actual.valid, "Float should be valid");
});
test("with negative integer.", function() {
var actual = axs.utils.isValidNumber("-100");
strictEqual(actual.value, -100, "Negative should be parsed");
ok(actual.valid, "Negative should be valid");
});
test("with negative float.", function() {
var actual = axs.utils.isValidNumber("-1.5");
strictEqual(actual.value, -1.5, "Negative should be parsed");
ok(actual.valid, "Negative should be valid");
});
test("with positive integer.", function() {
var actual = axs.utils.isValidNumber("+100");
strictEqual(actual.value, 100, "Positive should be parsed");
ok(actual.valid, "Positive should be valid");
});
test("with positive float.", function() {
var actual = axs.utils.isValidNumber("+1.5");
strictEqual(actual.value, 1.5, "Positive should be parsed");
ok(actual.valid, "Positive should be valid");
});
test("with Infinity.", function() {
var actual = axs.utils.isValidNumber("Infinity");
equal(actual.valid, false, "Infinity is not a real number");
ok(actual.reason, "There should be a reason");
});
test("with -Infinity.", function() {
var actual = axs.utils.isValidNumber("-Infinity");
equal(actual.valid, false, "-Infinity is not a real number");
ok(actual.reason, "There should be a reason");
});
module('isElementDisabled', {
setup: function () {
var fixture = document.getElementById('qunit-fixture');
var html = '<fieldset><legend>I am Legend<input/><span tabindex="0" role="checkbox"/></legend>';
html += '<legend>I am Legend Too<input/><span tabindex="0" role="checkbox"/></legend>';
html += '<input/><span tabindex="0" role="checkbox"/></fieldset>';
fixture.innerHTML = html;
}
});
test('nothing disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>input');
strictEqual(axs.utils.isElementDisabled(widget), false);
});
test('form control natively disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>input');
widget.setAttribute('disabled', 'false'); // also testing that disabled false is the same as disabled true
strictEqual(axs.utils.isElementDisabled(widget), true);
});
test('form control aria-disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>input');
widget.setAttribute('aria-disabled', 'true');
strictEqual(axs.utils.isElementDisabled(widget), true);
});
test('form control aria-disabled false', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>input');
widget.setAttribute('aria-disabled', 'false');
strictEqual(axs.utils.isElementDisabled(widget), false);
});
test('ARIA widget erroneously disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>[role]');
widget.setAttribute('disabled', 'disabled');
strictEqual(axs.utils.isElementDisabled(widget), false);
});
test('ARIA widget aria-disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var widget = fixture.querySelector('fieldset>[role]');
widget.setAttribute('aria-disabled', 'true');
strictEqual(axs.utils.isElementDisabled(widget), true);
});
test('container natively disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var container = fixture.querySelector('fieldset');
container.setAttribute('disabled', 'disabled');
var widget = container.querySelector('fieldset>input');
var actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, true, 'form control ');
widget = container.querySelector('legend:first-of-type>input');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, false, 'control in legend should not be disabled');
widget = container.querySelector('legend:nth-of-type(2)>input');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, true, 'control in 2nd legend should be disabled');
widget = container.querySelector('fieldset>[role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, false, 'ARIA widget should not be disabled');
widget = container.querySelector('legend:first-of-type [role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, false, 'ARIA widget in legend should not be disabled');
widget = container.querySelector('legend:nth-of-type(2) [role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, false, 'ARIA widget in 2nd legend should not be disabled');
});
(function() {
test('container aria-disabled', function() {
ariaDisabledOnContainerHelper(true);
});
test('container aria-disabled=false', function() {
ariaDisabledOnContainerHelper(false);
});
function ariaDisabledOnContainerHelper(ariaDisabled) {
var fixture = document.getElementById('qunit-fixture');
var container = fixture.querySelector('fieldset');
container.setAttribute('aria-disabled', ariaDisabled);
var widget = container.querySelector('fieldset>input');
var actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'form control');
widget = container.querySelector('legend:first-of-type>input');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'control in legend');
widget = container.querySelector('legend:nth-of-type(2)>input');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'control in 2nd legend');
widget = container.querySelector('fieldset [role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'ARIA widget');
widget = container.querySelector('legend:first-of-type [role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'ARIA widget in legend');
widget = container.querySelector('legend:nth-of-type(2) [role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, ariaDisabled, 'ARIA widget in 2nd legend');
}
})();
test('first fieldset legend aria-disabled', function() {
var fixture = document.getElementById('qunit-fixture');
var container = fixture.querySelector('legend:first-of-type');
container.setAttribute('aria-disabled', 'true');
var widget = container.querySelector('input');
var actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, true, 'form control should be disabled');
widget = container.querySelector('[role]');
actual = axs.utils.isElementDisabled(widget);
strictEqual(actual, true, 'ARIA widget should be disabled');
});
test('composedTreeSearch should ignore distributed nodes if browser does not support them', function() {
var fixture = document.getElementById('qunit-fixture');
if (fixture.createShadowRoot) {
var div = document.createElement('div');
fixture.appendChild(div);
// behave as if we don't have getDistributedNodes available in this browser
var r = HTMLContentElement.prototype.getDistributedNodes;
HTMLContentElement.prototype.getDistributedNodes = null;
var shadowroot = div.createShadowRoot();
shadowroot.innerHTML = '<content id="x" select="p"></content>';
var content = shadowroot.getElementById('x');
var found = [];
axs.dom.composedTreeSearch(div, null, {
preorder: function(e) {
found.push(e);
return true;
}
});
HTMLContentElement.prototype.getDistributedNodes = r;
ok(found.length === 2);
strictEqual(found[0], div);
strictEqual(found[1], content);
} else {
console.warn('Test platform does not support shadow DOM');
ok(true);
}
});