/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* import-globals-from ../../mochitest/attributes.js */
loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
/**
* Default textbox accessible attributes.
*/
const defaultAttributes = {
"margin-top": "0px",
"margin-right": "0px",
"margin-bottom": "0px",
"margin-left": "0px",
"text-align": "start",
"text-indent": "0px",
id: "textbox",
tag: "input",
display: "inline-block",
};
/**
* Test data has the format of:
* {
* desc {String} description for better logging
* expected {Object} expected attributes for given accessibles
* unexpected {Object} unexpected attributes for given accessibles
*
* action {?AsyncFunction} an optional action that awaits a change in
* attributes
* attrs {?Array} an optional list of attributes to update
* waitFor {?Number} an optional event to wait for
* }
*/
const attributesTests = [
{
desc: "Initiall accessible attributes",
expected: defaultAttributes,
unexpected: {
"line-number": "1",
"explicit-name": "true",
"container-live": "polite",
live: "polite",
},
},
{
desc: "@line-number attribute is present when textbox is focused",
async action(browser) {
await invokeFocus(browser, "textbox");
},
waitFor: EVENT_FOCUS,
expected: Object.assign({}, defaultAttributes, { "line-number": "1" }),
unexpected: {
"explicit-name": "true",
"container-live": "polite",
live: "polite",
},
},
{
desc: "@aria-live sets container-live and live attributes",
attrs: [
{
attr: "aria-live",
value: "polite",
},
],
expected: Object.assign({}, defaultAttributes, {
"line-number": "1",
"container-live": "polite",
live: "polite",
}),
unexpected: {
"explicit-name": "true",
},
},
{
desc: "@title attribute sets explicit-name attribute to true",
attrs: [
{
attr: "title",
value: "textbox",
},
],
expected: Object.assign({}, defaultAttributes, {
"line-number": "1",
"explicit-name": "true",
"container-live": "polite",
live: "polite",
}),
unexpected: {},
},
];
/**
* Test caching of accessible object attributes
*/
addAccessibleTask(
`
`,
async function (browser, accDoc) {
let textbox = findAccessibleChildByID(accDoc, "textbox");
for (let {
desc,
action,
attrs,
expected,
waitFor,
unexpected,
} of attributesTests) {
info(desc);
let onUpdate;
if (waitFor) {
onUpdate = waitForEvent(waitFor, "textbox");
}
if (action) {
await action(browser);
} else if (attrs) {
for (let { attr, value } of attrs) {
await invokeSetAttribute(browser, "textbox", attr, value);
}
}
await onUpdate;
testAttrs(textbox, expected);
testAbsentAttrs(textbox, unexpected);
}
},
{
// These tests don't work yet with the parent process cache.
topLevel: false,
iframe: false,
remoteIframe: false,
}
);
/**
* Test caching of the tag attribute.
*/
addAccessibleTask(
`
text
`,
async function (browser, docAcc) {
testAttrs(docAcc, { tag: "body" }, true);
const p = findAccessibleChildByID(docAcc, "p");
testAttrs(p, { tag: "p" }, true);
const textLeaf = p.firstChild;
testAbsentAttrs(textLeaf, { tag: "" });
const textarea = findAccessibleChildByID(docAcc, "textarea");
testAttrs(textarea, { tag: "textarea" }, true);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test caching of the text-input-type attribute.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
function testInputType(id, inputType) {
if (inputType == undefined) {
testAbsentAttrs(findAccessibleChildByID(docAcc, id), {
"text-input-type": "",
});
} else {
testAttrs(
findAccessibleChildByID(docAcc, id),
{ "text-input-type": inputType },
true
);
}
}
testInputType("default");
testInputType("email", "email");
testInputType("password", "password");
testInputType("text", "text");
testInputType("date", "date");
testInputType("time", "time");
testInputType("checkbox");
testInputType("radio");
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
/**
* Test caching of the display attribute.
*/
addAccessibleTask(
`
a
`,
async function (browser, docAcc) {
const div = findAccessibleChildByID(docAcc, "div");
testAttrs(div, { display: "block" }, true);
const ins = findAccessibleChildByID(docAcc, "ins");
testAttrs(ins, { display: "inline" }, true);
const textLeaf = ins.firstChild;
testAbsentAttrs(textLeaf, { display: "" });
const button = findAccessibleChildByID(docAcc, "button");
testAttrs(button, { display: "inline-block" }, true);
await invokeContentTask(browser, [], () => {
content.document.getElementById("ins").style.display = "block";
content.document.body.offsetTop; // Flush layout.
});
await untilCacheIs(
() => ins.attributes.getStringProperty("display"),
"block",
"ins display attribute changed to block"
);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test that there is no display attribute on image map areas.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
const normalArea = findAccessibleChildByID(docAcc, "normalArea");
testAbsentAttrs(normalArea, { display: "" });
const unslottedArea = findAccessibleChildByID(docAcc, "unslottedArea");
testAbsentAttrs(unslottedArea, { display: "" });
},
{ topLevel: true }
);
/**
* Test caching of the explicit-name attribute.
*/
addAccessibleTask(
`
content
`,
async function (browser, docAcc) {
const h1 = findAccessibleChildByID(docAcc, "h1");
testAbsentAttrs(h1, { "explicit-name": "" });
const buttonContent = findAccessibleChildByID(docAcc, "buttonContent");
testAbsentAttrs(buttonContent, { "explicit-name": "" });
const buttonLabel = findAccessibleChildByID(docAcc, "buttonLabel");
testAttrs(buttonLabel, { "explicit-name": "true" }, true);
const buttonEmpty = findAccessibleChildByID(docAcc, "buttonEmpty");
testAbsentAttrs(buttonEmpty, { "explicit-name": "" });
const buttonSummary = findAccessibleChildByID(docAcc, "buttonSummary");
testAbsentAttrs(buttonSummary, { "explicit-name": "" });
const div = findAccessibleChildByID(docAcc, "div");
testAbsentAttrs(div, { "explicit-name": "" });
info("Setting aria-label on h1");
let nameChanged = waitForEvent(EVENT_NAME_CHANGE, h1);
await invokeContentTask(browser, [], () => {
content.document.getElementById("h1").setAttribute("aria-label", "label");
});
await nameChanged;
testAttrs(h1, { "explicit-name": "true" }, true);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test caching of ARIA attributes that are exposed via object attributes.
*/
addAccessibleTask(
`
currentTrue
currentFalse
currentPage
currentBlah
foo
mutate
`,
async function (browser, docAcc) {
const currentTrue = findAccessibleChildByID(docAcc, "currentTrue");
testAttrs(currentTrue, { current: "true" }, true);
const currentFalse = findAccessibleChildByID(docAcc, "currentFalse");
testAbsentAttrs(currentFalse, { current: "" });
const currentPage = findAccessibleChildByID(docAcc, "currentPage");
testAttrs(currentPage, { current: "page" }, true);
// Test that token normalization works.
const currentBlah = findAccessibleChildByID(docAcc, "currentBlah");
testAttrs(currentBlah, { current: "true" }, true);
const haspopupMenu = findAccessibleChildByID(docAcc, "haspopupMenu");
testAttrs(haspopupMenu, { haspopup: "menu" }, true);
// Test normalization of integer values.
const rowColCountPositive = findAccessibleChildByID(
docAcc,
"rowColCountPositive"
);
testAttrs(
rowColCountPositive,
{ rowcount: "1000", colcount: "1000" },
true
);
const rowColIndexPositive = findAccessibleChildByID(
docAcc,
"rowColIndexPositive"
);
testAttrs(rowColIndexPositive, { rowindex: "100", colindex: "100" }, true);
const rowColCountNegative = findAccessibleChildByID(
docAcc,
"rowColCountNegative"
);
testAttrs(rowColCountNegative, { rowcount: "-1", colcount: "-1" }, true);
const rowColIndexNegative = findAccessibleChildByID(
docAcc,
"rowColIndexNegative"
);
testAbsentAttrs(rowColIndexNegative, { rowindex: "", colindex: "" });
const rowColCountInvalid = findAccessibleChildByID(
docAcc,
"rowColCountInvalid"
);
testAbsentAttrs(rowColCountInvalid, { rowcount: "", colcount: "" });
const rowColIndexInvalid = findAccessibleChildByID(
docAcc,
"rowColIndexInvalid"
);
testAbsentAttrs(rowColIndexInvalid, { rowindex: "", colindex: "" });
// Test that unknown aria- attributes get exposed.
const foo = findAccessibleChildByID(docAcc, "foo");
testAttrs(foo, { foo: "bar" }, true);
const mutate = findAccessibleChildByID(docAcc, "mutate");
testAttrs(mutate, { current: "true" }, true);
info("mutate: Removing aria-current");
let changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").removeAttribute("aria-current");
});
await changed;
testAbsentAttrs(mutate, { current: "" });
info("mutate: Adding aria-current");
changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
await invokeContentTask(browser, [], () => {
content.document
.getElementById("mutate")
.setAttribute("aria-current", "page");
});
await changed;
testAttrs(mutate, { current: "page" }, true);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test support for the xml-roles attribute.
*/
addAccessibleTask(
`
knownRole
emptyRole
unknownRole
multiRole
landmarkMarkup
landmarkMarkupWithRole
landmarkMarkupWithEmptyRole
markup
markupWithRole
markupWithEmptyRole
`,
async function (browser, docAcc) {
const knownRole = findAccessibleChildByID(docAcc, "knownRole");
testAttrs(knownRole, { "xml-roles": "main" }, true);
const emptyRole = findAccessibleChildByID(docAcc, "emptyRole");
testAbsentAttrs(emptyRole, { "xml-roles": "" });
const unknownRole = findAccessibleChildByID(docAcc, "unknownRole");
testAttrs(unknownRole, { "xml-roles": "foo" }, true);
const multiRole = findAccessibleChildByID(docAcc, "multiRole");
testAttrs(multiRole, { "xml-roles": "foo main" }, true);
const landmarkMarkup = findAccessibleChildByID(docAcc, "landmarkMarkup");
testAttrs(landmarkMarkup, { "xml-roles": "main" }, true);
const landmarkMarkupWithRole = findAccessibleChildByID(
docAcc,
"landmarkMarkupWithRole"
);
testAttrs(landmarkMarkupWithRole, { "xml-roles": "banner" }, true);
const landmarkMarkupWithEmptyRole = findAccessibleChildByID(
docAcc,
"landmarkMarkupWithEmptyRole"
);
testAttrs(landmarkMarkupWithEmptyRole, { "xml-roles": "main" }, true);
const markup = findAccessibleChildByID(docAcc, "markup");
testAttrs(markup, { "xml-roles": "article" }, true);
const markupWithRole = findAccessibleChildByID(docAcc, "markupWithRole");
testAttrs(markupWithRole, { "xml-roles": "banner" }, true);
const markupWithEmptyRole = findAccessibleChildByID(
docAcc,
"markupWithEmptyRole"
);
testAttrs(markupWithEmptyRole, { "xml-roles": "article" }, true);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test lie region attributes.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
const noLive = findAccessibleChildByID(docAcc, "noLive");
for (const acc of [noLive, noLive.firstChild]) {
testAbsentAttrs(acc, {
live: "",
"container-live": "",
"container-live-role": "",
atomic: "",
"container-atomic": "",
busy: "",
"container-busy": "",
relevant: "",
"container-relevant": "",
});
}
const liveMarkup = findAccessibleChildByID(docAcc, "liveMarkup");
testAttrs(liveMarkup, { live: "polite" }, true);
testAttrs(liveMarkup.firstChild, { "container-live": "polite" }, true);
const ariaLive = findAccessibleChildByID(docAcc, "ariaLive");
testAttrs(ariaLive, { live: "polite" }, true);
testAttrs(ariaLive.firstChild, { "container-live": "polite" }, true);
const liveRole = findAccessibleChildByID(docAcc, "liveRole");
testAttrs(liveRole, { live: "polite" }, true);
testAttrs(
liveRole.firstChild,
{ "container-live": "polite", "container-live-role": "log" },
true
);
const nonLiveRole = findAccessibleChildByID(docAcc, "nonLiveRole");
testAbsentAttrs(nonLiveRole, { live: "" });
testAbsentAttrs(nonLiveRole.firstChild, {
"container-live": "",
"container-live-role": "",
});
const other = findAccessibleChildByID(docAcc, "other");
testAttrs(
other,
{ atomic: "true", busy: "true", relevant: "additions" },
true
);
testAttrs(
other.firstChild,
{
"container-atomic": "true",
"container-busy": "true",
"container-relevant": "additions",
},
true
);
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test the id attribute.
*/
addAccessibleTask(
`
withId
`,
async function (browser, docAcc) {
const withId = findAccessibleChildByID(docAcc, "withId");
testAttrs(withId, { id: "withId" }, true);
const noId = findAccessibleChildByID(docAcc, "noIdParent").firstChild;
testAbsentAttrs(noId, { id: "" });
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
/**
* Test the valuetext attribute.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
const valuenow = findAccessibleChildByID(docAcc, "valuenow");
testAttrs(valuenow, { valuetext: "1" }, true);
const valuetext = findAccessibleChildByID(docAcc, "valuetext");
testAttrs(valuetext, { valuetext: "text" }, true);
const noValue = findAccessibleChildByID(docAcc, "noValue");
testAbsentAttrs(noValue, { valuetext: "valuetext" });
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
function untilCacheAttrIs(acc, attr, val, msg) {
return untilCacheOk(() => {
try {
return acc.attributes.getStringProperty(attr) == val;
} catch (e) {
return false;
}
}, msg);
}
function untilCacheAttrAbsent(acc, attr, msg) {
return untilCacheOk(() => {
try {
acc.attributes.getStringProperty(attr);
} catch (e) {
return true;
}
return false;
}, msg);
}
/**
* Test the class attribute.
*/
addAccessibleTask(
`
oneClass
multiClass
noClass
mutate
`,
async function (browser, docAcc) {
const oneClass = findAccessibleChildByID(docAcc, "oneClass");
testAttrs(oneClass, { class: "c1" }, true);
const multiClass = findAccessibleChildByID(docAcc, "multiClass");
testAttrs(multiClass, { class: "c1 c2" }, true);
const noClass = findAccessibleChildByID(docAcc, "noClass");
testAbsentAttrs(noClass, { class: "" });
const mutate = findAccessibleChildByID(docAcc, "mutate");
testAbsentAttrs(mutate, { class: "" });
info("Adding class to mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").className = "c1 c2";
});
await untilCacheAttrIs(mutate, "class", "c1 c2", "mutate class correct");
info("Removing class from mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").removeAttribute("class");
});
await untilCacheAttrAbsent(mutate, "class", "mutate class not present");
},
{ chrome: true, topLevel: true }
);
/**
* Test the src attribute.
*/
const kImgUrl = "https://example.com/a11y/accessible/tests/mochitest/moz.png";
addAccessibleTask(
`
`,
async function (browser, docAcc) {
const noAlt = findAccessibleChildByID(docAcc, "noAlt");
testAttrs(noAlt, { src: kImgUrl }, true);
if (browser.isRemoteBrowser) {
// To avoid wasting memory, we don't cache src if there's a name.
const alt = findAccessibleChildByID(docAcc, "alt");
testAbsentAttrs(alt, { src: "" });
}
const mutate = findAccessibleChildByID(docAcc, "mutate");
testAbsentAttrs(mutate, { src: "" });
info("Adding src to mutate");
await invokeContentTask(browser, [kImgUrl], url => {
content.document.getElementById("mutate").src = url;
});
await untilCacheAttrIs(mutate, "src", kImgUrl, "mutate src correct");
info("Removing src from mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").removeAttribute("src");
});
await untilCacheAttrAbsent(mutate, "src", "mutate src not present");
},
{ chrome: true, topLevel: true }
);
/**
* Test the placeholder attribute.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
const htmlWithLabel = findAccessibleChildByID(docAcc, "htmlWithLabel");
testAttrs(htmlWithLabel, { placeholder: "HTML" }, true);
const htmlNoLabel = findAccessibleChildByID(docAcc, "htmlNoLabel");
// placeholder is used as name, so not exposed as attribute.
testAbsentAttrs(htmlNoLabel, { placeholder: "" });
const ariaWithLabel = findAccessibleChildByID(docAcc, "ariaWithLabel");
testAttrs(ariaWithLabel, { placeholder: "ARIA" }, true);
const ariaNoLabel = findAccessibleChildByID(docAcc, "ariaNoLabel");
// No label doesn't impact aria-placeholder.
testAttrs(ariaNoLabel, { placeholder: "ARIA" }, true);
const both = findAccessibleChildByID(docAcc, "both");
testAttrs(both, { placeholder: "HTML" }, true);
const mutate = findAccessibleChildByID(docAcc, "mutate");
testAbsentAttrs(mutate, { placeholder: "" });
info("Adding label to mutate");
await invokeContentTask(browser, [], () => {
content.document
.getElementById("mutate")
.setAttribute("aria-label", "label");
});
await untilCacheAttrIs(
mutate,
"placeholder",
"HTML",
"mutate placeholder correct"
);
info("Removing mutate placeholder");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").removeAttribute("placeholder");
});
await untilCacheAttrAbsent(
mutate,
"placeholder",
"mutate placeholder not present"
);
info("Setting mutate aria-placeholder");
await invokeContentTask(browser, [], () => {
content.document
.getElementById("mutate")
.setAttribute("aria-placeholder", "ARIA");
});
await untilCacheAttrIs(
mutate,
"placeholder",
"ARIA",
"mutate placeholder correct"
);
info("Setting mutate placeholder");
await invokeContentTask(browser, [], () => {
content.document
.getElementById("mutate")
.setAttribute("placeholder", "HTML");
});
await untilCacheAttrIs(
mutate,
"placeholder",
"HTML",
"mutate placeholder correct"
);
},
{ chrome: true, topLevel: true }
);