diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /accessible/tests/browser/e10s | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/tests/browser/e10s')
58 files changed, 10137 insertions, 0 deletions
diff --git a/accessible/tests/browser/e10s/browser.toml b/accessible/tests/browser/e10s/browser.toml new file mode 100644 index 0000000000..dfac6b5219 --- /dev/null +++ b/accessible/tests/browser/e10s/browser.toml @@ -0,0 +1,125 @@ +[DEFAULT] +subsuite = "a11y" +support-files = [ + "head.js", + "doc_treeupdate_ariadialog.html", + "doc_treeupdate_ariaowns.html", + "doc_treeupdate_imagemap.html", + "doc_treeupdate_removal.xhtml", + "doc_treeupdate_visibility.html", + "doc_treeupdate_whitespace.html", + "fonts/Ahem.sjs", + "!/accessible/tests/browser/shared-head.js", + "!/accessible/tests/browser/*.jsm", + "!/accessible/tests/mochitest/*.js", + "!/accessible/tests/mochitest/events/slow_image.sjs", + "!/accessible/tests/mochitest/letters.gif", + "!/accessible/tests/mochitest/moz.png", +] +prefs = [ + "javascript.options.asyncstack_capture_debuggee_only=false", + "dom.element.popover.enabled=true" +] + +# Caching tests +["browser_caching_actions.js"] + +["browser_caching_attributes.js"] + +["browser_caching_description.js"] + +["browser_caching_document_props.js"] + +["browser_caching_domnodeid.js"] + +["browser_caching_hyperlink.js"] + +["browser_caching_innerHTML.js"] +skip-if = ["os != 'win'"] + +["browser_caching_interfaces.js"] + +["browser_caching_large_update.js"] + +["browser_caching_name.js"] + +["browser_caching_position.js"] + +["browser_caching_relations.js"] + +["browser_caching_relations_002.js"] + +["browser_caching_states.js"] + +["browser_caching_table.js"] + +["browser_caching_text_bounds.js"] + +["browser_caching_uniqueid.js"] + +["browser_caching_value.js"] + +# Events tests +["browser_events_announcement.js"] +skip-if = ["os == 'win'"] # Bug 1288839 + +["browser_events_caretmove.js"] + +["browser_events_hide.js"] + +["browser_events_show.js"] + +["browser_events_statechange.js"] + +["browser_events_textchange.js"] + +["browser_file_input.js"] + +["browser_language.js"] + +["browser_obj_group.js"] + +["browser_obj_group_002.js"] + +# Tree update tests +["browser_treeupdate_ariadialog.js"] + +["browser_treeupdate_ariaowns.js"] + +["browser_treeupdate_canvas.js"] + +["browser_treeupdate_csscontentvisibility.js"] + +["browser_treeupdate_cssoverflow.js"] + +["browser_treeupdate_doc.js"] + +["browser_treeupdate_gencontent.js"] + +["browser_treeupdate_hidden.js"] + +["browser_treeupdate_image.js"] + +["browser_treeupdate_imagemap.js"] + +["browser_treeupdate_list.js"] + +["browser_treeupdate_list_editabledoc.js"] + +["browser_treeupdate_listener.js"] + +["browser_treeupdate_move.js"] + +["browser_treeupdate_optgroup.js"] + +["browser_treeupdate_removal.js"] + +["browser_treeupdate_select_dropdown.js"] + +["browser_treeupdate_table.js"] + +["browser_treeupdate_textleaf.js"] + +["browser_treeupdate_visibility.js"] + +["browser_treeupdate_whitespace.js"] diff --git a/accessible/tests/browser/e10s/browser_caching_actions.js b/accessible/tests/browser/e10s/browser_caching_actions.js new file mode 100644 index 0000000000..893c818b75 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_actions.js @@ -0,0 +1,295 @@ +/* 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"; + +const gClickEvents = ["mousedown", "mouseup", "click"]; + +const gActionDescrMap = { + jump: "Jump", + press: "Press", + check: "Check", + uncheck: "Uncheck", + select: "Select", + open: "Open", + close: "Close", + switch: "Switch", + click: "Click", + collapse: "Collapse", + expand: "Expand", + activate: "Activate", + cycle: "Cycle", + "click ancestor": "Click ancestor", +}; + +async function testActions(browser, docAcc, id, expectedActions, domEvents) { + const acc = findAccessibleChildByID(docAcc, id); + is(acc.actionCount, expectedActions.length, "Correct action count"); + + let actionNames = []; + let actionDescriptions = []; + for (let i = 0; i < acc.actionCount; i++) { + actionNames.push(acc.getActionName(i)); + actionDescriptions.push(acc.getActionDescription(i)); + } + + is(actionNames.join(","), expectedActions.join(","), "Correct action names"); + is( + actionDescriptions.join(","), + expectedActions.map(a => gActionDescrMap[a]).join(","), + "Correct action descriptions" + ); + + if (!domEvents) { + return; + } + + // We need to set up the listener, and wait for the promise in two separate + // content tasks. + await invokeContentTask(browser, [id, domEvents], (_id, _domEvents) => { + let promises = _domEvents.map( + evtName => + new Promise(resolve => { + const listener = e => { + if (e.target.id == _id) { + content.removeEventListener(evtName, listener); + content.evtPromise = null; + resolve(42); + } + }; + content.addEventListener(evtName, listener); + }) + ); + content.evtPromise = Promise.all(promises); + }); + + acc.doAction(0); + + let eventFired = await invokeContentTask(browser, [], async () => { + await content.evtPromise; + return true; + }); + + ok(eventFired, `DOM events fired '${domEvents}'`); +} + +addAccessibleTask( + `<ul> + <li id="li_clickable1" onclick="">Clickable list item</li> + <li id="li_clickable2" onmousedown="">Clickable list item</li> + <li id="li_clickable3" onmouseup="">Clickable list item</li> + </ul> + + <img id="onclick_img" onclick="" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + + <a id="link1" href="#">linkable textleaf accessible</a> + <div id="link2" onclick="">linkable textleaf accessible</div> + + <a id="link3" href="#"> + <img id="link3img" alt="image in link" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + </a> + + <a href="about:mozilla" id="link4" target="_blank" rel="opener"> + <img src="../moz.png" id="link4img"> + </a> + <a id="link5" onmousedown=""> + <img src="../moz.png" id="link5img"> + </a> + <a id="link6" onclick=""> + <img src="../moz.png" id="link6img"> + </a> + <a id="link7" onmouseup=""> + <img src="../moz.png" id="link7img"> + </a> + + <div> + <label for="TextBox_t2" id="label1"> + <span>Explicit</span> + </label> + <input name="in2" id="TextBox_t2" type="text" maxlength="17"> + </div> + + <div onclick=""><p id="p_in_clickable_div">p in clickable div</p></div> + `, + async function (browser, docAcc) { + is(docAcc.actionCount, 0, "Doc should not have any actions"); + + const _testActions = async (id, expectedActions, domEvents) => { + await testActions(browser, docAcc, id, expectedActions, domEvents); + }; + + await _testActions("li_clickable1", ["click"], gClickEvents); + await _testActions("li_clickable2", ["click"], gClickEvents); + await _testActions("li_clickable3", ["click"], gClickEvents); + + await _testActions("onclick_img", ["click"], gClickEvents); + await _testActions("link1", ["jump"], gClickEvents); + await _testActions("link2", ["click"], gClickEvents); + await _testActions("link3", ["jump"], gClickEvents); + await _testActions("link3img", ["click ancestor"], gClickEvents); + await _testActions("link4", ["jump"], gClickEvents); + await _testActions("link4img", ["click ancestor"], gClickEvents); + await _testActions("link5", ["click"], gClickEvents); + await _testActions("link5img", ["click ancestor"], gClickEvents); + await _testActions("link6", ["click"], gClickEvents); + await _testActions("link6img", ["click ancestor"], gClickEvents); + await _testActions("link7", ["click"], gClickEvents); + await _testActions("link7img", ["click ancestor"], gClickEvents); + await _testActions("label1", ["click"], gClickEvents); + await _testActions("p_in_clickable_div", ["click ancestor"], gClickEvents); + + await invokeContentTask(browser, [], () => { + content.document + .getElementById("li_clickable1") + .removeAttribute("onclick"); + }); + + let acc = findAccessibleChildByID(docAcc, "li_clickable1"); + await untilCacheIs(() => acc.actionCount, 0, "li has no actions"); + let thrown = false; + try { + acc.doAction(0); + } catch (e) { + thrown = true; + } + ok(thrown, "doAction should throw exception"); + + // Remove 'for' from label + await invokeContentTask(browser, [], () => { + content.document.getElementById("label1").removeAttribute("for"); + }); + acc = findAccessibleChildByID(docAcc, "label1"); + await untilCacheIs(() => acc.actionCount, 0, "label has no actions"); + thrown = false; + try { + acc.doAction(0); + ok(false, "doAction should throw exception"); + } catch (e) { + thrown = true; + } + ok(thrown, "doAction should throw exception"); + + // Add 'longdesc' to image + await invokeContentTask(browser, [], () => { + content.document + .getElementById("onclick_img") + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + .setAttribute("longdesc", "http://example.com"); + }); + acc = findAccessibleChildByID(docAcc, "onclick_img"); + await untilCacheIs(() => acc.actionCount, 2, "img has 2 actions"); + await _testActions("onclick_img", ["click", "showlongdesc"]); + + // Remove 'onclick' from image with 'longdesc' + await invokeContentTask(browser, [], () => { + content.document.getElementById("onclick_img").removeAttribute("onclick"); + }); + acc = findAccessibleChildByID(docAcc, "onclick_img"); + await untilCacheIs(() => acc.actionCount, 1, "img has 1 actions"); + await _testActions("onclick_img", ["showlongdesc"]); + + // Remove 'href' from link and test linkable child + let link1Acc = findAccessibleChildByID(docAcc, "link1"); + is( + link1Acc.firstChild.getActionName(0), + "click ancestor", + "linkable child has click ancestor action" + ); + let onRecreation = waitForEvents({ + expected: [ + [EVENT_HIDE, link1Acc], + [EVENT_SHOW, "link1"], + ], + }); + await invokeContentTask(browser, [], () => { + let link1 = content.document.getElementById("link1"); + link1.removeAttribute("href"); + }); + await onRecreation; + link1Acc = findAccessibleChildByID(docAcc, "link1"); + await untilCacheIs(() => link1Acc.actionCount, 0, "link has no actions"); + is(link1Acc.firstChild.actionCount, 0, "linkable child's actions removed"); + + // Add a click handler to the body. Ensure it propagates to descendants. + await invokeContentTask(browser, [], () => { + content.document.body.onclick = () => {}; + }); + await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action"); + await _testActions("link1", ["click ancestor"]); + + await invokeContentTask(browser, [], () => { + content.document.body.onclick = null; + }); + await untilCacheIs(() => docAcc.actionCount, 0, "Doc has no actions"); + is(link1Acc.actionCount, 0, "link has no actions"); + + // Add a click handler to the root element. Ensure it propagates to + // descendants. + await invokeContentTask(browser, [], () => { + content.document.documentElement.onclick = () => {}; + }); + await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action"); + await _testActions("link1", ["click ancestor"]); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test access key. + */ +addAccessibleTask( + ` +<button id="noKey">noKey</button> +<button id="key" accesskey="a">key</button> + `, + async function (browser, docAcc) { + const noKey = findAccessibleChildByID(docAcc, "noKey"); + is(noKey.accessKey, "", "noKey has no accesskey"); + const key = findAccessibleChildByID(docAcc, "key"); + is(key.accessKey, MAC ? "⌃⌥a" : "Alt+Shift+a", "key has correct accesskey"); + + info("Changing accesskey"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("key").accessKey = "b"; + }); + await untilCacheIs( + () => key.accessKey, + MAC ? "⌃⌥b" : "Alt+Shift+b", + "Correct accesskey after change" + ); + + info("Removing accesskey"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("key").removeAttribute("accesskey"); + }); + await untilCacheIs( + () => key.accessKey, + "", + "Empty accesskey after removal" + ); + + info("Adding accesskey"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("key").accessKey = "c"; + }); + await untilCacheIs( + () => key.accessKey, + MAC ? "⌃⌥c" : "Alt+Shift+c", + "Correct accesskey after addition" + ); + }, + { + chrome: true, + topLevel: true, + iframe: false, // Bug 1796846 + remoteIframe: false, // Bug 1796846 + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_attributes.js b/accessible/tests/browser/e10s/browser_caching_attributes.js new file mode 100644 index 0000000000..139015061f --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_attributes.js @@ -0,0 +1,763 @@ +/* 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( + ` + <input id="textbox" value="hello">`, + 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( + ` +<p id="p">text</p> +<textarea id="textarea"></textarea> + `, + 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( + ` + <input id="default"> + <input id="email" type="email"> + <input id="password" type="password"> + <input id="text" type="text"> + <input id="date" type="date"> + <input id="time" type="time"> + <input id="checkbox" type="checkbox"> + <input id="radio" type="radio"> + `, + 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( + ` +<div id="div"> + <ins id="ins">a</ins> + <button id="button">b</button> +</div> +<p> + <span id="presentationalSpan" role="none" + style="display: block; position: absolute; top: 0; left: 0; translate: 1px;"> + a + </span> +</p> + `, + 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" + ); + + // This span has role="none", but we force a generic Accessible because it + // has a transform. role="none" might have been used to avoid exposing + // display: block, so ensure we don't expose that. + const presentationalSpan = findAccessibleChildByID( + docAcc, + "presentationalSpan" + ); + testAbsentAttrs(presentationalSpan, { display: "" }); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test that there is no display attribute on image map areas. + */ +addAccessibleTask( + ` +<map name="normalMap"> + <area id="normalArea" shape="default"> +</map> +<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#normalMap"> +<audio> + <map name="unslottedMap"> + <area id="unslottedArea" shape="default"> + </map> +</audio> +<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#unslottedMap"> + `, + 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( + ` +<h1 id="h1">content</h1> +<button id="buttonContent">content</button> +<button id="buttonLabel" aria-label="label">content</button> +<button id="buttonEmpty"></button> +<button id="buttonSummary"><details><summary>test</summary></details></button> +<div id="div"></div> + `, + 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( + ` +<div id="currentTrue" aria-current="true">currentTrue</div> +<div id="currentFalse" aria-current="false">currentFalse</div> +<div id="currentPage" aria-current="page">currentPage</div> +<div id="currentBlah" aria-current="blah">currentBlah</div> +<div id="haspopupMenu" aria-haspopup="menu">haspopup</div> +<div id="rowColCountPositive" role="table" aria-rowcount="1000" aria-colcount="1000"> + <div role="row"> + <div id="rowColIndexPositive" role="cell" aria-rowindex="100" aria-colindex="100">positive</div> + </div> +</div> +<div id="rowColCountNegative" role="table" aria-rowcount="-1" aria-colcount="-1"> + <div role="row"> + <div id="rowColIndexNegative" role="cell" aria-rowindex="-1" aria-colindex="-1">negative</div> + </div> +</div> +<div id="rowColCountInvalid" role="table" aria-rowcount="z" aria-colcount="z"> + <div role="row"> + <div id="rowColIndexInvalid" role="cell" aria-rowindex="z" aria-colindex="z">invalid</div> + </div> +</div> +<div id="foo" aria-foo="bar">foo</div> +<div id="mutate" aria-current="true">mutate</div> + `, + 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( + ` +<div id="knownRole" role="main">knownRole</div> +<div id="emptyRole" role="">emptyRole</div> +<div id="unknownRole" role="foo">unknownRole</div> +<div id="multiRole" role="foo main">multiRole</div> +<main id="landmarkMarkup">landmarkMarkup</main> +<main id="landmarkMarkupWithRole" role="banner">landmarkMarkupWithRole</main> +<main id="landmarkMarkupWithEmptyRole" role="">landmarkMarkupWithEmptyRole</main> +<article id="markup">markup</article> +<article id="markupWithRole" role="banner">markupWithRole</article> +<article id="markupWithEmptyRole" role="">markupWithEmptyRole</article> + `, + 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( + ` +<div id="noLive"><p>noLive</p></div> +<output id="liveMarkup"><p>liveMarkup</p></output> +<div id="ariaLive" aria-live="polite"><p>ariaLive</p></div> +<div id="liveRole" role="log"><p>liveRole</p></div> +<div id="nonLiveRole" role="group"><p>nonLiveRole</p></div> +<div id="other" aria-atomic="true" aria-busy="true" aria-relevant="additions"><p>other</p></div> + `, + 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( + ` +<p id="withId">withId</p> +<div id="noIdParent"><p>noId</p></div> + `, + 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( + ` +<div id="valuenow" role="slider" aria-valuenow="1"></div> +<div id="valuetext" role="slider" aria-valuetext="text"></div> +<div id="noValue" role="button"></div> + `, + 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( + ` +<div id="oneClass" class="c1">oneClass</div> +<div id="multiClass" class="c1 c2">multiClass</div> +<div id="noClass">noClass</div> +<div id="mutate">mutate</div> + `, + 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( + ` +<img id="noAlt" src="${kImgUrl}"> +<img id="alt" alt="alt" src="${kImgUrl}"> +<img id="mutate"> + `, + async function (browser, docAcc) { + const noAlt = findAccessibleChildByID(docAcc, "noAlt"); + testAttrs(noAlt, { src: kImgUrl }, true); + const alt = findAccessibleChildByID(docAcc, "alt"); + testAttrs(alt, { src: kImgUrl }, true); + + 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( + ` +<input id="htmlWithLabel" aria-label="label" placeholder="HTML"> +<input id="htmlNoLabel" placeholder="HTML"> +<input id="ariaWithLabel" aria-label="label" aria-placeholder="ARIA"> +<input id="ariaNoLabel" aria-placeholder="ARIA"> +<input id="both" aria-label="label" placeholder="HTML" aria-placeholder="ARIA"> +<input id="mutate" placeholder="HTML"> + `, + 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 } +); + +/** + * Test the ispopup attribute. + */ +addAccessibleTask( + `<div id="popover" popover>popover</div>`, + async function testIspopup(browser, docAcc) { + info("Showing popover"); + let shown = waitForEvent(EVENT_SHOW, "popover"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("popover").showPopover(); + }); + let popover = (await shown).accessible; + testAttrs(popover, { ispopup: "auto" }, true); + info("Setting popover to null"); + // Setting popover causes the Accessible to be recreated. + shown = waitForEvent(EVENT_SHOW, "popover"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("popover").popover = null; + }); + popover = (await shown).accessible; + testAbsentAttrs(popover, { ispopup: "" }); + info("Setting popover to manual and showing"); + shown = waitForEvent(EVENT_SHOW, "popover"); + await invokeContentTask(browser, [], () => { + const popoverDom = content.document.getElementById("popover"); + popoverDom.popover = "manual"; + popoverDom.showPopover(); + }); + popover = (await shown).accessible; + testAttrs(popover, { ispopup: "manual" }, true); + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_description.js b/accessible/tests/browser/e10s/browser_caching_description.js new file mode 100644 index 0000000000..d489620e16 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_description.js @@ -0,0 +1,280 @@ +/* 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/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * expected {String} expected description value for a given accessible + * attrs {?Array} an optional list of attributes to update + * waitFor {?Array} an optional list of accessible events to wait for when + * attributes are updated + * } + */ +const tests = [ + { + desc: "No description when there are no @alt, @title and @aria-describedby", + expected: "", + }, + { + desc: "Description from @aria-describedby attribute", + attrs: [ + { + attr: "aria-describedby", + value: "description", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "aria description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt attribute which is used as the name", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when @alt and " + + "@aria-describedby are not the same", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "another description", + }, + { + desc: "No description change when @alt is dropped but @aria-describedby remains", + attrs: [ + { + attr: "alt", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "another description", + }, + { + desc: + "Description from @aria-describedby attribute when @title (used for " + + "name) and @aria-describedby are not the same", + attrs: [ + { + attr: "title", + value: "title", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "another description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@title attribute which is used as the name", + attrs: [ + { + attr: "title", + value: "another description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: "No description with only @title attribute which is used as the name", + attrs: [ + { + attr: "aria-describedby", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @title attribute when @alt and @atitle are not the " + + "same", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "another description", + }, + { + desc: + "No description from @title since it is the same as the @alt " + + "attribute which is used as the name", + attrs: [ + { + attr: "alt", + value: "another description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt (used for name) and @title attributes", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when it is different " + + "from @alt (used for name) and @title attributes", + attrs: [ + { + attr: "aria-describedby", + value: "description", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "aria description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt attribute (used for name) but different from title", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when @alt (used for " + + "name) and @aria-describedby are not the same but @title and " + + "aria-describedby are", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "another description", + }, +]; + +/** + * Test caching of accessible object description + */ +addAccessibleTask( + ` + <p id="description">aria description</p> + <p id="description2">another description</p> + <img id="image" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" />`, + async function (browser, accDoc) { + let imgAcc = findAccessibleChildByID(accDoc, "image"); + + for (let { desc, waitFor, attrs, expected } of tests) { + info(desc); + let onUpdate; + if (waitFor) { + onUpdate = waitForOrderedEvents(waitFor); + } + if (attrs) { + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, "image", attr, value); + } + } + await onUpdate; + // When attribute change (alt) triggers reorder event, accessible will + // become defunct. + if (isDefunct(imgAcc)) { + imgAcc = findAccessibleChildByID(accDoc, "image"); + } + testDescr(imgAcc, expected); + } + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test that the description is updated when the content of a hidden aria-describedby + * subtree changes. + */ +addAccessibleTask( + ` +<button id="button" aria-describedby="desc"> +<div id="desc" hidden>a</div> + `, + async function (browser, docAcc) { + const button = findAccessibleChildByID(docAcc, "button"); + testDescr(button, "a"); + info("Changing desc textContent"); + let descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button); + await invokeContentTask(browser, [], () => { + content.document.getElementById("desc").textContent = "c"; + }); + await descChanged; + testDescr(button, "c"); + info("Prepending text node to desc"); + descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("desc") + .prepend(content.document.createTextNode("b")); + }); + await descChanged; + testDescr(button, "bc"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test aria-description, including mutations. + */ +addAccessibleTask( + `<button id="button" aria-description="a">button</button>`, + async function (browser, docAcc) { + const button = findAccessibleChildByID(docAcc, "button"); + testDescr(button, "a"); + info("Changing aria-description"); + let changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button); + await invokeSetAttribute(browser, "button", "aria-description", "b"); + await changed; + testDescr(button, "b"); + info("Removing aria-description"); + changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button); + await invokeSetAttribute(browser, "button", "aria-description"); + await changed; + testDescr(button, ""); + info("Setting aria-description"); + changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button); + await invokeSetAttribute(browser, "button", "aria-description", "c"); + await changed; + testDescr(button, "c"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_document_props.js b/accessible/tests/browser/e10s/browser_caching_document_props.js new file mode 100644 index 0000000000..cf3a4e5721 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_document_props.js @@ -0,0 +1,80 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "e10s/doc_treeupdate_whitespace.html", + async function (browser, docAcc) { + info("Testing top level doc"); + queryInterfaces(docAcc, [nsIAccessibleDocument]); + const topUrl = + (browser.isRemoteBrowser ? CURRENT_CONTENT_DIR : CURRENT_DIR) + + "e10s/doc_treeupdate_whitespace.html"; + is(docAcc.URL, topUrl, "Initial URL correct"); + is(docAcc.mimeType, "text/html", "Mime type is correct"); + info("Changing URL"); + await invokeContentTask(browser, [], () => { + content.history.pushState( + null, + "", + content.document.location.href + "/after" + ); + }); + is(docAcc.URL, topUrl + "/after", "URL correct after change"); + + // We can't use the harness to manage iframes for us because it uses data + // URIs for in-process iframes, but data URIs don't support + // history.pushState. + + async function testIframe() { + queryInterfaces(iframeDocAcc, [nsIAccessibleDocument]); + is(iframeDocAcc.URL, src, "Initial URL correct"); + is(iframeDocAcc.mimeType, "text/html", "Mime type is correct"); + info("Changing URL"); + await invokeContentTask(browser, [], async () => { + await SpecialPowers.spawn(content.iframe, [], () => { + content.history.pushState( + null, + "", + content.document.location.href + "/after" + ); + }); + }); + is(iframeDocAcc.URL, src + "/after", "URL correct after change"); + } + + info("Testing same origin (in-process) iframe"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let src = "https://example.com/initial.html"; + let loaded = waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + evt => evt.accessible.parent.parent == docAcc + ); + await invokeContentTask(browser, [src], cSrc => { + content.iframe = content.document.createElement("iframe"); + content.iframe.src = cSrc; + content.document.body.append(content.iframe); + }); + let iframeDocAcc = (await loaded).accessible; + await testIframe(); + + info("Testing different origin (out-of-process) iframe"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + src = "https://example.net/initial.html"; + loaded = waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + evt => evt.accessible.parent.parent == docAcc + ); + await invokeContentTask(browser, [src], cSrc => { + content.iframe.src = cSrc; + }); + iframeDocAcc = (await await loaded).accessible; + await testIframe(); + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_domnodeid.js b/accessible/tests/browser/e10s/browser_caching_domnodeid.js new file mode 100644 index 0000000000..722cc9a970 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_domnodeid.js @@ -0,0 +1,32 @@ +/* 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"; + +/** + * Test DOM ID caching on remotes. + */ +addAccessibleTask( + '<div id="div"></div>', + async function (browser, accDoc) { + const div = findAccessibleChildByID(accDoc, "div"); + ok(div, "Got accessible with 'div' ID."); + + let contentPromise = invokeContentTask(browser, [], () => { + content.document.getElementById("div").id = "foo"; + }); + // We don't await for content task to return because we want to exercise the + // untilCacheIs function and demonstrate that it can await for a passing + // `is` test. + await untilCacheIs( + () => div.id, + "foo", + "ID is correct and updated in cache" + ); + + // Don't leave test without the content task promise resolved. + await contentPromise; + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_hyperlink.js b/accessible/tests/browser/e10s/browser_caching_hyperlink.js new file mode 100644 index 0000000000..4b3f8a1bda --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_hyperlink.js @@ -0,0 +1,228 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function testLinkIndexAtOffset(id, offset, index) { + let htAcc = getAccessible(id, [nsIAccessibleHyperText]); + is( + htAcc.getLinkIndexAtOffset(offset), + index, + "Wrong link index at offset " + offset + " for ID " + id + "!" + ); +} + +function testThis( + paragraph, + docURI, + id, + charIndex, + expectedLinkIndex, + expectedAnchors, + expectedURIs, + valid = true +) { + testLinkIndexAtOffset(paragraph, charIndex, expectedLinkIndex); + + let linkAcc = paragraph.getLinkAt(expectedLinkIndex); + ok(linkAcc, "No accessible for link " + id + "!"); + + is(linkAcc.valid, valid, `${id} is valid.`); + + let linkIndex = paragraph.getLinkIndex(linkAcc); + is(linkIndex, expectedLinkIndex, "Wrong link index for " + id + "!"); + + is(linkAcc.anchorCount, expectedAnchors.length, "Correct number of anchors"); + for (let i = 0; i < expectedAnchors.length; i++) { + let uri = linkAcc.getURI(i); + is( + (uri ? uri.spec : "").replace(docURI, ""), + expectedURIs[i], + `Wrong anchor URI at ${i} for "${id}"` + ); + is( + getAccessibleDOMNodeID(linkAcc.getAnchor(i)), + expectedAnchors[i], + `Wrong anchor at ${i} for "${id}"` + ); + } +} + +/** + * Test hyperlinks + */ +addAccessibleTask( + ` + <p id="testParagraph"><br + >Simple link:<br + ><a id="NormalHyperlink" href="https://www.mozilla.org">Mozilla Foundation</a><br + >ARIA link:<br + ><span id="AriaHyperlink" role="link" + onclick="window.open('https://www.mozilla.org/');" + tabindex="0">Mozilla Foundation Home</span><br + >Invalid, non-focusable hyperlink:<br + ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true" + onclick="window.open('https:/www.mozilla.org/');">Invalid link</span><br + >Image map:<br + ><map name="atoz_map"><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" + id="b" + shape="rect"></area + ><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" + id="a" + shape="rect"></area></map + ><img width="447" id="imgmap" + height="15" + usemap="#atoz_map" + src="../letters.gif"></img><br + >Empty link:<br + ><a id="emptyLink" href=""><img src=""></img></a><br + >Link with embedded span<br + ><a id="LinkWithSpan" href="https://www.heise.de/"><span lang="de">Heise Online</span></a><br + >Named anchor, must not have "linked" state for it to be exposed correctly:<br + ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a> + </p> + `, + function (browser, accDoc) { + const paragraph = findAccessibleChildByID(accDoc, "testParagraph", [ + nsIAccessibleHyperText, + ]); + is(paragraph.linkCount, 7, "Wrong link count for paragraph!"); + + const docURI = accDoc.URL; + // normal hyperlink + testThis( + paragraph, + docURI, + "NormalHyperlink", + 14, + 0, + ["NormalHyperlink"], + ["https://www.mozilla.org/"] + ); + + // ARIA hyperlink + testThis( + paragraph, + docURI, + "AriaHyperlink", + 27, + 1, + ["AriaHyperlink"], + [""] + ); + + // ARIA hyperlink with status invalid + testThis( + paragraph, + docURI, + "InvalidAriaHyperlink", + 63, + 2, + ["InvalidAriaHyperlink"], + [""], + false + ); + + // image map, but not its link children. They are not part of hypertext. + testThis( + paragraph, + docURI, + "imgmap", + 76, + 3, + ["b", "a"], + [ + "https://www.bbc.co.uk/radio4/atoz/index.shtml#b", + "https://www.bbc.co.uk/radio4/atoz/index.shtml#a", + ] + ); + + // empty hyperlink + testThis(paragraph, docURI, "emptyLink", 90, 4, ["emptyLink"], [""]); + + // normal hyperlink with embedded span + testThis( + paragraph, + docURI, + "LinkWithSpan", + 116, + 5, + ["LinkWithSpan"], + ["https://www.heise.de/"] + ); + + // Named anchor + testThis(paragraph, docURI, "namedAnchor", 193, 6, ["namedAnchor"], [""]); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test paragraph with link + */ +addAccessibleTask( + ` + <p id="p"><a href="http://mozilla.org">mozilla.org</a></p> + `, + function (browser, accDoc) { + // Paragraph with link + const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]); + const link = p.getLinkAt(0); + is(link, p.getChildAt(0), "Wrong link for p2"); + is(p.linkCount, 1, "Wrong link count for p2"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test paragraph with link + */ +addAccessibleTask( + ` + <p id="p"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p> + `, + function (browser, accDoc) { + // Paragraph with link + const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]); + + // getLinkIndexAtOffset, causes the offsets to be cached; + testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link + testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link + testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node + testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node + testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node + testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node + testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link + testLinkIndexAtOffset(p, 9, 2); // the end, latest link + + // the second pass to make sure link indexes are calculated propertly from + // cached offsets. + testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link + testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link + testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node + testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node + testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node + testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node + testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link + testLinkIndexAtOffset(p, 9, 2); // the end, latest link + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_innerHTML.js b/accessible/tests/browser/e10s/browser_caching_innerHTML.js new file mode 100644 index 0000000000..7baee32e26 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_innerHTML.js @@ -0,0 +1,48 @@ +/* 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"; + +/** + * Test caching of innerHTML on math elements for Windows clients. + */ +addAccessibleTask( + ` +<p id="p">test</p> +<math id="math"><mfrac><mi>x</mi><mi>y</mi></mfrac></math> + `, + async function (browser, docAcc) { + const p = findAccessibleChildByID(docAcc, "p"); + let hasHtml; + try { + p.cache.getStringProperty("html"); + hasHtml = true; + } catch (e) { + hasHtml = false; + } + ok(!hasHtml, "p doesn't have cached html"); + + const math = findAccessibleChildByID(docAcc, "math"); + is( + math.cache.getStringProperty("html"), + "<mfrac><mi>x</mi><mi>y</mi></mfrac>", + "math cached html is correct" + ); + + info("Mutating math"); + await invokeContentTask(browser, [], () => { + content.document.querySelectorAll("mi")[1].textContent = "z"; + }); + await untilCacheIs( + () => math.cache.getStringProperty("html"), + "<mfrac><mi>x</mi><mi>z</mi></mfrac>", + "math cached html is correct after mutation" + ); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_interfaces.js b/accessible/tests/browser/e10s/browser_caching_interfaces.js new file mode 100644 index 0000000000..c83d486bc6 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_interfaces.js @@ -0,0 +1,59 @@ +/* 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"; + +/** + * Test caching of accessible interfaces + */ +addAccessibleTask( + ` + <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + <select id="select" multiple></select> + <input id="number-input" type="number"> + <table id="table"> + <tr><td id="cell"><a id="link" href="#">hello</a></td></tr> + </table> + `, + async function (browser, accDoc) { + ok( + accDoc instanceof nsIAccessibleDocument, + "Document has Document interface" + ); + ok( + accDoc instanceof nsIAccessibleHyperText, + "Document has HyperText interface" + ); + ok( + findAccessibleChildByID(accDoc, "img") instanceof nsIAccessibleImage, + "img has Image interface" + ); + ok( + findAccessibleChildByID(accDoc, "select") instanceof + nsIAccessibleSelectable, + "select has Selectable interface" + ); + ok( + findAccessibleChildByID(accDoc, "number-input") instanceof + nsIAccessibleValue, + "number-input has Value interface" + ); + ok( + findAccessibleChildByID(accDoc, "table") instanceof nsIAccessibleTable, + "table has Table interface" + ); + ok( + findAccessibleChildByID(accDoc, "cell") instanceof nsIAccessibleTableCell, + "cell has TableCell interface" + ); + ok( + findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperLink, + "link has HyperLink interface" + ); + ok( + findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperText, + "link has HyperText interface" + ); + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_large_update.js b/accessible/tests/browser/e10s/browser_caching_large_update.js new file mode 100644 index 0000000000..ccf8a86921 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_large_update.js @@ -0,0 +1,66 @@ +/* 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"; + +/** + * Test a large update which adds many thousands of Accessibles with a + * lot of content in each. + */ +addAccessibleTask( + `<main id="main" hidden></main>`, + async function (browser, docAcc) { + let shown = waitForEvent(EVENT_SHOW, "main"); + await invokeContentTask(browser, [], () => { + // Make a long string. + let text = ""; + for (let i = 0; i < 100; ++i) { + text += "a"; + } + // Create lots of nodes which include the long string. + const contMain = content.document.getElementById("main"); + // 15000 children of main. + for (let w = 0; w < 15000; ++w) { + // Each of those goes 9 deep. + let parent = contMain; + for (let d = 0; d < 10; ++d) { + const div = content.document.createElement("div"); + div.setAttribute("aria-label", `${w} ${d} ${text}`); + parent.append(div); + parent = div; + } + } + contMain.hidden = false; + }); + const main = (await shown).accessible; + is(main.childCount, 15000, "main has correct number of children"); + + // We don't want to output passes for every check, since that would output + // hundreds of thousands of lines, which slows the test to a crawl. Instead, + // output any failures and keep track of overall success/failure. + let treeOk = true; + function check(val, msg) { + if (!val) { + ok(false, msg); + treeOk = false; + } + } + + info("Checking tree"); + for (let w = 0; w < 15000; ++w) { + let acc = main.getChildAt(w); + let parent = main; + for (let d = 0; d < 10; ++d) { + check(acc, `Got child ${w} depth ${d}`); + const name = `${w} ${d}`; + check(acc.name.startsWith(name + " "), `${name}: correct name`); + check(acc.parent == parent, `${name}: correct parent`); + parent = acc; + acc = acc.firstChild; + } + } + // check() sets treeOk to false for any failure. + ok(treeOk, "Tree is correct"); + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_name.js b/accessible/tests/browser/e10s/browser_caching_name.js new file mode 100644 index 0000000000..55f506b85a --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_name.js @@ -0,0 +1,542 @@ +/* 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"; +requestLongerTimeout(2); + +/* import-globals-from ../../mochitest/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +/** + * Rules for name tests that are inspired by + * accessible/tests/mochitest/name/markuprules.xul + * + * Each element in the list of rules represents a name calculation rule for a + * particular test case. + * + * The rules have the following format: + * { attr } - calculated from attribute + * { elm } - calculated from another element + * { fromsubtree } - calculated from element's subtree + * + */ +const ARIARule = [{ attr: "aria-labelledby" }, { attr: "aria-label" }]; +const HTMLControlHeadRule = [...ARIARule, { elm: "label" }]; +const rules = { + CSSContent: [{ elm: "style" }, { fromsubtree: true }], + HTMLARIAGridCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }], + HTMLControl: [ + ...HTMLControlHeadRule, + { fromsubtree: true }, + { attr: "title" }, + ], + HTMLElm: [...ARIARule, { attr: "title" }], + HTMLImg: [...ARIARule, { attr: "alt" }, { attr: "title" }], + HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }], + HTMLInputButton: [ + ...HTMLControlHeadRule, + { attr: "value" }, + { attr: "title" }, + ], + HTMLInputImage: [ + ...HTMLControlHeadRule, + { attr: "alt" }, + { attr: "value" }, + { attr: "title" }, + ], + HTMLInputImageNoValidSrc: [ + ...HTMLControlHeadRule, + { attr: "alt" }, + { attr: "value" }, + ], + HTMLInputReset: [...HTMLControlHeadRule, { attr: "value" }], + HTMLInputSubmit: [...HTMLControlHeadRule, { attr: "value" }], + HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }], + HTMLLinkImage: [...ARIARule, { fromsubtree: true }, { attr: "title" }], + HTMLOption: [ + ...ARIARule, + { attr: "label" }, + { fromsubtree: true }, + { attr: "title" }, + ], + HTMLTable: [ + ...ARIARule, + { elm: "caption" }, + { attr: "summary" }, + { attr: "title" }, + ], +}; + +const markupTests = [ + { + id: "btn", + ruleset: "HTMLControl", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn">test4</label> + <button id="btn" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5">press me</button>`, + expected: ["test2 test3", "test1", "test4", "press me", "test5"], + }, + { + id: "btn", + ruleset: "HTMLInputButton", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn">test4</label> + <input id="btn" + type="button" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from al" + src="no name from src" + data="no name from data" + title="name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from value", + "name from title", + ], + }, + { + id: "btn-submit", + ruleset: "HTMLInputSubmit", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-submit">test4</label> + <input id="btn-submit" + type="submit" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from atl" + src="no name from src" + data="no name from data" + title="no name from title"/>`, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-reset", + ruleset: "HTMLInputReset", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-reset">test4</label> + <input id="btn-reset" + type="reset" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from alt" + src="no name from src" + data="no name from data" + title="no name from title"/>`, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-image", + ruleset: "HTMLInputImage", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-image">test4</label> + <input id="btn-image" + type="image" + aria-label="test1" + aria-labelledby="l1 l2" + alt="name from alt" + value="name from value" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png" + data="no name from data" + title="name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + "name from title", + ], + }, + { + id: "btn-image", + ruleset: "HTMLInputImageNoValidSrc", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-image">test4</label> + <input id="btn-image" + type="image" + aria-label="test1" + aria-labelledby="l1 l2" + alt="name from alt" + value="name from value" + data="no name from data" + title="no name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + ], + }, + { + id: "opt", + ruleset: "HTMLOption", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <select> + <option id="opt" + aria-label="test1" + aria-labelledby="l1 l2" + label="test4" + title="test5">option1</option> + <option>option2</option> + </select>`, + expected: ["test2 test3", "test1", "test4", "option1", "test5"], + }, + { + id: "img", + ruleset: "HTMLImg", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <img id="img" + aria-label="Logo of Mozilla" + aria-labelledby="l1 l2" + alt="Mozilla logo" + title="This is a logo" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`, + expected: [ + "test2 test3", + "Logo of Mozilla", + "Mozilla logo", + "This is a logo", + ], + }, + { + id: "tc", + ruleset: "HTMLElm", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="tc">test4</label> + <table> + <tr> + <td id="tc" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5"> + <p>This is a paragraph</p> + <a href="#">This is a link</a> + <ul> + <li>This is a list</li> + </ul> + </td> + </tr> + </table>`, + expected: ["test2 test3", "test1", "test5"], + }, + { + id: "gc", + ruleset: "HTMLARIAGridCell", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="gc">test4</label> + <table> + <tr> + <td id="gc" + role="gridcell" + aria-label="test1" + aria-labelledby="l1 l2" + title="This is a paragraph This is a link This is a list"> + <p>This is a paragraph</p> + <a href="#">This is a link</a> + <ul> + <li>Listitem1</li> + <li>Listitem2</li> + </ul> + </td> + </tr> + </table>`, + expected: [ + "test2 test3", + "test1", + "This is a paragraph This is a link \u2022 Listitem1 \u2022 Listitem2", + "This is a paragraph This is a link This is a list", + ], + }, + { + id: "t", + ruleset: "HTMLTable", + markup: ` + <span id="l1">lby_tst6_1</span> + <span id="l2">lby_tst6_2</span> + <label for="t">label_tst6</label> + <table id="t" + aria-label="arialabel_tst6" + aria-labelledby="l1 l2" + summary="summary_tst6" + title="title_tst6"> + <caption>caption_tst6</caption> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table>`, + expected: [ + "lby_tst6_1 lby_tst6_2", + "arialabel_tst6", + "caption_tst6", + "summary_tst6", + "title_tst6", + ], + }, + { + id: "btn", + ruleset: "CSSContent", + markup: ` + <div role="main"> + <style> + button::before { + content: "do not "; + } + </style> + <button id="btn">press me</button> + </div>`, + expected: ["do not press me", "press me"], + }, + { + // TODO: uncomment when Bug-1256382 is resoved. + // id: 'li', + // ruleset: 'CSSContent', + // markup: ` + // <style> + // ul { + // list-style-type: decimal; + // } + // </style> + // <ul id="ul"> + // <li id="li">Listitem</li> + // </ul>`, + // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`] + // }, { + id: "a", + ruleset: "HTMLLink", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <a id="a" + href="" + aria-label="test1" + aria-labelledby="l1 l2" + title="test4">test5</a>`, + expected: ["test2 test3", "test1", "test5", "test4"], + }, + { + id: "a-img", + ruleset: "HTMLLinkImage", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <a id="a-img" + href="" + aria-label="test1" + aria-labelledby="l1 l2" + title="test4"><img alt="test5"/></a>`, + expected: ["test2 test3", "test1", "test5", "test4"], + }, +]; + +/** + * Test accessible name that is calculated from an attribute, remove the + * attribute before proceeding to the next name test. If attribute removal + * results in a reorder or text inserted event - wait for it. If accessible + * becomes defunct, update its reference using the one that is attached to one + * of the above events. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, id } structure that contains an + * accessible and its content element + * id. + * @param {Object} rule current attr rule for name calculation + * @param {[type]} expected expected name value + */ +async function testAttrRule(browser, target, rule, expected) { + let { id, acc } = target; + let { attr } = rule; + + testName(acc, expected); + + let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); + await invokeContentTask(browser, [id, attr], (contentId, contentAttr) => { + content.document.getElementById(contentId).removeAttribute(contentAttr); + }); + let event = await nameChange; + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); +} + +/** + * Test accessible name that is calculated from an element name, remove the + * element before proceeding to the next name test. If element removal results + * in a reorder event - wait for it. If accessible becomes defunct, update its + * reference using the one that is attached to a possible reorder event. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, id } structure that contains an + * accessible and its content element + * id. + * @param {Object} rule current elm rule for name calculation + * @param {[type]} expected expected name value + */ +async function testElmRule(browser, target, rule, expected) { + let { id, acc } = target; + let { elm } = rule; + + testName(acc, expected); + let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); + + await invokeContentTask(browser, [elm], contentElm => { + content.document.querySelector(`${contentElm}`).remove(); + }); + let event = await nameChange; + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); +} + +/** + * Test accessible name that is calculated from its subtree, remove the subtree + * and wait for a reorder event before proceeding to the next name test. If + * accessible becomes defunct, update its reference using the one that is + * attached to a reorder event. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, id } structure that contains an + * accessible and its content element + * id. + * @param {Object} rule current subtree rule for name calculation + * @param {[type]} expected expected name value + */ +async function testSubtreeRule(browser, target, rule, expected) { + let { id, acc } = target; + + testName(acc, expected); + let nameChange = waitForEvent(EVENT_NAME_CHANGE, id); + + await invokeContentTask(browser, [id], contentId => { + let elm = content.document.getElementById(contentId); + while (elm.firstChild) { + elm.firstChild.remove(); + } + }); + let event = await nameChange; + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); +} + +/** + * Iterate over a list of rules and test accessible names for each one of the + * rules. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, id } structure that contains an + * accessible and its content element + * id. + * @param {Array} ruleset A list of rules to test a target with + * @param {Array} expected A list of expected name value for each rule + */ +async function testNameRule(browser, target, ruleset, expected) { + for (let i = 0; i < ruleset.length; ++i) { + let rule = ruleset[i]; + let testFn; + if (rule.attr) { + testFn = testAttrRule; + } else if (rule.elm) { + testFn = testElmRule; + } else if (rule.fromsubtree) { + testFn = testSubtreeRule; + } + await testFn(browser, target, rule, expected[i]); + } +} + +markupTests.forEach(({ id, ruleset, markup, expected }) => + addAccessibleTask( + markup, + async function (browser, accDoc) { + const observer = { + observe(subject, topic, data) { + const event = subject.QueryInterface(nsIAccessibleEvent); + console.log(eventToString(event)); + }, + }; + Services.obs.addObserver(observer, "accessible-event"); + // Find a target accessible from an accessible subtree. + let acc = findAccessibleChildByID(accDoc, id); + let target = { id, acc }; + await testNameRule(browser, target, rules[ruleset], expected); + Services.obs.removeObserver(observer, "accessible-event"); + }, + { iframe: true, remoteIframe: true } + ) +); + +/** + * Test caching of the document title. + */ +addAccessibleTask( + ``, + async function (browser, docAcc) { + let nameChanged = waitForEvent(EVENT_NAME_CHANGE, docAcc); + await invokeContentTask(browser, [], () => { + content.document.title = "new title"; + }); + await nameChanged; + testName(docAcc, "new title"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test that the name is updated when the content of a hidden aria-labelledby + * subtree changes. + */ +addAccessibleTask( + ` +<button id="button" aria-labelledby="label"> +<div id="label" hidden>a</div> + `, + async function (browser, docAcc) { + const button = findAccessibleChildByID(docAcc, "button"); + testName(button, "a"); + info("Changing label textContent"); + let nameChanged = waitForEvent(EVENT_NAME_CHANGE, button); + await invokeContentTask(browser, [], () => { + content.document.getElementById("label").textContent = "c"; + }); + await nameChanged; + testName(button, "c"); + info("Prepending text node to label"); + nameChanged = waitForEvent(EVENT_NAME_CHANGE, button); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("label") + .prepend(content.document.createTextNode("b")); + }); + await nameChanged; + testName(button, "bc"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_position.js b/accessible/tests/browser/e10s/browser_caching_position.js new file mode 100644 index 0000000000..5de246bb91 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_position.js @@ -0,0 +1,194 @@ +/* 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/layout.js */ +loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); + +function getCachedBounds(acc) { + let cachedBounds = ""; + try { + cachedBounds = acc.cache.getStringProperty("relative-bounds"); + } catch (e) { + ok(false, "Unable to fetch cached bounds from cache!"); + } + return cachedBounds; +} + +async function testCoordinates(accDoc, id, expectedWidthPx, expectedHeightPx) { + let acc = findAccessibleChildByID(accDoc, id, [Ci.nsIAccessibleImage]); + if (!acc) { + return; + } + + let screenX = {}; + let screenY = {}; + let windowX = {}; + let windowY = {}; + let parentX = {}; + let parentY = {}; + + // get screen coordinates. + acc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE, + screenX, + screenY + ); + // get window coordinates. + acc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE, + windowX, + windowY + ); + // get parent related coordinates. + acc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE, + parentX, + parentY + ); + // XXX For linked images, a negative parentY value is returned, and the + // screenY coordinate is the link's screenY coordinate minus 1. + // Until this is fixed, set parentY to -1 if it's negative. + if (parentY.value < 0) { + parentY.value = -1; + } + + // See if asking image for child at image's screen coordinates gives + // correct accessible. getChildAtPoint operates on screen coordinates. + let tempAcc = null; + try { + tempAcc = acc.getChildAtPoint(screenX.value, screenY.value); + } catch (e) {} + is(tempAcc, acc, "Wrong accessible returned for position of " + id + "!"); + + // get image's parent. + let imageParentAcc = null; + try { + imageParentAcc = acc.parent; + } catch (e) {} + ok(imageParentAcc, "no parent accessible for " + id + "!"); + + if (imageParentAcc) { + // See if parent's screen coordinates plus image's parent relative + // coordinates equal to image's screen coordinates. + let parentAccX = {}; + let parentAccY = {}; + let parentAccWidth = {}; + let parentAccHeight = {}; + imageParentAcc.getBounds( + parentAccX, + parentAccY, + parentAccWidth, + parentAccHeight + ); + is( + parentAccX.value + parentX.value, + screenX.value, + "Wrong screen x coordinate for " + id + "!" + ); + // XXX see bug 456344 + // is( + // parentAccY.value + parentY.value, + // screenY.value, + // "Wrong screen y coordinate for " + id + "!" + // ); + } + + let [expectedW, expectedH] = CSSToDevicePixels( + window, + expectedWidthPx, + expectedHeightPx + ); + let width = {}; + let height = {}; + acc.getImageSize(width, height); + is(width.value, expectedW, "Wrong width for " + id + "!"); + is(height.value, expectedH, "wrong height for " + id + "!"); +} + +addAccessibleTask( + ` + <br>Simple image:<br> + <img id="nonLinkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/> + <br>Linked image:<br> + <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> + <br>Image with longdesc:<br> + <img id="longdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html" + alt="Image of Mozilla logo"/> + <br>Image with invalid url in longdesc:<br> + <img id="invalidLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc src.html" + alt="Image of Mozilla logo"/> + <br>Image with click and longdesc:<br> + <img id="clickAndLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html" + alt="Another image of Mozilla logo" onclick="alert('Clicked!');"/> + + <br>image described by a link to be treated as longdesc<br> + <img id="longdesc2" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link" + alt="Second Image of Mozilla logo"/> + <a id="describing_link" href="longdesc_src.html">link to description of image</a> + + <br>Image described by a link to be treated as longdesc with whitespaces<br> + <img id="longdesc3" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link2" + alt="Second Image of Mozilla logo"/> + <a id="describing_link2" href="longdesc src.html">link to description of image</a> + + <br>Image with click:<br> + <img id="click" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" + alt="A third image of Mozilla logo" onclick="alert('Clicked, too!');"/> + `, + async function (browser, docAcc) { + // Test non-linked image + await testCoordinates(docAcc, "nonLinkedImage", 89, 38); + + // Test linked image + await testCoordinates(docAcc, "linkedImage", 89, 38); + + // Image with long desc + await testCoordinates(docAcc, "longdesc", 89, 38); + + // Image with invalid url in long desc + await testCoordinates(docAcc, "invalidLongdesc", 89, 38); + + // Image with click and long desc + await testCoordinates(docAcc, "clickAndLongdesc", 89, 38); + + // Image with click + await testCoordinates(docAcc, "click", 89, 38); + + // Image with long desc + await testCoordinates(docAcc, "longdesc2", 89, 38); + + // Image described by HTML:a@href with whitespaces + await testCoordinates(docAcc, "longdesc3", 89, 38); + } +); + +addAccessibleTask( + ` + <br>Linked image:<br> + <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> + `, + async function (browser, docAcc) { + const imgAcc = findAccessibleChildByID(docAcc, "linkedImage", [ + Ci.nsIAccessibleImage, + ]); + const origCachedBounds = getCachedBounds(imgAcc); + + await invokeContentTask(browser, [], () => { + const imgNode = content.document.getElementById("linkedImage"); + imgNode.style = "margin-left: 1000px; margin-top: 500px;"; + }); + + await untilCacheOk(() => { + return origCachedBounds != getCachedBounds(imgAcc); + }, "Cached bounds update after mutation"); + }, + { + // We can only access the `cache` attribute of an accessible when + // the cache is enabled and we're in a remote browser. + topLevel: true, + iframe: true, + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_relations.js b/accessible/tests/browser/e10s/browser_caching_relations.js new file mode 100644 index 0000000000..cc83930830 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_relations.js @@ -0,0 +1,291 @@ +/* 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"; +requestLongerTimeout(2); + +/** + * A test specification that has the following format: + * [ + * attr relevant aria attribute + * hostRelation corresponding host relation type + * dependantRelation corresponding dependant relation type + * ] + */ +const attrRelationsSpec = [ + ["aria-labelledby", RELATION_LABELLED_BY, RELATION_LABEL_FOR], + ["aria-describedby", RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR], + ["aria-controls", RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY], + ["aria-flowto", RELATION_FLOWS_TO, RELATION_FLOWS_FROM], + ["aria-details", RELATION_DETAILS, RELATION_DETAILS_FOR], + ["aria-errormessage", RELATION_ERRORMSG, RELATION_ERRORMSG_FOR], +]; + +/** + * Test caching of relations between accessible objects. + */ +addAccessibleTask( + ` + <div id="dependant1">label</div> + <div id="dependant2">label2</div> + <div role="checkbox" id="host"></div>`, + async function (browser, accDoc) { + for (let spec of attrRelationsSpec) { + await testRelated(browser, accDoc, ...spec); + } + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test caching of relations with respect to label objects and their "for" attr. + */ +addAccessibleTask( + ` + <input type="checkbox" id="dependant1"> + <input type="checkbox" id="dependant2"> + <label id="host">label</label>`, + async function (browser, accDoc) { + await testRelated( + browser, + accDoc, + "for", + RELATION_LABEL_FOR, + RELATION_LABELLED_BY + ); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test rel caching for element with existing relation attribute. + */ +addAccessibleTask( + `<div id="label">label</div><button id="button" aria-labelledby="label">`, + async function (browser, accDoc) { + const button = findAccessibleChildByID(accDoc, "button"); + const label = findAccessibleChildByID(accDoc, "label"); + + await testCachedRelation(button, RELATION_LABELLED_BY, label); + await testCachedRelation(label, RELATION_LABEL_FOR, button); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test caching of relations with respect to output objects and their "for" attr. + */ +addAccessibleTask( + ` + <form oninput="host.value=parseInt(dependant1.value)+parseInt(dependant2.value)"> + <input type="number" id="dependant1" value="50"> + + <input type="number" id="dependant2" value="25"> = + <output name="host" id="host"></output> + </form>`, + async function (browser, accDoc) { + await testRelated( + browser, + accDoc, + "for", + RELATION_CONTROLLED_BY, + RELATION_CONTROLLER_FOR + ); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test rel caching for <label> element with existing "for" attribute. + */ +addAccessibleTask( + `data:text/html,<label id="label" for="input">label</label><input id="input">`, + async function (browser, accDoc) { + const input = findAccessibleChildByID(accDoc, "input"); + const label = findAccessibleChildByID(accDoc, "label"); + await testCachedRelation(input, RELATION_LABELLED_BY, label); + await testCachedRelation(label, RELATION_LABEL_FOR, input); + }, + { iframe: true, remoteIframe: true } +); + +/* + * Test caching of relations with respect to label objects that are ancestors of + * their target. + */ +addAccessibleTask( + ` + <label id="host"> + <input type="checkbox" id="dependant1"> + </label>`, + async function (browser, accDoc) { + const input = findAccessibleChildByID(accDoc, "dependant1"); + const label = findAccessibleChildByID(accDoc, "host"); + + await testCachedRelation(input, RELATION_LABELLED_BY, label); + await testCachedRelation(label, RELATION_LABEL_FOR, input); + }, + { iframe: true, remoteIframe: true } +); + +/* + * Test EMBEDS on root accessible. + */ +addAccessibleTask( + `hello world`, + async function (browser, primaryDocAcc, secondaryDocAcc) { + // The root accessible should EMBED the top level + // content document. If this test runs in an iframe, + // the test harness will pass in doc accs for both the + // iframe (primaryDocAcc) and the top level remote + // browser (secondaryDocAcc). We should use the second + // one. + // If this is not in an iframe, we'll only get + // a single docAcc (primaryDocAcc) which refers to + // the top level content doc. + const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc; + await testRelation( + getRootAccessible(document), + RELATION_EMBEDS, + topLevelDoc + ); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/** + * Test CONTAINING_TAB_PANE + */ +addAccessibleTask( + `<p id="p">hello world</p>`, + async function (browser, primaryDocAcc, secondaryDocAcc) { + // The CONTAINING_TAB_PANE of any acc should be the top level + // content document. If this test runs in an iframe, + // the test harness will pass in doc accs for both the + // iframe (primaryDocAcc) and the top level remote + // browser (secondaryDocAcc). We should use the second + // one. + // If this is not in an iframe, we'll only get + // a single docAcc (primaryDocAcc) which refers to + // the top level content doc. + const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc; + await testCachedRelation( + findAccessibleChildByID(primaryDocAcc, "p"), + RELATION_CONTAINING_TAB_PANE, + topLevelDoc + ); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/* + * Test relation caching on link + */ +addAccessibleTask( + ` + <a id="link" href="#item">a</a> + <div id="item">hello</div> + <div id="item2">world</div> + <a id="link2" href="#anchor">b</a> + <a id="namedLink" name="anchor">c</a>`, + async function (browser, accDoc) { + const link = findAccessibleChildByID(accDoc, "link"); + const link2 = findAccessibleChildByID(accDoc, "link2"); + const namedLink = findAccessibleChildByID(accDoc, "namedLink"); + const item = findAccessibleChildByID(accDoc, "item"); + const item2 = findAccessibleChildByID(accDoc, "item2"); + + await testCachedRelation(link, RELATION_LINKS_TO, item); + await testCachedRelation(link2, RELATION_LINKS_TO, namedLink); + + await invokeContentTask(browser, [], () => { + content.document.getElementById("link").href = ""; + content.document.getElementById("namedLink").name = "newName"; + }); + + await testCachedRelation(link, RELATION_LINKS_TO, null); + await testCachedRelation(link2, RELATION_LINKS_TO, null); + + await invokeContentTask(browser, [], () => { + content.document.getElementById("link").href = "#item2"; + }); + + await testCachedRelation(link, RELATION_LINKS_TO, item2); + }, + { + chrome: true, + // IA2 doesn't have a LINKS_TO relation and Windows non-cached + // RemoteAccessible uses IA2, so we can't run these tests in this case. + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/* + * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria trees. + */ +addAccessibleTask( + ` + <div role="tree" id="tree"> + <div role="treeitem" id="treeitem">test</div> + <div role="treeitem" id="treeitem2">test</div> + </div>`, + async function (browser, accDoc) { + const tree = findAccessibleChildByID(accDoc, "tree"); + const treeItem = findAccessibleChildByID(accDoc, "treeitem"); + const treeItem2 = findAccessibleChildByID(accDoc, "treeitem2"); + + await testCachedRelation(tree, RELATION_NODE_PARENT_OF, [ + treeItem, + treeItem2, + ]); + await testCachedRelation(treeItem, RELATION_NODE_CHILD_OF, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/* + * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria lists. + */ +addAccessibleTask( + ` + <div id="l1" role="list"> + <div id="l1i1" role="listitem" aria-level="1">a</div> + <div id="l1i2" role="listitem" aria-level="2">b</div> + <div id="l1i3" role="listitem" aria-level="1">c</div> + </div>`, + async function (browser, accDoc) { + const list = findAccessibleChildByID(accDoc, "l1"); + const listItem1 = findAccessibleChildByID(accDoc, "l1i1"); + const listItem2 = findAccessibleChildByID(accDoc, "l1i2"); + const listItem3 = findAccessibleChildByID(accDoc, "l1i3"); + + await testCachedRelation(list, RELATION_NODE_PARENT_OF, [ + listItem1, + listItem3, + ]); + await testCachedRelation(listItem1, RELATION_NODE_CHILD_OF, list); + await testCachedRelation(listItem3, RELATION_NODE_CHILD_OF, list); + + await testCachedRelation(listItem1, RELATION_NODE_PARENT_OF, listItem2); + await testCachedRelation(listItem2, RELATION_NODE_CHILD_OF, listItem1); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/* + * Test NODE_CHILD_OF relation caching for JAWS window emulation special case. + */ +addAccessibleTask( + ``, + async function (browser, accDoc) { + await testCachedRelation(accDoc, RELATION_NODE_CHILD_OF, accDoc.parent); + }, + { topLevel: true, chrome: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_relations_002.js b/accessible/tests/browser/e10s/browser_caching_relations_002.js new file mode 100644 index 0000000000..072656eb5e --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_relations_002.js @@ -0,0 +1,366 @@ +/* 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"; +requestLongerTimeout(2); + +/** + * Test MEMBER_OF relation caching on HTML radio buttons + */ +addAccessibleTask( + ` + <input type="radio" id="r1">I have no name<br> + <input type="radio" id="r2">I also have no name<br> + <input type="radio" id="r3" name="n">I have a name<br> + <input type="radio" id="r4" name="a">I have a different name<br> + <fieldset role="radiogroup"> + <input type="radio" id="r5" name="n">I have an already used name + and am in a different part of the tree + <input type="radio" id="r6" name="r">I have a different name but am + in the same group + </fieldset>`, + async function (browser, accDoc) { + const r1 = findAccessibleChildByID(accDoc, "r1"); + const r2 = findAccessibleChildByID(accDoc, "r2"); + const r3 = findAccessibleChildByID(accDoc, "r3"); + const r4 = findAccessibleChildByID(accDoc, "r4"); + const r5 = findAccessibleChildByID(accDoc, "r5"); + const r6 = findAccessibleChildByID(accDoc, "r6"); + + await testCachedRelation(r1, RELATION_MEMBER_OF, null); + await testCachedRelation(r2, RELATION_MEMBER_OF, null); + await testCachedRelation(r3, RELATION_MEMBER_OF, [r3, r5]); + await testCachedRelation(r4, RELATION_MEMBER_OF, r4); + await testCachedRelation(r5, RELATION_MEMBER_OF, [r3, r5]); + await testCachedRelation(r6, RELATION_MEMBER_OF, r6); + + await invokeContentTask(browser, [], () => { + content.document.getElementById("r5").name = "a"; + }); + + await testCachedRelation(r3, RELATION_MEMBER_OF, r3); + await testCachedRelation(r4, RELATION_MEMBER_OF, [r5, r4]); + await testCachedRelation(r5, RELATION_MEMBER_OF, [r5, r4]); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/* + * Test MEMBER_OF relation caching on aria radio buttons + */ +addAccessibleTask( + ` + <div role="radio" id="r1">I have no radio group</div><br> + <fieldset role="radiogroup" id="fs"> + <div role="radio" id="r2">hello</div><br> + <div role="radio" id="r3">world</div><br> + </fieldset>`, + async function (browser, accDoc) { + const r1 = findAccessibleChildByID(accDoc, "r1"); + const r2 = findAccessibleChildByID(accDoc, "r2"); + let r3 = findAccessibleChildByID(accDoc, "r3"); + + await testCachedRelation(r1, RELATION_MEMBER_OF, null); + await testCachedRelation(r2, RELATION_MEMBER_OF, [r2, r3]); + await testCachedRelation(r3, RELATION_MEMBER_OF, [r2, r3]); + const r = waitForEvent(EVENT_INNER_REORDER, "fs"); + await invokeContentTask(browser, [], () => { + let innerRadio = content.document.getElementById("r3"); + content.document.body.appendChild(innerRadio); + }); + await r; + + r3 = findAccessibleChildByID(accDoc, "r3"); + await testCachedRelation(r1, RELATION_MEMBER_OF, null); + await testCachedRelation(r2, RELATION_MEMBER_OF, r2); + await testCachedRelation(r3, RELATION_MEMBER_OF, null); + }, + { + chrome: true, + iframe: true, + remoteIframe: true, + } +); + +/* + * Test mutation of LABEL relations via accessible shutdown. + */ +addAccessibleTask( + ` + <div id="d"></div> + <label id="l"> + <select id="s"> + `, + async function (browser, accDoc) { + const label = findAccessibleChildByID(accDoc, "l"); + const select = findAccessibleChildByID(accDoc, "s"); + const div = findAccessibleChildByID(accDoc, "d"); + + await testCachedRelation(label, RELATION_LABEL_FOR, select); + await testCachedRelation(select, RELATION_LABELLED_BY, label); + await testCachedRelation(div, RELATION_LABELLED_BY, null); + + const r = waitForEvent(EVENT_REORDER, "l"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("s").remove(); + }); + await r; + await invokeContentTask(browser, [], () => { + const l = content.document.getElementById("l"); + l.htmlFor = "d"; + }); + await testCachedRelation(label, RELATION_LABEL_FOR, div); + await testCachedRelation(div, RELATION_LABELLED_BY, label); + }, + { + chrome: false, + iframe: true, + remoteIframe: true, + topLevel: true, + } +); + +/* + * Test mutation of LABEL relations via DOM ID reuse. + */ +addAccessibleTask( + ` + <div id="label">before</div><input id="input" aria-labelledby="label"> + `, + async function (browser, accDoc) { + let label = findAccessibleChildByID(accDoc, "label"); + const input = findAccessibleChildByID(accDoc, "input"); + + await testCachedRelation(label, RELATION_LABEL_FOR, input); + await testCachedRelation(input, RELATION_LABELLED_BY, label); + + const r = waitForEvent(EVENT_REORDER, accDoc); + await invokeContentTask(browser, [], () => { + content.document.getElementById("label").remove(); + let l = content.document.createElement("div"); + l.id = "label"; + l.textContent = "after"; + content.document.body.insertBefore( + l, + content.document.getElementById("input") + ); + }); + await r; + label = findAccessibleChildByID(accDoc, "label"); + await testCachedRelation(label, RELATION_LABEL_FOR, input); + await testCachedRelation(input, RELATION_LABELLED_BY, label); + }, + { + chrome: true, + iframe: true, + remoteIframe: true, + } +); + +/* + * Test LINKS_TO relation caching an anchor with multiple hashes + */ +addAccessibleTask( + ` + <a id="link" href="#foo#bar">Origin</a><br> + <a id="anchor" name="foo#bar">Destination`, + async function (browser, accDoc) { + const link = findAccessibleChildByID(accDoc, "link"); + const anchor = findAccessibleChildByID(accDoc, "anchor"); + + await testCachedRelation(link, RELATION_LINKS_TO, anchor); + }, + { + chrome: true, + // IA2 doesn't have a LINKS_TO relation and Windows non-cached + // RemoteAccessible uses IA2, so we can't run these tests in this case. + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/* + * Test mutation of LABEL relations via accessible shutdown. + */ +addAccessibleTask( + ` + <div id="d"></div> + <label id="l"> + <select id="s"> + `, + async function (browser, accDoc) { + const label = findAccessibleChildByID(accDoc, "l"); + const select = findAccessibleChildByID(accDoc, "s"); + const div = findAccessibleChildByID(accDoc, "d"); + + await testCachedRelation(label, RELATION_LABEL_FOR, select); + await testCachedRelation(select, RELATION_LABELLED_BY, label); + await testCachedRelation(div, RELATION_LABELLED_BY, null); + await untilCacheOk(() => { + try { + // We should get an acc ID back from this, but we don't have a way of + // verifying its correctness -- it should be the ID of the select. + return label.cache.getStringProperty("for"); + } catch (e) { + ok(false, "Exception thrown while trying to read from the cache"); + return false; + } + }, "Label for relation exists"); + + const r = waitForEvent(EVENT_REORDER, "l"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("s").remove(); + }); + await r; + await untilCacheOk(() => { + try { + label.cache.getStringProperty("for"); + } catch (e) { + // This property should no longer exist in the cache, so we should + // get an exception if we try to fetch it. + return true; + } + return false; + }, "Label for relation exists"); + + await invokeContentTask(browser, [], () => { + const l = content.document.getElementById("l"); + l.htmlFor = "d"; + }); + await testCachedRelation(label, RELATION_LABEL_FOR, div); + await testCachedRelation(div, RELATION_LABELLED_BY, label); + }, + { + /** + * This functionality is broken in our LocalAcccessible implementation, + * so we avoid running this test in chrome or when the cache is off. + */ + chrome: false, + iframe: true, + remoteIframe: true, + topLevel: true, + } +); + +/** + * Test label relations on HTML figure/figcaption. + */ +addAccessibleTask( + ` +<figure id="figure1"> + before + <figcaption id="caption1">caption1</figcaption> + after +</figure> +<figure id="figure2" aria-labelledby="label"> + <figcaption id="caption2">caption2</figure> +</figure> +<div id="label">label</div> + `, + async function (browser, docAcc) { + const figure1 = findAccessibleChildByID(docAcc, "figure1"); + let caption1 = findAccessibleChildByID(docAcc, "caption1"); + await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1); + await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1); + + info("Hiding caption1"); + let mutated = waitForEvent(EVENT_HIDE, caption1); + await invokeContentTask(browser, [], () => { + content.document.getElementById("caption1").hidden = true; + }); + await mutated; + await testCachedRelation(figure1, RELATION_LABELLED_BY, null); + + info("Showing caption1"); + mutated = waitForEvent(EVENT_SHOW, "caption1"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("caption1").hidden = false; + }); + caption1 = (await mutated).accessible; + await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1); + await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1); + + const figure2 = findAccessibleChildByID(docAcc, "figure2"); + const caption2 = findAccessibleChildByID(docAcc, "caption2"); + const label = findAccessibleChildByID(docAcc, "label"); + await testCachedRelation(figure2, RELATION_LABELLED_BY, [label, caption2]); + await testCachedRelation(caption2, RELATION_LABEL_FOR, figure2); + await testCachedRelation(label, RELATION_LABEL_FOR, figure2); + }, + { chrome: true, topLevel: true } +); + +/** + * Test details relations on popovers and their invokers. + */ +addAccessibleTask( + ` +<button id="hide" popovertarget="popover" popovertargetaction="hide">hide</button> +<button id="toggle1" popovertarget="popover">toggle1</button> +<button id="toggle2">toggle2</button> +<button id="toggleSibling">toggleSibling</button> +<div id="popover" popover>popover</div> +<div id="details">details</div> + `, + async function testPopover(browser, docAcc) { + // The popover is hidden, so nothing should be referring to it. + const hide = findAccessibleChildByID(docAcc, "hide"); + await testCachedRelation(hide, RELATION_DETAILS, []); + const toggle1 = findAccessibleChildByID(docAcc, "toggle1"); + await testCachedRelation(toggle1, RELATION_DETAILS, []); + const toggle2 = findAccessibleChildByID(docAcc, "toggle2"); + await testCachedRelation(toggle2, RELATION_DETAILS, []); + const toggleSibling = findAccessibleChildByID(docAcc, "toggleSibling"); + await testCachedRelation(toggleSibling, RELATION_DETAILS, []); + + info("Showing popover"); + let shown = waitForEvent(EVENT_SHOW, "popover"); + toggle1.doAction(0); + const popover = (await shown).accessible; + await testCachedRelation(toggle1, RELATION_DETAILS, popover); + // toggle2 shouldn't have a details relation because it doesn't have a + // popovertarget. + await testCachedRelation(toggle2, RELATION_DETAILS, []); + // hide shouldn't have a details relation because its action is hide. + await testCachedRelation(hide, RELATION_DETAILS, []); + // toggleSibling shouldn't have a details relation because it is a sibling + // of the popover. + await testCachedRelation(toggleSibling, RELATION_DETAILS, []); + await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1); + + info("Setting toggle2 popovertargetaction"); + await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover"); + await testCachedRelation(toggle2, RELATION_DETAILS, popover); + await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]); + + info("Removing toggle2 popovertarget"); + await invokeSetAttribute(browser, "toggle2", "popovertarget", null); + await testCachedRelation(toggle2, RELATION_DETAILS, []); + await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1); + + info("Setting aria-details on toggle1"); + await invokeSetAttribute(browser, "toggle1", "aria-details", "details"); + const details = findAccessibleChildByID(docAcc, "details"); + // aria-details overrides popover. + await testCachedRelation(toggle1, RELATION_DETAILS, details); + await testCachedRelation(popover, RELATION_DETAILS_FOR, []); + + info("Removing aria-details from toggle1"); + await invokeSetAttribute(browser, "toggle1", "aria-details", null); + await testCachedRelation(toggle1, RELATION_DETAILS, popover); + await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1); + + info("Hiding popover"); + let hidden = waitForEvent(EVENT_HIDE, popover); + toggle1.doAction(0); + // The relations between toggle1 and popover are removed when popover shuts + // down. However, this doesn't cause a cache update notification. Therefore, + // to avoid timing out in testCachedRelation, we must wait for a hide event + // first. + await hidden; + await testCachedRelation(toggle1, RELATION_DETAILS, []); + }, + { chrome: false, topLevel: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js new file mode 100644 index 0000000000..37f8c46966 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_states.js @@ -0,0 +1,552 @@ +/* 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/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * expected {Array} expected states for a given accessible that have the + * following format: + * [ + * expected state, + * expected extra state, + * absent state, + * absent extra state + * ] + * attrs {?Array} an optional list of attributes to update + * } + */ + +// State caching tests for attribute changes +const attributeTests = [ + { + desc: + "Checkbox with @checked attribute set to true should have checked " + + "state", + attrs: [ + { + attr: "checked", + value: "true", + }, + ], + expected: [STATE_CHECKED, 0], + }, + { + desc: "Checkbox with no @checked attribute should not have checked state", + attrs: [ + { + attr: "checked", + }, + ], + expected: [0, 0, STATE_CHECKED], + }, +]; + +// State caching tests for ARIA changes +const ariaTests = [ + { + desc: "File input has busy state when @aria-busy attribute is set to true", + attrs: [ + { + attr: "aria-busy", + value: "true", + }, + ], + expected: [STATE_BUSY, 0, STATE_REQUIRED | STATE_INVALID], + }, + { + desc: + "File input has required state when @aria-required attribute is set " + + "to true", + attrs: [ + { + attr: "aria-required", + value: "true", + }, + ], + expected: [STATE_REQUIRED, 0, STATE_INVALID], + }, + { + desc: + "File input has invalid state when @aria-invalid attribute is set to " + + "true", + attrs: [ + { + attr: "aria-invalid", + value: "true", + }, + ], + expected: [STATE_INVALID, 0], + }, +]; + +// Extra state caching tests +const extraStateTests = [ + { + desc: + "Input has no extra enabled state when aria and native disabled " + + "attributes are set at once", + attrs: [ + { + attr: "aria-disabled", + value: "true", + }, + { + attr: "disabled", + value: "true", + }, + ], + expected: [0, 0, 0, EXT_STATE_ENABLED], + }, + { + desc: + "Input has an extra enabled state when aria and native disabled " + + "attributes are unset at once", + attrs: [ + { + attr: "aria-disabled", + }, + { + attr: "disabled", + }, + ], + expected: [0, EXT_STATE_ENABLED], + }, +]; + +async function runStateTests(browser, accDoc, id, tests) { + let acc = findAccessibleChildByID(accDoc, id); + for (let { desc, attrs, expected } of tests) { + const [expState, expExtState, absState, absExtState] = expected; + info(desc); + let onUpdate = waitForEvent(EVENT_STATE_CHANGE, evt => { + if (getAccessibleDOMNodeID(evt.accessible) != id) { + return false; + } + // Events can be fired for states other than the ones we're interested + // in. If this happens, the states we're expecting might not be exposed + // yet. + const scEvt = evt.QueryInterface(nsIAccessibleStateChangeEvent); + if (scEvt.isExtraState) { + if (scEvt.state & expExtState || scEvt.state & absExtState) { + return true; + } + return false; + } + return scEvt.state & expState || scEvt.state & absState; + }); + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, id, attr, value); + } + await onUpdate; + testStates(acc, ...expected); + } +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask( + ` + <input id="checkbox" type="checkbox"> + <input id="file" type="file"> + <input id="text">`, + async function (browser, accDoc) { + await runStateTests(browser, accDoc, "checkbox", attributeTests); + await runStateTests(browser, accDoc, "file", ariaTests); + await runStateTests(browser, accDoc, "text", extraStateTests); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test caching of the focused state. + */ +addAccessibleTask( + ` + <button id="b1">b1</button> + <button id="b2">b2</button> + `, + async function (browser, docAcc) { + const b1 = findAccessibleChildByID(docAcc, "b1"); + const b2 = findAccessibleChildByID(docAcc, "b2"); + + let focused = waitForEvent(EVENT_FOCUS, b1); + await invokeFocus(browser, "b1"); + await focused; + testStates(docAcc, 0, 0, STATE_FOCUSED); + testStates(b1, STATE_FOCUSED); + testStates(b2, 0, 0, STATE_FOCUSED); + + focused = waitForEvent(EVENT_FOCUS, b2); + await invokeFocus(browser, "b2"); + await focused; + testStates(b2, STATE_FOCUSED); + testStates(b1, 0, 0, STATE_FOCUSED); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test that the document initially gets the focused state. + * We can't do this in the test above because that test runs in iframes as well + * as a top level document. + */ +addAccessibleTask( + ` + <button id="b1">b1</button> + <button id="b2">b2</button> + `, + async function (browser, docAcc) { + testStates(docAcc, STATE_FOCUSED); + } +); + +/** + * Test caching of the focused state in iframes. + */ +addAccessibleTask( + ` + <button id="button">button</button> + `, + async function (browser, iframeDocAcc, topDocAcc) { + testStates(topDocAcc, STATE_FOCUSED); + const button = findAccessibleChildByID(iframeDocAcc, "button"); + testStates(button, 0, 0, STATE_FOCUSED); + let focused = waitForEvent(EVENT_FOCUS, button); + info("Focusing button in iframe"); + button.takeFocus(); + await focused; + testStates(topDocAcc, 0, 0, STATE_FOCUSED); + testStates(button, STATE_FOCUSED); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); + +/** + * Test caching of the focusable state in iframes which are initially visibility: hidden. + */ +addAccessibleTask( + ` +<button id="button"></button> +<span id="span" tabindex="-1">span</span>`, + async function (browser, topDocAcc) { + info("Changing visibility on iframe"); + let reordered = waitForEvent(EVENT_REORDER, topDocAcc); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], iframeId => { + content.document.getElementById(iframeId).style.visibility = ""; + }); + await reordered; + // The iframe doc a11y tree might not be built yet. + const iframeDoc = await TestUtils.waitForCondition(() => + findAccessibleChildByID(topDocAcc, DEFAULT_IFRAME_DOC_BODY_ID) + ); + // Log/verify whether this is an in-process or OOP iframe. + await comparePIDs(browser, gIsRemoteIframe); + const button = findAccessibleChildByID(iframeDoc, "button"); + testStates(button, STATE_FOCUSABLE); + const span = findAccessibleChildByID(iframeDoc, "span"); + ok(span, "span Accessible exists"); + testStates(span, STATE_FOCUSABLE); + }, + { + topLevel: false, + iframe: true, + remoteIframe: true, + iframeAttrs: { style: "visibility: hidden;" }, + skipFissionDocLoad: true, + } +); + +function checkOpacity(acc, present) { + let [, extraState] = getStates(acc); + let currOpacity = extraState & EXT_STATE_OPAQUE; + return present ? currOpacity : !currOpacity; +} + +/** + * Test caching of the OPAQUE1 state. + */ +addAccessibleTask( + ` + <div id="div">hello world</div> + `, + async function (browser, docAcc) { + const div = findAccessibleChildByID(docAcc, "div"); + await untilCacheOk(() => checkOpacity(div, true), "Found opaque state"); + + await invokeContentTask(browser, [], () => { + let elm = content.document.getElementById("div"); + elm.style = "opacity: 0.4;"; + elm.offsetTop; // Flush layout. + }); + + await untilCacheOk( + () => checkOpacity(div, false), + "Did not find opaque state" + ); + + await invokeContentTask(browser, [], () => { + let elm = content.document.getElementById("div"); + elm.style = "opacity: 1;"; + elm.offsetTop; // Flush layout. + }); + + await untilCacheOk(() => checkOpacity(div, true), "Found opaque state"); + }, + { iframe: true, remoteIframe: true, chrome: true } +); + +/** + * Test caching of the editable state. + */ +addAccessibleTask( + `<div id="div" contenteditable></div>`, + async function (browser, docAcc) { + const div = findAccessibleChildByID(docAcc, "div"); + testStates(div, 0, EXT_STATE_EDITABLE, 0, 0); + // Ensure that a contentEditable descendant doesn't cause editable to be + // exposed on the document. + testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE); + + info("Setting contentEditable on the body"); + let stateChanged = Promise.all([ + waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true), + waitForStateChange(docAcc, STATE_READONLY, false, false), + ]); + await invokeContentTask(browser, [], () => { + content.document.body.contentEditable = true; + }); + await stateChanged; + testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0); + + info("Clearing contentEditable on the body"); + stateChanged = Promise.all([ + waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true), + waitForStateChange(docAcc, STATE_READONLY, true, false), + ]); + await invokeContentTask(browser, [], () => { + content.document.body.contentEditable = false; + }); + await stateChanged; + testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE); + + info("Clearing contentEditable on div"); + stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, false, true); + await invokeContentTask(browser, [], () => { + content.document.getElementById("div").contentEditable = false; + }); + await stateChanged; + testStates(div, 0, 0, 0, EXT_STATE_EDITABLE); + + info("Setting contentEditable on div"); + stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, true, true); + await invokeContentTask(browser, [], () => { + content.document.getElementById("div").contentEditable = true; + }); + await stateChanged; + testStates(div, 0, EXT_STATE_EDITABLE, 0, 0); + + info("Setting designMode on document"); + stateChanged = Promise.all([ + waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true), + waitForStateChange(docAcc, STATE_READONLY, false, false), + ]); + await invokeContentTask(browser, [], () => { + content.document.designMode = "on"; + }); + await stateChanged; + testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0); + + info("Clearing designMode on document"); + stateChanged = Promise.all([ + waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true), + waitForStateChange(docAcc, STATE_READONLY, true, false), + ]); + await invokeContentTask(browser, [], () => { + content.document.designMode = "off"; + }); + await stateChanged; + testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE); + }, + { topLevel: true, iframe: true, remoteIframe: true, chrome: true } +); + +/** + * Test caching of the stale and busy states. + */ +addAccessibleTask( + `<iframe id="iframe"></iframe>`, + async function (browser, docAcc) { + const iframe = findAccessibleChildByID(docAcc, "iframe"); + info("Setting iframe src"); + // This iframe won't finish loading. Thus, it will get the stale state and + // won't fire a document load complete event. We use the reorder event on + // the iframe to know when the document has been created. + let reordered = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + content.document.getElementById("iframe").src = + 'data:text/html,<img src="http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs">'; + }); + const iframeDoc = (await reordered).accessible.firstChild; + testStates(iframeDoc, STATE_BUSY, EXT_STATE_STALE, 0, 0); + + info("Finishing load of iframe doc"); + let loadCompleted = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, iframeDoc); + await fetch( + "https://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete" + ); + await loadCompleted; + testStates(iframeDoc, 0, 0, STATE_BUSY, EXT_STATE_STALE); + }, + { topLevel: true, chrome: true } +); + +/** + * Test implicit selected state. + */ +addAccessibleTask( + ` +<div role="tablist"> + <div id="noSel" role="tab" tabindex="0">noSel</div> + <div id="selFalse" role="tab" aria-selected="false" tabindex="0">selFalse</div> +</div> +<div role="listbox" aria-multiselectable="true"> + <div id="multiNoSel" role="option" tabindex="0">multiNoSel</div> +</div> + `, + async function (browser, docAcc) { + const noSel = findAccessibleChildByID(docAcc, "noSel"); + testStates(noSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0); + info("Focusing noSel"); + let focused = waitForEvent(EVENT_FOCUS, noSel); + noSel.takeFocus(); + await focused; + testStates(noSel, STATE_FOCUSED | STATE_SELECTED, 0, 0, 0); + + const selFalse = findAccessibleChildByID(docAcc, "selFalse"); + testStates(selFalse, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0); + info("Focusing selFalse"); + focused = waitForEvent(EVENT_FOCUS, selFalse); + selFalse.takeFocus(); + await focused; + testStates(selFalse, STATE_FOCUSED, 0, STATE_SELECTED, 0); + + const multiNoSel = findAccessibleChildByID(docAcc, "multiNoSel"); + testStates(multiNoSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0); + info("Focusing multiNoSel"); + focused = waitForEvent(EVENT_FOCUS, multiNoSel); + multiNoSel.takeFocus(); + await focused; + testStates(multiNoSel, STATE_FOCUSED, 0, STATE_SELECTED, 0); + }, + { topLevel: true, iframe: true, remoteIframe: true, chrome: true } +); + +/** + * Test invalid state determined via DOM. + */ +addAccessibleTask( + `<input type="email" id="email">`, + async function (browser, docAcc) { + const email = findAccessibleChildByID(docAcc, "email"); + info("Focusing email"); + let focused = waitForEvent(EVENT_FOCUS, email); + email.takeFocus(); + await focused; + info("Typing a"); + let invalidChanged = waitForStateChange(email, STATE_INVALID, true); + EventUtils.sendString("a"); + await invalidChanged; + testStates(email, STATE_INVALID); + info("Typing @b"); + invalidChanged = waitForStateChange(email, STATE_INVALID, false); + EventUtils.sendString("@b"); + await invalidChanged; + testStates(email, 0, 0, STATE_INVALID); + info("Typing backspace"); + invalidChanged = waitForStateChange(email, STATE_INVALID, true); + EventUtils.synthesizeKey("KEY_Backspace"); + await invalidChanged; + testStates(email, STATE_INVALID); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); + +/** + * Test caching of the expanded state for popover target element. + */ +addAccessibleTask( + ` + <button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button> + <button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button> + <button id="toggle">toggle</button> + <div id="mypopover" popover> + Popover content + <button id="hide-inside" popovertarget="mypopover" popovertargetaction="hide">Hide inside popover</button> + </div> + `, + async function (browser, docAcc) { + const show = findAccessibleChildByID(docAcc, "show-popover-btn"); + const hide = findAccessibleChildByID(docAcc, "hide-popover-btn"); + testStates(show, STATE_COLLAPSED, 0); + testStates(hide, STATE_COLLAPSED, 0); + const toggle = findAccessibleChildByID(docAcc, "toggle"); + testStates( + toggle, + 0, + 0, + STATE_EXPANDED | STATE_COLLAPSED, + EXT_STATE_EXPANDABLE + ); + + info("Setting toggle's popovertarget"); + let stateChanged = waitForStateChange( + toggle, + EXT_STATE_EXPANDABLE, + true, + true + ); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("toggle") + .setAttribute("popovertarget", "mypopover"); + }); + await stateChanged; + + // Changes to the popover should fire events on all invokers. + const changeEvents = [ + [EVENT_STATE_CHANGE, show], + [EVENT_STATE_CHANGE, hide], + [EVENT_STATE_CHANGE, toggle], + ]; + info("Expanding popover"); + let onShowing = waitForEvents(changeEvents); + await show.doAction(0); + await onShowing; + testStates(show, STATE_EXPANDED, 0); + testStates(hide, STATE_EXPANDED, 0); + testStates(toggle, STATE_EXPANDED, 0); + const hideInside = findAccessibleChildByID(show, "hide-inside"); + testStates(hideInside, 0, 0, STATE_EXPANDED | STATE_COLLAPSED, 0); + + info("Collapsing popover"); + let onHiding = waitForEvents(changeEvents); + await hide.doAction(0); + await onHiding; + testStates(hide, STATE_COLLAPSED, 0); + testStates(show, STATE_COLLAPSED, 0); + testStates(toggle, STATE_COLLAPSED, 0); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_table.js b/accessible/tests/browser/e10s/browser_caching_table.js new file mode 100644 index 0000000000..9c8bcb9616 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_table.js @@ -0,0 +1,665 @@ +/* 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/. */ + +/** + * Test tables for both local and remote Accessibles. There is more extensive + * coverage in ../../mochitest/table. These tests are primarily to ensure that + * the cache works as expected and that there is consistency between local and + * remote. + */ + +"use strict"; + +/* import-globals-from ../../mochitest/table.js */ +/* import-globals-from ../../mochitest/attributes.js */ +loadScripts( + { name: "table.js", dir: MOCHITESTS_DIR }, + { name: "attributes.js", dir: MOCHITESTS_DIR } +); + +/** + * Test table counts, indexes, extents and implicit headers. + */ +addAccessibleTask( + ` +<table id="table"> + <thead> + <tr><th id="a">a</th><th id="bc" colspan="2">bc</th><th id="d">d</th></tr> + </thead> + <tbody> + <tr><th id="ei" rowspan="2">ei</th><td id="fj" rowspan="0">fj</td><td id="g">g</td><td id="h">h</td></tr> + <tr><td id="k">k</td></tr> + </tbody> +</table> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 3, "table rowCount correct"); + is(table.columnCount, 4, "table columnCount correct"); + testTableIndexes(table, [ + [0, 1, 1, 2], + [3, 4, 5, 6], + [3, 4, 7, -1], + ]); + const cells = {}; + for (const id of ["a", "bc", "d", "ei", "fj", "g", "h", "k"]) { + cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]); + } + is(cells.a.rowExtent, 1, "a rowExtent correct"); + is(cells.a.columnExtent, 1, "a columnExtent correct"); + is(cells.bc.rowExtent, 1, "bc rowExtent correct"); + is(cells.bc.columnExtent, 2, "bc columnExtent correct"); + is(cells.ei.rowExtent, 2, "ei rowExtent correct"); + is(cells.fj.rowExtent, 2, "fj rowExtent correct"); + testHeaderCells([ + { + cell: cells.ei, + rowHeaderCells: [], + columnHeaderCells: [cells.a], + }, + { + cell: cells.g, + rowHeaderCells: [cells.ei], + columnHeaderCells: [cells.bc], + }, + { + cell: cells.k, + rowHeaderCells: [cells.ei], + columnHeaderCells: [cells.bc], + }, + ]); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test table explicit headers. + */ +addAccessibleTask( + ` +<table id="table"> + <tr><th id="a">a</th><th id="b">b</th></tr> + <tr><td id="c" headers="b d">c</td><th scope="row" id="d">d</th></tr> + <tr><td id="e" headers="c f">e</td><td id="f">f</td></tr> +</table> + `, + async function (browser, docAcc) { + const cells = {}; + for (const id of ["a", "b", "c", "d", "e", "f"]) { + cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]); + } + testHeaderCells([ + { + cell: cells.c, + rowHeaderCells: [cells.d], + columnHeaderCells: [cells.b], + }, + { + cell: cells.e, + rowHeaderCells: [cells.f], + columnHeaderCells: [cells.c], + }, + ]); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test that an inner table doesn't impact an outer table. + */ +addAccessibleTask( + ` +<table id="outerTable"> + <tr><th id="outerCell">outerCell<table id="innerTable"> + <tr><th id="innerCell">a</th></tr></table> + </table></th></tr> +</table> + `, + async function (browser, docAcc) { + const outerTable = findAccessibleChildByID(docAcc, "outerTable", [ + nsIAccessibleTable, + ]); + is(outerTable.rowCount, 1, "outerTable rowCount correct"); + is(outerTable.columnCount, 1, "outerTable columnCount correct"); + const outerCell = findAccessibleChildByID(docAcc, "outerCell"); + is( + outerTable.getCellAt(0, 0), + outerCell, + "outerTable returns correct cell" + ); + const innerTable = findAccessibleChildByID(docAcc, "innerTable", [ + nsIAccessibleTable, + ]); + is(innerTable.rowCount, 1, "innerTable rowCount correct"); + is(innerTable.columnCount, 1, "innerTable columnCount correct"); + const innerCell = findAccessibleChildByID(docAcc, "innerCell"); + is( + innerTable.getCellAt(0, 0), + innerCell, + "innerTable returns correct cell" + ); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test table caption and summary. + */ +addAccessibleTask( + ` +<table id="t1"> + <caption id="c1">c1</caption> + <tr><th>a</th></tr> +</table> +<table id="t2" summary="s2"> + <tr><th>a</th></tr> +</table> +<table id="t3" summary="s3"> + <caption id="c3">c3</caption> + <tr><th>a</th></tr> +</table> + `, + async function (browser, docAcc) { + const t1 = findAccessibleChildByID(docAcc, "t1", [nsIAccessibleTable]); + const c1 = findAccessibleChildByID(docAcc, "c1"); + is(t1.caption, c1, "t1 caption correct"); + ok(!t1.summary, "t1 no summary"); + const t2 = findAccessibleChildByID(docAcc, "t2", [nsIAccessibleTable]); + ok(!t2.caption, "t2 caption is null"); + is(t2.summary, "s2", "t2 summary correct"); + const t3 = findAccessibleChildByID(docAcc, "t3", [nsIAccessibleTable]); + const c3 = findAccessibleChildByID(docAcc, "c3"); + is(t3.caption, c3, "t3 caption correct"); + is(t3.summary, "s3", "t3 summary correct"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test table layout guess. + */ +addAccessibleTask( + ` +<table id="layout"><tr><td>a</td></tr></table> +<table id="data"><tr><th>a</th></tr></table> +<table id="mutate"><tr><td>a</td><td>b</td></tr></table> +<div id="newTableContainer"></div> + `, + async function (browser, docAcc) { + const layout = findAccessibleChildByID(docAcc, "layout"); + testAttrs(layout, { "layout-guess": "true" }, true); + const data = findAccessibleChildByID(docAcc, "data"); + testAbsentAttrs(data, { "layout-guess": "true" }); + const mutate = findAccessibleChildByID(docAcc, "mutate"); + testAttrs(mutate, { "layout-guess": "true" }, true); + + info("mutate: Adding 5 rows"); + let reordered = waitForEvent(EVENT_REORDER, mutate); + await invokeContentTask(browser, [], () => { + const frag = content.document.createDocumentFragment(); + for (let r = 0; r < 6; ++r) { + const tr = content.document.createElement("tr"); + tr.innerHTML = "<td>a</td><td>b</td>"; + frag.append(tr); + } + content.document.getElementById("mutate").tBodies[0].append(frag); + }); + await reordered; + testAbsentAttrs(mutate, { "layout-guess": "true" }); + + info("mutate: Removing 5 rows"); + reordered = waitForEvent(EVENT_REORDER, mutate); + await invokeContentTask(browser, [], () => { + // Pause refresh driver so all the children removals below will + // be collated into the same tick and only one 'reorder' event will + // be dispatched. + content.windowUtils.advanceTimeAndRefresh(100); + + let tBody = content.document.getElementById("mutate").tBodies[0]; + for (let r = 0; r < 6; ++r) { + tBody.lastChild.remove(); + } + + // Resume refresh driver + content.windowUtils.restoreNormalRefresh(); + }); + await reordered; + testAttrs(mutate, { "layout-guess": "true" }, true); + + info("mutate: Adding new table"); + let shown = waitForEvent(EVENT_SHOW, "newTable"); + await invokeContentTask(browser, [], () => { + content.document.getElementById( + "newTableContainer" + ).innerHTML = `<table id="newTable"><tr><th>a</th></tr></table>`; + }); + let newTable = (await shown).accessible; + testAbsentAttrs(newTable, { "layout-guess": "true" }); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test table layout guess with border styling changes. + */ +addAccessibleTask( + ` + <table id="layout"><tr><td id="cell">a</td><td>b</td></tr> + <tr><td>c</td><td>d</td></tr><tr><td>c</td><td>d</td></tr></table> + `, + async function (browser, docAcc) { + const layout = findAccessibleChildByID(docAcc, "layout"); + testAttrs(layout, { "layout-guess": "true" }, true); + info("changing border style on table cell"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("cell").style.border = "1px solid black"; + content.document.body.offsetTop; // Flush layout. + }); + await untilCacheOk(() => { + // manually verify the attribute doesn't exist, since `testAbsentAttrs` + // has internal calls to ok() which fail if the cache hasn't yet updated + for (let prop of layout.attributes.enumerate()) { + if (prop.key == "layout-guess") { + return false; + } + } + return true; + }, "Table is a data table"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test ARIA grid. + */ +addAccessibleTask( + ` +<div id="grid" role="grid"> + <div role="rowgroup"> + <div role="row"><div id="a" role="columnheader">a</div><div id="b" role="columnheader">b</div></div> + </div> + <div tabindex="-1"> + <div role="row"><div id="c" role="rowheader">c</div><div id="d" role="gridcell">d</div></div> + </div> +</div> + `, + async function (browser, docAcc) { + const grid = findAccessibleChildByID(docAcc, "grid", [nsIAccessibleTable]); + is(grid.rowCount, 2, "grid rowCount correct"); + is(grid.columnCount, 2, "grid columnCount correct"); + testTableIndexes(grid, [ + [0, 1], + [2, 3], + ]); + const cells = {}; + for (const id of ["a", "b", "c", "d"]) { + cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]); + } + is(cells.a.rowExtent, 1, "a rowExtent correct"); + is(cells.a.columnExtent, 1, "a columnExtent correct"); + testHeaderCells([ + { + cell: cells.c, + rowHeaderCells: [], + columnHeaderCells: [cells.a], + }, + { + cell: cells.d, + rowHeaderCells: [cells.c], + columnHeaderCells: [cells.b], + }, + ]); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +function setNodeHidden(browser, id, hidden) { + return invokeContentTask(browser, [id, hidden], (cId, cHidden) => { + content.document.getElementById(cId).hidden = cHidden; + }); +} + +/** + * Test that the table is updated correctly when it is mutated. + */ +addAccessibleTask( + ` +<table id="table"> + <tr id="r1"><td>a</td><td id="b">b</td></tr> + <tr id="r2" hidden><td>c</td><td>d</td></tr> +</table> +<div id="owner"></div> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 1, "table rowCount correct"); + is(table.columnCount, 2, "table columnCount correct"); + testTableIndexes(table, [[0, 1]]); + info("Showing r2"); + let reordered = waitForEvent(EVENT_REORDER, table); + await setNodeHidden(browser, "r2", false); + await reordered; + is(table.rowCount, 2, "table rowCount correct"); + testTableIndexes(table, [ + [0, 1], + [2, 3], + ]); + info("Hiding r2"); + reordered = waitForEvent(EVENT_REORDER, table); + await setNodeHidden(browser, "r2", true); + await reordered; + is(table.rowCount, 1, "table rowCount correct"); + testTableIndexes(table, [[0, 1]]); + info("Hiding b"); + reordered = waitForEvent(EVENT_REORDER, "r1"); + await setNodeHidden(browser, "b", true); + await reordered; + is(table.columnCount, 1, "table columnCount correct"); + testTableIndexes(table, [[0]]); + info("Showing b"); + reordered = waitForEvent(EVENT_REORDER, "r1"); + await setNodeHidden(browser, "b", false); + await reordered; + is(table.columnCount, 2, "table columnCount correct"); + info("Moving b out of table using aria-owns"); + reordered = waitForEvent(EVENT_REORDER, "r1"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("owner").setAttribute("aria-owns", "b"); + }); + await reordered; + is(table.columnCount, 1, "table columnCount correct"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test the handling of ARIA tables with display: contents. + */ +addAccessibleTask( + ` +<div id="table" role="table" style="display: contents;"> + <div role="row"><div role="cell">a</div></div> +</div> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 1, "table rowCount correct"); + is(table.columnCount, 1, "table columnCount correct"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test a broken ARIA table with an invalid cell. + */ +addAccessibleTask( + ` +<div id="table" role="table"> + <div role="main"> + <div role="row"> + <div id="cell" role="cell">a</div> + </div> + </div> +</div> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 0, "table rowCount correct"); + is(table.columnCount, 0, "table columnCount correct"); + const cell = findAccessibleChildByID(docAcc, "cell"); + let queryOk = false; + try { + cell.QueryInterface(nsIAccessibleTableCell); + queryOk = true; + } catch (e) {} + ok(!queryOk, "Got nsIAccessibleTableCell on an invalid cell"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + remoteIframe: true, + } +); + +/** + * Test that building the cache for a malformed table with an iframe inside a + * row doesn't crash (bug 1800780). + */ +addAccessibleTask( + `<table><tr id="tr"></tr></table>`, + async function (browser, docAcc) { + let reordered = waitForEvent(EVENT_REORDER, "tr"); + await invokeContentTask(browser, [], () => { + const iframe = content.document.createElement("iframe"); + content.document.getElementById("tr").append(iframe); + }); + await reordered; + }, + { topLevel: true } +); + +/** + * Verify that table row and column information is correct when there are + * intervening generics between the table and a rowgroup. + */ +addAccessibleTask( + ` +<div id="table" role="grid"> + <div role="rowgroup"> + <div role="row"> + <div role="columnheader">a</div> + </div> + </div> + <div tabindex="-1" style="height: 1px; overflow: auto;"> + <div role="rowgroup"> + <div role="row"> + <div id="cell" role="gridcell">b</div> + </div> + </div> + </div> +</div> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + + info("Verifying that the table row and column counts are correct."); + is(table.rowCount, 2, "table rowCount correct"); + is(table.columnCount, 1, "table columnCount correct"); + + info("Verifying that the cell row and column extents are correct."); + const cell = findAccessibleChildByID(docAcc, "cell", [ + nsIAccessibleTableCell, + ]); + is(cell.rowExtent, 1, "cell rowExtent correct"); + is(cell.columnExtent, 1, "cell colExtent correct"); + is(cell.rowIndex, 1, "cell rowIndex correct"); + is(cell.columnIndex, 0, "cell columnIndex correct"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Verify that table row and column information is correct when there are + * intervening generics between rows and cells. + */ +addAccessibleTask( + ` +<div id="table" role="grid"> + <div role="rowgroup"> + <div role="row"> + <div role="columnheader">a</div> + </div> + </div> + <div role="rowgroup"> + <div role="row"> + <div tabindex="-1" style="height: 1px; overflow: auto;"> + <div id="cell" role="gridcell">b</div> + </div> + </div> + </div> +</div> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + + info("Verifying that the table row and column counts are correct."); + is(table.rowCount, 2, "table rowCount correct"); + is(table.columnCount, 1, "table columnCount correct"); + + info("Verifying that the cell row and column extents are correct."); + const cell = findAccessibleChildByID(docAcc, "cell", [ + nsIAccessibleTableCell, + ]); + is(cell.rowExtent, 1, "cell rowExtent correct"); + is(cell.columnExtent, 1, "cell colExtent correct"); + is(cell.rowIndex, 1, "cell rowIndex correct"); + is(cell.columnIndex, 0, "cell columnIndex correct"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Verify that we don't crash for authoring error like <table role="gridcell">. + */ +addAccessibleTask( + `<table id="table" role="gridcell">`, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table"); + ok(table, "Retrieved table Accessible"); + }, + { chrome: true, topLevel: true } +); + +/** + * Test ARIA tables in SVG. + */ +addAccessibleTask( + ` +<svg id="table" role="table"> + <text id="caption" role="caption">caption</text> + <g role="row"> + <text id="a" role="columnheader">a</text> + <text id="b" role="columnheader">b</text> + </g> + <g role="row"> + <text id="c" role="cell">c</text> + <text id="d" role="cell">d</text> + </g> +</svg> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 2, "table rowCount correct"); + is(table.columnCount, 2, "table columnCount correct"); + const caption = findAccessibleChildByID(docAcc, "caption"); + is(table.caption, caption, "table caption correct"); + testTableIndexes(table, [ + [0, 1], + [2, 3], + ]); + const cells = {}; + for (const id of ["a", "b", "c", "d"]) { + cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]); + } + testHeaderCells([ + { + cell: cells.c, + rowHeaderCells: [], + columnHeaderCells: [cells.a], + }, + { + cell: cells.d, + rowHeaderCells: [], + columnHeaderCells: [cells.b], + }, + ]); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); + +/** + * Verify that we don't crash for authoring error like <tr role="grid">. + */ +addAccessibleTask( + ` +<table id="table"> + <tr><th>a</th></tr> + <tr role="grid"><td id="b">b</td></tr> +</table> + `, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table", [ + nsIAccessibleTable, + ]); + is(table.rowCount, 1, "table rowCount correct"); + is(table.columnCount, 1, "table columnCount correct"); + const b = findAccessibleChildByID(docAcc, "b"); + let queryOk = false; + try { + b.QueryInterface(nsIAccessibleTableCell); + queryOk = true; + } catch (e) {} + ok(!queryOk, "No nsIAccessibleTableCell on invalid cell b"); + } +); diff --git a/accessible/tests/browser/e10s/browser_caching_text_bounds.js b/accessible/tests/browser/e10s/browser_caching_text_bounds.js new file mode 100644 index 0000000000..3e37bf7490 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_text_bounds.js @@ -0,0 +1,737 @@ +/* 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"; +requestLongerTimeout(3); + +/* import-globals-from ../../mochitest/layout.js */ +loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); + +// Note that testTextNode, testChar and testTextRange currently don't handle +// white space in the code that doesn't get rendered on screen. To work around +// this, ensure that containers you want to test are all on a single line in the +// test snippet. + +async function testTextNode(accDoc, browser, id) { + await testTextRange(accDoc, browser, id, 0, -1); +} + +async function testChar(accDoc, browser, id, idx) { + await testTextRange(accDoc, browser, id, idx, idx + 1); +} + +async function testTextRange(accDoc, browser, id, start, end) { + const r = await invokeContentTask( + browser, + [id, start, end], + (_id, _start, _end) => { + const htNode = content.document.getElementById(_id); + let [eX, eY, eW, eH] = [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + 0, + 0, + ]; + let traversed = 0; + let localStart = _start; + let endTraversal = false; + for (let element of htNode.childNodes) { + // ignore whitespace, but not embedded elements + let isEmbeddedElement = false; + if (element.length == undefined) { + let potentialTextContainer = element; + while ( + potentialTextContainer && + potentialTextContainer.length == undefined + ) { + potentialTextContainer = element.firstChild; + } + if (potentialTextContainer && potentialTextContainer.length) { + // If we can reach some text from this container, use that as part + // of our range. This is important when testing with intervening inline + // elements. ie. <pre><code>ab%0acd + element = potentialTextContainer; + } else if (element.firstChild) { + isEmbeddedElement = true; + } else { + continue; + } + } + if (element.length + traversed < _start) { + // If our start index is not within this + // node, keep looking. + traversed += element.length; + localStart -= element.length; + continue; + } + + let rect; + if (isEmbeddedElement) { + rect = element.getBoundingClientRect(); + } else { + const range = content.document.createRange(); + range.setStart(element, localStart); + + if (_end != -1 && _end - traversed <= element.length) { + // If the current node contains + // our end index, stop here. + endTraversal = true; + range.setEnd(element, _end - traversed); + } else { + range.setEnd(element, element.length); + } + + rect = range.getBoundingClientRect(); + } + + const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX; + const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY; + eX = Math.min(eX, rect.x); + eY = Math.min(eY, rect.y); + eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX); + eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY); + + if (endTraversal) { + break; + } + localStart = 0; + traversed += element.length; + } + return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)]; + } + ); + let hyperTextNode = findAccessibleChildByID(accDoc, id); + + // Add in the doc's screen coords because getBoundingClientRect + // is relative to the document, not the screen. This assumes the doc's + // screen coords are correct. We use getBoundsInCSSPixels to avoid factoring + // in the DPR ourselves. + let x = {}; + let y = {}; + let w = {}; + let h = {}; + accDoc.getBoundsInCSSPixels(x, y, w, h); + r[0] += x.value; + r[1] += y.value; + if (end != -1 && end - start == 1) { + // If we're only testing a character, use this function because it calls + // CharBounds() directly instead of TextBounds(). + testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE); + } else { + testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE); + } +} + +/** + * Since testTextChar can't handle non-rendered white space, this function first + * uses testTextChar to verify the first character and then ensures all + * characters thereafter have an incrementing x and a non-0 width. + */ +async function testLineWithNonRenderedSpace(docAcc, browser, id, length) { + await testChar(docAcc, browser, id, 0); + const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]); + let prevX = -1; + for (let offset = 0; offset < length; ++offset) { + const x = {}; + const y = {}; + const w = {}; + const h = {}; + acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE); + ok(x.value > prevX, `${id}: offset ${offset} x is larger (${x.value})`); + prevX = x.value; + ok(w.value > 0, `${id}: offset ${offset} width > 0`); + } +} + +/** + * Test the text range boundary for simple LtR text + */ +addAccessibleTask( + ` + <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2' style='font-family: monospace;'>ل</p> + <p id='p3' dir='ltr' style='font-family: monospace;'>Привіт Світ</p> + <pre id='p4' style='font-family: monospace;'>a%0abcdef</pre> + `, + async function (browser, accDoc) { + info("Testing simple LtR text"); + await testTextNode(accDoc, browser, "p1"); + await testTextNode(accDoc, browser, "p2"); + await testTextNode(accDoc, browser, "p3"); + await testTextNode(accDoc, browser, "p4"); + }, + { + iframe: true, + } +); + +/** + * Test the partial text range boundary for LtR text + */ +addAccessibleTask( + ` + <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2' dir='ltr' style='font-family: monospace;'>Привіт Світ</p> + `, + async function (browser, accDoc) { + info("Testing partial ranges in LtR text"); + await testTextRange(accDoc, browser, "p1", 0, 4); + await testTextRange(accDoc, browser, "p1", 2, 8); + await testTextRange(accDoc, browser, "p1", 12, 17); + await testTextRange(accDoc, browser, "p2", 0, 4); + await testTextRange(accDoc, browser, "p2", 2, 8); + await testTextRange(accDoc, browser, "p2", 6, 11); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test the text boundary for multiline LtR text + */ +addAccessibleTask( + ` + <p id='p4' dir='ltr' style='font-family: monospace;'>Привіт Світ<br>Привіт Світ</p> + <p id='p5' dir='ltr' style='font-family: monospace;'>Привіт Світ<br> Я ще трохи тексту в другому рядку</p> + <p id='p6' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p> + <p id='p7' style='font-family: monospace;'>hello world<br>hello world</p> + `, + async function (browser, accDoc) { + info("Testing multiline LtR text"); + await testTextNode(accDoc, browser, "p4"); + await testTextNode(accDoc, browser, "p5"); + // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping) + await testTextNode(accDoc, browser, "p7"); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test the text boundary for simple RtL text + */ +addAccessibleTask( + ` + <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2' dir='rtl' style='font-family: monospace;'>ل</p> + <p id='p3' dir='rtl' style='font-family: monospace;'>لل لللل لل</p> + <pre id='p4' dir='rtl' style='font-family: monospace;'>a%0abcdef</pre> + `, + async function (browser, accDoc) { + info("Testing simple RtL text"); + await testTextNode(accDoc, browser, "p1"); + await testTextNode(accDoc, browser, "p2"); + await testTextNode(accDoc, browser, "p3"); + await testTextNode(accDoc, browser, "p4"); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test the text boundary for multiline RtL text + */ +addAccessibleTask( + ` + <p id='p4' dir='rtl' style='font-family: monospace;'>لل لللل لل<br>لل لللل لل</p> + <p id='p5' dir='rtl' style='font-family: monospace;'>لل لللل لل<br> لل لل لل لل ل لل لل لل</p> + <p id='p6' dir='rtl' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p> + <p id='p7' dir='rtl' style='font-family: monospace;'>hello world<br>hello world</p> + `, + async function (browser, accDoc) { + info("Testing multiline RtL text"); + await testTextNode(accDoc, browser, "p4"); + //await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char + // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping) + await testTextNode(accDoc, browser, "p7"); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test the partial text range boundary for RtL text + */ +addAccessibleTask( + ` + <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2' dir='rtl' style='font-family: monospace;'>لل لللل لل</p> + `, + async function (browser, accDoc) { + info("Testing partial ranges in RtL text"); + await testTextRange(accDoc, browser, "p1", 0, 4); + await testTextRange(accDoc, browser, "p1", 2, 8); + await testTextRange(accDoc, browser, "p1", 12, 17); + await testTextRange(accDoc, browser, "p2", 0, 4); + await testTextRange(accDoc, browser, "p2", 2, 8); + await testTextRange(accDoc, browser, "p2", 6, 10); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test simple vertical text in rl and lr layouts + */ +addAccessibleTask( + ` + <div style="writing-mode: vertical-rl;"> + <p id='p1'>你好世界</p> + <p id='p2'>hello world</p> + <br> + <p id='p3'>こんにちは世界</p> + </div> + <div style="writing-mode: vertical-lr;"> + <p id='p4'>你好世界</p> + <p id='p5'>hello world</p> + <br> + <p id='p6'>こんにちは世界</p> + </div> + `, + async function (browser, accDoc) { + info("Testing vertical-rl"); + await testTextNode(accDoc, browser, "p1"); + await testTextNode(accDoc, browser, "p2"); + await testTextNode(accDoc, browser, "p3"); + info("Testing vertical-lr"); + await testTextNode(accDoc, browser, "p4"); + await testTextNode(accDoc, browser, "p5"); + await testTextNode(accDoc, browser, "p6"); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test multiline vertical-rl text + */ +addAccessibleTask( + ` + <p id='p1' style='writing-mode: vertical-rl;'>你好世界<br>你好世界</p> + <p id='p2' style='writing-mode: vertical-rl;'>hello world<br>hello world</p> + <br> + <p id='p3' style='writing-mode: vertical-rl;'>你好世界<br> 你好世界 你好世界</p> + <p id='p4' style='writing-mode: vertical-rl;'>hello world<br> hello world hello world</p> + `, + async function (browser, accDoc) { + info("Testing vertical-rl multiline"); + await testTextNode(accDoc, browser, "p1"); + await testTextNode(accDoc, browser, "p2"); + await testTextNode(accDoc, browser, "p3"); + // await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test text with embedded chars + */ +addAccessibleTask( + `<p id='p1' style='font-family: monospace;'>hello <a href="google.com">world</a></p> + <p id='p2' style='font-family: monospace;'>hello<br><a href="google.com">world</a></p> + <div id='d3'><p></p>hello world</div> + <div id='d4'>hello world<p></p></div> + <div id='d5'>oh<p></p>hello world</div>`, + async function (browser, accDoc) { + info("Testing embedded chars"); + await testTextNode(accDoc, browser, "p1"); + await testTextNode(accDoc, browser, "p2"); + await testTextNode(accDoc, browser, "d3"); + await testTextNode(accDoc, browser, "d4"); + await testTextNode(accDoc, browser, "d5"); + }, + { + topLevel: true, + iframe: true, + } +); + +/** + * Test bounds after text mutations. + */ +addAccessibleTask( + `<p id="p">a</p>`, + async function (browser, docAcc) { + await testTextNode(docAcc, browser, "p"); + const p = findAccessibleChildByID(docAcc, "p"); + info("Appending a character to text leaf"); + let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p); + await invokeContentTask(browser, [], () => { + content.document.getElementById("p").firstChild.data = "ab"; + }); + await textInserted; + await testTextNode(docAcc, browser, "p"); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test character bounds on the insertion point at the end of a text box. + */ +addAccessibleTask( + `<input id="input" value="a">`, + async function (browser, docAcc) { + const input = findAccessibleChildByID(docAcc, "input"); + testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test character bounds after non-br line break. + */ +addAccessibleTask( + ` + <style> + @font-face { + font-family: Ahem; + src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs); + } + pre { + font: 20px/20px Ahem; + } + </style> + <pre id="t">XX +XXX</pre>`, + async function (browser, docAcc) { + await testChar(docAcc, browser, "t", 3); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test character bounds in a pre with padding. + */ +addAccessibleTask( + ` + <style> + @font-face { + font-family: Ahem; + src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs); + } + pre { + font: 20px/20px Ahem; + padding: 20px; + } + </style> + <pre id="t">XX +XXX</pre>`, + async function (browser, docAcc) { + await testTextNode(docAcc, browser, "t"); + await testChar(docAcc, browser, "t", 3); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test text bounds with an invalid end offset. + */ +addAccessibleTask( + `<p id="p">a</p>`, + async function (browser, docAcc) { + const p = findAccessibleChildByID(docAcc, "p"); + testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE); + }, + { chrome: true, topLevel: !true } +); + +/** + * Test character bounds in an intervening inline element with non-br line breaks + */ +addAccessibleTask( + ` + <style> + @font-face { + font-family: Ahem; + src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs); + } + pre { + font: 20px/20px Ahem; + } + </style> + <pre id="t"><code role="none">XX +XXX +XX +X</pre>`, + async function (browser, docAcc) { + await testChar(docAcc, browser, "t", 0); + await testChar(docAcc, browser, "t", 3); + await testChar(docAcc, browser, "t", 7); + await testChar(docAcc, browser, "t", 10); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test character bounds in an intervening inline element with margins + * and with non-br line breaks + */ +addAccessibleTask( + ` + <style> + @font-face { + font-family: Ahem; + src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs); + } + </style> + <div>hello<pre id="t" style="margin-left:100px;margin-top:30px;background-color:blue;">XX +XXX +XX +X</pre></div>`, + async function (browser, docAcc) { + await testChar(docAcc, browser, "t", 0); + await testChar(docAcc, browser, "t", 3); + await testChar(docAcc, browser, "t", 7); + await testChar(docAcc, browser, "t", 10); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test text bounds in a textarea after scrolling. + */ +addAccessibleTask( + ` +<textarea id="textarea" rows="1">a +b +c</textarea> + `, + async function (browser, docAcc) { + // We can't use testChar because Range.getBoundingClientRect isn't supported + // inside textareas. + const textarea = findAccessibleChildByID(docAcc, "textarea"); + textarea.QueryInterface(nsIAccessibleText); + const oldY = {}; + textarea.getCharacterExtents( + 4, + {}, + oldY, + {}, + {}, + COORDTYPE_SCREEN_RELATIVE + ); + info("Moving textarea caret to c"); + await invokeContentTask(browser, [], () => { + const textareaDom = content.document.getElementById("textarea"); + textareaDom.focus(); + textareaDom.selectionStart = 4; + }); + await waitForContentPaint(browser); + const newY = {}; + textarea.getCharacterExtents( + 4, + {}, + newY, + {}, + {}, + COORDTYPE_SCREEN_RELATIVE + ); + ok(newY.value < oldY.value, "y coordinate smaller after scrolling down"); + }, + { chrome: true, topLevel: true, iframe: !true } +); + +/** + * Test magic offsets with GetCharacter/RangeExtents. + */ +addAccessibleTask( + `<input id="input" value="abc">`, + async function (browser, docAcc) { + const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]); + info("Setting caret and focusing input"); + let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + await invokeContentTask(browser, [], () => { + const inputDom = content.document.getElementById("input"); + inputDom.selectionStart = inputDom.selectionEnd = 1; + inputDom.focus(); + }); + await caretMoved; + is(input.caretOffset, 1, "input caretOffset is 1"); + let expectedX = {}; + let expectedY = {}; + let expectedW = {}; + let expectedH = {}; + let magicX = {}; + let magicY = {}; + let magicW = {}; + let magicH = {}; + input.getCharacterExtents( + 1, + expectedX, + expectedY, + expectedW, + expectedH, + COORDTYPE_SCREEN_RELATIVE + ); + input.getCharacterExtents( + nsIAccessibleText.TEXT_OFFSET_CARET, + magicX, + magicY, + magicW, + magicH, + COORDTYPE_SCREEN_RELATIVE + ); + Assert.deepEqual( + [magicX.value, magicY.value, magicW.value, magicH.value], + [expectedX.value, expectedY.value, expectedW.value, expectedH.value], + "GetCharacterExtents correct with TEXT_OFFSET_CARET" + ); + input.getRangeExtents( + 1, + 3, + expectedX, + expectedY, + expectedW, + expectedH, + COORDTYPE_SCREEN_RELATIVE + ); + input.getRangeExtents( + nsIAccessibleText.TEXT_OFFSET_CARET, + nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT, + magicX, + magicY, + magicW, + magicH, + COORDTYPE_SCREEN_RELATIVE + ); + Assert.deepEqual( + [magicX.value, magicY.value, magicW.value, magicH.value], + [expectedX.value, expectedY.value, expectedW.value, expectedH.value], + "GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT" + ); + }, + { chrome: true, topLevel: true, remoteIframe: !true } +); + +/** + * Test wrapped text and pre-formatted text beginning with an empty line. + */ +addAccessibleTask( + ` +<style> + #wrappedText { + width: 3ch; + font-family: monospace; + } +</style> +<p id="wrappedText"><a href="https://example.com/">a</a>b cd</p> +<p id="emptyFirstLine" style="white-space: pre-line;"> +foo</p> + `, + async function (browser, docAcc) { + await testChar(docAcc, browser, "wrappedText", 0); + await testChar(docAcc, browser, "wrappedText", 1); + await testChar(docAcc, browser, "wrappedText", 2); + await testChar(docAcc, browser, "wrappedText", 3); + await testChar(docAcc, browser, "wrappedText", 4); + + // We can't use testChar for emptyFirstLine because it doesn't handle white + // space properly. Instead, verify that the first character is at the top + // left of the text leaf. + const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [ + nsIAccessibleText, + ]); + const emptyFirstLineLeaf = emptyFirstLine.firstChild; + const leafX = {}; + const leafY = {}; + emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {}); + testTextPos( + emptyFirstLine, + 0, + [leafX.value, leafY.value], + COORDTYPE_SCREEN_RELATIVE + ); + }, + { chrome: true, topLevel: true, remoteIframe: !true } +); + +/** + * Test character bounds in an intervening inline element with non-br line breaks + */ +addAccessibleTask( + ` + <style> + @font-face { + font-family: Ahem; + src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs); + } + pre { + font: 20px/20px Ahem; + } + </style> + <pre><code id="t" role="group">XX +XXX +XX +X</pre>`, + async function (browser, docAcc) { + await testChar(docAcc, browser, "t", 0); + await testChar(docAcc, browser, "t", 3); + await testChar(docAcc, browser, "t", 7); + await testChar(docAcc, browser, "t", 10); + }, + { + chrome: true, + topLevel: true, + iframe: true, + } +); + +/** + * Test character bounds where content white space isn't rendered. + */ +addAccessibleTask( + ` +<p id="single">a b</p> +<p id="multi"><ins>a </ins> +b</p> +<pre id="pre">a b</pre> + `, + async function (browser, docAcc) { + await testLineWithNonRenderedSpace(docAcc, browser, "single", 3); + await testLineWithNonRenderedSpace(docAcc, browser, "multi", 2); + for (let offset = 0; offset < 4; ++offset) { + await testChar(docAcc, browser, "pre", offset); + } + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_uniqueid.js b/accessible/tests/browser/e10s/browser_caching_uniqueid.js new file mode 100644 index 0000000000..92eb2fe998 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_uniqueid.js @@ -0,0 +1,30 @@ +/* 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"; + +/** + * Test UniqueID property. + */ +addAccessibleTask( + '<div id="div"></div>', + async function (browser, accDoc) { + const div = findAccessibleChildByID(accDoc, "div"); + const accUniqueID = await invokeContentTask(browser, [], () => { + const accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + return accService.getAccessibleFor(content.document.getElementById("div")) + .uniqueID; + }); + + is( + accUniqueID, + div.uniqueID, + "Both proxy and the accessible return correct unique ID." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_value.js b/accessible/tests/browser/e10s/browser_caching_value.js new file mode 100644 index 0000000000..2b968b5948 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_value.js @@ -0,0 +1,457 @@ +/* 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/states.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * id {String} given accessible DOMNode ID + * expected {String} expected value for a given accessible + * action {?AsyncFunction} an optional action that awaits a value change + * attrs {?Array} an optional list of attributes to update + * waitFor {?Number} an optional value change event to wait for + * } + */ +const valueTests = [ + { + desc: "Initially value is set to 1st element of select", + id: "select", + expected: "1st", + }, + { + desc: "Value should update to 3rd when 3 is pressed", + id: "select", + async action(browser) { + await invokeFocus(browser, "select"); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("3", {}, content); + }); + }, + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: "3rd", + }, + { + desc: "Initially value is set to @aria-valuenow for slider", + id: "slider", + expected: ["5", 5, 0, 7, 0], + }, + { + desc: "Value should change when currentValue is called", + id: "slider", + async action(browser, acc) { + acc.QueryInterface(nsIAccessibleValue); + acc.currentValue = 4; + }, + waitFor: EVENT_VALUE_CHANGE, + expected: ["4", 4, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuenow is updated", + id: "slider", + attrs: [ + { + attr: "aria-valuenow", + value: "6", + }, + ], + waitFor: EVENT_VALUE_CHANGE, + expected: ["6", 6, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuetext is set", + id: "slider", + attrs: [ + { + attr: "aria-valuetext", + value: "plain", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: ["plain", 6, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuetext is updated", + id: "slider", + attrs: [ + { + attr: "aria-valuetext", + value: "hey!", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: ["hey!", 6, 0, 7, 0], + }, + { + desc: "Value should change to @aria-valuetext when @aria-valuenow is removed", + id: "slider", + attrs: [ + { + attr: "aria-valuenow", + }, + ], + expected: ["hey!", 3.5, 0, 7, 0], + }, + { + desc: "Initially value is not set for combobox", + id: "combobox", + expected: "", + }, + { + desc: "Value should change when @value attribute is updated", + id: "combobox", + attrs: [ + { + attr: "value", + value: "hello", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: "hello", + }, + { + desc: "Initially value corresponds to @value attribute for progress", + id: "progress", + expected: "22%", + }, + { + desc: "Value should change when @value attribute is updated", + id: "progress", + attrs: [ + { + attr: "value", + value: "50", + }, + ], + waitFor: EVENT_VALUE_CHANGE, + expected: "50%", + }, + { + desc: "Setting currentValue on a progress accessible should fail", + id: "progress", + async action(browser, acc) { + acc.QueryInterface(nsIAccessibleValue); + try { + acc.currentValue = 25; + ok(false, "Setting currValue on progress element should fail"); + } catch (e) {} + }, + expected: "50%", + }, + { + desc: "Initially value corresponds to @value attribute for range", + id: "range", + expected: "6", + }, + { + desc: "Value should change when slider is moved", + id: "range", + async action(browser) { + await invokeFocus(browser, "range"); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("VK_LEFT", {}, content); + }); + }, + waitFor: EVENT_VALUE_CHANGE, + expected: "5", + }, + { + desc: "Value should change when currentValue is called", + id: "range", + async action(browser, acc) { + acc.QueryInterface(nsIAccessibleValue); + acc.currentValue = 4; + }, + waitFor: EVENT_VALUE_CHANGE, + expected: "4", + }, + { + desc: "Initially textbox value is text subtree", + id: "textbox", + expected: "Some rich text", + }, + { + desc: "Textbox value changes when subtree changes", + id: "textbox", + async action(browser) { + await invokeContentTask(browser, [], () => { + let boldText = content.document.createElement("strong"); + boldText.textContent = " bold"; + content.document.getElementById("textbox").appendChild(boldText); + }); + }, + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: "Some rich text bold", + }, +]; + +/** + * Like testValue in accessible/tests/mochitest/value.js, but waits for cache + * updates. + */ +async function testValue(acc, value, currValue, minValue, maxValue, minIncr) { + const pretty = prettyName(acc); + await untilCacheIs(() => acc.value, value, `Wrong value of ${pretty}`); + + await untilCacheIs( + () => acc.currentValue, + currValue, + `Wrong current value of ${pretty}` + ); + await untilCacheIs( + () => acc.minimumValue, + minValue, + `Wrong minimum value of ${pretty}` + ); + await untilCacheIs( + () => acc.maximumValue, + maxValue, + `Wrong maximum value of ${pretty}` + ); + await untilCacheIs( + () => acc.minimumIncrement, + minIncr, + `Wrong minimum increment value of ${pretty}` + ); +} + +/** + * Test caching of accessible object values + */ +addAccessibleTask( + ` + <div id="slider" role="slider" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="7">slider</div> + <select id="select"> + <option>1st</option> + <option>2nd</option> + <option>3rd</option> + </select> + <input id="combobox" role="combobox" aria-autocomplete="inline"> + <progress id="progress" value="22" max="100"></progress> + <input type="range" id="range" min="0" max="10" value="6"> + <div contenteditable="yes" role="textbox" id="textbox">Some <a href="#">rich</a> text</div>`, + async function (browser, accDoc) { + for (let { desc, id, action, attrs, expected, waitFor } of valueTests) { + info(desc); + let acc = findAccessibleChildByID(accDoc, id); + let onUpdate; + + if (waitFor) { + onUpdate = waitForEvent(waitFor, id); + } + + if (action) { + await action(browser, acc); + } else if (attrs) { + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, id, attr, value); + } + } + + await onUpdate; + if (Array.isArray(expected)) { + acc.QueryInterface(nsIAccessibleValue); + await testValue(acc, ...expected); + } else { + is(acc.value, expected, `Correct value for ${prettyName(acc)}`); + } + } + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test caching of link URL values. + */ +addAccessibleTask( + `<a id="link" href="https://example.com/">Test</a>`, + async function (browser, docAcc) { + let link = findAccessibleChildByID(docAcc, "link"); + is(link.value, "https://example.com/", "link initial value correct"); + const textLeaf = link.firstChild; + is(textLeaf.value, "https://example.com/", "link initial value correct"); + + info("Changing link href"); + await invokeSetAttribute(browser, "link", "href", "https://example.net/"); + await untilCacheIs( + () => link.value, + "https://example.net/", + "link value correct after change" + ); + + info("Removing link href"); + let onRecreation = waitForEvents({ + expected: [ + [EVENT_HIDE, link], + [EVENT_SHOW, "link"], + ], + }); + await invokeSetAttribute(browser, "link", "href"); + await onRecreation; + link = findAccessibleChildByID(docAcc, "link"); + await untilCacheIs(() => link.value, "", "link value empty after removal"); + + info("Setting link href"); + onRecreation = waitForEvents({ + expected: [ + [EVENT_HIDE, link], + [EVENT_SHOW, "link"], + ], + }); + await invokeSetAttribute(browser, "link", "href", "https://example.com/"); + await onRecreation; + link = findAccessibleChildByID(docAcc, "link"); + await untilCacheIs( + () => link.value, + "https://example.com/", + "link value correct after change" + ); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test caching of active state for select options - see bug 1788143. + */ +addAccessibleTask( + ` + <select id="select"> + <option id="first_option">First</option> + <option id="second_option">Second</option> + </select>`, + async function (browser, docAcc) { + const select = findAccessibleChildByID(docAcc, "select"); + is(select.value, "First", "Select initial value correct"); + + // Focus the combo box. + await invokeFocus(browser, "select"); + + // Select the second option (drop-down collapsed). + let p = waitForEvents({ + expected: [ + [EVENT_SELECTION, "second_option"], + [EVENT_TEXT_VALUE_CHANGE, "select"], + ], + unexpected: [ + stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, true, true), + stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, false, true), + ], + }); + await invokeContentTask(browser, [], () => { + content.document.getElementById("select").selectedIndex = 1; + }); + await p; + + is(select.value, "Second", "Select value correct after changing option"); + + // Expand the combobox dropdown. + p = waitForEvent(EVENT_STATE_CHANGE, "ContentSelectDropdown"); + EventUtils.synthesizeKey("VK_SPACE"); + await p; + + p = waitForEvents({ + expected: [ + [EVENT_SELECTION, "first_option"], + [EVENT_TEXT_VALUE_CHANGE, "select"], + [EVENT_HIDE, "ContentSelectDropdown"], + ], + unexpected: [ + stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, true, true), + stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, false, true), + ], + }); + + // Press the up arrow to select the first option (drop-down expanded). + // Then, press Enter to confirm the selection and close the dropdown. + // We do both of these together to unify testing across platforms, since + // events are not entirely consistent on Windows vs. Linux + macOS. + EventUtils.synthesizeKey("VK_UP"); + EventUtils.synthesizeKey("VK_RETURN"); + await p; + + is( + select.value, + "First", + "Select value correct after changing option back" + ); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test combobox values for non-editable comboboxes. + */ +addAccessibleTask( + ` + <div id="combo-div-1" role="combobox">value</div> + <div id="combo-div-2" role="combobox"> + <div role="listbox"> + <div role="option">value</div> + </div> + </div> + <div id="combo-div-3" role="combobox"> + <div role="group">value</div> + </div> + <div id="combo-div-4" role="combobox">foo + <div role="listbox"> + <div role="option">bar</div> + </div> + </div> + + <input id="combo-input-1" role="combobox" value="value" disabled></input> + <input id="combo-input-2" role="combobox" value="value" disabled>testing</input> + + <div id="combo-div-selected" role="combobox"> + <div role="listbox"> + <div aria-selected="true" role="option">value</div> + </div> + </div> +`, + async function (browser, docAcc) { + const comboDiv1 = findAccessibleChildByID(docAcc, "combo-div-1"); + const comboDiv2 = findAccessibleChildByID(docAcc, "combo-div-2"); + const comboDiv3 = findAccessibleChildByID(docAcc, "combo-div-3"); + const comboDiv4 = findAccessibleChildByID(docAcc, "combo-div-4"); + const comboInput1 = findAccessibleChildByID(docAcc, "combo-input-1"); + const comboInput2 = findAccessibleChildByID(docAcc, "combo-input-2"); + const comboDivSelected = findAccessibleChildByID( + docAcc, + "combo-div-selected" + ); + + // Text as a descendant of the combobox: included in the value. + is(comboDiv1.value, "value", "Combobox value correct"); + + // Text as the descendant of a listbox: excluded from the value. + is(comboDiv2.value, "", "Combobox value correct"); + + // Text as the descendant of some other role that includes text in name computation. + // Here, the group role contains the text node with "value" in it. + is(comboDiv3.value, "value", "Combobox value correct"); + + // Some descendant text included, but text descendant of a listbox excluded. + is(comboDiv4.value, "foo", "Combobox value correct"); + + // Combobox inputs with explicit value report that value. + is(comboInput1.value, "value", "Combobox value correct"); + is(comboInput2.value, "value", "Combobox value correct"); + + // Combobox role with aria-selected reports correct value. + is(comboDivSelected.value, "value", "Combobox value correct"); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_announcement.js b/accessible/tests/browser/e10s/browser_events_announcement.js new file mode 100644 index 0000000000..046a7706e3 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_announcement.js @@ -0,0 +1,30 @@ +/* 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"; + +addAccessibleTask( + `<p id="p">abc</p>`, + async function (browser, accDoc) { + let acc = findAccessibleChildByID(accDoc, "p"); + let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE); + let evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "please", "announcement matches."); + is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches"); + + onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE); + evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "do it", "announcement matches."); + is( + evt.priority, + nsIAccessibleAnnouncementEvent.ASSERTIVE, + "priority matches" + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_caretmove.js b/accessible/tests/browser/e10s/browser_events_caretmove.js new file mode 100644 index 0000000000..dff6586bf3 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_caretmove.js @@ -0,0 +1,22 @@ +/* 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"; + +/** + * Test caret move event and its interface: + * - caretOffset + */ +addAccessibleTask( + '<input id="textbox" value="hello"/>', + async function (browser) { + let onCaretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textbox"); + await invokeFocus(browser, "textbox"); + let event = await onCaretMoved; + + let caretMovedEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent); + is(caretMovedEvent.caretOffset, 5, "Correct caret offset."); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_hide.js b/accessible/tests/browser/e10s/browser_events_hide.js new file mode 100644 index 0000000000..77bd70c0f6 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_hide.js @@ -0,0 +1,44 @@ +/* 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"; + +/** + * Test hide event and its interface: + * - targetParent + * - targetNextSibling + * - targetPrevSibling + */ +addAccessibleTask( + ` + <div id="parent"> + <div id="previous"></div> + <div id="to-hide"></div> + <div id="next"></div> + </div>`, + async function (browser, accDoc) { + let acc = findAccessibleChildByID(accDoc, "to-hide"); + let onHide = waitForEvent(EVENT_HIDE, acc); + await invokeSetStyle(browser, "to-hide", "visibility", "hidden"); + let event = await onHide; + let hideEvent = event.QueryInterface(Ci.nsIAccessibleHideEvent); + + is( + getAccessibleDOMNodeID(hideEvent.targetParent), + "parent", + "Correct target parent." + ); + is( + getAccessibleDOMNodeID(hideEvent.targetNextSibling), + "next", + "Correct target next sibling." + ); + is( + getAccessibleDOMNodeID(hideEvent.targetPrevSibling), + "previous", + "Correct target previous sibling." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_show.js b/accessible/tests/browser/e10s/browser_events_show.js new file mode 100644 index 0000000000..fb03ce2329 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_show.js @@ -0,0 +1,22 @@ +/* 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"; + +/** + * Test show event + */ +addAccessibleTask( + '<div id="div" style="visibility: hidden;"></div>', + async function (browser) { + let onShow = waitForEvent(EVENT_SHOW, "div"); + await invokeSetStyle(browser, "div", "visibility", "visible"); + let showEvent = await onShow; + ok( + showEvent.accessibleDocument instanceof nsIAccessibleDocument, + "Accessible document not present." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_statechange.js b/accessible/tests/browser/e10s/browser_events_statechange.js new file mode 100644 index 0000000000..a510c5b9b5 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_statechange.js @@ -0,0 +1,71 @@ +/* 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/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function checkStateChangeEvent(event, state, isExtraState, isEnabled) { + let scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + is(scEvent.state, state, "Correct state of the statechange event."); + is( + scEvent.isExtraState, + isExtraState, + "Correct extra state bit of the statechange event." + ); + is(scEvent.isEnabled, isEnabled, "Correct state of statechange event state"); +} + +// Insert mock source into the iframe to be able to verify the right document +// body id. +let iframeSrc = `data:text/html, + <html> + <head> + <meta charset='utf-8'/> + <title>Inner Iframe</title> + </head> + <body id='iframe'></body> + </html>`; + +/** + * Test state change event and its interface: + * - state + * - isExtraState + * - isEnabled + */ +addAccessibleTask( + ` + <iframe id="iframe" src="${iframeSrc}"></iframe> + <input id="checkbox" type="checkbox" />`, + async function (browser) { + // Test state change + let onStateChange = waitForEvent(EVENT_STATE_CHANGE, "checkbox"); + // Set checked for a checkbox. + await invokeContentTask(browser, [], () => { + content.document.getElementById("checkbox").checked = true; + }); + let event = await onStateChange; + + checkStateChangeEvent(event, STATE_CHECKED, false, true); + testStates(event.accessible, STATE_CHECKED, 0); + + // Test extra state + onStateChange = waitForEvent(EVENT_STATE_CHANGE, "iframe"); + // Set design mode on. + await invokeContentTask(browser, [], () => { + content.document.getElementById("iframe").contentDocument.designMode = + "on"; + }); + event = await onStateChange; + + checkStateChangeEvent(event, EXT_STATE_EDITABLE, true, true); + testStates(event.accessible, 0, EXT_STATE_EDITABLE); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_textchange.js b/accessible/tests/browser/e10s/browser_events_textchange.js new file mode 100644 index 0000000000..f39ecea8c4 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_textchange.js @@ -0,0 +1,119 @@ +/* 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"; + +function checkTextChangeEvent( + event, + id, + text, + start, + end, + isInserted, + isFromUserInput +) { + let tcEvent = event.QueryInterface(nsIAccessibleTextChangeEvent); + is(tcEvent.start, start, `Correct start offset for ${prettyName(id)}`); + is(tcEvent.length, end - start, `Correct length for ${prettyName(id)}`); + is( + tcEvent.isInserted, + isInserted, + `Correct isInserted flag for ${prettyName(id)}` + ); + is(tcEvent.modifiedText, text, `Correct text for ${prettyName(id)}`); + is( + tcEvent.isFromUserInput, + isFromUserInput, + `Correct value of isFromUserInput for ${prettyName(id)}` + ); + ok( + tcEvent.accessibleDocument instanceof nsIAccessibleDocument, + "Accessible document not present." + ); +} + +async function changeText(browser, id, value, events) { + let onEvents = waitForOrderedEvents( + events.map(({ isInserted }) => { + let eventType = isInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; + return [eventType, id]; + }) + ); + // Change text in the subtree. + await invokeContentTask(browser, [id, value], (contentId, contentValue) => { + content.document.getElementById(contentId).firstChild.textContent = + contentValue; + }); + let resolvedEvents = await onEvents; + + events.forEach(({ isInserted, str, offset }, idx) => + checkTextChangeEvent( + resolvedEvents[idx], + id, + str, + offset, + offset + str.length, + isInserted, + false + ) + ); +} + +async function removeTextFromInput(browser, id, value, start, end) { + let onTextRemoved = waitForEvent(EVENT_TEXT_REMOVED, id); + // Select text and delete it. + await invokeContentTask( + browser, + [id, start, end], + (contentId, contentStart, contentEnd) => { + let el = content.document.getElementById(contentId); + el.focus(); + el.setSelectionRange(contentStart, contentEnd); + } + ); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.sendChar("VK_DELETE", content); + }); + + let event = await onTextRemoved; + checkTextChangeEvent(event, id, value, start, end, false, true); +} + +/** + * Test text change event and its interface: + * - start + * - length + * - isInserted + * - modifiedText + * - isFromUserInput + */ +addAccessibleTask( + ` + <p id="p">abc</p> + <input id="input" value="input" />`, + async function (browser) { + let events = [ + { isInserted: false, str: "abc", offset: 0 }, + { isInserted: true, str: "def", offset: 0 }, + ]; + await changeText(browser, "p", "def", events); + + // Adding text should not send events with diffs for non-editable text. + // We do this to avoid screen readers reading out confusing diffs for + // live regions. + events = [ + { isInserted: false, str: "def", offset: 0 }, + { isInserted: true, str: "deDEFf", offset: 0 }, + ]; + await changeText(browser, "p", "deDEFf", events); + + // Test isFromUserInput property. + await removeTextFromInput(browser, "input", "n", 1, 2); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_file_input.js b/accessible/tests/browser/e10s/browser_file_input.js new file mode 100644 index 0000000000..238e48740e --- /dev/null +++ b/accessible/tests/browser/e10s/browser_file_input.js @@ -0,0 +1,77 @@ +/* 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/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` +<input type="file" id="noName"> +<input type="file" id="ariaLabel" aria-label="ariaLabel"> +<label>wrappingLabel <input type="file" id="wrappingLabel"></label> +<label for="labelFor">labelFor</label> <input type="file" id="labelFor"> +<input type="file" id="title" title="title"> + `, + async function (browser, docAcc) { + const browseButton = "Browse…"; + const noFileSuffix = `${browseButton} No file selected.`; + const noName = findAccessibleChildByID(docAcc, "noName"); + testName(noName, noFileSuffix); + const ariaLabel = findAccessibleChildByID(docAcc, "ariaLabel"); + testName(ariaLabel, `ariaLabel ${noFileSuffix}`); + const wrappingLabel = findAccessibleChildByID(docAcc, "wrappingLabel"); + testName(wrappingLabel, `wrappingLabel ${noFileSuffix}`); + const labelFor = findAccessibleChildByID(docAcc, "labelFor"); + testName(labelFor, `labelFor ${noFileSuffix}`); + const title = findAccessibleChildByID(docAcc, "title"); + testName(title, noFileSuffix); + testDescr(title, "title"); + + // Test that the name of the button changes correctly when a file is chosen. + function chooseFile(id) { + return invokeContentTask(browser, [id], contentId => { + const MockFilePicker = content.SpecialPowers.MockFilePicker; + MockFilePicker.init(content); + MockFilePicker.useBlobFile(); + MockFilePicker.returnValue = MockFilePicker.returnOK; + const input = content.document.getElementById(contentId); + const inputReceived = new Promise(resolve => + input.addEventListener( + "input", + event => { + MockFilePicker.cleanup(); + resolve(event.target.files[0].name); + }, + { once: true } + ) + ); + input.click(); + return inputReceived; + }); + } + + info("noName: Choosing file"); + let nameChanged = waitForEvent(EVENT_NAME_CHANGE, "noName"); + const fn = await chooseFile("noName"); + // e.g. "Browse…helloworld.txt" + const withFileSuffix = `${browseButton} ${fn}`; + await nameChanged; + testName(noName, withFileSuffix); + + info("ariaLabel: Choosing file"); + nameChanged = waitForEvent(EVENT_NAME_CHANGE, "ariaLabel"); + await chooseFile("ariaLabel"); + await nameChanged; + testName(ariaLabel, `ariaLabel ${withFileSuffix}`); + + info("wrappingLabel: Choosing file"); + nameChanged = waitForEvent(EVENT_NAME_CHANGE, "wrappingLabel"); + await chooseFile("wrappingLabel"); + await nameChanged; + testName(wrappingLabel, `wrappingLabel ${withFileSuffix}`); + }, + { topLevel: true, chrome: true } +); diff --git a/accessible/tests/browser/e10s/browser_language.js b/accessible/tests/browser/e10s/browser_language.js new file mode 100644 index 0000000000..684d915693 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_language.js @@ -0,0 +1,29 @@ +/* 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"; + +addAccessibleTask( + ` +<script> + // We can't include the html element in snippets, so set lang on it here. + document.documentElement.lang = "en"; +</script> +<div id="inheritEn"></div> +<div id="de" lang="de"> + <div id="inheritDe"></div> + <div id="fr" lang="fr"></div> +</div> + `, + async function (browser, docAcc) { + is(docAcc.language, "en", "Document language correct"); + const inheritEn = findAccessibleChildByID(docAcc, "inheritEn"); + is(inheritEn.language, "en", "inheritEn language correct"); + const de = findAccessibleChildByID(docAcc, "de"); + is(de.language, "de", "de language correct"); + const fr = findAccessibleChildByID(docAcc, "fr"); + is(fr.language, "fr", "fr language correct"); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_obj_group.js b/accessible/tests/browser/e10s/browser_obj_group.js new file mode 100644 index 0000000000..7e22b8b491 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_obj_group.js @@ -0,0 +1,430 @@ +/* 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 }); + +/** + * select elements + */ +addAccessibleTask( + `<select> + <option id="opt1-nosize">option1</option> + <option id="opt2-nosize">option2</option> + <option id="opt3-nosize">option3</option> + <option id="opt4-nosize">option4</option> + </select> + + <select size="4"> + <option id="opt1">option1</option> + <option id="opt2">option2</option> + </select> + + <select size="4"> + <optgroup id="select2_optgroup" label="group"> + <option id="select2_opt1">option1</option> + <option id="select2_opt2">option2</option> + </optgroup> + <option id="select2_opt3">option3</option> + <option id="select2_opt4">option4</option> + </select>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // HTML select with no size attribute. + testGroupAttrs(getAcc("opt1-nosize"), 1, 4); + testGroupAttrs(getAcc("opt2-nosize"), 2, 4); + testGroupAttrs(getAcc("opt3-nosize"), 3, 4); + testGroupAttrs(getAcc("opt4-nosize"), 4, 4); + + // Container should have item count and not hierarchical + testGroupParentAttrs(getAcc("opt1-nosize").parent, 4, false); + + // //////////////////////////////////////////////////////////////////////// + // HTML select + testGroupAttrs(getAcc("opt1"), 1, 2); + testGroupAttrs(getAcc("opt2"), 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // HTML select with optgroup + testGroupAttrs(getAcc("select2_opt3"), 1, 2, 1); + testGroupAttrs(getAcc("select2_opt4"), 2, 2, 1); + testGroupAttrs(getAcc("select2_opt1"), 1, 2, 2); + testGroupAttrs(getAcc("select2_opt2"), 2, 2, 2); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +/** + * HTML radios + */ +addAccessibleTask( + `<form> + <input type="radio" id="radio1" name="group1"/> + <input type="radio" id="radio2" name="group1"/> + </form> + + <input type="radio" id="radio3" name="group2"/> + <label><input type="radio" id="radio4" name="group2"/></label> + + <form> + <input type="radio" style="display: none;" name="group3"> + <input type="radio" id="radio5" name="group3"> + <input type="radio" id="radio6" name="group4"> + </form> + + <input type="radio" id="radio7">`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" within form + testGroupAttrs(getAcc("radio1"), 1, 2); + testGroupAttrs(getAcc("radio2"), 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" within document + testGroupAttrs(getAcc("radio3"), 1, 2); + // radio4 is wrapped in a label + testGroupAttrs(getAcc("radio4"), 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // Hidden HTML input@type="radio" + testGroupAttrs(getAcc("radio5"), 1, 1); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" with different name but same parent + testGroupAttrs(getAcc("radio6"), 1, 1); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" with no name + testGroupAttrs(getAcc("radio7"), 0, 0); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +/** + * lists + */ +addAccessibleTask( + `<ul id="ul"> + <li id="li1">Oranges</li> + <li id="li2">Apples</li> + <li id="li3">Bananas</li> + </ul> + + <ol id="ol"> + <li id="li4">Oranges</li> + <li id="li5">Apples</li> + <li id="li6">Bananas + <ul id="ol_nested"> + <li id="n_li4">Oranges</li> + <li id="n_li5">Apples</li> + <li id="n_li6">Bananas</li> + </ul> + </li> + </ol> + + <span role="list" id="aria-list_1"> + <span role="listitem" id="li7">Oranges</span> + <span role="listitem" id="li8">Apples</span> + <span role="listitem" id="li9">Bananas</span> + </span> + + <span role="list" id="aria-list_2"> + <span role="listitem" id="li10">Oranges</span> + <span role="listitem" id="li11">Apples</span> + <span role="listitem" id="li12">Bananas + <span role="list" id="aria-list_2_1"> + <span role="listitem" id="n_li10">Oranges</span> + <span role="listitem" id="n_li11">Apples</span> + <span role="listitem" id="n_li12">Bananas</span> + </span> + </span> + </span> + + <div role="list" id="aria-list_3"> + <div role="listitem" id="lgt_li1">Item 1 + <div role="group"> + <div role="listitem" id="lgt_li1_nli1">Item 1A</div> + <div role="listitem" id="lgt_li1_nli2">Item 1B</div> + </div> + </div> + <div role="listitem" id="lgt_li2">Item 2 + <div role="group"> + <div role="listitem" id="lgt_li2_nli1">Item 2A</div> + <div role="listitem" id="lgt_li2_nli2">Item 2B</div> + </div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // HTML ul/ol + testGroupAttrs(getAcc("li1"), 1, 3); + testGroupAttrs(getAcc("li2"), 2, 3); + testGroupAttrs(getAcc("li3"), 3, 3); + + // ul should have item count and not hierarchical + testGroupParentAttrs(getAcc("ul"), 3, false); + + // //////////////////////////////////////////////////////////////////////// + // HTML ul/ol (nested lists) + + testGroupAttrs(getAcc("li4"), 1, 3, 1); + testGroupAttrs(getAcc("li5"), 2, 3, 1); + testGroupAttrs(getAcc("li6"), 3, 3, 1); + // ol with nested list should have 1st level item count and be hierarchical + testGroupParentAttrs(getAcc("ol"), 3, true); + + testGroupAttrs(getAcc("n_li4"), 1, 3, 2); + testGroupAttrs(getAcc("n_li5"), 2, 3, 2); + testGroupAttrs(getAcc("n_li6"), 3, 3, 2); + // nested ol should have item count and be hierarchical + testGroupParentAttrs(getAcc("ol_nested"), 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list + testGroupAttrs(getAcc("li7"), 1, 3); + testGroupAttrs(getAcc("li8"), 2, 3); + testGroupAttrs(getAcc("li9"), 3, 3); + // simple flat aria list + testGroupParentAttrs(getAcc("aria-list_1"), 3, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list (nested lists: list -> listitem -> list -> listitem) + testGroupAttrs(getAcc("li10"), 1, 3, 1); + testGroupAttrs(getAcc("li11"), 2, 3, 1); + testGroupAttrs(getAcc("li12"), 3, 3, 1); + // aria list with nested list + testGroupParentAttrs(getAcc("aria-list_2"), 3, true); + + testGroupAttrs(getAcc("n_li10"), 1, 3, 2); + testGroupAttrs(getAcc("n_li11"), 2, 3, 2); + testGroupAttrs(getAcc("n_li12"), 3, 3, 2); + // nested aria list. + testGroupParentAttrs(getAcc("aria-list_2_1"), 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list (nested lists: list -> listitem -> group -> listitem) + testGroupAttrs(getAcc("lgt_li1"), 1, 2, 1); + testGroupAttrs(getAcc("lgt_li1_nli1"), 1, 2, 2); + testGroupAttrs(getAcc("lgt_li1_nli2"), 2, 2, 2); + testGroupAttrs(getAcc("lgt_li2"), 2, 2, 1); + testGroupAttrs(getAcc("lgt_li2_nli1"), 1, 2, 2); + testGroupAttrs(getAcc("lgt_li2_nli2"), 2, 2, 2); + // aria list with nested list + testGroupParentAttrs(getAcc("aria-list_3"), 2, true); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<ul role="menubar" id="menubar"> + <li role="menuitem" aria-haspopup="true" id="menu_item1">File + <ul role="menu" id="menu"> + <li role="menuitem" id="menu_item1.1">New</li> + <li role="menuitem" id="menu_item1.2">Open…</li> + <li role="separator">-----</li> + <li role="menuitem" id="menu_item1.3">Item</li> + <li role="menuitemradio" id="menu_item1.4">Radio</li> + <li role="menuitemcheckbox" id="menu_item1.5">Checkbox</li> + </ul> + </li> + <li role="menuitem" aria-haspopup="false" id="menu_item2">Help</li> + </ul>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox) + testGroupAttrs(getAcc("menu_item1"), 1, 2); + testGroupAttrs(getAcc("menu_item2"), 2, 2); + testGroupAttrs(getAcc("menu_item1.1"), 1, 2); + testGroupAttrs(getAcc("menu_item1.2"), 2, 2); + testGroupAttrs(getAcc("menu_item1.3"), 1, 3); + testGroupAttrs(getAcc("menu_item1.4"), 2, 3); + testGroupAttrs(getAcc("menu_item1.5"), 3, 3); + // menu bar item count + testGroupParentAttrs(getAcc("menubar"), 2, false); + // Bug 1492529. Menu should have total number of items 5 from both sets, + // but only has the first 2 item set. + todoAttr(getAcc("menu"), "child-item-count", "5"); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<ul id="tablist_1" role="tablist"> + <li id="tab_1" role="tab">Crust</li> + <li id="tab_2" role="tab">Veges</li> + <li id="tab_3" role="tab">Carnivore</li> + </ul>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tab + testGroupAttrs(getAcc("tab_1"), 1, 3); + testGroupAttrs(getAcc("tab_2"), 2, 3); + testGroupAttrs(getAcc("tab_3"), 3, 3); + // tab list tab count + testGroupParentAttrs(getAcc("tablist_1"), 3, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<ul id="rg1" role="radiogroup"> + <li id="r1" role="radio" aria-checked="false">Thai</li> + <li id="r2" role="radio" aria-checked="false">Subway</li> + <li id="r3" role="radio" aria-checked="false">Jimmy Johns</li> + </ul>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA radio + testGroupAttrs(getAcc("r1"), 1, 3); + testGroupAttrs(getAcc("r2"), 2, 3); + testGroupAttrs(getAcc("r3"), 3, 3); + // explicit aria radio group + testGroupParentAttrs(getAcc("rg1"), 3, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<table role="tree" id="tree_1"> + <tr role="presentation"> + <td role="treeitem" aria-expanded="true" aria-level="1" + id="ti1">vegetables</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti2">cucumber</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti3">carrot</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-expanded="false" aria-level="1" + id="ti4">cars</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti5">mercedes</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti6">BMW</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti7">Audi</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="1" id="ti8">people</td> + </tr> + </table> + + <ul role="tree" id="tree_2"> + <li role="treeitem" id="tree2_ti1">Item 1 + <ul role="group"> + <li role="treeitem" id="tree2_ti1a">Item 1A</li> + <li role="treeitem" id="tree2_ti1b">Item 1B</li> + </ul> + </li> + <li role="treeitem" id="tree2_ti2">Item 2 + <ul role="group"> + <li role="treeitem" id="tree2_ti2a">Item 2A</li> + <li role="treeitem" id="tree2_ti2b">Item 2B</li> + </ul> + </li> + </div> + + <div role="tree" id="tree_3"> + <div role="treeitem" id="tree3_ti1">Item 1</div> + <div role="group"> + <li role="treeitem" id="tree3_ti1a">Item 1A</li> + <li role="treeitem" id="tree3_ti1b">Item 1B</li> + </div> + <div role="treeitem" id="tree3_ti2">Item 2</div> + <div role="group"> + <div role="treeitem" id="tree3_ti2a">Item 2A</div> + <div role="treeitem" id="tree3_ti2b">Item 2B</div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree + testGroupAttrs(getAcc("ti1"), 1, 3, 1); + testGroupAttrs(getAcc("ti2"), 1, 2, 2); + testGroupAttrs(getAcc("ti3"), 2, 2, 2); + testGroupAttrs(getAcc("ti4"), 2, 3, 1); + testGroupAttrs(getAcc("ti5"), 1, 3, 2); + testGroupAttrs(getAcc("ti6"), 2, 3, 2); + testGroupAttrs(getAcc("ti7"), 3, 3, 2); + testGroupAttrs(getAcc("ti8"), 3, 3, 1); + testGroupParentAttrs(getAcc("tree_1"), 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree (tree -> treeitem -> group -> treeitem) + testGroupAttrs(getAcc("tree2_ti1"), 1, 2, 1); + testGroupAttrs(getAcc("tree2_ti1a"), 1, 2, 2); + testGroupAttrs(getAcc("tree2_ti1b"), 2, 2, 2); + testGroupAttrs(getAcc("tree2_ti2"), 2, 2, 1); + testGroupAttrs(getAcc("tree2_ti2a"), 1, 2, 2); + testGroupAttrs(getAcc("tree2_ti2b"), 2, 2, 2); + testGroupParentAttrs(getAcc("tree_2"), 2, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree (tree -> treeitem, group -> treeitem) + testGroupAttrs(getAcc("tree3_ti1"), 1, 2, 1); + testGroupAttrs(getAcc("tree3_ti1a"), 1, 2, 2); + testGroupAttrs(getAcc("tree3_ti1b"), 2, 2, 2); + testGroupAttrs(getAcc("tree3_ti2"), 2, 2, 1); + testGroupAttrs(getAcc("tree3_ti2a"), 1, 2, 2); + testGroupAttrs(getAcc("tree3_ti2b"), 2, 2, 2); + testGroupParentAttrs(getAcc("tree_3"), 2, true); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); diff --git a/accessible/tests/browser/e10s/browser_obj_group_002.js b/accessible/tests/browser/e10s/browser_obj_group_002.js new file mode 100644 index 0000000000..54cad4a019 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_obj_group_002.js @@ -0,0 +1,390 @@ +/* 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 }); + +addAccessibleTask( + `<table role="grid" id="grid"> + <tr role="row" id="grid_row1"> + <td role="gridcell" id="grid_cell1">cell1</td> + <td role="gridcell" id="grid_cell2">cell2</td> + </tr> + <tr role="row" id="grid_row2"> + <td role="gridcell" id="grid_cell3">cell3</td> + <td role="gridcell" id="grid_cell4">cell4</td> + </tr> + </table>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA grid + testGroupAttrs(getAcc("grid_row1"), 1, 2); + testAbsentAttrs(getAcc("grid_cell1"), { posinset: "", setsize: "" }); + testAbsentAttrs(getAcc("grid_cell2"), { posinset: "", setsize: "" }); + + testGroupAttrs(getAcc("grid_row2"), 2, 2); + testAbsentAttrs(getAcc("grid_cell3"), { posinset: "", setsize: "" }); + testAbsentAttrs(getAcc("grid_cell4"), { posinset: "", setsize: "" }); + testGroupParentAttrs(getAcc("grid"), 2, false, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div role="treegrid" id="treegrid" aria-colcount="4"> + <div role="row" aria-level="1" id="treegrid_row1"> + <div role="gridcell" id="treegrid_cell1">cell1</div> + <div role="gridcell" id="treegrid_cell2">cell2</div> + </div> + <div role="row" aria-level="2" id="treegrid_row2"> + <div role="gridcell" id="treegrid_cell3">cell1</div> + <div role="gridcell" id="treegrid_cell4">cell2</div> + </div> + <div role="row" id="treegrid_row3"> + <div role="gridcell" id="treegrid_cell5">cell1</div> + <div role="gridcell" id="treegrid_cell6">cell2</div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA treegrid + testGroupAttrs(getAcc("treegrid_row1"), 1, 2, 1); + testAbsentAttrs(getAcc("treegrid_cell1"), { posinset: "", setsize: "" }); + testAbsentAttrs(getAcc("treegrid_cell2"), { posinset: "", setsize: "" }); + + testGroupAttrs(getAcc("treegrid_row2"), 1, 1, 2); + testAbsentAttrs(getAcc("treegrid_cell3"), { posinset: "", setsize: "" }); + testAbsentAttrs(getAcc("treegrid_cell4"), { posinset: "", setsize: "" }); + + testGroupAttrs(getAcc("treegrid_row3"), 2, 2, 1); + testAbsentAttrs(getAcc("treegrid_cell5"), { posinset: "", setsize: "" }); + testAbsentAttrs(getAcc("treegrid_cell6"), { posinset: "", setsize: "" }); + + testGroupParentAttrs(getAcc("treegrid"), 2, true); + // row child item count provided by parent grid's aria-colcount + testGroupParentAttrs(getAcc("treegrid_row1"), 4, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div id="headings"> + <h1 id="h1">heading1</h1> + <h2 id="h2">heading2</h2> + <h3 id="h3">heading3</h3> + <h4 id="h4">heading4</h4> + <h5 id="h5">heading5</h5> + <h6 id="h6">heading6</h6> + <div id="ariaHeadingNoLevel" role="heading">ariaHeadingNoLevel</div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // HTML headings + testGroupAttrs(getAcc("h1"), 0, 0, 1); + testGroupAttrs(getAcc("h2"), 0, 0, 2); + testGroupAttrs(getAcc("h3"), 0, 0, 3); + testGroupAttrs(getAcc("h4"), 0, 0, 4); + testGroupAttrs(getAcc("h5"), 0, 0, 5); + testGroupAttrs(getAcc("h6"), 0, 0, 6); + testGroupAttrs(getAcc("ariaHeadingNoLevel"), 0, 0, 2); + // No child item counts or "tree" flag for parent of headings + testAbsentAttrs(getAcc("headings"), { "child-item-count": "", tree: "" }); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<ul id="combo1" role="combobox">Password + <li id="combo1_opt1" role="option">Xyzzy</li> + <li id="combo1_opt2" role="option">Plughs</li> + <li id="combo1_opt3" role="option">Shazaam</li> + <li id="combo1_opt4" role="option">JoeSentMe</li> + </ul>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA combobox + testGroupAttrs(getAcc("combo1_opt1"), 1, 4); + testGroupAttrs(getAcc("combo1_opt2"), 2, 4); + testGroupAttrs(getAcc("combo1_opt3"), 3, 4); + testGroupAttrs(getAcc("combo1_opt4"), 4, 4); + testGroupParentAttrs(getAcc("combo1"), 4, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div role="table" aria-colcount="4" aria-rowcount="2" id="table"> + <div role="row" id="table_row" aria-rowindex="2"> + <div role="cell" id="table_cell" aria-colindex="3">cell</div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA table + testGroupAttrs(getAcc("table_cell"), 3, 4); + testGroupAttrs(getAcc("table_row"), 2, 2); + + // grid child item count provided by aria-rowcount + testGroupParentAttrs(getAcc("table"), 2, false); + // row child item count provided by parent grid's aria-colcount + testGroupParentAttrs(getAcc("table_row"), 4, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div role="grid" aria-readonly="true"> + <div tabindex="-1"> + <div role="row" id="wrapped_row_1"> + <div role="gridcell">cell content</div> + </div> + </div> + <div tabindex="-1"> + <div role="row" id="wrapped_row_2"> + <div role="gridcell">cell content</div> + </div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // Attributes calculated even when row is wrapped in a div. + testGroupAttrs(getAcc("wrapped_row_1"), 1, 2, null); + testGroupAttrs(getAcc("wrapped_row_2"), 2, 2, null); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div role="list" aria-owns="t1_li1 t1_li2 t1_li3" id="aria-list_4"> + <div role="listitem" id="t1_li2">Apples</div> + <div role="listitem" id="t1_li1">Oranges</div> + </div> + <div role="listitem" id="t1_li3">Bananas</div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list constructed by ARIA owns + testGroupAttrs(getAcc("t1_li1"), 1, 3); + testGroupAttrs(getAcc("t1_li2"), 2, 3); + testGroupAttrs(getAcc("t1_li3"), 3, 3); + testGroupParentAttrs(getAcc("aria-list_4"), 3, false); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<!-- ARIA comments, 1 level, group pos and size calculation --> + <article> + <p id="comm_single_1" role="comment">Comment 1</p> + <p id="comm_single_2" role="comment">Comment 2</p> + </article> + + <!-- Nested comments --> + <article> + <div id="comm_nested_1" role="comment"><p>Comment 1 level 1</p> + <div id="comm_nested_1_1" role="comment"><p>Comment 1 level 2</p></div> + <div id="comm_nested_1_2" role="comment"><p>Comment 2 level 2</p></div> + </div> + <div id="comm_nested_2" role="comment"><p>Comment 2 level 1</p> + <div id="comm_nested_2_1" role="comment"><p>Comment 3 level 2</p> + <div id="comm_nested_2_1_1" role="comment"><p>Comment 1 level 3</p></div> + </div> + </div> + <div id="comm_nested_3" role="comment"><p>Comment 3 level 1</p></div> + </article>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // Test group attributes of ARIA comments + testGroupAttrs(getAcc("comm_single_1"), 1, 2, 1); + testGroupAttrs(getAcc("comm_single_2"), 2, 2, 1); + testGroupAttrs(getAcc("comm_nested_1"), 1, 3, 1); + testGroupAttrs(getAcc("comm_nested_1_1"), 1, 2, 2); + testGroupAttrs(getAcc("comm_nested_1_2"), 2, 2, 2); + testGroupAttrs(getAcc("comm_nested_2"), 2, 3, 1); + testGroupAttrs(getAcc("comm_nested_2_1"), 1, 1, 2); + testGroupAttrs(getAcc("comm_nested_2_1_1"), 1, 1, 3); + testGroupAttrs(getAcc("comm_nested_3"), 3, 3, 1); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +addAccessibleTask( + `<div role="tree" id="tree4"><div role="treeitem" + id="tree4_ti1">Item 1</div><div role="treeitem" + id="tree4_ti2">Item 2</div></div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // Test that group position information updates after deleting node. + testGroupAttrs(getAcc("tree4_ti1"), 1, 2, 1); + testGroupAttrs(getAcc("tree4_ti2"), 2, 2, 1); + testGroupParentAttrs(getAcc("tree4"), 2, true); + + let p = waitForEvent(EVENT_REORDER, "tree4"); + invokeContentTask(browser, [], () => { + content.document.getElementById("tree4_ti1").remove(); + }); + + await p; + testGroupAttrs(getAcc("tree4_ti2"), 1, 1, 1); + testGroupParentAttrs(getAcc("tree4"), 1, true); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +// Verify that intervening SECTION accs in ARIA compound widgets do not split +// up the group info for descendant owned elements. Test various types of +// widgets that should all be treated the same. +addAccessibleTask( + `<div role="tree" id="tree"> + <div tabindex="0"> + <div role="treeitem" id="ti1">treeitem 1</div> + </div> + <div tabindex="0"> + <div role="treeitem" id="ti2">treeitem 2</div> + </div> + </div> + <div role="listbox" id="listbox"> + <div tabindex="0"> + <div role="option" id="opt1">option 1</div> + </div> + <div tabindex="0"> + <div role="option" id="opt2">option 2</div> + </div> + </div> + <div role="list" id="list"> + <div tabindex="0"> + <div role="listitem" id="li1">listitem 1</div> + </div> + <div tabindex="0"> + <div role="listitem" id="li2">listitem 2</div> + </div> + </div> + <div role="menu" id="menu"> + <div tabindex="0"> + <div role="menuitem" id="mi1">menuitem 1</div> + </div> + <div tabindex="0"> + <div role="menuitem" id="mi2">menuitem 2</div> + </div> + </div> + <div role="radiogroup" id="radiogroup"> + <div tabindex="0"> + <div role="radio" id="r1">radio 1</div> + </div> + <div tabindex="0"> + <div role="radio" id="r2">radio 2</div> + </div> + </div> +`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + testGroupAttrs(getAcc("ti1"), 1, 2, 1); + testGroupAttrs(getAcc("ti2"), 2, 2, 1); + + testGroupAttrs(getAcc("opt1"), 1, 2, 0); + testGroupAttrs(getAcc("opt2"), 2, 2, 0); + + testGroupAttrs(getAcc("li1"), 1, 2, 0); + testGroupAttrs(getAcc("li2"), 2, 2, 0); + + testGroupAttrs(getAcc("mi1"), 1, 2, 0); + testGroupAttrs(getAcc("mi2"), 2, 2, 0); + + testGroupAttrs(getAcc("r1"), 1, 2, 0); + testGroupAttrs(getAcc("r2"), 2, 2, 0); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); + +// Verify that non-generic accessibles (like buttons) correctly split the group +// info of descendant owned elements. +addAccessibleTask( + `<div role="tree" id="tree"> + <div role="button"> + <div role="treeitem" id="ti1">first</div> + </div> + <div tabindex="0"> + <div role="treeitem" id="ti2">second</div> + </div> + </div>`, + async function (browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + testGroupAttrs(getAcc("ti1"), 1, 1, 1); + testGroupAttrs(getAcc("ti2"), 1, 1, 1); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js new file mode 100644 index 0000000000..6d5995531e --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js @@ -0,0 +1,45 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Test ARIA Dialog +addAccessibleTask( + "e10s/doc_treeupdate_ariadialog.html", + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + role: ROLE_DOCUMENT, + children: [], + }); + + // Make dialog visible and update its inner content. + let onShow = waitForEvent(EVENT_SHOW, "dialog"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("dialog").style.display = "block"; + }); + await onShow; + + testAccessibleTree(accDoc, { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_DIALOG, + children: [ + { + role: ROLE_PUSHBUTTON, + children: [{ role: ROLE_TEXT_LEAF }], + }, + { + role: ROLE_ENTRY, + }, + ], + }, + ], + }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js new file mode 100644 index 0000000000..78f52d3162 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js @@ -0,0 +1,457 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testContainer1(browser, accDoc) { + const id = "t1_container"; + const docID = getAccessibleDOMNodeID(accDoc); + const acc = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree test ==================================== */ + // children are swapped by ARIA owns + let tree = { + SECTION: [{ CHECKBUTTON: [{ SECTION: [] }] }, { PUSHBUTTON: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================ Change ARIA owns ====================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // checkbox, native order + { PUSHBUTTON: [] }, // button, rearranged by ARIA own + { SECTION: [] }, // subdiv from the subtree, ARIA owned + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove ARIA owns ====================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns"); + await onReorder; + + // children follow the DOM order + tree = { + SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }], + }; + testAccessibleTree(acc, tree); + + /* ================ Set ARIA owns ========================================= */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // checkbox + { PUSHBUTTON: [] }, // button, rearranged by ARIA own + { SECTION: [] }, // subdiv from the subtree, ARIA owned + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Add ID to ARIA owns =================================== */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute( + browser, + id, + "aria-owns", + "t1_button t1_subdiv t1_group" + ); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // t1_checkbox + { PUSHBUTTON: [] }, // button, t1_button + { SECTION: [] }, // subdiv from the subtree, t1_subdiv + { GROUPING: [] }, // group from outside, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Append element ======================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let div = content.document.createElement("div"); + div.setAttribute("id", "t1_child3"); + div.setAttribute("role", "radio"); + content.document.getElementById(contentId).appendChild(div); + }); + await onReorder; + + // children are invalidated, they includes aria-owns swapped kids and + // newly inserted child. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // existing explicit, t1_checkbox + { RADIOBUTTON: [] }, // new explicit, t1_child3 + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { SECTION: [] }, // ARIA owned, t1_subdiv + { GROUPING: [] }, // ARIA owned, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove element ======================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + content.document.getElementById("t1_span").remove(); + }); + await onReorder; + + // subdiv should go away + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // explicit, t1_checkbox + { RADIOBUTTON: [] }, // explicit, t1_child3 + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { GROUPING: [] }, // ARIA owned, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove ID ============================================= */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute(browser, "t1_group", "id"); + await onReorder; + + tree = { + SECTION: [ + { CHECKBUTTON: [] }, + { RADIOBUTTON: [] }, + { PUSHBUTTON: [] }, // ARIA owned, t1_button + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Set ID ================================================ */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute(browser, "t1_grouptmp", "id", "t1_group"); + await onReorder; + + tree = { + SECTION: [ + { CHECKBUTTON: [] }, + { RADIOBUTTON: [] }, + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { GROUPING: [] }, // ARIA owned, t1_group, previously t1_grouptmp + ], + }; + testAccessibleTree(acc, tree); +} + +async function removeContainer(browser, accDoc) { + const id = "t2_container1"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [ + { CHECKBUTTON: [] }, // ARIA owned, 't2_owned' + ], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("t2_container2") + .removeChild(content.document.getElementById("t2_container3")); + }); + await onReorder; + + tree = { + SECTION: [], + }; + testAccessibleTree(acc, tree); +} + +async function stealAndRecacheChildren(browser, accDoc) { + const id1 = "t3_container1"; + const id2 = "t3_container2"; + const acc1 = findAccessibleChildByID(accDoc, id1); + const acc2 = findAccessibleChildByID(accDoc, id2); + + /* ================ Attempt to steal from other ARIA owns ================= */ + let onReorder = waitForEvent(EVENT_REORDER, id2); + await invokeSetAttribute(browser, id2, "aria-owns", "t3_child"); + await invokeContentTask(browser, [id2], id => { + let div = content.document.createElement("div"); + div.setAttribute("role", "radio"); + content.document.getElementById(id).appendChild(div); + }); + await onReorder; + + let tree = { + SECTION: [ + { CHECKBUTTON: [] }, // ARIA owned + ], + }; + testAccessibleTree(acc1, tree); + + tree = { + SECTION: [{ RADIOBUTTON: [] }], + }; + testAccessibleTree(acc2, tree); +} + +async function showHiddenElement(browser, accDoc) { + const id = "t4_container1"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [{ RADIOBUTTON: [] }], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetStyle(browser, "t4_child1", "display", "block"); + await onReorder; + + tree = { + SECTION: [{ CHECKBUTTON: [] }, { RADIOBUTTON: [] }], + }; + testAccessibleTree(acc, tree); +} + +async function rearrangeARIAOwns(browser, accDoc) { + const id = "t5_container"; + const acc = findAccessibleChildByID(accDoc, id); + const tests = [ + { + val: "t5_checkbox t5_radio t5_button", + roleList: ["CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON"], + }, + { + val: "t5_radio t5_button t5_checkbox", + roleList: ["RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON"], + }, + ]; + + for (let { val, roleList } of tests) { + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", val); + await onReorder; + + let tree = { SECTION: [] }; + for (let role of roleList) { + let ch = {}; + ch[role] = []; + tree.SECTION.push(ch); + } + testAccessibleTree(acc, tree); + } +} + +async function removeNotARIAOwnedEl(browser, accDoc) { + const id = "t6_container"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [{ TEXT_LEAF: [] }, { GROUPING: [] }], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + content.document + .getElementById(contentId) + .removeChild(content.document.getElementById("t6_span")); + }); + await onReorder; + + tree = { + SECTION: [{ GROUPING: [] }], + }; + testAccessibleTree(acc, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_ariaowns.html", + async function (browser, accDoc) { + await testContainer1(browser, accDoc); + await removeContainer(browser, accDoc); + await stealAndRecacheChildren(browser, accDoc); + await showHiddenElement(browser, accDoc); + await rearrangeARIAOwns(browser, accDoc); + await removeNotARIAOwnedEl(browser, accDoc); + }, + { iframe: true, remoteIframe: true } +); + +// Test owning an ancestor which isn't created yet with an iframe in the +// subtree. +addAccessibleTask( + ` + <span id="a"> + <div id="b" aria-owns="c"></div> + </span> + <div id="c"> + <iframe></iframe> + </div> + <script> + document.getElementById("c").setAttribute("aria-owns", "a"); + </script> + `, + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + DOCUMENT: [ + { + // b + SECTION: [ + { + // c + SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }], + }, + ], + }, + ], + }); + } +); + +// Verify that removing the parent of a DOM-sibling aria-owned child keeps the +// formerly-owned child in the tree. +addAccessibleTask( + `<input id='x'></input><div aria-owns='x'></div>`, + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + DOCUMENT: [{ SECTION: [{ ENTRY: [] }] }], + }); + + info("Removing the div that aria-owns a DOM sibling"); + let onReorder = waitForEvent(EVENT_REORDER, accDoc); + await invokeContentTask(browser, [], () => { + content.document.querySelector("div").remove(); + }); + await onReorder; + + info("Verifying that the formerly-owned child is still present"); + testAccessibleTree(accDoc, { + DOCUMENT: [{ ENTRY: [] }], + }); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that removing the parent of multiple DOM-sibling aria-owned children +// keeps all formerly-owned children in the tree. +addAccessibleTask( + `<input id='x'></input><input id='y'><div aria-owns='x y'></div>`, + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + DOCUMENT: [ + { + SECTION: [{ ENTRY: [] }, { ENTRY: [] }], + }, + ], + }); + + info("Removing the div that aria-owns DOM siblings"); + let onReorder = waitForEvent(EVENT_REORDER, accDoc); + await invokeContentTask(browser, [], () => { + content.document.querySelector("div").remove(); + }); + await onReorder; + + info("Verifying that the formerly-owned children are still present"); + testAccessibleTree(accDoc, { + DOCUMENT: [{ ENTRY: [] }, { ENTRY: [] }], + }); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that reordering owned elements by changing the aria-owns attribute +// properly reorders owned elements. +addAccessibleTask( + ` +<div id="container" aria-owns="b d c a"> + <div id="a" role="button"></div> + <div id="b" role="checkbox"></div> +</div> +<div id="c" role="radio"></div> +<div id="d"></div>`, + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + DOCUMENT: [ + { + SECTION: [ + { CHECKBUTTON: [] }, // b + { SECTION: [] }, // d + { RADIOBUTTON: [] }, // c + { PUSHBUTTON: [] }, // a + ], + }, + ], + }); + + info("Removing the div that aria-owns other elements"); + let onReorder = waitForEvent(EVENT_REORDER, accDoc); + await invokeContentTask(browser, [], () => { + content.document.querySelector("#container").remove(); + }); + await onReorder; + + info( + "Verify DOM children are removed, order of remaining elements is correct" + ); + testAccessibleTree(accDoc, { + DOCUMENT: [ + { RADIOBUTTON: [] }, // c + { SECTION: [] }, // d + ], + }); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that we avoid sending unwanted hide events when doing multiple +// aria-owns relocations in a single tick. Note that we're avoiding testing +// chrome here since parent process locals don't track moves in the same way, +// meaning our mechanism for avoiding duplicate hide events doesn't work. +addAccessibleTask( + ` +<div id='b' aria-owns='a'></div> +<div id='d'></div> +<dd id='f'> + <div id='a' aria-owns='d'></div> +</dd> + `, + async function (browser, accDoc) { + const b = findAccessibleChildByID(accDoc, "b"); + const waitFor = { + expected: [ + [EVENT_HIDE, b], + [EVENT_SHOW, "d"], + [EVENT_REORDER, accDoc], + ], + unexpected: [ + [EVENT_HIDE, "d"], + [EVENT_REORDER, "a"], + ], + }; + info( + "Verifying that events are fired properly after doing two aria-owns relocations" + ); + await contentSpawnMutation(browser, waitFor, function () { + content.document.querySelector("#b").remove(); + content.document.querySelector("#f").remove(); + }); + }, + { chrome: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_canvas.js b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js new file mode 100644 index 0000000000..ad7338f725 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js @@ -0,0 +1,28 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <canvas id="canvas"> + <div id="dialog" role="dialog" style="display: none;"></div> + </canvas>`, + async function (browser, accDoc) { + let canvas = findAccessibleChildByID(accDoc, "canvas"); + let dialog = findAccessibleChildByID(accDoc, "dialog"); + + testAccessibleTree(canvas, { CANVAS: [] }); + + let onShow = waitForEvent(EVENT_SHOW, "dialog"); + await invokeSetStyle(browser, "dialog", "display", "block"); + await onShow; + + testAccessibleTree(dialog, { DIALOG: [] }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js new file mode 100644 index 0000000000..8a1f93d0be --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js @@ -0,0 +1,105 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const snippet = ` + <style> + #target { + width: 150px; + height: 150px; + background-color: lightblue; + } + #child { + width: 100px; + height: 100px; + background-color: lightgreen; + } + #content-child { + width: 100px; + height: 100px; + background-color: green; + display: contents; + } + .hidden { + content-visibility: hidden; + } + .auto { + content-visibility: auto; + } + #hidden-subtree-2 { + visibility: hidden; + } + </style> + <div class="hidden" id="target"> + <div id="child">A</div> + <div id="content-child">B</div> + <div id="hidden-subtree-1" class="hidden">C</div> + <div id="hidden-subtree-2">D</div> + <div id="shadow-host"></div> + </div> + <script> + const host = document.querySelector("#shadow-host"); + const shadowRoot = host.attachShadow({ mode: "open" }); + shadowRoot.innerHTML = "<div id='shadowDiv'>E</div>"; + </script> + `; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["layout.css.content-visibility.enabled", true]], + }); +}); + +async function setContentVisibility(browser, value) { + let mutationPromise = (() => { + switch (value) { + case "hidden": + return waitForEvent(EVENT_REORDER, "target"); + case "auto": + return waitForEvents({ + expected: [ + [EVENT_REORDER, "child"], + [EVENT_REORDER, "content-child"], + [EVENT_REORDER, "shadowDiv"], + [EVENT_REORDER, "target"], + ], + }); + default: + throw new Error(`unexpected content-visibility: ${value}`); + } + })(); + + // Change the value of `content-visibility` property for the target + info(`Setting content-visibility: ${value}`); + await invokeSetAttribute(browser, "target", "class", value); + await mutationPromise; +} + +addAccessibleTask( + snippet, + async function (browser, accDoc) { + const target = findAccessibleChildByID(accDoc, "target"); + + info("Initial Accessibility Structure Test"); + testAccessibleTree(target, { SECTION: [] }); + + await setContentVisibility(browser, "auto"); + testAccessibleTree(target, { + SECTION: [ + { SECTION: [{ TEXT_LEAF: [] }] } /* child */, + { SECTION: [{ TEXT_LEAF: [] }] } /* content-child */, + { SECTION: [] } /* hidden-subtree-1 */, + { SECTION: [{ SECTION: [{ TEXT_LEAF: [] }] }] } /* shadow-host */, + ], + }); + + await setContentVisibility(browser, "hidden"); + testAccessibleTree(target, { SECTION: [] }); + }, + { iframe: true, remoteIframe: true, chrome: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js new file mode 100644 index 0000000000..4d18f1c08d --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js @@ -0,0 +1,60 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <div id="container"><div id="scrollarea" style="overflow:auto;"><input>`, + async function (browser, accDoc) { + const id1 = "container"; + const container = findAccessibleChildByID(accDoc, id1); + + /* ================= Change scroll range ================================== */ + let tree = { + SECTION: [ + { + // container + SECTION: [ + { + // scroll area + ENTRY: [], // child content + }, + ], + }, + ], + }; + testAccessibleTree(container, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id1); + await invokeContentTask(browser, [id1], id => { + let doc = content.document; + doc.getElementById("scrollarea").style.width = "20px"; + doc.getElementById(id).appendChild(doc.createElement("input")); + }); + await onReorder; + + tree = { + SECTION: [ + { + // container + SECTION: [ + { + // scroll area + ENTRY: [], // child content + }, + ], + }, + { + ENTRY: [], // inserted input + }, + ], + }; + testAccessibleTree(container, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_doc.js b/accessible/tests/browser/e10s/browser_treeupdate_doc.js new file mode 100644 index 0000000000..982b039762 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_doc.js @@ -0,0 +1,320 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const iframeSrc = `data:text/html, + <html> + <head> + <meta charset='utf-8'/> + <title>Inner Iframe</title> + </head> + <body id='inner-iframe'></body> + </html>`; + +addAccessibleTask( + ` + <iframe id="iframe" src="${iframeSrc}"></iframe>`, + async function (browser, accDoc) { + // ID of the iframe that is being tested + const id = "inner-iframe"; + + let iframe = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree check =================================== */ + let tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Write iframe document ================================ */ + let reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newHTMLNode = docNode.createElement("html"); + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Wave"); + newBodyNode.id = contentId; + newBodyNode.appendChild(newTextNode); + newHTMLNode.appendChild(newBodyNode); + docNode.replaceChild(newHTMLNode, docNode.documentElement); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Wave", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Replace iframe HTML element ========================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + // We can't use open/write/close outside of iframe document because of + // security error. + let script = docNode.createElement("script"); + script.textContent = ` + document.open(); + document.write('<body id="${contentId}">hello</body>'); + document.close();`; + docNode.body.appendChild(script); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Replace iframe body ================================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.id = contentId; + newBodyNode.appendChild(newTextNode); + newBodyNode.setAttribute("role", "application"); + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_APPLICATION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Open iframe document ================================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Open document. + let docNode = content.document.getElementById("iframe").contentDocument; + let script = docNode.createElement("script"); + script.textContent = ` + function closeMe() { + document.write('Works?'); + document.close(); + } + window.closeMe = closeMe; + document.open(); + document.write('<body id="${contentId}"></body>');`; + docNode.body.appendChild(script); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Close iframe document ================================ */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + // Write and close document. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.write("Works?"); + docNode.close(); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Works?", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Remove HTML from iframe document ===================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + // Remove HTML element. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.firstChild.remove(); + }); + let event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Insert HTML to iframe document ======================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Insert HTML element. + let docNode = content.document.getElementById("iframe").contentDocument; + let html = docNode.createElement("html"); + let body = docNode.createElement("body"); + let text = docNode.createTextNode("Haha"); + body.appendChild(text); + body.id = contentId; + html.appendChild(body); + docNode.appendChild(html); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Haha", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Remove body from iframe document ===================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + // Remove body element. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.documentElement.removeChild(docNode.body); + }); + event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================ Insert element under document element while body missed */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + let docNode = content.document.getElementById("iframe").contentDocument; + let inputNode = (content.window.inputNode = + docNode.createElement("input")); + docNode.documentElement.appendChild(inputNode); + }); + event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + DOCUMENT: [{ ENTRY: [] }], + }; + testAccessibleTree(iframe, tree); + + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + let docEl = + content.document.getElementById("iframe").contentDocument + .documentElement; + // Remove aftermath of this test before next test starts. + docEl.firstChild.remove(); + }); + // Make sure reorder event was fired and that the input was removed. + await reorderEventPromise; + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Insert body to iframe document ======================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Write and close document. + let docNode = content.document.getElementById("iframe").contentDocument; + // Insert body element. + let body = docNode.createElement("body"); + let text = docNode.createTextNode("Yo ho ho i butylka roma!"); + body.appendChild(text); + body.id = contentId; + docNode.documentElement.appendChild(body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Yo ho ho i butylka roma!", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Change source ======================================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, "iframe"); + await invokeSetAttribute( + browser, + "iframe", + "src", + `data:text/html,<html><body id="${id}"><input></body></html>` + ); + event = await reorderEventPromise; + + tree = { + INTERNAL_FRAME: [{ DOCUMENT: [{ ENTRY: [] }] }], + }; + testAccessibleTree(event.accessible, tree); + iframe = findAccessibleChildByID(event.accessible, id); + + /* ================= Replace iframe body on ARIA role body ================ */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.appendChild(newTextNode); + newBodyNode.setAttribute("role", "application"); + newBodyNode.id = contentId; + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_APPLICATION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js new file mode 100644 index 0000000000..95406d96cf --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js @@ -0,0 +1,94 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <style> + .gentext:before { + content: "START" + } + .gentext:after { + content: "END" + } + </style> + <div id="container1"></div> + <div id="container2"><div id="container2_child">text</div></div>`, + async function (browser, accDoc) { + const id1 = "container1"; + const id2 = "container2"; + let container1 = findAccessibleChildByID(accDoc, id1); + let container2 = findAccessibleChildByID(accDoc, id2); + + let tree = { + SECTION: [], // container + }; + testAccessibleTree(container1, tree); + + tree = { + SECTION: [ + { + // container2 + SECTION: [ + { + // container2 child + TEXT_LEAF: [], // primary text + }, + ], + }, + ], + }; + testAccessibleTree(container2, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id1); + // Create and add an element with CSS generated content to container1 + await invokeContentTask(browser, [id1], id => { + let node = content.document.createElement("div"); + node.textContent = "text"; + node.setAttribute("class", "gentext"); + content.document.getElementById(id).appendChild(node); + }); + await onReorder; + + tree = { + SECTION: [ + // container + { + SECTION: [ + // inserted node + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ], + }, + ], + }; + testAccessibleTree(container1, tree); + + onReorder = waitForEvent(EVENT_REORDER, "container2_child"); + // Add CSS generated content to an element in container2's subtree + await invokeSetAttribute(browser, "container2_child", "class", "gentext"); + await onReorder; + + tree = { + SECTION: [ + // container2 + { + SECTION: [ + // container2 child + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ], + }, + ], + }; + testAccessibleTree(container2, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_hidden.js b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js new file mode 100644 index 0000000000..d3817a003b --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js @@ -0,0 +1,32 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function setHidden(browser, value) { + let onReorder = waitForEvent(EVENT_REORDER, "container"); + await invokeSetAttribute(browser, "child", "hidden", value); + await onReorder; +} + +addAccessibleTask( + '<div id="container"><input id="child"></div>', + async function (browser, accDoc) { + let container = findAccessibleChildByID(accDoc, "container"); + + testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] }); + + // Set @hidden attribute + await setHidden(browser, "true"); + testAccessibleTree(container, { SECTION: [] }); + + // Remove @hidden attribute + await setHidden(browser); + testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_image.js b/accessible/tests/browser/e10s/browser_treeupdate_image.js new file mode 100644 index 0000000000..cf45de65e0 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_image.js @@ -0,0 +1,192 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const IMG_ID = "img"; +const ALT_TEXT = "some-text"; +const ARIA_LABEL = "some-label"; + +// Verify that granting alt text adds the graphic accessible. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`, + async function (browser, accDoc) { + // Test initial state; the img has empty alt text so it should not be in the tree. + const acc = findAccessibleChildByID(accDoc, IMG_ID); + ok(!acc, "Image has no Accessible"); + + // Add the alt text. The graphic should have been inserted into the tree. + info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`); + const shown = waitForEvent(EVENT_SHOW, IMG_ID); + await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT); + await shown; + let tree = { + role: ROLE_GRAPHIC, + name: ALT_TEXT, + children: [], + }; + testAccessibleTree(acc, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that the graphic accessible exists even with a missing alt attribute. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png"/>`, + async function (browser, accDoc) { + // Test initial state; the img has no alt attribute so the name is empty. + const acc = findAccessibleChildByID(accDoc, IMG_ID); + let tree = { + role: ROLE_GRAPHIC, + name: null, + children: [], + }; + testAccessibleTree(acc, tree); + + // Add the alt text. The graphic should still be present in the tree. + info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`); + const shown = waitForEvent(EVENT_NAME_CHANGE, IMG_ID); + await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT); + await shown; + tree = { + role: ROLE_GRAPHIC, + name: ALT_TEXT, + children: [], + }; + testAccessibleTree(acc, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that removing alt text removes the graphic accessible. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt="${ALT_TEXT}"/>`, + async function (browser, accDoc) { + // Test initial state; the img has alt text so it should be in the tree. + let acc = findAccessibleChildByID(accDoc, IMG_ID); + let tree = { + role: ROLE_GRAPHIC, + name: ALT_TEXT, + children: [], + }; + testAccessibleTree(acc, tree); + + // Set the alt text empty. The graphic should have been removed from the tree. + info(`Setting empty alt text for img id ${IMG_ID}`); + const hidden = waitForEvent(EVENT_HIDE, acc); + await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => { + let elm = content.document.getElementById(id); + elm.setAttribute(attr, value); + }); + await hidden; + acc = findAccessibleChildByID(accDoc, IMG_ID); + ok(!acc, "Image has no Accessible"); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that the presence of an aria-label creates an accessible, even if +// there is no alt text. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" aria-label="${ARIA_LABEL}" alt=""/>`, + async function (browser, accDoc) { + // Test initial state; the img has empty alt text, but it does have an + // aria-label, so it should be in the tree. + const acc = findAccessibleChildByID(accDoc, IMG_ID); + let tree = { + role: ROLE_GRAPHIC, + name: ARIA_LABEL, + children: [], + }; + testAccessibleTree(acc, tree); + + // Add the alt text. The graphic should still be in the tree. + info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`); + await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT); + tree = { + role: ROLE_GRAPHIC, + name: ARIA_LABEL, + children: [], + }; + testAccessibleTree(acc, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that the presence of a click listener results in the graphic +// accessible's presence in the tree. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`, + async function (browser, accDoc) { + // Add a click listener to the img element. + info(`Adding click listener to img id '${IMG_ID}'`); + const shown = waitForEvent(EVENT_SHOW, IMG_ID); + await invokeContentTask(browser, [IMG_ID], id => { + content.document.getElementById(id).addEventListener("click", () => {}); + }); + await shown; + + // Test initial state; the img has empty alt text, but it does have a click + // listener, so it should be in the tree. + let acc = findAccessibleChildByID(accDoc, IMG_ID); + let tree = { + role: ROLE_GRAPHIC, + name: null, + children: [], + }; + testAccessibleTree(acc, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that the presentation role prevents creation of the graphic accessible. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" role="presentation"/>`, + async function (browser, accDoc) { + // Test initial state; the img is presentational and should not be in the tree. + const acc = findAccessibleChildByID(accDoc, IMG_ID); + ok(!acc, "Image has no Accessible"); + + // Add some alt text. There should still be no accessible for the img in the tree. + info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`); + await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT); + ok(!acc, "Image has no Accessible"); + + // Remove the presentation role. The accessible should be created. + info(`Removing presentation role from img id ${IMG_ID}`); + const shown = waitForEvent(EVENT_SHOW, IMG_ID); + await invokeSetAttribute(browser, IMG_ID, "role", ""); + await shown; + let tree = { + role: ROLE_GRAPHIC, + name: ALT_TEXT, + children: [], + }; + testAccessibleTree(acc, tree); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +// Verify that setting empty alt text on a hidden image does not crash. +// See Bug 1799208 for more info. +addAccessibleTask( + `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" hidden/>`, + async function (browser, accDoc) { + // Test initial state; should be no accessible since img is hidden. + const acc = findAccessibleChildByID(accDoc, IMG_ID); + ok(!acc, "Image has no Accessible"); + + // Add empty alt text. We shouldn't crash. + info(`Adding empty alt text "" to img id '${IMG_ID}'`); + await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => { + let elm = content.document.getElementById(id); + elm.setAttribute(attr, value); + }); + ok(true, "Setting empty alt text on a hidden image did not crash"); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js new file mode 100644 index 0000000000..82fbd3427e --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js @@ -0,0 +1,190 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testImageMap(browser, accDoc) { + const id = "imgmap"; + const acc = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree test ==================================== */ + let tree = { + IMAGE_MAP: [{ role: ROLE_LINK, name: "b", children: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Insert area ========================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let areaElm = content.document.createElement("area"); + let mapNode = content.document.getElementById("map"); + areaElm.setAttribute( + "href", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + ); + areaElm.setAttribute("coords", "0,0,13,14"); + areaElm.setAttribute("alt", "a"); + areaElm.setAttribute("shape", "rect"); + mapNode.insertBefore(areaElm, mapNode.firstChild); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "a", children: [] }, + { role: ROLE_LINK, name: "b", children: [] }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Append area ========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let areaElm = content.document.createElement("area"); + let mapNode = content.document.getElementById("map"); + areaElm.setAttribute( + "href", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://www.bbc.co.uk/radio4/atoz/index.shtml#c" + ); + areaElm.setAttribute("coords", "34,0,47,14"); + areaElm.setAttribute("alt", "c"); + areaElm.setAttribute("shape", "rect"); + mapNode.appendChild(areaElm); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "a", children: [] }, + { role: ROLE_LINK, name: "b", children: [] }, + { role: ROLE_LINK, name: "c", children: [] }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Remove area ========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let mapNode = content.document.getElementById("map"); + mapNode.removeChild(mapNode.firstElementChild); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "b", children: [] }, + { role: ROLE_LINK, name: "c", children: [] }, + ], + }; + testAccessibleTree(acc, tree); +} + +async function testContainer(browser) { + const id = "container"; + /* ================= Remove name on map =================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, "map", "name"); + let event = await onReorder; + const acc = event.accessible; + + let tree = { + SECTION: [{ GRAPHIC: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Restore name on map ================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, "map", "name", "atoz_map"); + // XXX: force repainting of the image (see bug 745788 for details). + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeMouse( + content.document.getElementById("imgmap"), + 10, + 10, + { type: "mousemove" }, + content + ); + }); + await onReorder; + + tree = { + SECTION: [ + { + IMAGE_MAP: [{ LINK: [] }, { LINK: [] }], + }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Remove map =========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let mapNode = content.document.getElementById("map"); + mapNode.remove(); + }); + await onReorder; + + tree = { + SECTION: [{ GRAPHIC: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Insert map =========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let map = content.document.createElement("map"); + let area = content.document.createElement("area"); + + map.setAttribute("name", "atoz_map"); + map.setAttribute("id", "map"); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + area.setAttribute("href", "http://www.bbc.co.uk/radio4/atoz/index.shtml#b"); + area.setAttribute("coords", "17,0,30,14"); + area.setAttribute("alt", "b"); + area.setAttribute("shape", "rect"); + + map.appendChild(area); + content.document.getElementById(contentId).appendChild(map); + }); + await onReorder; + + tree = { + SECTION: [ + { + IMAGE_MAP: [{ LINK: [] }], + }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Hide image map ======================================= */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetStyle(browser, "imgmap", "display", "none"); + await onReorder; + + tree = { + SECTION: [], + }; + testAccessibleTree(acc, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_imagemap.html", + async function (browser, accDoc) { + await waitForImageMap(browser, accDoc); + await testImageMap(browser, accDoc); + await testContainer(browser); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list.js b/accessible/tests/browser/e10s/browser_treeupdate_list.js new file mode 100644 index 0000000000..2ca14d5572 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_list.js @@ -0,0 +1,52 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function setDisplayAndWaitForReorder(browser, value) { + let onReorder = waitForEvent(EVENT_REORDER, "ul"); + await invokeSetStyle(browser, "li", "display", value); + return onReorder; +} + +addAccessibleTask( + ` + <ul id="ul"> + <li id="li">item1</li> + </ul>`, + async function (browser, accDoc) { + let li = findAccessibleChildByID(accDoc, "li"); + let bullet = li.firstChild; + let accTree = { + role: ROLE_LISTITEM, + children: [ + { + role: ROLE_LISTITEM_MARKER, + children: [], + }, + { + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + testAccessibleTree(li, accTree); + + await setDisplayAndWaitForReorder(browser, "none"); + + ok(isDefunct(li), "Check that li is defunct."); + ok(isDefunct(bullet), "Check that bullet is defunct."); + + let event = await setDisplayAndWaitForReorder(browser, "list-item"); + + testAccessibleTree( + findAccessibleChildByID(event.accessible, "li"), + accTree + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js new file mode 100644 index 0000000000..dd678d93fa --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js @@ -0,0 +1,48 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<ol id="list"></ol>', + async function (browser, accDoc) { + let list = findAccessibleChildByID(accDoc, "list"); + + testAccessibleTree(list, { + role: ROLE_LIST, + children: [], + }); + + await invokeSetAttribute( + browser, + currentContentDoc(), + "contentEditable", + "true" + ); + let onReorder = waitForEvent(EVENT_REORDER, "list"); + await invokeContentTask(browser, [], () => { + let li = content.document.createElement("li"); + li.textContent = "item"; + content.document.getElementById("list").appendChild(li); + }); + await onReorder; + + testAccessibleTree(list, { + role: ROLE_LIST, + children: [ + { + role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER, name: "1. ", children: [] }, + { role: ROLE_TEXT_LEAF, children: [] }, + ], + }, + ], + }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_listener.js b/accessible/tests/browser/e10s/browser_treeupdate_listener.js new file mode 100644 index 0000000000..735f7871af --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_listener.js @@ -0,0 +1,38 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<span id="parent"><span id="child"></span></span>', + async function (browser, accDoc) { + is( + findAccessibleChildByID(accDoc, "parent"), + null, + "Check that parent is not accessible." + ); + is( + findAccessibleChildByID(accDoc, "child"), + null, + "Check that child is not accessible." + ); + + let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc); + // Add an event listener to parent. + await invokeContentTask(browser, [], () => { + content.window.dummyListener = () => {}; + content.document + .getElementById("parent") + .addEventListener("click", content.window.dummyListener); + }); + await onReorder; + + let tree = { TEXT: [] }; + testAccessibleTree(findAccessibleChildByID(accDoc, "parent"), tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_move.js b/accessible/tests/browser/e10s/browser_treeupdate_move.js new file mode 100644 index 0000000000..8ed6188ef3 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_move.js @@ -0,0 +1,84 @@ +/* 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/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test moving Accessibles: + * 1. A moved Accessible keeps the same Accessible. + * 2. If the moved Accessible is focused, it remains focused. + * 3. A child of the moved Accessible also keeps the same Accessible. + * 4. A child removed at the same time as the move gets shut down. + */ +addAccessibleTask( + ` +<div id="scrollable" role="presentation" style="height: 1px;"> + <div contenteditable id="textbox" role="textbox"> + <h1 id="heading">Heading</h1> + <p id="para">Para</p> + </div> + <iframe id="iframe" src="https://example.com/"></iframe> +</div> + `, + async function (browser, docAcc) { + const textbox = findAccessibleChildByID(docAcc, "textbox"); + const heading = findAccessibleChildByID(docAcc, "heading"); + const para = findAccessibleChildByID(docAcc, "para"); + const iframe = findAccessibleChildByID(docAcc, "iframe"); + const iframeDoc = iframe.firstChild; + ok(iframeDoc, "iframe contains a document"); + + let focused = waitForEvent(EVENT_FOCUS, textbox); + textbox.takeFocus(); + await focused; + testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT); + + let reordered = waitForEvent(EVENT_REORDER, docAcc); + await invokeContentTask(browser, [], () => { + // scrollable wasn't in the a11y tree, but this will force it to be created. + // textbox will be moved inside it. + content.document.getElementById("scrollable").style.overflow = "scroll"; + content.document.getElementById("heading").remove(); + }); + await reordered; + // Despite the move, ensure textbox is still alive and is focused. + testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT); + // Ensure para (a child of textbox) is also still alive. + ok(!isDefunct(para), "para is alive"); + // heading was a child of textbox, but was removed when textbox + // was moved. Ensure it is dead. + ok(isDefunct(heading), "heading is dead"); + // Ensure the iframe and its embedded document are alive. + ok(!isDefunct(iframe), "iframe is alive"); + ok(!isDefunct(iframeDoc), "iframeDoc is alive"); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test that moving a subtree containing an iframe doesn't cause assertions or + * crashes. Note that aria-owns moves Accessibles even if it is set before load. + */ +addAccessibleTask( + ` +<div id="container"> + <iframe id="iframe"></iframe> + <div aria-owns="iframe"></div> +</div> + `, + async function (browser, docAcc) { + const container = findAccessibleChildByID(docAcc, "container"); + testAccessibleTree(container, { + SECTION: [{ SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] }], + }); + }, + { topLevel: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js new file mode 100644 index 0000000000..ec7eed0919 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js @@ -0,0 +1,100 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<select id="select"></select>', + async function (browser, accDoc) { + let select = findAccessibleChildByID(accDoc, "select"); + + let onEvent = waitForEvent(EVENT_REORDER, "select"); + // Create a combobox with grouping and 2 standalone options + await invokeContentTask(browser, [], () => { + let doc = content.document; + let contentSelect = doc.getElementById("select"); + let optGroup = doc.createElement("optgroup"); + + for (let i = 0; i < 2; i++) { + let opt = doc.createElement("option"); + opt.value = i; + opt.text = "Option: Value " + i; + optGroup.appendChild(opt); + } + contentSelect.add(optGroup, null); + + for (let i = 0; i < 2; i++) { + let opt = doc.createElement("option"); + contentSelect.add(opt, null); + } + contentSelect.firstChild.firstChild.id = "option1Node"; + }); + let event = await onEvent; + let option1Node = findAccessibleChildByID(event.accessible, "option1Node"); + + let tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [ + { + GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }], + }, + { + COMBOBOX_OPTION: [], + }, + { + COMBOBOX_OPTION: [], + }, + ], + }, + ], + }; + testAccessibleTree(select, tree); + ok(!isDefunct(option1Node), "option shouldn't be defunct"); + + onEvent = waitForEvent(EVENT_REORDER, "select"); + // Remove grouping from combobox + await invokeContentTask(browser, [], () => { + let contentSelect = content.document.getElementById("select"); + contentSelect.firstChild.remove(); + }); + await onEvent; + + tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }], + }, + ], + }; + testAccessibleTree(select, tree); + ok( + isDefunct(option1Node), + "removed option shouldn't be accessible anymore!" + ); + + onEvent = waitForEvent(EVENT_REORDER, "select"); + // Remove all options from combobox + await invokeContentTask(browser, [], () => { + let contentSelect = content.document.getElementById("select"); + while (contentSelect.length) { + contentSelect.remove(0); + } + }); + await onEvent; + + tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [], + }, + ], + }; + testAccessibleTree(select, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_removal.js b/accessible/tests/browser/e10s/browser_treeupdate_removal.js new file mode 100644 index 0000000000..6b5246f0bf --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_removal.js @@ -0,0 +1,58 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "e10s/doc_treeupdate_removal.xhtml", + async function (browser, accDoc) { + ok( + isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table should be accessible" + ); + + // Move the_table element into hidden subtree. + let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("the_displaynone") + .appendChild(content.document.getElementById("the_table")); + }); + await onReorder; + + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table in display none tree shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_row")), + "row shouldn't be accessible" + ); + + // Remove the_row element (since it did not have accessible, no event needed). + await invokeContentTask(browser, [], () => { + content.document.body.removeChild( + content.document.getElementById("the_row") + ); + }); + + // make sure no accessibles have stuck around. + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_row")), + "row shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_displayNone")), + "display none things shouldn't be accessible" + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js new file mode 100644 index 0000000000..a82fc4c04d --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js @@ -0,0 +1,73 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const snippet = ` +<select id="select"> + <option>o1</option> + <optgroup label="g1"> + <option>g1o1</option> + <option>g1o2</option> + </optgroup> + <optgroup label="g2"> + <option>g2o1</option> + <option>g2o2</option> + </optgroup> + <option>o2</option> +</select> +`; + +addAccessibleTask( + snippet, + async function (browser, accDoc) { + await invokeFocus(browser, "select"); + // Expand the select. A dropdown item should get focus. + // Note that the dropdown is rendered in the parent process. + let focused = waitForEvent( + EVENT_FOCUS, + event => event.accessible.role == ROLE_COMBOBOX_OPTION, + "Dropdown item focused after select expanded" + ); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content); + }); + info("Waiting for parent focus"); + let event = await focused; + let dropdown = event.accessible.parent; + + let selectedOptionChildren = []; + if (MAC) { + // Checkmark is part of the Mac menu styling. + selectedOptionChildren = [{ STATICTEXT: [] }]; + } + let tree = { + COMBOBOX_LIST: [ + { COMBOBOX_OPTION: selectedOptionChildren }, + { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] }, + { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] }, + { COMBOBOX_OPTION: [] }, + ], + }; + testAccessibleTree(dropdown, tree); + + // Collapse the select. Focus should return to the select. + focused = waitForEvent( + EVENT_FOCUS, + "select", + "select focused after collapsed" + ); + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + info("Waiting for child focus"); + await focused; + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_table.js b/accessible/tests/browser/e10s/browser_treeupdate_table.js new file mode 100644 index 0000000000..c188a06044 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_table.js @@ -0,0 +1,48 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <table id="table"> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table>`, + async function (browser, accDoc) { + let table = findAccessibleChildByID(accDoc, "table"); + + let tree = { + TABLE: [ + { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] }, + ], + }; + testAccessibleTree(table, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "table"); + await invokeContentTask(browser, [], () => { + // append a caption, it should appear as a first element in the + // accessible tree. + let doc = content.document; + let caption = doc.createElement("caption"); + caption.textContent = "table caption"; + doc.getElementById("table").appendChild(caption); + }); + await onReorder; + + tree = { + TABLE: [ + { CAPTION: [{ TEXT_LEAF: [] }] }, + { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] }, + ], + }; + testAccessibleTree(table, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js new file mode 100644 index 0000000000..0c617e7026 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js @@ -0,0 +1,38 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function removeTextData(browser, accessible, id, role) { + let tree = { + role, + children: [{ role: ROLE_TEXT_LEAF, name: "text" }], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + content.document.getElementById(contentId).firstChild.textContent = ""; + }); + await onReorder; + + tree = { role, children: [] }; + testAccessibleTree(accessible, tree); +} + +addAccessibleTask( + ` + <p id="p">text</p> + <pre id="pre">text</pre>`, + async function (browser, accDoc) { + let p = findAccessibleChildByID(accDoc, "p"); + let pre = findAccessibleChildByID(accDoc, "pre"); + await removeTextData(browser, p, "p", ROLE_PARAGRAPH); + await removeTextData(browser, pre, "pre", ROLE_TEXT_CONTAINER); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_visibility.js b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js new file mode 100644 index 0000000000..636a00e210 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js @@ -0,0 +1,342 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testTreeOnHide(browser, accDoc, containerID, id, before, after) { + let acc = findAccessibleChildByID(accDoc, containerID); + testAccessibleTree(acc, before); + + let onReorder = waitForEvent(EVENT_REORDER, containerID); + await invokeSetStyle(browser, id, "visibility", "hidden"); + await onReorder; + + testAccessibleTree(acc, after); +} + +async function test3(browser, accessible) { + let tree = { + SECTION: [ + // container + { + SECTION: [ + // parent + { + SECTION: [ + // child + { TEXT_LEAF: [] }, + ], + }, + ], + }, + { + SECTION: [ + // parent2 + { + SECTION: [ + // child2 + { TEXT_LEAF: [] }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "t3_container"); + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("t3_container").style.color = "red"; + doc.getElementById("t3_parent").style.visibility = "hidden"; + doc.getElementById("t3_parent2").style.visibility = "hidden"; + }); + await onReorder; + + tree = { + SECTION: [ + // container + { + SECTION: [ + // child + { TEXT_LEAF: [] }, + ], + }, + { + SECTION: [ + // child2 + { TEXT_LEAF: [] }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); +} + +async function test4(browser, accessible) { + let tree = { + SECTION: [{ TABLE: [{ ROW: [{ CELL: [] }] }] }], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "t4_parent"); + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("t4_container").style.color = "red"; + doc.getElementById("t4_child").style.visibility = "visible"; + }); + await onReorder; + + tree = { + SECTION: [ + { + TABLE: [ + { + ROW: [ + { + CELL: [ + { + SECTION: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_visibility.html", + async function (browser, accDoc) { + let t3Container = findAccessibleChildByID(accDoc, "t3_container"); + let t4Container = findAccessibleChildByID(accDoc, "t4_container"); + + await testTreeOnHide( + browser, + accDoc, + "t1_container", + "t1_parent", + { + SECTION: [ + { + SECTION: [ + { + SECTION: [{ TEXT_LEAF: [] }], + }, + ], + }, + ], + }, + { + SECTION: [ + { + SECTION: [{ TEXT_LEAF: [] }], + }, + ], + } + ); + + await testTreeOnHide( + browser, + accDoc, + "t2_container", + "t2_grandparent", + { + SECTION: [ + { + // container + SECTION: [ + { + // grand parent + SECTION: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + + await test3(browser, t3Container); + await test4(browser, t4Container); + + await testTreeOnHide( + browser, + accDoc, + "t5_container", + "t5_subcontainer", + { + SECTION: [ + { + // container + SECTION: [ + { + // subcontainer + TABLE: [ + { + ROW: [ + { + CELL: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + + await testTreeOnHide( + browser, + accDoc, + "t6_container", + "t6_subcontainer", + { + SECTION: [ + { + // container + SECTION: [ + { + // subcontainer + TABLE: [ + { + ROW: [ + { + CELL: [ + { + TABLE: [ + { + // nested table + ROW: [ + { + CELL: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js new file mode 100644 index 0000000000..78ab47cd51 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js @@ -0,0 +1,69 @@ +/* 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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "e10s/doc_treeupdate_whitespace.html", + async function (browser, accDoc) { + let container1 = findAccessibleChildByID(accDoc, "container1"); + let container2Parent = findAccessibleChildByID(accDoc, "container2-parent"); + + let tree = { + SECTION: [ + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + ], + }; + testAccessibleTree(container1, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "container1"); + // Remove img1 from container1 + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("container1").removeChild(doc.getElementById("img1")); + }); + await onReorder; + + tree = { + SECTION: [{ GRAPHIC: [] }, { TEXT_LEAF: [] }, { GRAPHIC: [] }], + }; + testAccessibleTree(container1, tree); + + tree = { + SECTION: [{ LINK: [] }, { LINK: [{ GRAPHIC: [] }] }], + }; + testAccessibleTree(container2Parent, tree); + + onReorder = waitForEvent(EVENT_REORDER, "container2-parent"); + // Append an img with valid src to container2 + await invokeContentTask(browser, [], () => { + let doc = content.document; + let img = doc.createElement("img"); + img.setAttribute( + "src", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/a11y/accessible/tests/mochitest/moz.png" + ); + doc.getElementById("container2").appendChild(img); + }); + await onReorder; + + tree = { + SECTION: [ + { LINK: [{ GRAPHIC: [] }] }, + { TEXT_LEAF: [] }, + { LINK: [{ GRAPHIC: [] }] }, + ], + }; + testAccessibleTree(container2Parent, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html new file mode 100644 index 0000000000..089391523d --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html @@ -0,0 +1,23 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update ARIA Dialog Test</title> + </head> + <body id="body"> + <div id="dialog" role="dialog" style="display: none;"> + <table id="table" role="presentation" + style="display: block; position: absolute; top: 88px; left: 312.5px; z-index: 10010;"> + <tbody> + <tr> + <td role="presentation"> + <div role="presentation"> + <a id="a" role="button">text</a> + </div> + <input id="input"> + </td> + </tr> + </tbody> + </table> + </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html new file mode 100644 index 0000000000..38b5c333a1 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html @@ -0,0 +1,44 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update ARIA Owns Test</title> + </head> + <body id="body"> + <div id="t1_container" aria-owns="t1_checkbox t1_button"> + <div role="button" id="t1_button"></div> + <div role="checkbox" id="t1_checkbox"> + <span id="t1_span"> + <div id="t1_subdiv"></div> + </span> + </div> + </div> + <div id="t1_group" role="group"></div> + <div id="t1_grouptmp" role="group"></div> + + <div id="t2_container1" aria-owns="t2_owned"></div> + <div id="t2_container2"> + <div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div> + </div> + + <div id="t3_container1" aria-owns="t3_child"></div> + <div id="t3_child" role="checkbox"></div> + <div id="t3_container2"></div> + + <div id="t4_container1" aria-owns="t4_child1 t4_child2"></div> + <div id="t4_container2"> + <div id="t4_child1" style="display:none" role="checkbox"></div> + <div id="t4_child2" role="radio"></div> + </div> + + <div id="t5_container"> + <div role="button" id="t5_button"></div> + <div role="checkbox" id="t5_checkbox"></div> + <div role="radio" id="t5_radio"></div> + </div> + + <div id="t6_container" aria-owns="t6_fake"> + <span id="t6_span">hey</span> + </div> + <div id="t6_fake" role="group"></div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html new file mode 100644 index 0000000000..4dd230fc28 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html @@ -0,0 +1,21 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update Imagemap Test</title> + </head> + <body id="body"> + <map name="atoz_map" id="map"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + </map> + + <div id="container"> + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"><!-- + Important: no whitespace between the <img> and the </div>, so we + don't end up with textframes there, because those would be reflected + in our accessible tree in some cases. + --></div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml new file mode 100644 index 0000000000..9c59fb9d11 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>Tree Update Removal Test</title> + </head> + <body id="body"> + <div id="the_displaynone" style="display: none;"></div> + <table id="the_table"></table> + <tr id="the_row"></tr> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_visibility.html b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html new file mode 100644 index 0000000000..00213b2b70 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html @@ -0,0 +1,78 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update Visibility Test</title> + </head> + <body id="body"> + <!-- hide parent while child stays visible --> + <div id="t1_container"> + <div id="t1_parent"> + <div id="t1_child" style="visibility: visible">text</div> + </div> + </div> + + <!-- hide grandparent while its children stay visible --> + <div id="t2_container"> + <div id="t2_grandparent"> + <div id="t2_parent"> + <div id="t2_child" style="visibility: visible">text</div> + <div id="t2_child2" style="visibility: visible">text</div> + </div> + </div> + </div> + + <!-- change container style, hide parents while their children stay visible --> + <div id="t3_container"> + <div id="t3_parent"> + <div id="t3_child" style="visibility: visible">text</div> + </div> + <div id="t3_parent2"> + <div id="t3_child2" style="visibility: visible">text</div> + </div> + </div> + + <!-- change container style, show child inside the table --> + <div id="t4_container"> + <table> + <tr> + <td id="t4_parent"> + <div id="t4_child" style="visibility: hidden;">text</div> + </td> + </tr> + </table> + </div> + + <!-- hide subcontainer while child inside the table stays visible --> + <div id="t5_container"> + <div id="t5_subcontainer"> + <table> + <tr> + <td> + <div id="t5_child" style="visibility: visible;">text</div> + </td> + </tr> + </table> + </div> + </div> + + <!-- hide subcontainer while its child and child inside the nested table stays visible --> + <div id="t6_container"> + <div id="t6_subcontainer"> + <table> + <tr> + <td> + <table> + <tr> + <td> + <div id="t6_child" style="visibility: visible;">text</div> + </td> + </tr> + </table> + </td> + </tr> + </table> + <div id="t6_child2" style="visibility: visible">text</div> + </div> + </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html new file mode 100644 index 0000000000..18a65d8192 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>Whitespace text accessible creation/destruction</title> + </head> + <body id="body"> + <div id="container1"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img id="img1" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> </div> + <div id="container2-parent"> <a id="container2" href=""></a> <a href=""><img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/fonts/Ahem.sjs b/accessible/tests/browser/e10s/fonts/Ahem.sjs new file mode 100644 index 0000000000..e801a801ab --- /dev/null +++ b/accessible/tests/browser/e10s/fonts/Ahem.sjs @@ -0,0 +1,241 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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"; + +/* + * A CORS-enabled font resource. + */ + +const FONT_BYTES = atob( + "AAEAAAALAIAAAwAwT1MvMnhQSo0AAAE4AAAAYGNtYXAP1hZGAAAFbAAABnJnYXNwABcACQAAMLAA" + + "AAAQZ2x5ZkmzdNoAAAvgAAAaZGhlYWTWok4cAAAAvAAAADZoaGVhBwoEFgAAAPQAAAAkaG10eLkg" + + "AH0AAAGYAAAD1GxvY2EgdSciAAAmRAAAAextYXhwAPgACQAAARgAAAAgbmFtZX4UjLgAACgwAAAG" + + "aHBvc3SN0B2KAAAumAAAAhgAAQAAAAEAQhIXUWdfDzz1AAkD6AAAAACzb19ZAAAAAMAtq0kAAP84" + + "A+gDIAAAAAMAAgAAAAAAAAABAAADIP84AAAD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAA9QABAAAA" + + "9QAIAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAID6AGQAAUAAAK8AooAAACPArwCigAAAcUAMgED" + + "AAACAAQJAAAAAAAAgAAArxAAIEgAAAAAAAAAAFczQwAAQAAg8AIDIP84AAADIADIIAABEUAAAAAD" + + "IAMgAAAAIAAAA+gAfQAAAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" + + "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" + + "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" + + "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" + + "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" + + "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" + + "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" + + "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" + + "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" + + "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" + + "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" + + "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" + + "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" + + "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" + + "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" + + "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" + + "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" + + "A+gAAAPoAAAD6AAAA+gAAAPoAAAAAAADAAAAAwAABEwAAQAAAAAAHAADAAEAAAImAAYCCgAAAAAB" + + "AAABAAAAAAAAAAAAAAAAAAAAAQACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAQAAAAAAAwAEAAUABgAHAAgACQAAAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkA" + + "GgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2" + + "ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIA" + + "UwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAAAAYQBiAGMAZABlAGYAZwBoAGkAagBrAGwAbQBu" + + "AG8AcABxAHIAcwB0AHUAdgB3AHgAeQB6AHsAfAB9AH4AfwCAANsAgQCCAIMAhADdAIUAhgCHAIgA" + + "4wCJAIoA6gCLAIwA6ACNAOsA7ACOAI8A5ADmAOUA1ADpAJAAkQDTAJIAkwCUAJUAlgDnANEA7QDS" + + "AJcAmADeAAMAmgCbAJwAzgDPANUA1gDYANkAnQCeAJ8A7gCgANAA4gChAOAA4QAAAAAA3ACiANcA" + + "2gDfAKMApAClAKYApwCoAKkAqgCrAKwArQAAAK4ArwCwALEAsgCzALQAtQC2ALcAuAC5ALoAuwC8" + + "AAQCJgAAAE4AQAAFAA4AJgB+AP8BMQFTAXgBkgLHAskC3QOUA6kDvAPAIBAgFCAaIB4gIiAmIDAg" + + "OiBEISIhJiICIgYiDyISIhoiHiIrIkgiYCJlIvIlyvAC//8AAAAgACgAoAExAVIBeAGSAsYCyQLY" + + "A5QDqQO8A8AgECATIBggHCAgICYgMCA5IEQhIiEmIgIiBiIPIhEiGSIeIisiSCJgImQi8iXK8AD/" + + "///j/+IAAP+B/3z/WP8/AAD97AAA/T79KvzT/RTf/+DCAADgvOC74Ljgr+Cn4J7fwd+t3uLezN7W" + + "AAAAAN7K3r7epd6K3ofd+9skEO8AAQAAAAAASgAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAP4A" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOwA7gAAAAAAAAAAAAAAAAAAAAAAAACZAJUAggCDAKEAjgC9" + + "AIQAigCIAJAAlwCWAMQAhwC1AIEAjQDHAMgAiQCPAIUAogC5AMYAkQCYAMoAyQDLAJQAmgClAKMA" + + "mwBhAGIAiwBjAKcAZACkAKYAqwCoAKkAqgC+AGUArgCsAK0AnABmAMUAjACxAK8AsABnAMAAwgCG" + + "AGkAaABqAGwAawBtAJIAbgBwAG8AcQByAHQAcwB1AHYAvwB3AHkAeAB6AHwAewCfAJMAfgB9AH8A" + + "gADBAMMAoACzALwAtgC3ALgAuwC0ALoAnQCeANcA5gDEAKIA5wAEAiYAAABOAEAABQAOACYAfgD/" + + "ATEBUwF4AZICxwLJAt0DlAOpA7wDwCAQIBQgGiAeICIgJiAwIDogRCEiISYiAiIGIg8iEiIaIh4i" + + "KyJIImAiZSLyJcrwAv//AAAAIAAoAKABMQFSAXgBkgLGAskC2AOUA6kDvAPAIBAgEyAYIBwgICAm" + + "IDAgOSBEISIhJiICIgYiDyIRIhkiHiIrIkgiYCJkIvIlyvAA////4//iAAD/gf98/1j/PwAA/ewA" + + "AP0+/Sr80/0U3//gwgAA4Lzgu+C44K/gp+Ce38Hfrd7i3sze1gAAAADeyt6+3qXeit6H3fvbJBDv" + + "AAEAAAAAAEoAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AADsAO4AAAAAAAAAAAAAAAAAAAAAAAAAmQCVAIIAgwChAI4AvQCEAIoAiACQAJcAlgDEAIcAtQCB" + + "AI0AxwDIAIkAjwCFAKIAuQDGAJEAmADKAMkAywCUAJoApQCjAJsAYQBiAIsAYwCnAGQApACmAKsA" + + "qACpAKoAvgBlAK4ArACtAJwAZgDFAIwAsQCvALAAZwDAAMIAhgBpAGgAagBsAGsAbQCSAG4AcABv" + + "AHEAcgB0AHMAdQB2AL8AdwB5AHgAegB8AHsAnwCTAH4AfQB/AIAAwQDDAKAAswC8ALYAtwC4ALsA" + + "tAC6AJ0AngDXAOYAxACiAOcAAAACAH0AAANrAyAAAwAHAAAzESERJSERIX0C7v2PAfT+DAMg/OB9" + + "AiYAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" + + "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" + + "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" + + "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" + + "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" + + "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" + + "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" + + "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" + + "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" + + "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" + + "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" + + "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" + + "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" + + "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" + + "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" + + "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" + + "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" + + "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" + + "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" + + "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" + + "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" + + "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" + + "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" + + "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" + + "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" + + "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" + + "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" + + "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" + + "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" + + "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" + + "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" + + "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" + + "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" + + "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" + + "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" + + "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" + + "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" + + "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" + + "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AAAAAMAADEhFSED6PwYyAAAAQAA/zgD6AMgAAMA" + + "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" + + "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" + + "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" + + "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" + + "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" + + "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" + + "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" + + "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" + + "GAMg/BgAAAABAAAAAAPoAyAAAwAAESERIQPo/BgDIPzgAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" + + "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" + + "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" + + "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" + + "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" + + "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" + + "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" + + "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" + + "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" + + "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" + + "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" + + "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" + + "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" + + "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" + + "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" + + "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" + + "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" + + "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" + + "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" + + "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" + + "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" + + "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" + + "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" + + "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" + + "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" + + "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" + + "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" + + "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" + + "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" + + "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" + + "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" + + "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" + + "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" + + "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" + + "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" + + "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" + + "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" + + "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" + + "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" + + "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" + + "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" + + "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" + + "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" + + "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" + + "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" + + "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" + + "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" + + "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" + + "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" + + "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" + + "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" + + "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" + + "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" + + "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" + + "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" + + "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" + + "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" + + "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" + + "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" + + "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" + + "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" + + "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" + + "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" + + "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" + + "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" + + "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" + + "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" + + "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" + + "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" + + "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" + + "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAAABQAFAAU" + + "ABQAIgAwAD4ATABaAGgAdgCEAJIAoACuALwAygDYAOYA9AECARABHgEsAToBSAFWAWQBcgGAAY4B" + + "nAGqAbgBxgHUAeIB8AH+AgwCGgIoAjYCRAJSAmACbgJ8AooCmAKmArQCwgLQAt4C7AL6AwgDFgMk" + + "AzIDQANOA1wDagN4A4YDlAOiA7ADvgPMA9oD6AP2BAQEEgQgBC4EPARKBFgEZARyBIAEjgScBKoE" + + "uATGBNQE4gTwBP4FDAUaBSgFNgVEBVIFYAVuBXwFigWYBaYFtAXCBdAF3gXsBfoGCAYWBiQGMgZA" + + "Bk4GXAZqBngGhgaUBqIGsAa+BswG2gboBvYHBAcSByAHLgc8B0oHWAdmB3QHggeQB54HrAe6B8gH" + + "1gfkB/IIAAgOCBwIKgg4CDgIRghUCGIIcAh+CIwImgioCLYIxAjSCOAI7gj8CQoJGAkmCTQJQglQ" + + "CV4JbAl6CYgJlgmkCbIJwAnOCdwJ6gn4CgYKFAoiCjAKPgpMCloKaAp2CoQKkgqgCq4KvArKCtgK" + + "5gr0CwILEAseCywLOgtIC1YLZAtyC4ALjgucC6oLuAvGC9QL4gvwC/4MDAwaDCgMNgxEDFIMYAxu" + + "DHwMigyYDKYMtAzCDNAM3gzsDPoNCA0WDSQNMgAAABsBSgAAAAAAAAAAAZ4AAAAAAAAAAAABAAgB" + + "ngAAAAAAAAACAA4BpgAAAAAAAAADACABtAAAAAAAAAAEAAgB1AAAAAAAAAAFABYB3AAAAAAAAAAG" + + "AAgB8gABAAAAAAAAAM8B+gABAAAAAAABAAQCyQABAAAAAAACAAcCzQABAAAAAAADABAC1AABAAAA" + + "AAAEAAQC5AABAAAAAAAFAAsC6AABAAAAAAAGAAQC8wABAAAAAAAQAAQC9wABAAAAAAARAAcC+wAB" + + "AAAAAAASAAQDAgADAAEECQAAAZ4DBgADAAEECQABAAgEpAADAAEECQACAA4ErAADAAEECQADACAE" + + "ugADAAEECQAEAAgE2gADAAEECQAFABYE4gADAAEECQAGAAgE+AADAAEECQAQAAgFAAADAAEECQAR" + + "AA4FCAADAAEECQASAAgFFgBNAG8AcwB0ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAA" + + "dABoAGUAIABlAG0AIABzAHEAdQBhAHIAZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABl" + + "ACAAYQBuAGQAIAAiAHAAIgAsACAAdwBoAGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8A" + + "ZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0AIAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBz" + + "AGUAZgB1AGwAIABmAG8AcgAgAHQAZQBzAHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4A" + + "IABzAHkAcwB0AGUAbQBzAC4AIABQAHIAbwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBh" + + "AGgAcgBuAGUAcgAgAGYAbwByACAAdABoAGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAA" + + "YgByAG8AdwBzAGUAcgAgAHQAZQBzAHQAaQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBl" + + "AHIAcwBpAG8AbgAgADEALgAxACAAQQBoAGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4A" + + "MQBBAGgAZQBtTW9zdCBjaGFyYWN0ZXJzIGFyZSB0aGUgZW0gc3F1YXJlLCBleGNlcHQgJkVBY3V0" + + "ZSBhbmQgInAiLCB3aGljaCBzaG93IGFzY2VudC9kZXNjZW50IGZyb20gdGhlIGJhc2VsaW5lLiBV" + + "c2VmdWwgZm9yIHRlc3RpbmcgY29tcG9zaXRpb24gc3lzdGVtcy4gUHJvZHVjZWQgYnkgVG9kZCBG" + + "YWhybmVyIGZvciB0aGUgQ1NTIFNhbXVyYWkncyBicm93c2VyIHRlc3RpbmcuQWhlbVJlZ3VsYXJW" + + "ZXJzaW9uIDEuMSBBaGVtQWhlbVZlcnNpb24gMS4xQWhlbUFoZW1SZWd1bGFyQWhlbQBNAG8AcwB0" + + "ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAAdABoAGUAIABlAG0AIABzAHEAdQBhAHIA" + + "ZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABlACAAYQBuAGQAIAAiAHAAIgAsACAAdwBo" + + "AGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8AZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0A" + + "IAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBzAGUAZgB1AGwAIABmAG8AcgAgAHQAZQBz" + + "AHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4AIABzAHkAcwB0AGUAbQBzAC4AIABQAHIA" + + "bwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBhAGgAcgBuAGUAcgAgAGYAbwByACAAdABo" + + "AGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAAYgByAG8AdwBzAGUAcgAgAHQAZQBzAHQA" + + "aQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgAxACAAQQBo" + + "AGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4AMQBBAGgAZQBtAEEAaABlAG0AUgBlAGcA" + + "dQBsAGEAcgBBAGgAZQBtAAIAAAAAAAD/ewAUAAAAAQAAAAAAAAAAAAAAAAAAAAAA9QAAAQIAAgAD" + + "AAQABQAGAAcACAAJAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAA" + + "IQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9" + + "AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkA" + + "WgBbAFwAXQBeAF8AYABhAGIAYwBkAGUAZgBnAGgAaQBqAGsAbABtAG4AbwBwAHEAcgBzAHQAdQB2" + + "AHcAeAB5AHoAewB8AH0AfgB/AIAAgQCDAIQAhQCGAIgAiQCKAIsAjQCOAJAAkQCTAJYAlwCdAJ4A" + + "oAChAKIAowCkAKkAqgCsAK0ArgCvALYAtwC4ALoAvQDDAMcAyADJAMoAywDMAM0AzgDPANAA0QDT" + + "ANQA1QDWANcA2ADZANoA2wDcAN0A3gDfAOAA4QDoAOkA6gDrAOwA7QDuAO8A8ADxAPIA8wD0APUA" + + "9gAAAAAAsACxALsApgCoAJ8AmwCyALMAxAC0ALUAxQCCAMIAhwCrAMYAvgC/ALwAjACYAJoAmQCl" + + "AJIAnACPAJQAlQCnALkA0gDAAMEBAwACAQQETlVMTAJIVANERUwAAAADAAgAAgAQAAH//wAD" +); + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/octet-stream", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.write(FONT_BYTES); +} diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js new file mode 100644 index 0000000000..bdbcb7445f --- /dev/null +++ b/accessible/tests/browser/e10s/head.js @@ -0,0 +1,192 @@ +/* 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"; + +/* exported testCachedRelation, testRelated */ + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js and relations.js. +/* import-globals-from ../../mochitest/relations.js */ +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR }, + { name: "relations.js", dir: MOCHITESTS_DIR } +); + +/** + * Test the accessible relation. + * + * @param identifier [in] identifier to get an accessible, may be ID + * attribute or DOM element or accessible object + * @param relType [in] relation type (see constants above) + * @param relatedIdentifiers [in] identifier or array of identifiers of + * expected related accessibles + */ +async function testCachedRelation(identifier, relType, relatedIdentifiers) { + const relDescr = getRelationErrorMsg(identifier, relType); + const relDescrStart = getRelationErrorMsg(identifier, relType, true); + info(`Testing ${relDescr}`); + + if (!relatedIdentifiers) { + await untilCacheOk(function () { + let r = getRelationByType(identifier, relType); + if (r) { + info(`Fetched ${r.targetsCount} relations from cache`); + } else { + info("Could not fetch relations"); + } + return r && !r.targetsCount; + }, relDescrStart + " has no targets, as expected"); + return; + } + + const relatedIds = + relatedIdentifiers instanceof Array + ? relatedIdentifiers + : [relatedIdentifiers]; + await untilCacheOk(function () { + let r = getRelationByType(identifier, relType); + if (r) { + info( + `Fetched ${r.targetsCount} relations from cache, looking for ${relatedIds.length}` + ); + } else { + info("Could not fetch relations"); + } + + return r && r.targetsCount == relatedIds.length; + }, "Found correct number of expected relations"); + + let targets = []; + for (let idx = 0; idx < relatedIds.length; idx++) { + targets.push(getAccessible(relatedIds[idx])); + } + + if (targets.length != relatedIds.length) { + return; + } + + await untilCacheOk(function () { + const relation = getRelationByType(identifier, relType); + const actualTargets = relation ? relation.getTargets() : null; + if (!actualTargets) { + info("Could not fetch relations"); + return false; + } + + // Check if all given related accessibles are targets of obtained relation. + for (let idx = 0; idx < targets.length; idx++) { + let isFound = false; + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + if (targets[idx] == relatedAcc) { + isFound = true; + break; + } + } + + if (!isFound) { + info( + prettyName(relatedIds[idx]) + + " could not be found in relation: " + + relDescr + ); + return false; + } + } + + return true; + }, "All given related accessibles are targets of fetched relation."); + + await untilCacheOk(function () { + const relation = getRelationByType(identifier, relType); + const actualTargets = relation ? relation.getTargets() : null; + if (!actualTargets) { + info("Could not fetch relations"); + return false; + } + + // Check if all obtained targets are given related accessibles. + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + let wasFound = false; + for (let idx = 0; idx < targets.length; idx++) { + if (relatedAcc == targets[idx]) { + wasFound = true; + } + } + if (!wasFound) { + info( + prettyName(relatedAcc) + + " was found, but shouldn't be in relation: " + + relDescr + ); + return false; + } + } + return true; + }, "No unexpected targets found."); +} + +async function testRelated( + browser, + accDoc, + attr, + hostRelation, + dependantRelation +) { + let host = findAccessibleChildByID(accDoc, "host"); + let dependant1 = findAccessibleChildByID(accDoc, "dependant1"); + let dependant2 = findAccessibleChildByID(accDoc, "dependant2"); + + /** + * Test data has the format of: + * { + * desc {String} description for better logging + * attrs {?Array} an optional list of attributes to update + * expected {Array} expected relation values for dependant1, dependant2 + * and host respectively. + * } + */ + const tests = [ + { + desc: "No attribute", + expected: [null, null, null], + }, + { + desc: "Set attribute", + attrs: [{ key: attr, value: "dependant1" }], + expected: [host, null, dependant1], + }, + { + desc: "Change attribute", + attrs: [{ key: attr, value: "dependant2" }], + expected: [null, host, dependant2], + }, + { + desc: "Remove attribute", + attrs: [{ key: attr }], + expected: [null, null, null], + }, + ]; + + for (let { desc, attrs, expected } of tests) { + info(desc); + + if (attrs) { + for (let { key, value } of attrs) { + await invokeSetAttribute(browser, "host", key, value); + } + } + + await testCachedRelation(dependant1, dependantRelation, expected[0]); + await testCachedRelation(dependant2, dependantRelation, expected[1]); + await testCachedRelation(host, hostRelation, expected[2]); + } +} |