/* 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 }); /* import-globals-from ../../mochitest/states.js */ loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); requestLongerTimeout(2); function invokeSetAriaOwns( browser, id, children = null, elementReflection = false ) { if (!elementReflection) { return invokeSetAttribute(browser, id, "aria-owns", children); } return invokeContentTask( browser, [id, children], (contentId, contentChildrenIds) => { let elm = content.document.getElementById(contentId); if (contentChildrenIds) { elm.ariaOwnsElements = contentChildrenIds .split(" ") .map(childId => content.document.getElementById(childId)); } else { elm.ariaOwnsElements = null; } } ); } async function testContainer1(browser, accDoc, elementReflection = false) { 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 invokeSetAriaOwns( browser, id, "t1_button t1_subdiv", elementReflection ); 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 invokeSetAriaOwns(browser, id, null, elementReflection); await onReorder; // children follow the DOM order tree = { SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }], }; testAccessibleTree(acc, tree); /* ================ Set ARIA owns ========================================= */ onReorder = waitForEvent(EVENT_REORDER, id); await invokeSetAriaOwns( browser, id, "t1_button t1_subdiv", elementReflection ); 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, elementReflection) { 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 invokeSetAriaOwns(browser, id2, "t3_child", elementReflection); 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, elementReflection) { 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 invokeSetAriaOwns(browser, id, val, elementReflection); 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 } ); addAccessibleTask( "e10s/doc_treeupdate_ariaowns.html", async function (browser, accDoc) { await testContainer1(browser, accDoc, true); await removeContainer(browser, accDoc); await stealAndRecacheChildren(browser, accDoc, true); await showHiddenElement(browser, accDoc); await rearrangeARIAOwns(browser, accDoc, true); await removeNotARIAOwnedEl(browser, accDoc); }, { iframe: true, remoteIframe: true } ); // Test owning an ancestor which isn't created yet with an iframe in the // subtree. addAccessibleTask( `
`, 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( `
`, 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( `
`, 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( `
`, 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( `
`, 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 } ); /** * Test relation defaults via element internals */ addAccessibleTask( `
`, async function (browser, accDoc) { let listbox = findAccessibleChildByID(accDoc, "listbox"); is(listbox.children.length, 3, "got children"); let onReorder = waitForEvent(EVENT_REORDER, "listbox"); invokeSetAriaOwns(browser, "listbox", "l4"); await onReorder; } ); /** * Test insertion of relocated by ID child after initial load */ addAccessibleTask( `
`, async function (browser, accDoc) { const a = findAccessibleChildByID(accDoc, "a"); is(a.children.length, 0, "'a' has no children"); const waitFor = { expected: [ [EVENT_SHOW, "b"], [EVENT_INNER_REORDER, a], [EVENT_REORDER, accDoc], ], }; await contentSpawnMutation(browser, waitFor, function () { const b = content.document.createElement("div"); b.id = "b"; content.document.body.appendChild(b); }); is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child"); } ); /** * Test insertion of relocated by child element reflection after initial load */ addAccessibleTask(`
`, async function (browser, accDoc) { const a = findAccessibleChildByID(accDoc, "a"); is(a.children.length, 0, "'a' has no children"); // Create div and add it to a's ariaOwnsElements. // The refresh ticks called in contentSpawnMutation // will cause a relocation to be scheduled and performed. // Nothing will happen because 'b' is not parented yet. let waitFor = { unexpected: [ [EVENT_SHOW, "b"], [EVENT_INNER_REORDER, a], [EVENT_REORDER, accDoc], ], }; await contentSpawnMutation(browser, waitFor, function () { content.b = content.document.createElement("div"); content.b.id = "b"; content.document.getElementById("a").ariaOwnsElements = [content.b]; }); // Parent 'b'. It should relocate into 'a'. waitFor = { expected: [ [EVENT_SHOW, "b"], [EVENT_INNER_REORDER, a], [EVENT_REORDER, accDoc], ], }; await contentSpawnMutation(browser, waitFor, function () { content.document.body.appendChild(content.b); }); is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child"); }); /* * Test to assure that aria-owned elements are not relocated into an editable subtree. */ addAccessibleTask( `

Hello

`, async function (browser, accDoc) { const p = findAccessibleChildByID(accDoc, "p"); const textbox = findAccessibleChildByID(accDoc, "textbox"); testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0); isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn"); is(textbox.value, "Hello"); let expectedEvents = Promise.all([ waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true), waitForEvent(EVENT_INNER_REORDER, p), ]); await invokeContentTask(browser, [], () => { content.document.getElementById("textbox").contentEditable = false; }); await expectedEvents; is(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn"); is(textbox.value, "Hello World"); expectedEvents = Promise.all([ waitForStateChange(textbox, EXT_STATE_EDITABLE, true, true), waitForEvent(EVENT_INNER_REORDER, p), ]); await invokeContentTask(browser, [], () => { content.document.getElementById("textbox").contentEditable = true; }); await expectedEvents; isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn"); is(textbox.value, "Hello"); } ); /* * Test to ensure that aria-owned elements are not relocated out of editable subtree. */ addAccessibleTask( `

Hello

`, async function (browser, accDoc) { const p = findAccessibleChildByID(accDoc, "p"); const textbox = findAccessibleChildByID(accDoc, "textbox"); testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0); is( getAccessibleDOMNodeID(textbox.parent), "p2", "editable root can be relocated" ); isnot( getAccessibleDOMNodeID(p.lastChild), "btn", "editable element cannot be relocated" ); is(textbox.value, "World"); let expectedEvents = Promise.all([ waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true), waitForEvent(EVENT_REORDER, p), ]); await invokeContentTask(browser, [], () => { content.document.getElementById("textbox").contentEditable = false; }); await expectedEvents; is( getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns readonly relocated btn" ); is(textbox.value, ""); is( getAccessibleDOMNodeID(textbox.parent), "p2", "textbox is still relocated" ); } ); addAccessibleTask( `
`, async (browser, accDoc) => { const combobox = findAccessibleChildByID(accDoc, "box"); const listbox = findAccessibleChildByID(accDoc, "listbox"); testStates(combobox, 0, EXT_STATE_EDITABLE, 0, 0); is(combobox.childCount, 0, "combobox has no children"); await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, [listbox]); await testCachedRelation(listbox, RELATION_CONTROLLED_BY, [combobox]); let expectedEvents = Promise.all([ waitForStateChange(combobox, EXT_STATE_EDITABLE, false, true), waitForEvent(EVENT_REORDER, accDoc), ]); await invokeContentTask(browser, [], () => { content.document.getElementById("box").contentEditable = false; }); await expectedEvents; await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, []); await testCachedRelation(listbox, RELATION_CONTROLLED_BY, []); is(combobox.childCount, 1, "combobox has listbox"); } );