/* 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 } ); async function synthFocus(browser, container, item) { let focusPromise = waitForEvent(EVENT_FOCUS, item); await invokeContentTask(browser, [container], _container => { let elm = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_container); elm.focus(); }); await focusPromise; } async function changeARIAActiveDescendant( browser, container, itemId, prevItemId, elementReflection ) { let expectedEvents = [[EVENT_FOCUS, itemId]]; if (prevItemId) { info("A state change of the previous item precedes the new one."); expectedEvents.push( stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) ); } expectedEvents.push( stateChangeEventArgs(itemId, EXT_STATE_ACTIVE, true, true) ); let expectedPromise = waitForEvents(expectedEvents); await invokeContentTask( browser, [container, itemId, elementReflection], (_container, _itemId, _elementReflection) => { let getElm = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document); let elm = getElm(_container); if (_elementReflection) { elm.ariaActiveDescendantElement = getElm(_itemId); } else { elm.setAttribute("aria-activedescendant", _itemId); } } ); await expectedPromise; } async function clearARIAActiveDescendant( browser, container, prevItemId, defaultId, elementReflection ) { let expectedEvents = [[EVENT_FOCUS, defaultId || container]]; if (prevItemId) { expectedEvents.push( stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) ); } if (defaultId) { expectedEvents.push( stateChangeEventArgs(defaultId, EXT_STATE_ACTIVE, true, true) ); } let expectedPromise = waitForEvents(expectedEvents); await invokeContentTask( browser, [container, elementReflection], (_container, _elementReflection) => { let elm = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_container); if (_elementReflection) { elm.ariaActiveDescendantElement = null; } else { elm.removeAttribute("aria-activedescendant"); } } ); await expectedPromise; } async function insertItemNFocus( browser, container, newItemID, prevItemId, elementReflection ) { let expectedEvents = [ [EVENT_SHOW, newItemID], [EVENT_FOCUS, newItemID], ]; if (prevItemId) { info("A state change of the previous item precedes the new one."); expectedEvents.push( stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) ); } expectedEvents.push( stateChangeEventArgs(newItemID, EXT_STATE_ACTIVE, true, true) ); let expectedPromise = waitForEvents(expectedEvents); await invokeContentTask( browser, [container, newItemID, elementReflection], (_container, _newItemID, _elementReflection) => { let elm = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_container); let itemElm = content.document.createElement("div"); itemElm.setAttribute("id", _newItemID); itemElm.setAttribute("role", "listitem"); itemElm.textContent = _newItemID; elm.appendChild(itemElm); if (_elementReflection) { elm.ariaActiveDescendantElement = itemElm; } else { elm.setAttribute("aria-activedescendant", _newItemID); } } ); await expectedPromise; } async function moveARIAActiveDescendantID(browser, fromID, toID) { let expectedEvents = [ [EVENT_FOCUS, toID], stateChangeEventArgs(toID, EXT_STATE_ACTIVE, true, true), ]; let expectedPromise = waitForEvents(expectedEvents); await invokeContentTask(browser, [fromID, toID], (_fromID, _toID) => { let orig = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_toID); if (orig) { orig.id = ""; } ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_fromID).id = _toID; }); await expectedPromise; } async function changeARIAActiveDescendantInvalid( browser, container, invalidID = "invalid", prevItemId = null ) { let expectedEvents = [[EVENT_FOCUS, container]]; if (prevItemId) { expectedEvents.push( stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) ); } let expectedPromise = waitForEvents(expectedEvents); await invokeContentTask( browser, [container, invalidID], (_container, _invalidID) => { let elm = ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)(_container); elm.setAttribute("aria-activedescendant", _invalidID); } ); await expectedPromise; } const LISTBOX_MARKUP = `
item1
item2
roaming
roaming2
item3
`; async function basicListboxTest(browser, elementReflection) { await synthFocus(browser, "listbox", "item1"); await changeARIAActiveDescendant( browser, "listbox", "item2", "item1", elementReflection ); await changeARIAActiveDescendant( browser, "listbox", "item3", "item2", elementReflection ); info("Focus out of listbox"); await synthFocus(browser, "combobox_entry", "combobox_entry"); await changeARIAActiveDescendant( browser, "combobox", "combobox_option2", null, elementReflection ); await changeARIAActiveDescendant( browser, "combobox", "combobox_option1", null, elementReflection ); info("Focus back in listbox"); await synthFocus(browser, "listbox", "item3"); await insertItemNFocus( browser, "listbox", "item4", "item3", elementReflection ); await clearARIAActiveDescendant( browser, "listbox", "item4", null, elementReflection ); await changeARIAActiveDescendant( browser, "listbox", "item1", null, elementReflection ); } addAccessibleTask( LISTBOX_MARKUP, async function (browser, docAcc) { info("Test aria-activedescendant content attribute"); await basicListboxTest(browser, false); await changeARIAActiveDescendantInvalid( browser, "listbox", "invalid", "item1" ); await changeARIAActiveDescendant(browser, "listbox", "roaming"); await moveARIAActiveDescendantID(browser, "roaming2", "roaming"); await changeARIAActiveDescendantInvalid( browser, "listbox", "roaming3", "roaming" ); await moveARIAActiveDescendantID(browser, "roaming", "roaming3"); }, { topLevel: true, chrome: true } ); addAccessibleTask( LISTBOX_MARKUP, async function (browser, docAcc) { info("Test ariaActiveDescendantElement element reflection"); await basicListboxTest(browser, true); }, { topLevel: true, chrome: true } ); addAccessibleTask( `
option
`, async function (browser, docAcc) { info("Test aria-activedescendant non-descendant"); await synthFocus( browser, "activedesc_nondesc_input", "activedesc_nondesc_option" ); }, { topLevel: true, chrome: true } ); addAccessibleTask( `
`, async function (browser, docAcc) { info("Test aria-activedescendant in shadow root"); // We want to retrieve elements using their IDs inside the shadow root, so // we define a custom get element by ID method that our utility functions // above call into if it exists. await invokeContentTask(browser, [], () => { content.document._testGetElementById = id => content.document.getElementById("shadow").shadowRoot.getElementById(id); }); await synthFocus(browser, "shadowListbox", "shadowItem1"); await changeARIAActiveDescendant( browser, "shadowListbox", "shadowItem2", "shadowItem1" ); info("Do it again with element reflection"); await changeARIAActiveDescendant( browser, "shadowListbox", "shadowItem1", "shadowItem2", true ); }, { topLevel: true, chrome: true } ); addAccessibleTask( `
`, async function (browser, docAcc) { info("Test simultaneous insertion, relocation and aria-activedescendant"); await synthFocus( browser, "comboboxWithHiddenList", "comboboxWithHiddenList" ); testStates( findAccessibleChildByID(docAcc, "comboboxWithHiddenList"), STATE_FOCUSED ); let evtProm = Promise.all([ waitForEvent(EVENT_FOCUS, "hiddenListOption"), waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true), ]); await invokeContentTask(browser, [], () => { info("hiddenList is owned, so unhiding causes insertion and relocation."); ( content.document._testGetElementById || content.document.getElementById ).bind(content.document)("hiddenList").hidden = false; content.document .getElementById("comboboxWithHiddenList") .setAttribute("aria-activedescendant", "hiddenListOption"); }); await evtProm; testStates( findAccessibleChildByID(docAcc, "hiddenListOption"), STATE_FOCUSED ); }, { topLevel: true, chrome: true } ); addAccessibleTask( `
`, async function (browser, docAcc) { await synthFocus(browser, "custom-listbox1", "l1_3"); let evtProm = Promise.all([ waitForEvent(EVENT_FOCUS, "l1_2"), waitForStateChange("l1_3", EXT_STATE_ACTIVE, false, true), waitForStateChange("l1_2", EXT_STATE_ACTIVE, true, true), ]); await invokeContentTask(browser, [], () => { content.document.getElementById( "custom-listbox1" ).internals.ariaActiveDescendantElement = content.document.getElementById("l1_2"); }); await evtProm; evtProm = Promise.all([ waitForEvent(EVENT_FOCUS, "custom-listbox1"), waitForStateChange("l1_2", EXT_STATE_ACTIVE, false, true), ]); await invokeContentTask(browser, [], () => { content.document.getElementById( "custom-listbox1" ).internals.ariaActiveDescendantElement = null; }); await evtProm; await synthFocus(browser, "custom-listbox2", "l2_1"); await clearARIAActiveDescendant(browser, "custom-listbox2", "l2_1", "l2_3"); } );