From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- accessible/tests/browser/mac/browser.ini | 58 + accessible/tests/browser/mac/browser_app.js | 351 +++ accessible/tests/browser/mac/browser_aria_busy.js | 44 + .../browser/mac/browser_aria_controls_flowto.js | 92 + .../tests/browser/mac/browser_aria_current.js | 58 + .../tests/browser/mac/browser_aria_expanded.js | 45 + .../tests/browser/mac/browser_aria_haspopup.js | 320 +++ .../tests/browser/mac/browser_attributed_text.js | 144 ++ accessible/tests/browser/mac/browser_bounds.js | 77 + .../tests/browser/mac/browser_details_summary.js | 69 + accessible/tests/browser/mac/browser_focus.js | 44 + accessible/tests/browser/mac/browser_heading.js | 77 + accessible/tests/browser/mac/browser_hierarchy.js | 75 + accessible/tests/browser/mac/browser_input.js | 225 ++ .../tests/browser/mac/browser_label_title.js | 111 + accessible/tests/browser/mac/browser_link.js | 231 ++ .../tests/browser/mac/browser_live_regions.js | 165 ++ accessible/tests/browser/mac/browser_mathml.js | 151 ++ accessible/tests/browser/mac/browser_menulist.js | 103 + accessible/tests/browser/mac/browser_navigate.js | 394 ++++ accessible/tests/browser/mac/browser_outline.js | 566 +++++ .../tests/browser/mac/browser_outline_xul.js | 274 +++ .../tests/browser/mac/browser_popupbutton.js | 166 ++ .../tests/browser/mac/browser_radio_position.js | 321 +++ accessible/tests/browser/mac/browser_range.js | 190 ++ accessible/tests/browser/mac/browser_required.js | 175 ++ .../tests/browser/mac/browser_rich_listbox.js | 73 + .../tests/browser/mac/browser_roles_elements.js | 334 +++ accessible/tests/browser/mac/browser_rootgroup.js | 246 ++ accessible/tests/browser/mac/browser_rotor.js | 1752 ++++++++++++++ .../tests/browser/mac/browser_selectables.js | 342 +++ accessible/tests/browser/mac/browser_table.js | 629 +++++ .../tests/browser/mac/browser_text_basics.js | 380 +++ accessible/tests/browser/mac/browser_text_input.js | 657 ++++++ accessible/tests/browser/mac/browser_text_leaf.js | 83 + .../tests/browser/mac/browser_text_selection.js | 187 ++ .../browser/mac/browser_toggle_radio_check.js | 304 +++ accessible/tests/browser/mac/browser_webarea.js | 77 + accessible/tests/browser/mac/doc_aria_tabs.html | 95 + accessible/tests/browser/mac/doc_menulist.xhtml | 19 + .../tests/browser/mac/doc_rich_listbox.xhtml | 22 + .../tests/browser/mac/doc_textmarker_test.html | 2424 ++++++++++++++++++++ accessible/tests/browser/mac/doc_tree.xhtml | 59 + accessible/tests/browser/mac/head.js | 133 ++ 44 files changed, 12342 insertions(+) create mode 100644 accessible/tests/browser/mac/browser.ini create mode 100644 accessible/tests/browser/mac/browser_app.js create mode 100644 accessible/tests/browser/mac/browser_aria_busy.js create mode 100644 accessible/tests/browser/mac/browser_aria_controls_flowto.js create mode 100644 accessible/tests/browser/mac/browser_aria_current.js create mode 100644 accessible/tests/browser/mac/browser_aria_expanded.js create mode 100644 accessible/tests/browser/mac/browser_aria_haspopup.js create mode 100644 accessible/tests/browser/mac/browser_attributed_text.js create mode 100644 accessible/tests/browser/mac/browser_bounds.js create mode 100644 accessible/tests/browser/mac/browser_details_summary.js create mode 100644 accessible/tests/browser/mac/browser_focus.js create mode 100644 accessible/tests/browser/mac/browser_heading.js create mode 100644 accessible/tests/browser/mac/browser_hierarchy.js create mode 100644 accessible/tests/browser/mac/browser_input.js create mode 100644 accessible/tests/browser/mac/browser_label_title.js create mode 100644 accessible/tests/browser/mac/browser_link.js create mode 100644 accessible/tests/browser/mac/browser_live_regions.js create mode 100644 accessible/tests/browser/mac/browser_mathml.js create mode 100644 accessible/tests/browser/mac/browser_menulist.js create mode 100644 accessible/tests/browser/mac/browser_navigate.js create mode 100644 accessible/tests/browser/mac/browser_outline.js create mode 100644 accessible/tests/browser/mac/browser_outline_xul.js create mode 100644 accessible/tests/browser/mac/browser_popupbutton.js create mode 100644 accessible/tests/browser/mac/browser_radio_position.js create mode 100644 accessible/tests/browser/mac/browser_range.js create mode 100644 accessible/tests/browser/mac/browser_required.js create mode 100644 accessible/tests/browser/mac/browser_rich_listbox.js create mode 100644 accessible/tests/browser/mac/browser_roles_elements.js create mode 100644 accessible/tests/browser/mac/browser_rootgroup.js create mode 100644 accessible/tests/browser/mac/browser_rotor.js create mode 100644 accessible/tests/browser/mac/browser_selectables.js create mode 100644 accessible/tests/browser/mac/browser_table.js create mode 100644 accessible/tests/browser/mac/browser_text_basics.js create mode 100644 accessible/tests/browser/mac/browser_text_input.js create mode 100644 accessible/tests/browser/mac/browser_text_leaf.js create mode 100644 accessible/tests/browser/mac/browser_text_selection.js create mode 100644 accessible/tests/browser/mac/browser_toggle_radio_check.js create mode 100644 accessible/tests/browser/mac/browser_webarea.js create mode 100644 accessible/tests/browser/mac/doc_aria_tabs.html create mode 100644 accessible/tests/browser/mac/doc_menulist.xhtml create mode 100644 accessible/tests/browser/mac/doc_rich_listbox.xhtml create mode 100644 accessible/tests/browser/mac/doc_textmarker_test.html create mode 100644 accessible/tests/browser/mac/doc_tree.xhtml create mode 100644 accessible/tests/browser/mac/head.js (limited to 'accessible/tests/browser/mac') diff --git a/accessible/tests/browser/mac/browser.ini b/accessible/tests/browser/mac/browser.ini new file mode 100644 index 0000000000..f88cfc56ca --- /dev/null +++ b/accessible/tests/browser/mac/browser.ini @@ -0,0 +1,58 @@ +[DEFAULT] +subsuite = a11y +skip-if = os != 'mac' +support-files = + head.js + doc_aria_tabs.html + doc_textmarker_test.html + doc_rich_listbox.xhtml + doc_menulist.xhtml + doc_tree.xhtml + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + !/accessible/tests/mochitest/moz.png +prefs = + javascript.options.asyncstack_capture_debuggee_only=false + +[browser_app.js] +https_first_disabled = true +[browser_aria_current.js] +[browser_aria_expanded.js] +[browser_details_summary.js] +[browser_label_title.js] +[browser_range.js] +[browser_roles_elements.js] +[browser_table.js] +[browser_selectables.js] +[browser_radio_position.js] +[browser_toggle_radio_check.js] +[browser_link.js] +[browser_aria_haspopup.js] +[browser_required.js] +[browser_popupbutton.js] +[browser_mathml.js] +[browser_input.js] +[browser_focus.js] +[browser_text_leaf.js] +[browser_webarea.js] +[browser_text_basics.js] +[browser_text_input.js] +skip-if = + os == "mac" # Bug 1778821 +[browser_rotor.js] +[browser_rootgroup.js] +[browser_text_selection.js] +[browser_navigate.js] +[browser_outline.js] +[browser_outline_xul.js] +[browser_hierarchy.js] +[browser_menulist.js] +[browser_rich_listbox.js] +[browser_live_regions.js] +[browser_aria_busy.js] +[browser_aria_controls_flowto.js] +[browser_attributed_text.js] +[browser_bounds.js] +[browser_heading.js] diff --git a/accessible/tests/browser/mac/browser_app.js b/accessible/tests/browser/mac/browser_app.js new file mode 100644 index 0000000000..7bb69e273a --- /dev/null +++ b/accessible/tests/browser/mac/browser_app.js @@ -0,0 +1,351 @@ +/* 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 getMacAccessible(accOrElmOrID) { + return new Promise(resolve => { + let intervalId = setInterval(() => { + let acc = getAccessible(accOrElmOrID); + if (acc) { + clearInterval(intervalId); + resolve( + acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface) + ); + } + }, 10); + }); +} + +/** + * Test a11yUtils announcements are exposed to VO + */ +add_task(async () => { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html," + ); + const alert = document.getElementById("a11y-announcement"); + ok(alert, "Found alert to send announcements"); + + const alerted = waitForMacEvent("AXAnnouncementRequested", (iface, data) => { + return data.AXAnnouncementKey == "hello world"; + }); + + A11yUtils.announce({ + raw: "hello world", + }); + await alerted; + await BrowserTestUtils.removeTab(tab); +}); + +/** + * Test browser tabs + */ +add_task(async () => { + let newTabs = await Promise.all([ + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,Two" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,Three" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,Four" + ), + ]); + + // Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs + is(gBrowser.tabs.length, 4, "We now have 4 open tabs"); + + let tablist = await getMacAccessible("tabbrowser-tabs"); + is( + tablist.getAttributeValue("AXRole"), + "AXTabGroup", + "Correct role for tablist" + ); + + let tabMacAccs = tablist.getAttributeValue("AXTabs"); + is(tabMacAccs.length, 4, "4 items in AXTabs"); + + let selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + + let tab = selectedTabs[0]; + is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab"); + is( + tab.getAttributeValue("AXSubrole"), + "AXTabButton", + "Correct subrole for tab" + ); + is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab"); + + let tabToSelect = tabMacAccs[2]; + is( + tabToSelect.getAttributeValue("AXTitle"), + "Three", + "Correct title for tab" + ); + + let actions = tabToSelect.actionNames; + ok(true, actions); + ok(actions.includes("AXPress"), "Has switch action"); + + // When tab is clicked selection of tab group changes, + // and focus goes to the web area. Wait for both. + let evt = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXRole") == "AXWebArea" + ), + ]); + tabToSelect.performAction("AXPress"); + await evt; + + selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + is( + selectedTabs[0].getAttributeValue("AXTitle"), + "Three", + "Correct title for tab" + ); + + // Close all open tabs + await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t))); +}); + +/** + * Test ignored invisible items in root + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:license", + }, + async browser => { + let root = await getMacAccessible(document); + let rootChildCount = () => root.getAttributeValue("AXChildren").length; + + // With no popups, the root accessible has 5 visible children: + // 1. Tab bar (#TabsToolbar) + // 2. Navigation bar (#nav-bar) + // 3. Content area (#tabbrowser-tabpanels) + // 4. Some fullscreen pointer grabber (#fullscreen-and-pointerlock-wrapper) + // 5. Accessibility announcements dialog (#a11y-announcement) + let baseRootChildCount = 5; + is( + rootChildCount(), + baseRootChildCount, + "Root with no popups has 5 children" + ); + + // Open a context menu + const menu = document.getElementById("contentAreaContextMenu"); + if ( + Services.prefs.getBoolPref("widget.macos.native-context-menus", false) + ) { + // Native context menu - do not expect accessibility notifications. + let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown"); + EventUtils.synthesizeMouseAtCenter(document.body, { + type: "contextmenu", + }); + await popupshown; + + is( + rootChildCount(), + baseRootChildCount, + "Native context menus do not show up in the root children" + ); + + // Close context menu + let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + menu.hidePopup(); + await popuphidden; + } else { + // Non-native menu + EventUtils.synthesizeMouseAtCenter(document.body, { + type: "contextmenu", + }); + await waitForMacEvent("AXMenuOpened"); + + // Now root has 1 more child + is(rootChildCount(), baseRootChildCount + 1, "Root has 1 more child"); + + // Close context menu + let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu"); + EventUtils.synthesizeKey("KEY_Escape"); + await BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + await closed; + } + + // We're back to base child count + is(rootChildCount(), baseRootChildCount, "Root has original child count"); + + // Open site identity popup + document.getElementById("identity-icon-box").click(); + const identityPopup = document.getElementById("identity-popup"); + await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown"); + + // Now root has another child + is(rootChildCount(), baseRootChildCount + 1, "Root has another child"); + + // Close popup + EventUtils.synthesizeKey("KEY_Escape"); + await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden"); + + // We're back to the base child count + is(rootChildCount(), baseRootChildCount, "Root has the base child count"); + } + ); +}); + +/** + * Tests for location bar + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + url: "http://example.com", + }, + async browser => { + let input = await getMacAccessible("urlbar-input"); + is( + input.getAttributeValue("AXValue"), + "example.com", + "Location bar has correct value" + ); + } + ); +}); + +/** + * Test context menu + */ +add_task(async () => { + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + ok(true, "We cannot inspect native context menu contents; skip this test."); + return; + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: 'data:text/html,link', + }, + async browser => { + if (!Services.search.isInitialized) { + let aStatus = await Services.search.init(); + Assert.ok(Components.isSuccessCode(aStatus)); + Assert.ok(Services.search.isInitialized); + } + + const hasContainers = + Services.prefs.getBoolPref("privacy.userContext.enabled") && + !!ContextualIdentityService.getPublicIdentities().length; + info(`${hasContainers ? "Do" : "Don't"} expect containers item.`); + const hasInspectA11y = + Services.prefs.getBoolPref("devtools.everOpened", false) || + Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0; + info(`${hasInspectA11y ? "Do" : "Don't"} expect inspect a11y item.`); + + // synthesize a right click on the link to open the link context menu + let menu = document.getElementById("contentAreaContextMenu"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#exampleLink", + { type: "contextmenu" }, + browser + ); + await waitForMacEvent("AXMenuOpened"); + + menu = await getMacAccessible(menu); + let menuChildren = menu.getAttributeValue("AXChildren"); + const expectedChildCount = 12 + +hasContainers + +hasInspectA11y; + is( + menuChildren.length, + expectedChildCount, + `Context menu on link contains ${expectedChildCount} items.` + ); + // items at indicies 3, 9, and 11 are the splitters when containers exist + // everything else should be a menu item, otherwise indicies of splitters are + // 3, 8, and 10 + const splitterIndicies = hasContainers ? [4, 9, 11] : [3, 8, 10]; + for (let i = 0; i < menuChildren.length; i++) { + if (splitterIndicies.includes(i)) { + is( + menuChildren[i].getAttributeValue("AXRole"), + "AXSplitter", + "found splitter in menu" + ); + } else { + is( + menuChildren[i].getAttributeValue("AXRole"), + "AXMenuItem", + "found menu item in menu" + ); + } + } + + // check the containers sub menu in depth if it exists + if (hasContainers) { + is( + menuChildren[1].getAttributeValue("AXVisibleChildren"), + null, + "Submenu 1 has no visible chldren when hidden" + ); + + // focus the first submenu + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await waitForMacEvent("AXMenuOpened"); + + // after the submenu is opened, refetch it + menu = document.getElementById("contentAreaContextMenu"); + menu = await getMacAccessible(menu); + menuChildren = menu.getAttributeValue("AXChildren"); + + // verify submenu-menuitem's attributes + is( + menuChildren[1].getAttributeValue("AXChildren").length, + 1, + "Submenu 1 has one child when open" + ); + const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0]; + is( + subMenu.getAttributeValue("AXRole"), + "AXMenu", + "submenu has role of menu" + ); + const subMenuChildren = subMenu.getAttributeValue("AXChildren"); + is(subMenuChildren.length, 4, "sub menu has 4 children"); + is( + subMenu.getAttributeValue("AXVisibleChildren").length, + 4, + "submenu has 4 visible children" + ); + + // close context menu + EventUtils.synthesizeKey("KEY_Escape"); + await waitForMacEvent("AXMenuClosed"); + } + + EventUtils.synthesizeKey("KEY_Escape"); + await waitForMacEvent("AXMenuClosed"); + } + ); +}); diff --git a/accessible/tests/browser/mac/browser_aria_busy.js b/accessible/tests/browser/mac/browser_aria_busy.js new file mode 100644 index 0000000000..e75d334e29 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_busy.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"; + +/* 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 aria-busy + */ +addAccessibleTask( + `
Hello
`, + async (browser, accDoc) => { + let section = getNativeInterface(accDoc, "section"); + + ok(!section.getAttributeValue("AXElementBusy"), "section is not busy"); + + let busyChanged = waitForMacEvent("AXElementBusyChanged", "section"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("section") + .setAttribute("aria-busy", "true"); + }); + await busyChanged; + + ok(section.getAttributeValue("AXElementBusy"), "section is busy"); + + busyChanged = waitForMacEvent("AXElementBusyChanged", "section"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("section") + .setAttribute("aria-busy", "false"); + }); + await busyChanged; + + ok(!section.getAttributeValue("AXElementBusy"), "section is not busy"); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_controls_flowto.js b/accessible/tests/browser/mac/browser_aria_controls_flowto.js new file mode 100644 index 0000000000..5950a60399 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_controls_flowto.js @@ -0,0 +1,92 @@ +/* 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 aria-controls + */ +addAccessibleTask( + ` +
Information.
+
More information.
`, + async (browser, accDoc) => { + const getAriaControls = id => + JSON.stringify( + getNativeInterface(accDoc, id) + .getAttributeValue("AXARIAControls") + .map(e => e.getAttributeValue("AXDOMIdentifier")) + ); + + await untilCacheIs( + () => getAriaControls("info-button"), + JSON.stringify(["info"]), + "Info-button has correct initial controls" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("info-button") + .setAttribute("aria-controls", "info more-info"); + }); + + await untilCacheIs( + () => getAriaControls("info-button"), + JSON.stringify(["info", "more-info"]), + "Info-button has correct controls after mutation" + ); + } +); + +function getLinkedUIElements(accDoc, id) { + return JSON.stringify( + getNativeInterface(accDoc, id) + .getAttributeValue("AXLinkedUIElements") + .map(e => e.getAttributeValue("AXDOMIdentifier")) + ); +} + +/** + * Test aria-flowto + */ +addAccessibleTask( + ` +
Information.
+
More information.
`, + async (browser, accDoc) => { + await untilCacheIs( + () => getLinkedUIElements(accDoc, "info-button"), + JSON.stringify(["info"]), + "Info-button has correct initial linked elements" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("info-button") + .setAttribute("aria-flowto", "info more-info"); + }); + + await untilCacheIs( + () => getLinkedUIElements(accDoc, "info-button"), + JSON.stringify(["info", "more-info"]), + "Info-button has correct linked elements after mutation" + ); + } +); + +/** + * Test aria-controls + */ +addAccessibleTask( + ` + +
Information.
`, + async (browser, accDoc) => { + await untilCacheIs( + () => getLinkedUIElements(accDoc, "dog-radio"), + JSON.stringify(["cat-radio", "dog-radio", "info"]), + "dog-radio has correct linked elements" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_current.js b/accessible/tests/browser/mac/browser_aria_current.js new file mode 100644 index 0000000000..02c7a71b67 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_current.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 */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test aria-current + */ +addAccessibleTask( + `OneTwo`, + async (browser, accDoc) => { + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + + is( + one.getAttributeValue("AXARIACurrent"), + "page", + "Correct aria-current for #one" + ); + is( + two.getAttributeValue("AXARIACurrent"), + null, + "Correct aria-current for #two" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("one") + .setAttribute("aria-current", "step"); + }); + + is( + one.getAttributeValue("AXARIACurrent"), + "step", + "Correct aria-current for #one" + ); + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("one").removeAttribute("aria-current"); + }); + await stateChanged; + + is( + one.getAttributeValue("AXARIACurrent"), + null, + "Correct aria-current for #one" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_expanded.js b/accessible/tests/browser/mac/browser_aria_expanded.js new file mode 100644 index 0000000000..48fb615266 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_expanded.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/states.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); + +// Test aria-expanded on a button +addAccessibleTask( + `hello world
+
+ goodbye`, + async (browser, accDoc) => { + let button = getNativeInterface(accDoc, "b"); + is(button.getAttributeValue("AXExpanded"), 0, "button is not expanded"); + + let stateChanged = Promise.all([ + waitForStateChange("b", STATE_EXPANDED, true), + waitForStateChange("b", STATE_COLLAPSED, false), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("b") + .setAttribute("aria-expanded", "true"); + }); + await stateChanged; + is(button.getAttributeValue("AXExpanded"), 1, "button is expanded"); + + stateChanged = Promise.all([ + waitForStateChange("b", STATE_EXPANDED, false), + waitForStateChange("b", EXT_STATE_EXPANDABLE, false, true), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("b").removeAttribute("aria-expanded"); + }); + await stateChanged; + + ok( + !button.attributeNames.includes("AXExpanded"), + "button has no expanded attr" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_haspopup.js b/accessible/tests/browser/mac/browser_aria_haspopup.js new file mode 100644 index 0000000000..57f1e50f65 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_haspopup.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 */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test aria-haspopup + */ +addAccessibleTask( + ` + + + + + + + + + + + + + `, + async (browser, accDoc) => { + // FALSE + let falseID = getNativeInterface(accDoc, "false"); + is( + falseID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button with false" + ); + is( + falseID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue val for button with false" + ); + let attrChanged = waitForEvent(EVENT_STATE_CHANGE, "false"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("false") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + falseID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for false" + ); + is( + falseID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with true" + ); + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "false"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("false").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + falseID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for false" + ); + is( + falseID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button after remove" + ); + + // MENU + let menuID = getNativeInterface(accDoc, "menu"); + is( + menuID.getAttributeValue("AXPopupValue"), + "menu", + "Correct AXPopupValue val for button with menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with menu" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("menu") + .setAttribute("aria-haspopup", "true"); + }); + + await untilCacheIs( + () => menuID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with menu" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "menu"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("menu").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + await untilCacheIs( + () => menuID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button after remove" + ); + + // LISTBOX + let listboxID = getNativeInterface(accDoc, "listbox"); + is( + listboxID.getAttributeValue("AXPopupValue"), + "listbox", + "Correct AXPopupValue for button with listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with listbox" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("listbox") + .setAttribute("aria-haspopup", "true"); + }); + + await untilCacheIs( + () => listboxID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with listbox" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("listbox") + .removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + listboxID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with listbox" + ); + + // TREE + let treeID = getNativeInterface(accDoc, "tree"); + is( + treeID.getAttributeValue("AXPopupValue"), + "tree", + "Correct AXPopupValue for button with tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with tree" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("tree") + .setAttribute("aria-haspopup", "true"); + }); + + await untilCacheIs( + () => treeID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with tree" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "tree"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("tree").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + treeID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with tree after remove" + ); + + // GRID + let gridID = getNativeInterface(accDoc, "grid"); + is( + gridID.getAttributeValue("AXPopupValue"), + "grid", + "Correct AXPopupValue for button with grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with grid" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("grid") + .setAttribute("aria-haspopup", "true"); + }); + + await untilCacheIs( + () => gridID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with grid" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "grid"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("grid").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + gridID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with grid after remove" + ); + + // DIALOG + let dialogID = getNativeInterface(accDoc, "dialog"); + is( + dialogID.getAttributeValue("AXPopupValue"), + "dialog", + "Correct AXPopupValue for button with dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with dialog" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("dialog") + .setAttribute("aria-haspopup", "true"); + }); + + await untilCacheIs( + () => dialogID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with dialog" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("dialog") + .removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + dialogID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with dialog after remove" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_attributed_text.js b/accessible/tests/browser/mac/browser_attributed_text.js new file mode 100644 index 0000000000..6f6200751c --- /dev/null +++ b/accessible/tests/browser/mac/browser_attributed_text.js @@ -0,0 +1,144 @@ +/* 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 read-only attributed strings +addAccessibleTask( + `

hello world

+

this is a test

`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [ + macDoc.getAttributeValue("AXStartTextMarker"), + macDoc.getAttributeValue("AXEndTextMarker"), + ] + ); + + let attributedText = macDoc.getParameterizedAttributeValue( + "AXAttributedStringForTextMarkerRange", + range + ); + + let attributesList = attributedText.map( + ({ + string, + AXForegroundColor, + AXBackgroundColor, + AXUnderline, + AXUnderlineColor, + AXHeadingLevel, + AXFont, + AXLink, + AXMarkedMisspelled, + }) => [ + string, + AXForegroundColor, + AXBackgroundColor, + AXUnderline, + AXUnderlineColor, + AXHeadingLevel, + AXFont.AXFontSize, + AXLink ? AXLink.getAttributeValue("AXDOMIdentifier") : null, + AXMarkedMisspelled, + ] + ); + + Assert.deepEqual(attributesList, [ + // string, fg color, bg color, underline, underline color, heading level, font size, link id, misspelled + ["hello ", "#000000", "#ffffff", null, null, 1, 32, null, null], + ["world", "#0000ee", "#ffffff", 1, "#0000ee", 1, 32, "a1", null], + ["this ", "#000000", "#ffffff", null, null, null, 16, null, null], + ["is", "#ff0000", "#ffff00", null, null, null, 16, null, 1], + [" ", "#000000", "#ffffff", null, null, null, 16, null, null], + ["a", "#000000", "#ffffff", 1, "#008000", null, 16, null, null], + [" ", "#000000", "#ffffff", null, null, null, 16, null, null], + ["test", "#0000ee", "#ffffff", 1, "#0000ee", null, 16, "a2", null], + ]); + + // Test different NSRange parameters for AXAttributedStringForRange + let worldLeaf = findAccessibleChildByID(accDoc, "a1").firstChild; + let wordStaticText = worldLeaf.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + attributedText = wordStaticText.getParameterizedAttributeValue( + "AXAttributedStringForRange", + NSRange(4, 1) + ); + is(attributedText.length, 1, "Last character is in single attribute run"); + is(attributedText[0].string, "d", "Last character matches"); + + attributedText = wordStaticText.getParameterizedAttributeValue( + "AXAttributedStringForRange", + NSRange(5, 1) + ); + is(attributedText.length, 0, "Range is past accessible bounds"); + } +); + +// Test misspelling in text area +addAccessibleTask( + ``, + async (browser, accDoc) => { + let textArea = getNativeInterface(accDoc, "t"); + let spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t"); + textArea.setAttributeValue("AXFocused", true); + + let attributedText = []; + + // For some internal reason we get several text attribute change events + // before the attributed text returned provides the misspelling attributes. + while (true) { + await spellDone; + + let range = textArea.getAttributeValue("AXVisibleCharacterRange"); + attributedText = textArea.getParameterizedAttributeValue( + "AXAttributedStringForRange", + NSRange(...range) + ); + + if (attributedText.length != 3) { + spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t"); + } else { + break; + } + } + + ok(attributedText[1].AXMarkedMisspelled); + } +); + +// Test getting a span of attributed text that includes an empty input element. +addAccessibleTask(`hello world`, async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [ + macDoc.getAttributeValue("AXStartTextMarker"), + macDoc.getAttributeValue("AXEndTextMarker"), + ] + ); + + let attributedText = macDoc.getParameterizedAttributeValue( + "AXAttributedStringForTextMarkerRange", + range + ); + + let text = macDoc.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range + ); + + is(attributedText.length, 1, "Empty input does not break up attribute run."); + is(attributedText[0].string, `hello world `, "Attributed string is correct"); + is(text, `hello world `, "Unattributed string is correct"); +}); diff --git a/accessible/tests/browser/mac/browser_bounds.js b/accessible/tests/browser/mac/browser_bounds.js new file mode 100644 index 0000000000..09343d7c9d --- /dev/null +++ b/accessible/tests/browser/mac/browser_bounds.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"; + +/** + * Test position, size for onscreen content + */ +addAccessibleTask( + `I am some extra content
+
hello

+
hello world
I am some text
`, + async (browser, accDoc) => { + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null"); + ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null"); + + // AXSize and AXPosition are composed of AXFrame components, so we + // test them here instead of calling AXFrame directly. + const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize"); + const [worldWidth, worldHeight] = world.getAttributeValue("AXSize"); + ok(helloWidth > 0, "Hello has a positive width"); + ok(helloHeight > 0, "Hello has a positive height"); + ok(worldWidth > 0, "World has a positive width"); + ok(worldHeight > 0, "World has a positive height"); + ok(helloHeight < worldHeight, "Hello has a smaller height than world"); + ok(helloWidth < worldWidth, "Hello has a smaller width than world"); + + // Note: these are mac screen coords, so our origin is bottom left + const [helloX, helloY] = hello.getAttributeValue("AXPosition"); + const [worldX, worldY] = world.getAttributeValue("AXPosition"); + ok(helloX > 0, "Hello has a positive X"); + ok(helloY > 0, "Hello has a positive Y"); + ok(worldX > 0, "World has a positive X"); + ok(worldY > 0, "World has a positive Y"); + ok(helloY > worldY, "Hello has a larger Y than world"); + ok(helloX == worldX, "Hello and world have the same X"); + } +); + +/** + * Test position, size for offscreen content + */ +addAccessibleTask( + `I am some extra content
+
hello

+
hello world
I am some text
`, + async (browser, accDoc) => { + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null"); + ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null"); + + // AXSize and AXPosition are composed of AXFrame components, so we + // test them here instead of calling AXFrame directly. + const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize"); + const [worldWidth, worldHeight] = world.getAttributeValue("AXSize"); + ok(helloWidth > 0, "Hello has a positive width"); + ok(helloHeight > 0, "Hello has a positive height"); + ok(worldWidth > 0, "World has a positive width"); + ok(worldHeight > 0, "World has a positive height"); + ok(helloHeight < worldHeight, "Hello has a smaller height than world"); + ok(helloWidth < worldWidth, "Hello has a smaller width than world"); + + // Note: these are mac screen coords, so our origin is bottom left + const [helloX, helloY] = hello.getAttributeValue("AXPosition"); + const [worldX, worldY] = world.getAttributeValue("AXPosition"); + ok(helloX < 0, "Hello has a negative X"); + ok(helloY > 0, "Hello has a positive Y"); + ok(worldX < 0, "World has a negative X"); + ok(worldY > 0, "World has a positive Y"); + ok(helloY > worldY, "Hello has a larger Y than world"); + ok(helloX == worldX, "Hello and world have the same X"); + } +); diff --git a/accessible/tests/browser/mac/browser_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js new file mode 100644 index 0000000000..6157707f79 --- /dev/null +++ b/accessible/tests/browser/mac/browser_details_summary.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 */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test details/summary + */ +addAccessibleTask( + `
Foo

Bar

`, + async (browser, accDoc) => { + let details = getNativeInterface(accDoc, "details"); + is( + details.getAttributeValue("AXRole"), + "AXGroup", + "Correct role for details" + ); + is( + details.getAttributeValue("AXSubrole"), + "AXDetails", + "Correct subrole for details" + ); + + let detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 1, "collapsed details has only one child"); + + let summary = detailsChildren[0]; + is( + summary.getAttributeValue("AXRole"), + "AXButton", + "Correct role for summary" + ); + is( + summary.getAttributeValue("AXSubrole"), + "AXSummary", + "Correct subrole for summary" + ); + is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed"); + + let actions = summary.actionNames; + ok(actions.includes("AXPress"), "Summary Has press action"); + + let stateChanged = waitForStateChange("summary", STATE_EXPANDED, true); + summary.performAction("AXPress"); + // The reorder gecko event notifies us of a tree change. + await stateChanged; + is(summary.getAttributeValue("AXExpanded"), 1, "Summary is expanded"); + + detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 2, "collapsed details has only one child"); + + stateChanged = waitForStateChange("summary", STATE_EXPANDED, false); + summary.performAction("AXPress"); + // The reorder gecko event notifies us of a tree change. + await stateChanged; + is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed 2"); + + detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 1, "collapsed details has only one child"); + } +); diff --git a/accessible/tests/browser/mac/browser_focus.js b/accessible/tests/browser/mac/browser_focus.js new file mode 100644 index 0000000000..6bceb06c6c --- /dev/null +++ b/accessible/tests/browser/mac/browser_focus.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 focusability + */ +addAccessibleTask( + ` +
hello
+ `, + async (browser, accDoc) => { + let ariabutton = getNativeInterface(accDoc, "ariabutton"); + let button = getNativeInterface(accDoc, "button"); + + is( + ariabutton.getAttributeValue("AXFocused"), + 0, + "aria button is not focused" + ); + + is(button.getAttributeValue("AXFocused"), 0, "button is not focused"); + + ok( + !ariabutton.isAttributeSettable("AXFocused"), + "aria button should not be focusable" + ); + + ok(button.isAttributeSettable("AXFocused"), "button is focusable"); + + let evt = waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == "button" + ); + + button.setAttributeValue("AXFocused", true); + + await evt; + + is(button.getAttributeValue("AXFocused"), 1, "button is focused"); + } +); diff --git a/accessible/tests/browser/mac/browser_heading.js b/accessible/tests/browser/mac/browser_heading.js new file mode 100644 index 0000000000..fd8c12883d --- /dev/null +++ b/accessible/tests/browser/mac/browser_heading.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"; + +/** + * Test whether line break code in text content will be removed + * and extra whitespaces will be trimmed. + */ +addAccessibleTask( + ` +

We’re building a richer search experience

+

+We’re building a +richest +search experience +

+ `, + async (browser, accDoc) => { + const singleLineContentHeading = getNativeInterface( + accDoc, + "single-line-content" + ); + is( + singleLineContentHeading.getAttributeValue("AXTitle"), + "We’re building a richer search experience" + ); + + const multiLinesContentHeading = getNativeInterface( + accDoc, + "multi-lines-content" + ); + is( + multiLinesContentHeading.getAttributeValue("AXTitle"), + "We’re building a richest search experience" + ); + } +); + +/** + * Test AXTitle/AXDescription attributes of heading elements + */ +addAccessibleTask( + ` +

Hello world

+

Hello

+

Hello

+ `, + async (browser, accDoc) => { + const a = getNativeInterface(accDoc, "a"); + is( + a.getAttributeValue("AXTitle"), + "Hello world", + "Correct AXTitle for 'a'" + ); + ok( + !a.getAttributeValue("AXDescription"), + "'a' Should not have AXDescription" + ); + + const b = getNativeInterface(accDoc, "b"); + is(b.getAttributeValue("AXTitle"), "Hello", "Correct AXTitle for 'b'"); + ok( + !b.getAttributeValue("AXDescription"), + "'b' Should not have AXDescription" + ); + + const c = getNativeInterface(accDoc, "c"); + is( + c.getAttributeValue("AXDescription"), + "Goodbye", + "Correct AXDescription for 'c'" + ); + ok(!c.getAttributeValue("AXTitle"), "'c' Should not have AXTitle"); + } +); diff --git a/accessible/tests/browser/mac/browser_hierarchy.js b/accessible/tests/browser/mac/browser_hierarchy.js new file mode 100644 index 0000000000..8a97e55c07 --- /dev/null +++ b/accessible/tests/browser/mac/browser_hierarchy.js @@ -0,0 +1,75 @@ +/* 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 AXIndexForChildUIElement + */ +addAccessibleTask( + `

Hello strange world`, + (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + + let children = p.getAttributeValue("AXChildren"); + is(children.length, 3, "p has 3 children"); + is( + children[1].getAttributeValue("AXDOMIdentifier"), + "link", + "second child is link" + ); + + let index = p.getParameterizedAttributeValue( + "AXIndexForChildUIElement", + children[1] + ); + is(index, 1, "link is second child"); + } +); + +/** + * Test textbox with more than one child + */ +addAccessibleTask( + `

Hello strange world
`, + (browser, accDoc) => { + let textbox = getNativeInterface(accDoc, "textbox"); + + is( + textbox.getAttributeValue("AXChildren").length, + 3, + "textbox has 3 children" + ); + } +); + +/** + * Test textbox with one child + */ +addAccessibleTask( + `
Hello
`, + async (browser, accDoc) => { + let textbox = getNativeInterface(accDoc, "textbox"); + + is( + textbox.getAttributeValue("AXChildren").length, + 0, + "textbox with one child is pruned" + ); + + let reorder = waitForEvent(EVENT_REORDER, "textbox"); + await SpecialPowers.spawn(browser, [], () => { + let link = content.document.createElement("a"); + link.textContent = "World"; + content.document.getElementById("textbox").appendChild(link); + }); + await reorder; + + is( + textbox.getAttributeValue("AXChildren").length, + 2, + "textbox with two child is not pruned" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_input.js b/accessible/tests/browser/mac/browser_input.js new file mode 100644 index 0000000000..7fa20a9d4b --- /dev/null +++ b/accessible/tests/browser/mac/browser_input.js @@ -0,0 +1,225 @@ +/* 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 selectedTextEventPromises(stateChangeType) { + return [ + waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateChangeType == stateChangeType && + elem.getAttributeValue("AXDOMIdentifier") == "body" + ); + }), + waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateChangeType == stateChangeType && + elem.getAttributeValue("AXDOMIdentifier") == "input" + ); + }), + ]; +} + +async function testInput(browser, accDoc) { + let input = getNativeInterface(accDoc, "input"); + + is(input.getAttributeValue("AXDescription"), "Name", "Correct input label"); + is(input.getAttributeValue("AXTitle"), "", "Correct input title"); + is(input.getAttributeValue("AXValue"), "Elmer Fudd", "Correct input value"); + is( + input.getAttributeValue("AXNumberOfCharacters"), + 10, + "Correct length of value" + ); + + ok(input.attributeNames.includes("AXSelectedText"), "Has AXSelectedText"); + ok( + input.attributeNames.includes("AXSelectedTextRange"), + "Has AXSelectedTextRange" + ); + + let evt = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged", "input"), + ...selectedTextEventPromises(AXTextStateChangeTypeSelectionMove), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + await evt; + + evt = Promise.all( + selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend) + ); + await SpecialPowers.spawn(browser, [], () => { + let elm = content.document.getElementById("input"); + if (elm.setSelectionRange) { + elm.setSelectionRange(6, 9); + } else { + let r = new content.Range(); + let textNode = elm.firstElementChild.firstChild; + r.setStart(textNode, 6); + r.setEnd(textNode, 9); + + let s = content.getSelection(); + s.removeAllRanges(); + s.addRange(r); + } + }); + await evt; + + is( + input.getAttributeValue("AXSelectedText"), + "Fud", + "Correct text is selected" + ); + + Assert.deepEqual( + input.getAttributeValue("AXSelectedTextRange"), + [6, 3], + "correct range selected" + ); + + ok( + input.isAttributeSettable("AXSelectedTextRange"), + "AXSelectedTextRange is settable" + ); + + evt = Promise.all( + selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend) + ); + input.setAttributeValue("AXSelectedTextRange", NSRange(1, 7)); + await evt; + + Assert.deepEqual( + input.getAttributeValue("AXSelectedTextRange"), + [1, 7], + "correct range selected" + ); + + is( + input.getAttributeValue("AXSelectedText"), + "lmer Fu", + "Correct text is selected" + ); + + let domSelection = await SpecialPowers.spawn(browser, [], () => { + let elm = content.document.querySelector("input#input"); + if (elm) { + return elm.value.substring(elm.selectionStart, elm.selectionEnd); + } + + return content.getSelection().toString(); + }); + + is(domSelection, "lmer Fu", "correct DOM selection"); + + is( + input.getParameterizedAttributeValue("AXStringForRange", NSRange(3, 5)), + "er Fu", + "AXStringForRange works" + ); +} + +/** + * Input selection test + */ +addAccessibleTask( + ``, + testInput +); + +/** + * contenteditable selection test + */ +addAccessibleTask( + `
+

Elmer Fudd

+
`, + testInput +); + +/** + * test contenteditable with selection that extends past editable part + */ +addAccessibleTask( + `Elmer Fudd is the name`, + async (browser, accDoc) => { + let evt = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged", "input"), + waitForMacEvent("AXSelectedTextChanged", "body"), + waitForMacEvent("AXSelectedTextChanged", "input"), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + await evt; + + evt = waitForEvent(EVENT_TEXT_CARET_MOVED); + await SpecialPowers.spawn(browser, [], () => { + let input = content.document.getElementById("input"); + let notinput = content.document.getElementById("notinput"); + + let r = new content.Range(); + r.setStart(input.firstChild, 4); + r.setEnd(notinput.firstChild, 6); + + let s = content.getSelection(); + s.removeAllRanges(); + s.addRange(r); + }); + await evt; + + let input = getNativeInterface(accDoc, "input"); + + is( + input.getAttributeValue("AXSelectedText"), + "r Fudd", + "Correct text is selected in #input" + ); + + is( + stringForRange( + input, + input.getAttributeValue("AXSelectedTextMarkerRange") + ), + "r Fudd is the", + "Correct text is selected in document" + ); + } +); + +/** + * test nested content editables and their ancestor getters. + */ +addAccessibleTask( + `
+

Bob Loblaw's

+
+ Law Blog +
+
`, + (browser, accDoc) => { + let link = getNativeInterface(accDoc, "link"); + let innerLink = getNativeInterface(accDoc, "inner_link"); + + let idmatches = (elem, id) => { + is(elem.getAttributeValue("AXDOMIdentifier"), id, "Matches ID"); + }; + + idmatches(link.getAttributeValue("AXEditableAncestor"), "outer"); + idmatches(link.getAttributeValue("AXFocusableAncestor"), "outer"); + idmatches(link.getAttributeValue("AXHighestEditableAncestor"), "outer"); + + idmatches(innerLink.getAttributeValue("AXEditableAncestor"), "inner"); + idmatches(innerLink.getAttributeValue("AXFocusableAncestor"), "inner"); + idmatches( + innerLink.getAttributeValue("AXHighestEditableAncestor"), + "outer" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_label_title.js b/accessible/tests/browser/mac/browser_label_title.js new file mode 100644 index 0000000000..2532247e0f --- /dev/null +++ b/accessible/tests/browser/mac/browser_label_title.js @@ -0,0 +1,111 @@ +/* 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 different labeling/titling schemes for text fields + */ +addAccessibleTask( + ` + + `, + (browser, accDoc) => { + let n1 = getNativeInterface(accDoc, "n1"); + let n1Label = n1.getAttributeValue("AXTitleUIElement"); + // XXX: In Safari the label is an AXText with an AXValue, + // here it is an AXGroup witth an AXTitle + is(n1Label.getAttributeValue("AXTitle"), "Label"); + + let n2 = getNativeInterface(accDoc, "n2"); + is(n2.getAttributeValue("AXDescription"), "TwoLabels"); + + let n3 = getNativeInterface(accDoc, "n3"); + is(n3.getAttributeValue("AXDescription"), "ARIA Label"); + } +); + +/** + * Test to see that named groups get labels + */ +addAccessibleTask( + `
Fields
`, + (browser, accDoc) => { + let fieldset = getNativeInterface(accDoc, "fieldset"); + is(fieldset.getAttributeValue("AXDescription"), "Fields"); + } +); + +/** + * Test to see that list items don't get titled groups + */ +addAccessibleTask( + ` + `, + (browser, accDoc) => { + let unstyledItem = getNativeInterface(accDoc, "unstyled-item"); + is(unstyledItem.getAttributeValue("AXTitle"), ""); + + let styledItem = getNativeInterface(accDoc, "unstyled-item"); + is(styledItem.getAttributeValue("AXTitle"), ""); + } +); + +/** + * Test that we fire a title changed notification + */ +addAccessibleTask( + `
`, + async (browser, accDoc) => { + let elem = getNativeInterface(accDoc, "elem"); + is(elem.getAttributeValue("AXTitle"), "Hello world"); + let evt = waitForMacEvent("AXTitleChanged", "elem"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("elem") + .setAttribute("aria-label", "Hello universe"); + }); + await evt; + is(elem.getAttributeValue("AXTitle"), "Hello universe"); + } +); + +/** + * Test articles supply only labels not titles + */ +addAccessibleTask( + `
`, + async (browser, accDoc) => { + let article = getNativeInterface(accDoc, "article"); + is(article.getAttributeValue("AXDescription"), "Hello world"); + ok(!article.getAttributeValue("AXTitle")); + } +); + +/** + * Test text and number inputs supply only labels not titles + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let input = getNativeInterface(accDoc, "input"); + is(input.getAttributeValue("AXDescription"), "The best number you know of"); + ok(!input.getAttributeValue("AXTitle")); + let evt = waitForEvent(EVENT_SHOW, "input"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").setAttribute("type", "number"); + }); + await evt; + input = getNativeInterface(accDoc, "input"); + is(input.getAttributeValue("AXDescription"), "The best number you know of"); + ok(!input.getAttributeValue("AXTitle")); + } +); diff --git a/accessible/tests/browser/mac/browser_link.js b/accessible/tests/browser/mac/browser_link.js new file mode 100644 index 0000000000..3ec62f4c6d --- /dev/null +++ b/accessible/tests/browser/mac/browser_link.js @@ -0,0 +1,231 @@ +/* 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 } +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +/** + * Test visited link properties. + */ +addAccessibleTask( + ` + I am a non-visited link
+ `, + async (browser, accDoc) => { + let link = getNativeInterface(accDoc, "link"); + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link"); + + is(link.getAttributeValue("AXVisited"), 0, "Link has not been visited"); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await PlacesTestUtils.addVisits(["http://www.example.com/"]); + + await stateChanged; + is(link.getAttributeValue("AXVisited"), 1, "Link has been visited"); + + // Ensure history is cleared before running + await PlacesUtils.history.clear(); + } +); + +function waitForLinkedChange(id, isEnabled) { + return waitForEvent(EVENT_STATE_CHANGE, e => { + e.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + e.state == STATE_LINKED && + !e.isExtraState && + isEnabled == e.isEnabled && + id == getAccessibleDOMNodeID(e.accessible) + ); + }); +} + +/** + * Test linked vs unlinked anchor tags + */ +addAccessibleTask( + ` + I am a link link + I am a link-ish link + I am a non-link link + `, + async (browser, accDoc) => { + let link1 = getNativeInterface(accDoc, "link1"); + is( + link1.getAttributeValue("AXRole"), + "AXLink", + "a[href] gets correct link role" + ); + ok( + link1.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link1.attributeNames.includes("AXURL"), "Link has URL attribute"); + + let link2 = getNativeInterface(accDoc, "link2"); + is( + link2.getAttributeValue("AXRole"), + "AXLink", + "a[onclick] gets correct link role" + ); + ok( + link2.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link2.attributeNames.includes("AXURL"), "Link has URL attribute"); + + let link3 = getNativeInterface(accDoc, "link3"); + is( + link3.getAttributeValue("AXRole"), + "AXGroup", + "bare gets correct group role" + ); + + let stateChanged = waitForLinkedChange("link1", false); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link1").removeAttribute("href"); + }); + await stateChanged; + is( + link1.getAttributeValue("AXRole"), + "AXGroup", + " stripped from href gets group role" + ); + + stateChanged = waitForLinkedChange("link2", false); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link2").removeAttribute("onclick"); + }); + await stateChanged; + is( + link2.getAttributeValue("AXRole"), + "AXGroup", + " stripped from onclick gets group role" + ); + + stateChanged = waitForLinkedChange("link3", true); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("link3") + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + .setAttribute("href", "http://example.com"); + }); + await stateChanged; + is( + link3.getAttributeValue("AXRole"), + "AXLink", + "href added to bare a gets link role" + ); + + ok( + link3.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute"); + } +); + +/** + * Test anchors and linked ui elements attr + */ +addAccessibleTask( + ` + I am a link + I am a link with an empty anchor + I am a link with no corresponding element + I am a link with a corresponding element + I jump to an empty element + I jump to a named element + I jump to an empty named element +

I am that element

+

+ I have a name + +

I have no name and no ID

+

+ `, + async (browser, accDoc) => { + let link0 = getNativeInterface(accDoc, "link0"); + let link1 = getNativeInterface(accDoc, "link1"); + let link2 = getNativeInterface(accDoc, "link2"); + let link3 = getNativeInterface(accDoc, "link3"); + let link4 = getNativeInterface(accDoc, "link4"); + let link5 = getNativeInterface(accDoc, "link5"); + let link6 = getNativeInterface(accDoc, "link6"); + + is( + link0.getAttributeValue("AXLinkedUIElements").length, + 0, + "Link 0 has no linked UI elements" + ); + is( + link1.getAttributeValue("AXLinkedUIElements").length, + 0, + "Link 1 has no linked UI elements" + ); + is( + link2.getAttributeValue("AXLinkedUIElements").length, + 0, + "Link 2 has no linked UI elements" + ); + is( + link3.getAttributeValue("AXLinkedUIElements").length, + 1, + "Link 3 has one linked UI element" + ); + is( + link3 + .getAttributeValue("AXLinkedUIElements")[0] + .getAttributeValue("AXTitle"), + "I am that element", + "Link 3 is linked to the heading" + ); + is( + link4.getAttributeValue("AXLinkedUIElements").length, + 1, + "Link 4 has one linked UI element" + ); + is( + link4 + .getAttributeValue("AXLinkedUIElements")[0] + .getAttributeValue("AXTitle"), + null, + "Link 4 is linked to the heading" + ); + is( + link5.getAttributeValue("AXLinkedUIElements").length, + 1, + "Link 5 has one linked UI element" + ); + is( + link5 + .getAttributeValue("AXLinkedUIElements")[0] + .getAttributeValue("AXTitle"), + "I have a name", + "Link 5 is linked to a named element" + ); + is( + link6.getAttributeValue("AXLinkedUIElements").length, + 1, + "Link 6 has one linked UI element" + ); + is( + link6 + .getAttributeValue("AXLinkedUIElements")[0] + .getAttributeValue("AXTitle"), + "", + "Link 6 is linked to an empty named element" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js new file mode 100644 index 0000000000..10a03120f8 --- /dev/null +++ b/accessible/tests/browser/mac/browser_live_regions.js @@ -0,0 +1,165 @@ +/* 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 live region creation and removal. + */ +addAccessibleTask( + ` +
Polite region
+
Assertive region
+ `, + async (browser, accDoc) => { + let politeRegion = getNativeInterface(accDoc, "polite"); + ok( + !politeRegion.attributeNames.includes("AXARIALive"), + "region is not live" + ); + + let liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "polite"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("polite") + .setAttribute("aria-atomic", "true"); + content.document + .getElementById("polite") + .setAttribute("aria-live", "polite"); + }); + await liveRegionAdded; + is( + politeRegion.getAttributeValue("AXARIALive"), + "polite", + "region is now live" + ); + ok(politeRegion.getAttributeValue("AXARIAAtomic"), "region is atomic"); + is( + politeRegion.getAttributeValue("AXARIARelevant"), + "removals", + "region has defined aria-relevant" + ); + + let assertiveRegion = getNativeInterface(accDoc, "assertive"); + is( + assertiveRegion.getAttributeValue("AXARIALive"), + "assertive", + "region is assertive" + ); + ok( + !assertiveRegion.getAttributeValue("AXARIAAtomic"), + "region is not atomic" + ); + is( + assertiveRegion.getAttributeValue("AXARIARelevant"), + "additions text", + "region has default aria-relevant" + ); + + let liveRegionRemoved = waitForEvent( + EVENT_LIVE_REGION_REMOVED, + "assertive" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("assertive").removeAttribute("aria-live"); + }); + await liveRegionRemoved; + ok(!assertiveRegion.getAttributeValue("AXARIALive"), "region is not live"); + + liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "new-region"); + await SpecialPowers.spawn(browser, [], () => { + let newRegionElm = content.document.createElement("div"); + newRegionElm.id = "new-region"; + newRegionElm.setAttribute("aria-live", "assertive"); + content.document.body.appendChild(newRegionElm); + }); + await liveRegionAdded; + + let newRegion = getNativeInterface(accDoc, "new-region"); + is( + newRegion.getAttributeValue("AXARIALive"), + "assertive", + "region is assertive" + ); + + let loadComplete = Promise.all([ + waitForMacEvent("AXLoadComplete"), + waitForMacEvent("AXLiveRegionCreated", "region-1"), + waitForMacEvent("AXLiveRegionCreated", "region-2"), + waitForMacEvent("AXLiveRegionCreated", "status"), + waitForMacEvent("AXLiveRegionCreated", "output"), + ]); + + await SpecialPowers.spawn(browser, [], () => { + content.location = `data:text/html;charset=utf-8, +
+
+
+ +
+ `; + }); + let webArea = (await loadComplete)[0]; + + is(webArea.getAttributeValue("AXRole"), "AXWebArea", "web area yeah"); + const searchPred = { + AXSearchKey: "AXLiveRegionSearchKey", + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + const liveRegions = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + Assert.deepEqual( + liveRegions.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["region-1", "region-2", "alert", "status", "output"], + "SearchPredicate returned all live regions" + ); + } +); + +/** + * Test live region changes + */ +addAccessibleTask( + ` +
+ The time is 4:55pm + + +
+ `, + async (browser, accDoc) => { + let liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("time").textContent = "4:56pm"; + }); + await liveRegionChanged; + ok(true, "changed textContent"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("p").style.display = "block"; + }); + await liveRegionChanged; + ok(true, "changed display style to block"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("p").style.display = "none"; + }); + await liveRegionChanged; + ok(true, "changed display style to none"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("button") + .setAttribute("aria-label", "Stop"); + }); + await liveRegionChanged; + ok(true, "changed aria-label"); + } +); diff --git a/accessible/tests/browser/mac/browser_mathml.js b/accessible/tests/browser/mac/browser_mathml.js new file mode 100644 index 0000000000..1afaa8399f --- /dev/null +++ b/accessible/tests/browser/mac/browser_mathml.js @@ -0,0 +1,151 @@ +/* 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 testMathAttr(iface, attr, subrole, textLeafValue) { + ok(iface.attributeNames.includes(attr), `Object has ${attr} attribute`); + let value = iface.getAttributeValue(attr); + is( + value.getAttributeValue("AXSubrole"), + subrole, + `${attr} value has correct subrole` + ); + + if (textLeafValue) { + let children = value.getAttributeValue("AXChildren"); + is(children.length, 1, `${attr} value has one child`); + + is( + children[0].getAttributeValue("AXRole"), + "AXStaticText", + `${attr} value's child is static text` + ); + is( + children[0].getAttributeValue("AXValue"), + textLeafValue, + `${attr} value has correct text` + ); + } +} + +addAccessibleTask( + ` + + -1 + + `, + async (browser, accDoc) => { + let math = getNativeInterface(accDoc, "math"); + is( + math.getAttributeValue("AXSubrole"), + "AXDocumentMath", + "Math element has correct subrole" + ); + + let sqrt = getNativeInterface(accDoc, "sqrt"); + is( + sqrt.getAttributeValue("AXSubrole"), + "AXMathSquareRoot", + "msqrt has correct subrole" + ); + + testMathAttr(sqrt, "AXMathRootRadicand", "AXMathIdentifier", "-1"); + } +); + +addAccessibleTask( + ` + + x + 3 + + `, + async (browser, accDoc) => { + let root = getNativeInterface(accDoc, "root"); + is( + root.getAttributeValue("AXSubrole"), + "AXMathRoot", + "mroot has correct subrole" + ); + + testMathAttr(root, "AXMathRootRadicand", "AXMathIdentifier", "x"); + testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3"); + } +); + +addAccessibleTask( + ` + + a + b + + `, + async (browser, accDoc) => { + let fraction = getNativeInterface(accDoc, "fraction"); + is( + fraction.getAttributeValue("AXSubrole"), + "AXMathFraction", + "mfrac has correct subrole" + ); + ok(fraction.attributeNames.includes("AXMathFractionNumerator")); + ok(fraction.attributeNames.includes("AXMathFractionDenominator")); + ok(fraction.attributeNames.includes("AXMathLineThickness")); + + // Bug 1639745 + todo_is(fraction.getAttributeValue("AXMathLineThickness"), 1); + + testMathAttr(fraction, "AXMathFractionNumerator", "AXMathIdentifier", "a"); + testMathAttr( + fraction, + "AXMathFractionDenominator", + "AXMathIdentifier", + "b" + ); + } +); + +addAccessibleTask( + ` + + + 0 + 1 + + `, + async (browser, accDoc) => { + let subsup = getNativeInterface(accDoc, "subsup"); + is( + subsup.getAttributeValue("AXSubrole"), + "AXMathSubscriptSuperscript", + "msubsup has correct subrole" + ); + + testMathAttr(subsup, "AXMathSubscript", "AXMathNumber", "0"); + testMathAttr(subsup, "AXMathSuperscript", "AXMathNumber", "1"); + testMathAttr(subsup, "AXMathBase", "AXMathOperator", "∫"); + } +); + +addAccessibleTask( + ` + + + 0 + + + `, + async (browser, accDoc) => { + let underover = getNativeInterface(accDoc, "underover"); + is( + underover.getAttributeValue("AXSubrole"), + "AXMathUnderOver", + "munderover has correct subrole" + ); + + testMathAttr(underover, "AXMathUnder", "AXMathNumber", "0"); + testMathAttr(underover, "AXMathOver", "AXMathIdentifier", "∞"); + testMathAttr(underover, "AXMathBase", "AXMathOperator", "∫"); + } +); diff --git a/accessible/tests/browser/mac/browser_menulist.js b/accessible/tests/browser/mac/browser_menulist.js new file mode 100644 index 0000000000..b26a0be782 --- /dev/null +++ b/accessible/tests/browser/mac/browser_menulist.js @@ -0,0 +1,103 @@ +/* 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 */ +/* 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 }, + { name: "attributes.js", dir: MOCHITESTS_DIR } +); + +addAccessibleTask( + "mac/doc_menulist.xhtml", + async (browser, accDoc) => { + const menulist = getNativeInterface(accDoc, "defaultZoom"); + + let actions = menulist.actionNames; + ok(actions.includes("AXPress"), "menu has press action"); + + let event = waitForMacEvent("AXMenuOpened"); + menulist.performAction("AXPress"); + const menupopup = await event; + + const menuItems = menupopup.getAttributeValue("AXChildren"); + is(menuItems.length, 4, "Found four children in menulist"); + is( + menuItems[0].getAttributeValue("AXTitle"), + "50%", + "First item has correct title" + ); + is( + menuItems[1].getAttributeValue("AXTitle"), + "100%", + "Second item has correct title" + ); + is( + menuItems[2].getAttributeValue("AXTitle"), + "150%", + "Third item has correct title" + ); + is( + menuItems[3].getAttributeValue("AXTitle"), + "200%", + "Fourth item has correct title" + ); + }, + { topLevel: false, chrome: true } +); + +addAccessibleTask( + "mac/doc_menulist.xhtml", + async (browser, accDoc) => { + const menulist = getNativeInterface(accDoc, "defaultZoom"); + + const actions = menulist.actionNames; + ok(actions.includes("AXPress"), "menu has press action"); + let event = waitForMacEvent("AXMenuOpened"); + menulist.performAction("AXPress"); + await event; + + const menu = menulist.getAttributeValue("AXChildren")[0]; + ok(menu, "Menulist contains menu"); + const children = menu.getAttributeValue("AXChildren"); + is(children.length, 4, "Menu has 4 items"); + + // Menu is open, initial focus should land on the first item + is( + children[0].getAttributeValue("AXSelected"), + 1, + "First menu item is selected" + ); + // focus the second item, and verify it is selected + event = waitForMacEvent("AXFocusedUIElementChanged", (iface, data) => { + try { + return iface.getAttributeValue("AXTitle") == "100%"; + } catch (e) { + return false; + } + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await event; + + is( + children[0].getAttributeValue("AXSelected"), + 0, + "First menu item is no longer selected" + ); + is( + children[1].getAttributeValue("AXSelected"), + 1, + "Second menu item is selected" + ); + // press the second item, check for selected event + event = waitForMacEvent("AXMenuItemSelected"); + children[1].performAction("AXPress"); + await event; + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_navigate.js b/accessible/tests/browser/mac/browser_navigate.js new file mode 100644 index 0000000000..69486676e4 --- /dev/null +++ b/accessible/tests/browser/mac/browser_navigate.js @@ -0,0 +1,394 @@ +/* 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 navigation of same/different type content + */ +addAccessibleTask( + `

hello

+ world
+ I am a link +

goodbye

`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXSameTypeSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + const hello = getNativeInterface(accDoc, "hello"); + const goodbye = getNativeInterface(accDoc, "goodbye"); + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + searchPred.AXStartElement = hello; + + let sameItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(sameItem.length, 1, "Found one item"); + is( + "goodbye", + sameItem[0].getAttributeValue("AXTitle"), + "Found correct item of same type" + ); + + searchPred.AXDirection = "AXDirectionPrevious"; + searchPred.AXStartElement = goodbye; + sameItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(sameItem.length, 1, "Found one item"); + is( + "hello", + sameItem[0].getAttributeValue("AXTitle"), + "Found correct item of same type" + ); + + searchPred.AXSearchKey = "AXDifferentTypeSearchKey"; + let diffItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(diffItem.length, 1, "Found one item"); + is( + "I am a link", + diffItem[0].getAttributeValue("AXValue"), + "Found correct item of different type" + ); + } +); + +/** + * Test navigation of heading levels + */ +addAccessibleTask( + ` +

a

+

b

+

c

+

d

+
e
+
f
+

g

+

h

+

i

+

j

+
k
+
l
+ this is some regular text that should be ignored + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingLevel1SearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let h1Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h1Count, "Found two h1 items"); + + let h1s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const a = getNativeInterface(accDoc, "a"); + const g = getNativeInterface(accDoc, "g"); + + is( + a.getAttributeValue("AXValue"), + h1s[0].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + is( + g.getAttributeValue("AXValue"), + h1s[1].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel2SearchKey"; + + let h2Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h2Count, "Found two h2 items"); + + let h2s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const b = getNativeInterface(accDoc, "b"); + const h = getNativeInterface(accDoc, "h"); + + is( + b.getAttributeValue("AXValue"), + h2s[0].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + is( + h.getAttributeValue("AXValue"), + h2s[1].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel3SearchKey"; + + let h3Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h3Count, "Found two h3 items"); + + let h3s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const c = getNativeInterface(accDoc, "c"); + const i = getNativeInterface(accDoc, "i"); + + is( + c.getAttributeValue("AXValue"), + h3s[0].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + is( + i.getAttributeValue("AXValue"), + h3s[1].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel4SearchKey"; + + let h4Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h4Count, "Found two h4 items"); + + let h4s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const d = getNativeInterface(accDoc, "d"); + const j = getNativeInterface(accDoc, "j"); + + is( + d.getAttributeValue("AXValue"), + h4s[0].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + is( + j.getAttributeValue("AXValue"), + h4s[1].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel5SearchKey"; + + let h5Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h5Count, "Found two h5 items"); + + let h5s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const e = getNativeInterface(accDoc, "e"); + const k = getNativeInterface(accDoc, "k"); + + is( + e.getAttributeValue("AXValue"), + h5s[0].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + is( + k.getAttributeValue("AXValue"), + h5s[1].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel6SearchKey"; + + let h6Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h6Count, "Found two h6 items"); + + let h6s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const f = getNativeInterface(accDoc, "f"); + const l = getNativeInterface(accDoc, "l"); + + is( + f.getAttributeValue("AXValue"), + h6s[0].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + + is( + l.getAttributeValue("AXValue"), + h6s[1].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + } +); + +/* + * Test rotor with blockquotes + */ +addAccessibleTask( + ` +
hello I am a blockquote
+
+ I am also a blockquote of the same level +
+
but I have a different level
+
+ `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXBlockquoteSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let bquotes = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(bquotes.length, 3, "Found three blockquotes"); + + const first = getNativeInterface(accDoc, "first"); + const second = getNativeInterface(accDoc, "second"); + const third = getNativeInterface(accDoc, "third"); + console.log("values :"); + console.log(first.getAttributeValue("AXValue")); + is( + first.getAttributeValue("AXValue"), + bquotes[0].getAttributeValue("AXValue"), + "Found correct first blockquote" + ); + + is( + second.getAttributeValue("AXValue"), + bquotes[1].getAttributeValue("AXValue"), + "Found correct second blockquote" + ); + + is( + third.getAttributeValue("AXValue"), + bquotes[2].getAttributeValue("AXValue"), + "Found correct third blockquote" + ); + } +); + +/* + * Test rotor with graphics + */ +addAccessibleTask( + ` + image one
+ + image two + + + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXGraphicSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let images = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(images.length, 3, "Found three images"); + + const img1 = getNativeInterface(accDoc, "img1"); + const img2 = getNativeInterface(accDoc, "img2"); + const img3 = getNativeInterface(accDoc, "img3"); + + is( + img1.getAttributeValue("AXDescription"), + images[0].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img2.getAttributeValue("AXDescription"), + images[1].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img3.getAttributeValue("AXDescription"), + images[2].getAttributeValue("AXDescription"), + "Found correct image" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_outline.js b/accessible/tests/browser/mac/browser_outline.js new file mode 100644 index 0000000000..ba211fdf4b --- /dev/null +++ b/accessible/tests/browser/mac/browser_outline.js @@ -0,0 +1,566 @@ +/* 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 outline, outline rows with computed properties + */ +addAccessibleTask( + ` +

+ Foods +

+ + `, + async (browser, accDoc) => { + const outline = getNativeInterface(accDoc, "outline"); + is( + outline.getAttributeValue("AXRole"), + "AXOutline", + "Correct role for outline" + ); + + const outChildren = outline.getAttributeValue("AXChildren"); + is(outChildren.length, 2, "Outline has two direct children"); + is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = outline.getAttributeValue("AXRows"); + is(outRows.length, 4, "Outline has four rows"); + is( + outRows[0].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[0].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children, only group" + ); + is( + outRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[1].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[1].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of group" + ); + is( + outRows[1].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[1].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is( + outRows[2].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[2].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[2].getAttributeValue("AXDisclosedRows").length, + 1, + "Row has one row child" + ); + is( + outRows[2].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[3].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[3] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[2].getAttributeValue("AXDescription"), + "Row is direct child of row[2]" + ); + is( + outRows[3].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[3].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + let evt = waitForMacEvent("AXRowExpanded", "vegetables"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("vegetables") + .setAttribute("aria-expanded", "true"); + }); + await evt; + is( + outRows[2].getAttributeValue("AXDisclosing"), + 1, + "Row is disclosing after being expanded" + ); + + evt = waitForMacEvent("AXRowCollapsed", "vegetables"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("vegetables") + .setAttribute("aria-expanded", "false"); + }); + await evt; + is( + outRows[2].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing after being collapsed again" + ); + } +); + +/** + * Test outline, outline rows with declared properties + */ +addAccessibleTask( + ` +

+ Foods +

+ + `, + async (browser, accDoc) => { + const outline = getNativeInterface(accDoc, "outline"); + is( + outline.getAttributeValue("AXRole"), + "AXOutline", + "Correct role for outline" + ); + + const outChildren = outline.getAttributeValue("AXChildren"); + is(outChildren.length, 2, "Outline has two direct children"); + is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = outline.getAttributeValue("AXRows"); + is(outRows.length, 9, "Outline has nine rows"); + is( + outRows[0].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[0].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no direct row children, has list" + ); + is( + outRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[2].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[2].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of group" + ); + is( + outRows[2].getAttributeValue("AXDisclosedRows").length, + 2, + "Row has two row children" + ); + is( + outRows[2].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + is( + outRows[3].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[3] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[2].getAttributeValue("AXDescription"), + "Row is direct child of row 2" + ); + + is( + outRows[3].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[3].getAttributeValue("AXDisclosureLevel"), + 2, + "Row is level two" + ); + + is( + outRows[5].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[5].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[5].getAttributeValue("AXDisclosedRows").length, + 1, + "Row has no one row child" + ); + is( + outRows[5].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[6].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[6] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[5].getAttributeValue("AXDescription"), + "Row is direct child of row 5" + ); + is( + outRows[6].getAttributeValue("AXDisclosedRows").length, + 2, + "Row has two row children" + ); + is( + outRows[6].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + is( + outRows[7].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[7] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[6].getAttributeValue("AXDescription"), + "Row is direct child of row 6" + ); + is( + outRows[7].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[7].getAttributeValue("AXDisclosureLevel"), + 2, + "Row is level two" + ); + } +); + +// Test outline that isn't built with li/uls gets correct desc +addAccessibleTask( + ` +
+ + +
+ `, + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + is(tree.getAttributeValue("AXRole"), "AXOutline", "Correct role for tree"); + + const treeItems = tree.getAttributeValue("AXChildren"); + is(treeItems.length, 2, "Outline has two direct children"); + is(treeItems[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(treeItems[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = tree.getAttributeValue("AXRows"); + is(outRows.length, 2, "Outline has two rows"); + + is( + outRows[0].getAttributeValue("AXDescription"), + "My files", + "files labelled correctly" + ); + is( + outRows[1].getAttributeValue("AXDescription"), + "Shared items", + "shared items labelled correctly" + ); + } +); + +// Test outline registers AXDisclosed attr as settable +addAccessibleTask( + ` +
+ +
Shared items
+
+ `, + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + const treeItems = tree.getAttributeValue("AXChildren"); + + is(treeItems.length, 2, "Outline has two direct children"); + is(treeItems[0].getAttributeValue("AXDisclosing"), 0); + is(treeItems[1].getAttributeValue("AXDisclosing"), 1); + + is(treeItems[0].isAttributeSettable("AXDisclosing"), true); + is(treeItems[1].isAttributeSettable("AXDisclosing"), true); + + // attempt to change attribute values + treeItems[0].setAttributeValue("AXDisclosing", 1); + treeItems[0].setAttributeValue("AXDisclosing", 0); + + // verify they're unchanged + is(treeItems[0].getAttributeValue("AXDisclosing"), 0); + is(treeItems[1].getAttributeValue("AXDisclosing"), 1); + } +); + +// Test outline rows correctly expose checkable, checked/unchecked/mixed status +addAccessibleTask( + ` +
+
+ Leaf 1 +
+
+ Leaf 2 +
+
+ Leaf 3 +
+
+ Leaf 4 +
+
+ + `, + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + const treeItems = tree.getAttributeValue("AXChildren"); + + is(treeItems.length, 4, "Outline has four direct children"); + is( + treeItems[0].getAttributeValue("AXValue"), + 0, + "Child one is not checked" + ); + is(treeItems[1].getAttributeValue("AXValue"), 1, "Child two is checked"); + is( + treeItems[2].getAttributeValue("AXValue"), + null, + "Child three is not checkable and has no val" + ); + is(treeItems[3].getAttributeValue("AXValue"), 2, "Child four is mixed"); + + let stateChanged = Promise.all([ + waitForMacEvent("AXValueChanged", "l1"), + waitForStateChange("l1", STATE_CHECKED, true), + ]); + // We should get a state change event for checked. + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("l1") + .setAttribute("aria-checked", "true"); + }); + await stateChanged; + is(treeItems[0].getAttributeValue("AXValue"), 1, "Child one is checked"); + + stateChanged = Promise.all([ + waitForMacEvent("AXValueChanged", "l2"), + waitForMacEvent("AXValueChanged", "l2"), + waitForStateChange("l2", STATE_CHECKED, false), + waitForStateChange("l2", STATE_CHECKABLE, false), + ]); + // We should get a state change event for both checked and checkable, + // and value changes for both. + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("l2").removeAttribute("aria-checked"); + }); + await stateChanged; + is( + treeItems[1].getAttributeValue("AXValue"), + null, + "Child two is not checkable and has no val" + ); + + stateChanged = Promise.all([ + waitForMacEvent("AXValueChanged", "l3"), + waitForMacEvent("AXValueChanged", "l3"), + waitForStateChange("l3", STATE_CHECKED, true), + waitForStateChange("l3", STATE_CHECKABLE, true), + ]); + // We should get a state change event for both checked and checkable, + // and value changes for each. + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("l3") + .setAttribute("aria-checked", "true"); + }); + await stateChanged; + is(treeItems[2].getAttributeValue("AXValue"), 1, "Child three is checked"); + + stateChanged = Promise.all([ + waitForMacEvent("AXValueChanged", "l4"), + waitForMacEvent("AXValueChanged", "l4"), + waitForStateChange("l4", STATE_MIXED, false), + waitForStateChange("l4", STATE_CHECKABLE, false), + ]); + // We should get a state change event for both mixed and checkable, + // and value changes for each. + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("l4").removeAttribute("aria-checked"); + }); + await stateChanged; + is( + treeItems[3].getAttributeValue("AXValue"), + null, + "Child four is not checkable and has no value" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_outline_xul.js b/accessible/tests/browser/mac/browser_outline_xul.js new file mode 100644 index 0000000000..66eebebf50 --- /dev/null +++ b/accessible/tests/browser/mac/browser_outline_xul.js @@ -0,0 +1,274 @@ +/* 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( + "mac/doc_tree.xhtml", + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + is( + tree.getAttributeValue("AXRole"), + "AXOutline", + "Found tree with role outline" + ); + // XUL trees store all rows as direct children of the outline, + // so we should see nine here instead of just three: + // (Groceries, Fruits, Veggies) + const treeChildren = tree.getAttributeValue("AXChildren"); + is(treeChildren.length, 9, "Found nine direct children"); + + const treeCols = tree.getAttributeValue("AXColumns"); + is(treeCols.length, 1, "Found one column in tree"); + + // Here, we should get only outline rows, not the title + const treeRows = tree.getAttributeValue("AXRows"); + is(treeRows.length, 8, "Found 8 total rows"); + + is( + treeRows[0].getAttributeValue("AXDescription"), + "Fruits", + "Located correct first row, row has correct desc" + ); + is( + treeRows[0].getAttributeValue("AXDisclosing"), + 1, + "Fruits is disclosing" + ); + is( + treeRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Fruits is disclosed by outline" + ); + is( + treeRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Fruits is level zero" + ); + let disclosedRows = treeRows[0].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Fruits discloses two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Apple", + "fruits discloses apple" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Orange", + "fruits discloses orange" + ); + + is( + treeRows[1].getAttributeValue("AXDescription"), + "Apple", + "Located correct second row, row has correct desc" + ); + is( + treeRows[1].getAttributeValue("AXDisclosing"), + 0, + "Apple is not disclosing" + ); + is( + treeRows[1] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Fruits", + "Apple is disclosed by fruits" + ); + is( + treeRows[1].getAttributeValue("AXDisclosureLevel"), + 1, + "Apple is level one" + ); + is( + treeRows[1].getAttributeValue("AXDisclosedRows").length, + 0, + "Apple does not disclose rows" + ); + + is( + treeRows[2].getAttributeValue("AXDescription"), + "Orange", + "Located correct third row, row has correct desc" + ); + is( + treeRows[2].getAttributeValue("AXDisclosing"), + 0, + "Orange is not disclosing" + ); + is( + treeRows[2] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Fruits", + "Orange is disclosed by fruits" + ); + is( + treeRows[2].getAttributeValue("AXDisclosureLevel"), + 1, + "Orange is level one" + ); + is( + treeRows[2].getAttributeValue("AXDisclosedRows").length, + 0, + "Orange does not disclose rows" + ); + + is( + treeRows[3].getAttributeValue("AXDescription"), + "Veggies", + "Located correct fourth row, row has correct desc" + ); + is( + treeRows[3].getAttributeValue("AXDisclosing"), + 1, + "Veggies is disclosing" + ); + is( + treeRows[3].getAttributeValue("AXDisclosedByRow"), + null, + "Veggies is disclosed by outline" + ); + is( + treeRows[3].getAttributeValue("AXDisclosureLevel"), + 0, + "Veggies is level zero" + ); + disclosedRows = treeRows[3].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Veggies discloses two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Green Veggies", + "Veggies discloses green veggies" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Squash", + "Veggies discloses squash" + ); + + is( + treeRows[4].getAttributeValue("AXDescription"), + "Green Veggies", + "Located correct fifth row, row has correct desc" + ); + is( + treeRows[4].getAttributeValue("AXDisclosing"), + 1, + "Green veggies is disclosing" + ); + is( + treeRows[4] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Veggies", + "Green Veggies is disclosed by veggies" + ); + is( + treeRows[4].getAttributeValue("AXDisclosureLevel"), + 1, + "Green veggies is level one" + ); + disclosedRows = treeRows[4].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Green veggies has two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Spinach", + "Green veggies discloses spinach" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Peas", + "Green veggies discloses peas" + ); + + is( + treeRows[5].getAttributeValue("AXDescription"), + "Spinach", + "Located correct sixth row, row has correct desc" + ); + is( + treeRows[5].getAttributeValue("AXDisclosing"), + 0, + "Spinach is not disclosing" + ); + is( + treeRows[5] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Green Veggies", + "Spinach is disclosed by green veggies" + ); + is( + treeRows[5].getAttributeValue("AXDisclosureLevel"), + 2, + "Spinach is level two" + ); + is( + treeRows[5].getAttributeValue("AXDisclosedRows").length, + 0, + "Spinach does not disclose rows" + ); + + is( + treeRows[6].getAttributeValue("AXDescription"), + "Peas", + "Located correct seventh row, row has correct desc" + ); + is( + treeRows[6].getAttributeValue("AXDisclosing"), + 0, + "Peas is not disclosing" + ); + is( + treeRows[6] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Green Veggies", + "Peas is disclosed by green veggies" + ); + is( + treeRows[6].getAttributeValue("AXDisclosureLevel"), + 2, + "Peas is level two" + ); + is( + treeRows[6].getAttributeValue("AXDisclosedRows").length, + 0, + "Peas does not disclose rows" + ); + + is( + treeRows[7].getAttributeValue("AXDescription"), + "Squash", + "Located correct eighth row, row has correct desc" + ); + is( + treeRows[7].getAttributeValue("AXDisclosing"), + 0, + "Squash is not disclosing" + ); + is( + treeRows[7] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Veggies", + "Squash is disclosed by veggies" + ); + is( + treeRows[7].getAttributeValue("AXDisclosureLevel"), + 1, + "Squash is level one" + ); + is( + treeRows[7].getAttributeValue("AXDisclosedRows").length, + 0, + "Squash does not disclose rows" + ); + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_popupbutton.js b/accessible/tests/browser/mac/browser_popupbutton.js new file mode 100644 index 0000000000..2d5ff1ac35 --- /dev/null +++ b/accessible/tests/browser/mac/browser_popupbutton.js @@ -0,0 +1,166 @@ +/* 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 dropdown select element +addAccessibleTask( + ``, + async (browser, accDoc) => { + // Test combobox + let select = getNativeInterface(accDoc, "select"); + is( + select.getAttributeValue("AXRole"), + "AXPopUpButton", + "select has AXPopupButton role" + ); + ok(select.attributeNames.includes("AXValue"), "select advertises AXValue"); + is( + select.getAttributeValue("AXValue"), + "One", + "select has correctt initial value" + ); + ok( + !select.attributeNames.includes("AXHasPopup"), + "select does not advertise AXHasPopup" + ); + is( + select.getAttributeValue("AXHasPopup"), + null, + "select does not provide value for AXHasPopup" + ); + + ok(select.actionNames.includes("AXPress"), "Selectt has press action"); + // These four events happen in quick succession when select is pressed + let events = Promise.all([ + waitForMacEvent("AXMenuOpened"), + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent( + "AXFocusedUIElementChanged", + e => e.getAttributeValue("AXRole") == "AXPopUpButton" + ), + waitForMacEvent( + "AXFocusedUIElementChanged", + e => e.getAttributeValue("AXRole") == "AXMenuItem" + ), + ]); + select.performAction("AXPress"); + // Only capture the target of AXMenuOpened (first element) + let [menu] = await events; + + is(menu.getAttributeValue("AXRole"), "AXMenu", "dropdown has AXMenu role"); + is( + menu.getAttributeValue("AXSelectedChildren").length, + 1, + "dropdown has single selected child" + ); + + let selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is(selectedChildren[0].getAttributeValue("AXRole"), "AXMenuItem"); + is(selectedChildren[0].getAttributeValue("AXTitle"), "One"); + + let menuParent = menu.getAttributeValue("AXParent"); + is( + menuParent.getAttributeValue("AXRole"), + "AXPopUpButton", + "dropdown parent is a popup button" + ); + + let menuItems = menu.getAttributeValue("AXChildren").map(c => { + return [ + c.getAttributeValue("AXMenuItemMarkChar"), + c.getAttributeValue("AXRole"), + c.getAttributeValue("AXTitle"), + c.getAttributeValue("AXEnabled"), + ]; + }); + + Assert.deepEqual( + menuItems, + [ + ["✓", "AXMenuItem", "One", true], + [null, "AXMenuItem", "Two", true], + [null, "AXMenuItem", "Three", true], + [null, "AXMenuItem", "Four", false], + ], + "Menu items have correct checkmark on current value, correctt roles, correct titles, and correct AXEnabled value" + ); + + events = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let [, menuItem] = await events; + is( + menuItem.getAttributeValue("AXTitle"), + "Two", + "Focused menu item has correct title" + ); + + selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is( + selectedChildren[0].getAttributeValue("AXTitle"), + "Two", + "Selected child matches focused item" + ); + + events = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + EventUtils.synthesizeKey("KEY_ArrowDown"); + [, menuItem] = await events; + is( + menuItem.getAttributeValue("AXTitle"), + "Three", + "Focused menu item has correct title" + ); + + selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is( + selectedChildren[0].getAttributeValue("AXTitle"), + "Three", + "Selected child matches focused item" + ); + + events = Promise.all([ + waitForMacEvent("AXMenuClosed"), + waitForMacEvent("AXFocusedUIElementChanged"), + waitForMacEvent("AXSelectedChildrenChanged"), + ]); + menuItem.performAction("AXPress"); + let [, newFocus] = await events; + is( + newFocus.getAttributeValue("AXRole"), + "AXPopUpButton", + "Newly focused element is AXPopupButton" + ); + is( + newFocus.getAttributeValue("AXDOMIdentifier"), + "select", + "Should return focus to select" + ); + is( + newFocus.getAttributeValue("AXValue"), + "Three", + "select has correct new value" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_radio_position.js b/accessible/tests/browser/mac/browser_radio_position.js new file mode 100644 index 0000000000..76f518a91e --- /dev/null +++ b/accessible/tests/browser/mac/browser_radio_position.js @@ -0,0 +1,321 @@ +/* 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 getChildRoles(parent) { + return parent + .getAttributeValue("AXChildren") + .map(c => c.getAttributeValue("AXRole")); +} + +function getLinkedTitles(element) { + return element + .getAttributeValue("AXLinkedUIElements") + .map(c => c.getAttributeValue("AXTitle")); +} + +/** + * Test radio group + */ +addAccessibleTask( + `
+ + + +
`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let item2 = getNativeInterface(accDoc, "radioGroupItem2"); + let item3 = getNativeInterface(accDoc, "radioGroupItem3"); + let titleList = ["Regular crust", "Deep dish", "Thin crust"]; + + Assert.deepEqual( + titleList, + [item1, item2, item3].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 1 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 2 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + + linkedElems = item3.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 3 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item3), + titleList, + "Item three has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add to a radio group + */ +addAccessibleTask( + `
+ +
`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + + is(linkedElems.length, 1, "Item 1 has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + item1.getAttributeValue("AXTitle"), + "Item 1 is first element" + ); + + let reorder = waitForEvent(EVENT_REORDER, "radioGroup"); + await SpecialPowers.spawn(browser, [], () => { + let d = content.document.createElement("div"); + d.setAttribute("role", "radio"); + content.document.getElementById("radioGroup").appendChild(d); + }); + await reorder; + + let radioGroup = getNativeInterface(accDoc, "radioGroup"); + let groupMembers = radioGroup.getAttributeValue("AXChildren"); + is(groupMembers.length, 2, "Radio group has two members"); + let item2 = groupMembers[1]; + item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let titleList = ["Option One", ""]; + + Assert.deepEqual( + titleList, + [item1, item2].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 1 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 2 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for single group + */ +addAccessibleTask( + ` + + `, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "Cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "Dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for different groups + */ +addAccessibleTask( + ` + + `, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is only element" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Dog has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + dog.getAttributeValue("AXTitle"), + "Dog is only element" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 0, "Catdog has no linked UI elem"); + } +); + +/** + * Test input[type=radio] for single group across DOM + */ +addAccessibleTask( + ` +
+ + + +
+
+ +
`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add of input[type=radio] in a single group + */ +addAccessibleTask( + `
`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let container = getNativeInterface(accDoc, "container"); + + let containerChildren = container.getAttributeValue("AXChildren"); + is(containerChildren.length, 1, "container has one button"); + is( + containerChildren[0].getAttributeValue("AXRole"), + "AXRadioButton", + "Container child is radio button" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has 1 linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is first element" + ); + let reorder = waitForEvent(EVENT_REORDER, "container"); + await SpecialPowers.spawn(browser, [], () => { + let input = content.document.createElement("input"); + input.setAttribute("type", "radio"); + input.setAttribute("name", "animal"); + content.document.getElementById("container").appendChild(input); + }); + await reorder; + + container = getNativeInterface(accDoc, "container"); + containerChildren = container.getAttributeValue("AXChildren"); + + is(containerChildren.length, 2, "container has two children"); + + Assert.deepEqual( + getChildRoles(container), + ["AXRadioButton", "AXRadioButton"], + "Both children are radio buttons" + ); + + linkedElems = containerChildren[0].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Cat has 2 linked elements"); + + linkedElems = containerChildren[1].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "New button has 2 linked elements"); + } +); diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js new file mode 100644 index 0000000000..430e41d6ea --- /dev/null +++ b/accessible/tests/browser/mac/browser_range.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 */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Verify that the value of a slider input can be incremented/decremented + * Test input[type=range] + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let range = getNativeInterface(accDoc, "range"); + is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role"); + is(range.getAttributeValue("AXValue"), 1, "Correct initial value"); + + let actions = range.actionNames; + ok(actions.includes("AXDecrement"), "Has decrement action"); + ok(actions.includes("AXIncrement"), "Has increment action"); + + let evt = waitForMacEvent("AXValueChanged"); + range.performAction("AXIncrement"); + await evt; + is(range.getAttributeValue("AXValue"), 11, "Correct increment value"); + + evt = waitForMacEvent("AXValueChanged"); + range.performAction("AXDecrement"); + await evt; + is(range.getAttributeValue("AXValue"), 1, "Correct decrement value"); + + evt = waitForMacEvent("AXValueChanged"); + // Adjust value via script in content + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("range").value = 41; + }); + await evt; + is( + range.getAttributeValue("AXValue"), + 41, + "Correct value from content change" + ); + } +); + +/** + * Verify that the value of a slider input can be set directly + * Test input[type=range] + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let nextValue = 21; + let range = getNativeInterface(accDoc, "range"); + is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role"); + is(range.getAttributeValue("AXValue"), 1, "Correct initial value"); + + ok(range.isAttributeSettable("AXValue"), "Range AXValue is settable."); + + let evt = waitForMacEvent("AXValueChanged"); + range.setAttributeValue("AXValue", nextValue); + await evt; + is(range.getAttributeValue("AXValue"), nextValue, "Correct updated value"); + } +); + +/** + * Verify that the value of a number input can be incremented/decremented + * Test input[type=number] + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let number = getNativeInterface(accDoc, "number"); + is( + number.getAttributeValue("AXRole"), + "AXIncrementor", + "Correct AXIncrementor role" + ); + is(number.getAttributeValue("AXValue"), 11, "Correct initial value"); + + let actions = number.actionNames; + ok(actions.includes("AXDecrement"), "Has decrement action"); + ok(actions.includes("AXIncrement"), "Has increment action"); + + let evt = waitForMacEvent("AXValueChanged"); + number.performAction("AXIncrement"); + await evt; + is(number.getAttributeValue("AXValue"), 11.05, "Correct increment value"); + + evt = waitForMacEvent("AXValueChanged"); + number.performAction("AXDecrement"); + await evt; + is(number.getAttributeValue("AXValue"), 11, "Correct decrement value"); + + evt = waitForMacEvent("AXValueChanged"); + // Adjust value via script in content + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("number").value = 42; + }); + await evt; + is( + number.getAttributeValue("AXValue"), + 42, + "Correct value from content change" + ); + } +); + +/** + * Test Min, Max, Orientation, ValueDescription + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let nextValue = 21; + let number = getNativeInterface(accDoc, "number"); + is( + number.getAttributeValue("AXRole"), + "AXIncrementor", + "Correct AXIncrementor role" + ); + is(number.getAttributeValue("AXValue"), 11, "Correct initial value"); + + ok(number.isAttributeSettable("AXValue"), "Range AXValue is settable."); + + let evt = waitForMacEvent("AXValueChanged"); + number.setAttributeValue("AXValue", nextValue); + await evt; + is(number.getAttributeValue("AXValue"), nextValue, "Correct updated value"); + } +); + +/** + * Verify that the value of a number input can be set directly + * Test input[type=number] + */ +addAccessibleTask( + `
`, + async (browser, accDoc) => { + let slider = getNativeInterface(accDoc, "slider"); + is( + slider.getAttributeValue("AXValueDescription"), + "High", + "Correct value description" + ); + is( + slider.getAttributeValue("AXOrientation"), + "AXHorizontalOrientation", + "Correct orientation" + ); + is(slider.getAttributeValue("AXMinValue"), 0, "Correct min value"); + is(slider.getAttributeValue("AXMaxValue"), 3, "Correct max value"); + + let evt = waitForMacEvent("AXValueChanged"); + await invokeContentTask(browser, [], () => { + const s = content.document.getElementById("slider"); + s.setAttribute("aria-valuetext", "Low"); + }); + await evt; + is( + slider.getAttributeValue("AXValueDescription"), + "Low", + "Correct value description" + ); + + evt = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "slider"); + await invokeContentTask(browser, [], () => { + const s = content.document.getElementById("slider"); + s.setAttribute("aria-orientation", "vertical"); + s.setAttribute("aria-valuemin", "-1"); + s.setAttribute("aria-valuemax", "5"); + }); + await evt; + is( + slider.getAttributeValue("AXOrientation"), + "AXVerticalOrientation", + "Correct orientation" + ); + is(slider.getAttributeValue("AXMinValue"), -1, "Correct min value"); + is(slider.getAttributeValue("AXMaxValue"), 5, "Correct max value"); + } +); diff --git a/accessible/tests/browser/mac/browser_required.js b/accessible/tests/browser/mac/browser_required.js new file mode 100644 index 0000000000..2109d265ab --- /dev/null +++ b/accessible/tests/browser/mac/browser_required.js @@ -0,0 +1,175 @@ +/* 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 required and aria-required attributes on checkboxes + * and radio buttons. + */ +addAccessibleTask( + ` +
+ +
+ +
+ +
+ +
+ `, + async (browser, accDoc) => { + // Check initial AXRequired values are correct + let radio = getNativeInterface(accDoc, "radio"); + is( + radio.getAttributeValue("AXRequired"), + 1, + "Correct required val for radio" + ); + + let ariaRadio = getNativeInterface(accDoc, "ariaRadio"); + is( + ariaRadio.getAttributeValue("AXRequired"), + 1, + "Correct required val for ariaRadio" + ); + + let checkbox = getNativeInterface(accDoc, "checkbox"); + is( + checkbox.getAttributeValue("AXRequired"), + 1, + "Correct required val for checkbox" + ); + + let ariaCheckbox = getNativeInterface(accDoc, "ariaCheckbox"); + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 1, + "Correct required val for ariaCheckbox" + ); + + // Change aria-required, verify AXRequired is updated + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaCheckbox") + .setAttribute("aria-required", "false"); + }); + await stateChanged; + + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 0, + "Correct required after false set for ariaCheckbox" + ); + + // Change aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaCheckbox") + .setAttribute("aria-required", "true"); + }); + await stateChanged; + + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 1, + "Correct required after true set for ariaCheckbox" + ); + + // Remove aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaCheckbox") + .removeAttribute("aria-required"); + }); + await stateChanged; + + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for ariaCheckbox" + ); + + // Change aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaRadio") + .setAttribute("aria-required", "false"); + }); + await stateChanged; + + is( + ariaRadio.getAttributeValue("AXRequired"), + 0, + "Correct required after false set for ariaRadio" + ); + + // Change aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaRadio") + .setAttribute("aria-required", "true"); + }); + await stateChanged; + + is( + ariaRadio.getAttributeValue("AXRequired"), + 1, + "Correct required after true set for ariaRadio" + ); + + // Remove aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaRadio") + .removeAttribute("aria-required"); + }); + await stateChanged; + + is( + ariaRadio.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for ariaRadio" + ); + + // Remove required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "checkbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("checkbox").removeAttribute("required"); + }); + await stateChanged; + + is( + checkbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for checkbox" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "radio"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("radio").removeAttribute("required"); + }); + await stateChanged; + + is( + checkbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for radio" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_rich_listbox.js b/accessible/tests/browser/mac/browser_rich_listbox.js new file mode 100644 index 0000000000..97dd6785bb --- /dev/null +++ b/accessible/tests/browser/mac/browser_rich_listbox.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/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "mac/doc_rich_listbox.xhtml", + async (browser, accDoc) => { + const categories = getNativeInterface(accDoc, "categories"); + const categoriesChildren = categories.getAttributeValue("AXChildren"); + is(categoriesChildren.length, 4, "Found listbox and 4 items"); + + const general = getNativeInterface(accDoc, "general"); + is( + general.getAttributeValue("AXTitle"), + "general", + "general has appropriate title" + ); + is( + categoriesChildren[0].getAttributeValue("AXTitle"), + general.getAttributeValue("AXTitle"), + "Found general listitem" + ); + is( + general.getAttributeValue("AXEnabled"), + 1, + "general is enabled, not dimmed" + ); + + const home = getNativeInterface(accDoc, "home"); + is(home.getAttributeValue("AXTitle"), "home", "home has appropriate title"); + is( + categoriesChildren[1].getAttributeValue("AXTitle"), + home.getAttributeValue("AXTitle"), + "Found home listitem" + ); + is(home.getAttributeValue("AXEnabled"), 1, "Home is enabled, not dimmed"); + + const search = getNativeInterface(accDoc, "search"); + is( + search.getAttributeValue("AXTitle"), + "search", + "search has appropriate title" + ); + is( + categoriesChildren[2].getAttributeValue("AXTitle"), + search.getAttributeValue("AXTitle"), + "Found search listitem" + ); + is( + search.getAttributeValue("AXEnabled"), + 1, + "search is enabled, not dimmed" + ); + + const privacy = getNativeInterface(accDoc, "privacy"); + is( + privacy.getAttributeValue("AXTitle"), + "privacy", + "privacy has appropriate title" + ); + is( + categoriesChildren[3].getAttributeValue("AXTitle"), + privacy.getAttributeValue("AXTitle"), + "Found privacy listitem" + ); + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_roles_elements.js b/accessible/tests/browser/mac/browser_roles_elements.js new file mode 100644 index 0000000000..be9b27367e --- /dev/null +++ b/accessible/tests/browser/mac/browser_roles_elements.js @@ -0,0 +1,334 @@ +/* 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 different HTML elements for their roles and subroles + */ +function testRoleAndSubRole(accDoc, id, axRole, axSubRole, axRoleDescription) { + let el = getNativeInterface(accDoc, id); + if (axRole) { + is( + el.getAttributeValue("AXRole"), + axRole, + "AXRole for " + id + " is " + axRole + ); + } + if (axSubRole) { + is( + el.getAttributeValue("AXSubrole"), + axSubRole, + "Subrole for " + id + " is " + axSubRole + ); + } + if (axRoleDescription) { + is( + el.getAttributeValue("AXRoleDescription"), + axRoleDescription, + "Subrole for " + id + " is " + axRoleDescription + ); + } +} + +addAccessibleTask( + ` + +
+ + + +
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + Deleted text +
term
definition
+
+ Inserted text + meter text here + sub text here + sup text here + + + + + g + + + rect + + + circle + + + ellipse + + + line + + + polygon + + + polyline + + + path + + + image + + `, + (browser, accDoc) => { + // WAI-ARIA landmark subroles, regardless of AXRole + testRoleAndSubRole(accDoc, "application", null, "AXLandmarkApplication"); + testRoleAndSubRole(accDoc, "banner", null, "AXLandmarkBanner"); + testRoleAndSubRole( + accDoc, + "complementary", + null, + "AXLandmarkComplementary" + ); + testRoleAndSubRole(accDoc, "contentinfo", null, "AXLandmarkContentInfo"); + testRoleAndSubRole(accDoc, "form", null, "AXLandmarkForm"); + testRoleAndSubRole(accDoc, "main", null, "AXLandmarkMain"); + testRoleAndSubRole(accDoc, "navigation", null, "AXLandmarkNavigation"); + testRoleAndSubRole(accDoc, "search", null, "AXLandmarkSearch"); + testRoleAndSubRole(accDoc, "searchbox", null, "AXSearchField"); + + // DPub roles map into two categories, sample one of each + testRoleAndSubRole( + accDoc, + "dPubNavigation", + "AXGroup", + "AXLandmarkNavigation" + ); + testRoleAndSubRole(accDoc, "dPubRegion", "AXGroup", "AXLandmarkRegion"); + + // ARIA widget roles + testRoleAndSubRole(accDoc, "alert", null, "AXApplicationAlert"); + testRoleAndSubRole( + accDoc, + "alertdialog", + "AXGroup", + "AXApplicationAlertDialog", + "alert dialog" + ); + testRoleAndSubRole(accDoc, "article", null, "AXDocumentArticle"); + testRoleAndSubRole(accDoc, "code", "AXGroup", "AXCodeStyleGroup"); + testRoleAndSubRole(accDoc, "dialog", null, "AXApplicationDialog", "dialog"); + testRoleAndSubRole(accDoc, "ariaDocument", null, "AXDocument"); + testRoleAndSubRole(accDoc, "log", null, "AXApplicationLog"); + testRoleAndSubRole(accDoc, "marquee", null, "AXApplicationMarquee"); + testRoleAndSubRole(accDoc, "ariaMath", null, "AXDocumentMath"); + testRoleAndSubRole(accDoc, "note", null, "AXDocumentNote"); + testRoleAndSubRole(accDoc, "ariaRegion", null, "AXLandmarkRegion"); + testRoleAndSubRole(accDoc, "ariaStatus", "AXGroup", "AXApplicationStatus"); + testRoleAndSubRole(accDoc, "switch", "AXCheckBox", "AXSwitch"); + testRoleAndSubRole(accDoc, "timer", null, "AXApplicationTimer"); + testRoleAndSubRole(accDoc, "tooltip", "AXGroup", "AXUserInterfaceTooltip"); + testRoleAndSubRole(accDoc, "menuitemradio", "AXMenuItem", null); + testRoleAndSubRole(accDoc, "menuitemcheckbox", "AXMenuItem", null); + testRoleAndSubRole(accDoc, "datetime", "AXGroup", null); + // XXX for datetime elements, we spoof the role via the title, since + // providing the correct role results in the internal elements being + // unreachable by VO + is( + getNativeInterface(accDoc, "datetime").getAttributeValue("AXTitle"), + "date field" + ); + + // Text boxes + testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea"); + testRoleAndSubRole(accDoc, "textbox_singleline", "AXTextField"); + testRoleAndSubRole(accDoc, "textArea", "AXTextArea"); + testRoleAndSubRole(accDoc, "textInput", "AXTextField"); + + // True HTML5 search field + testRoleAndSubRole(accDoc, "htmlSearch", "AXTextField", "AXSearchField"); + + // A button morphed into a toggle by ARIA + testRoleAndSubRole(accDoc, "toggle", "AXCheckBox", "AXToggle"); + + // A banana button + testRoleAndSubRole(accDoc, "banana", "AXButton", null, "banana"); + + // Other elements + testRoleAndSubRole(accDoc, "deletion", "AXGroup", "AXDeleteStyleGroup"); + testRoleAndSubRole(accDoc, "dl", "AXList", "AXDescriptionList"); + testRoleAndSubRole(accDoc, "dt", "AXGroup", "AXTerm"); + testRoleAndSubRole(accDoc, "dd", "AXGroup", "AXDescription"); + testRoleAndSubRole(accDoc, "hr", "AXSplitter", "AXContentSeparator"); + testRoleAndSubRole(accDoc, "insertion", "AXGroup", "AXInsertStyleGroup"); + testRoleAndSubRole( + accDoc, + "meter", + "AXLevelIndicator", + null, + "level indicator" + ); + testRoleAndSubRole(accDoc, "sub", "AXGroup", "AXSubscriptStyleGroup"); + testRoleAndSubRole(accDoc, "sup", "AXGroup", "AXSuperscriptStyleGroup"); + + // Some SVG stuff + testRoleAndSubRole(accDoc, "svg", "AXImage"); + testRoleAndSubRole(accDoc, "g", "AXGroup"); + testRoleAndSubRole(accDoc, "rect", "AXImage"); + testRoleAndSubRole(accDoc, "circle", "AXImage"); + testRoleAndSubRole(accDoc, "ellipse", "AXImage"); + testRoleAndSubRole(accDoc, "line", "AXImage"); + testRoleAndSubRole(accDoc, "polygon", "AXImage"); + testRoleAndSubRole(accDoc, "polyline", "AXImage"); + testRoleAndSubRole(accDoc, "path", "AXImage"); + testRoleAndSubRole(accDoc, "image", "AXImage"); + } +); + +addAccessibleTask( + ` +
+ Logo +

Non-image figure content

+
Old Mozilla logo
+
`, + (browser, accDoc) => { + let figure = getNativeInterface(accDoc, "figure"); + ok(!figure.getAttributeValue("AXTitle"), "Figure should not have a title"); + is( + figure.getAttributeValue("AXDescription"), + "Old Mozilla logo", + "Correct figure label" + ); + is(figure.getAttributeValue("AXRole"), "AXGroup", "Correct figure role"); + is( + figure.getAttributeValue("AXRoleDescription"), + "figure", + "Correct figure role description" + ); + + let img = getNativeInterface(accDoc, "img"); + ok(!img.getAttributeValue("AXTitle"), "img should not have a title"); + is(img.getAttributeValue("AXDescription"), "Logo", "Correct img label"); + is(img.getAttributeValue("AXRole"), "AXImage", "Correct img role"); + is( + img.getAttributeValue("AXRoleDescription"), + "image", + "Correct img role description" + ); + + let figcaption = getNativeInterface(accDoc, "figcaption"); + ok( + !figcaption.getAttributeValue("AXTitle"), + "figcaption should not have a title" + ); + ok( + !figcaption.getAttributeValue("AXDescription"), + "figcaption should not have a label" + ); + is( + figcaption.getAttributeValue("AXRole"), + "AXGroup", + "Correct figcaption role" + ); + is( + figcaption.getAttributeValue("AXRoleDescription"), + "group", + "Correct figcaption role description" + ); + } +); + +addAccessibleTask(``, async (browser, accDoc) => { + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should be an AXWebArea" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let roleChanged = waitForMacEvent("AXMozRoleChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("role", "application"); + }); + await roleChanged; + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should retain AXWebArea role" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let rootGroup = webArea.getAttributeValue("AXChildren")[0]; + is(rootGroup.getAttributeValue("AXRole"), "AXGroup"); + is(rootGroup.getAttributeValue("AXSubrole"), "AXLandmarkApplication"); +}); diff --git a/accessible/tests/browser/mac/browser_rootgroup.js b/accessible/tests/browser/mac/browser_rootgroup.js new file mode 100644 index 0000000000..a8f4297d64 --- /dev/null +++ b/accessible/tests/browser/mac/browser_rootgroup.js @@ -0,0 +1,246 @@ +/* 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 document with no single group child + */ +addAccessibleTask( + `

hello

world

`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + is( + rootGroup.getAttributeValue("AXChildren").length, + 2, + "Root group has two children" + ); + + // From bottom-up + let p1 = getNativeInterface(accDoc, "p1"); + rootGroup = p1.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + } +); + +/** + * Test document with a top-level group + */ +addAccessibleTask( + `

hello

world

`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXDOMIdentifier"), + "group", + "Root group is a document element" + ); + + // Adding an 'application' role to the body should + // create a root group with an application subrole. + let evt = waitForMacEvent("AXMozRoleChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("role", "application"); + }); + await evt; + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "doc still has web area role" + ); + is( + doc.getAttributeValue("AXRoleDescription"), + "HTML Content", + "doc has correct role description" + ); + ok( + !doc.attributeNames.includes("AXSubrole"), + "sub role not available on web area" + ); + + rootGroup = doc.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + is( + rootGroup.getAttributeValue("AXRole"), + "AXGroup", + "root group has AXGroup role" + ); + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXLandmarkApplication", + "root group has application subrole" + ); + is( + rootGroup.getAttributeValue("AXRoleDescription"), + "application", + "root group has application role description" + ); + } +); + +/** + * Test document with body[role=application] and a top-level group + */ +addAccessibleTask( + `

hello

world

`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "doc still has web area role" + ); + is( + doc.getAttributeValue("AXRoleDescription"), + "HTML Content", + "doc has correct role description" + ); + ok( + !doc.attributeNames.includes("AXSubrole"), + "sub role not available on web area" + ); + + let rootGroup = doc.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + is( + rootGroup.getAttributeValue("AXRole"), + "AXGroup", + "root group has AXGroup role" + ); + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXLandmarkApplication", + "root group has application subrole" + ); + is( + rootGroup.getAttributeValue("AXRoleDescription"), + "application", + "root group has application role description" + ); + }, + { contentDocBodyAttrs: { role: "application" } } +); + +/** + * Test document with a single button + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + let rootGroupChildren = rootGroup.getAttributeValue("AXChildren"); + is(rootGroupChildren.length, 1, "Root group has one children"); + + is( + rootGroupChildren[0].getAttributeValue("AXRole"), + "AXButton", + "Button is child of root group" + ); + + // From bottom-up + let button = getNativeInterface(accDoc, "button"); + rootGroup = button.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + } +); + +/** + * Test document with dialog role and heading + */ +addAccessibleTask( + ` +

+ We're building a richer search experience +

+ `, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + is(rootGroup.getAttributeValue("AXRole"), "AXGroup", "Inherits role"); + + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXApplicationDialog", + "Inherits subrole" + ); + let rootGroupChildren = rootGroup.getAttributeValue("AXChildren"); + is(rootGroupChildren.length, 1, "Root group has one child"); + + is( + rootGroupChildren[0].getAttributeValue("AXRole"), + "AXHeading", + "Heading is child of root group" + ); + + // From bottom-up + let heading = getNativeInterface(accDoc, "h"); + rootGroup = heading.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Parent is generated root group" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js new file mode 100644 index 0000000000..3f13506757 --- /dev/null +++ b/accessible/tests/browser/mac/browser_rotor.js @@ -0,0 +1,1752 @@ +/* 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 }); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `

hello


world


goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, headingCount, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + hello.getAttributeValue("AXTitle"), + headings[0].getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + world.getAttributeValue("AXTitle"), + headings[1].getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with heading and empty search text + */ +addAccessibleTask( + `

hello


world


goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXSearchText: "", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(headingCount, 2, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + headings[0].getAttributeValue("AXTitle"), + hello.getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + headings[1].getAttributeValue("AXTitle"), + world.getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with articles + */ +addAccessibleTask( + `
+

Google Chrome

+

Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!

+
+ +
+

Mozilla Firefox

+

Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.

+
+ +
+

Microsoft Edge

+

Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.

+
`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXArticleSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const articleCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, articleCount, "Found three articles"); + + const articles = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const google = getNativeInterface(accDoc, "google"); + const moz = getNativeInterface(accDoc, "moz"); + const microsoft = getNativeInterface(accDoc, "microsoft"); + + is( + google.getAttributeValue("AXTitle"), + articles[0].getAttributeValue("AXTitle"), + "Found correct first article" + ); + is( + moz.getAttributeValue("AXTitle"), + articles[1].getAttributeValue("AXTitle"), + "Found correct second article" + ); + is( + microsoft.getAttributeValue("AXTitle"), + articles[2].getAttributeValue("AXTitle"), + "Found correct third article" + ); + } +); + +/** + * Test rotor with tables + */ +addAccessibleTask( + ` + + + + + + + + + + + + + + + + +
ShapeColorDo I like it?
TriangleGreenNo
SquareRedYes
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Grocery ItemQuantity
Onions2
Yogurt1
Spinach1
Cherries12
Carrots5
+
+
+
+
+ I am a tiny aria table +
+
+
+
+ + + + + + + + + + + + + + + +
ABCDE
FGHIJ
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXTableSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const tableCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, tableCount, "Found four tables"); + + const tables = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const shapes = getNativeInterface(accDoc, "shapes"); + const food = getNativeInterface(accDoc, "food"); + const ariaTable = getNativeInterface(accDoc, "ariaTable"); + const grid = getNativeInterface(accDoc, "grid"); + + is( + shapes.getAttributeValue("AXColumnCount"), + tables[0].getAttributeValue("AXColumnCount"), + "Found correct first table" + ); + is( + food.getAttributeValue("AXColumnCount"), + tables[1].getAttributeValue("AXColumnCount"), + "Found correct second table" + ); + is( + ariaTable.getAttributeValue("AXColumnCount"), + tables[2].getAttributeValue("AXColumnCount"), + "Found correct third table" + ); + is( + grid.getAttributeValue("AXColumnCount"), + tables[3].getAttributeValue("AXColumnCount"), + "Found correct fourth table" + ); + } +); + +/** + * Test rotor with landmarks + */ +addAccessibleTask( + ` + + + + +
+ I am some text in a main element +
+ +
+

Heading in footer

+
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXLandmarkSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const landmarkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, landmarkCount, "Found four landmarks"); + + const landmarks = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const header = getNativeInterface(accDoc, "header"); + const nav = getNativeInterface(accDoc, "nav"); + const main = getNativeInterface(accDoc, "main"); + const footer = getNativeInterface(accDoc, "footer"); + + is( + header.getAttributeValue("AXSubrole"), + landmarks[0].getAttributeValue("AXSubrole"), + "Found correct first landmark" + ); + is( + nav.getAttributeValue("AXSubrole"), + landmarks[1].getAttributeValue("AXSubrole"), + "Found correct second landmark" + ); + is( + main.getAttributeValue("AXSubrole"), + landmarks[2].getAttributeValue("AXSubrole"), + "Found correct third landmark" + ); + is( + footer.getAttributeValue("AXSubrole"), + landmarks[3].getAttributeValue("AXSubrole"), + "Found correct fourth landmark" + ); + } +); + +/** + * Test rotor with aria landmarks + */ +addAccessibleTask( + ` + + + + +
+ I am some text in a main element +
+ + + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXLandmarkSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const landmarkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, landmarkCount, "Found four landmarks"); + + const landmarks = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const banner = getNativeInterface(accDoc, "banner"); + const nav = getNativeInterface(accDoc, "nav"); + const main = getNativeInterface(accDoc, "main"); + const contentinfo = getNativeInterface(accDoc, "contentinfo"); + + is( + banner.getAttributeValue("AXSubrole"), + landmarks[0].getAttributeValue("AXSubrole"), + "Found correct first landmark" + ); + is( + nav.getAttributeValue("AXSubrole"), + landmarks[1].getAttributeValue("AXSubrole"), + "Found correct second landmark" + ); + is( + main.getAttributeValue("AXSubrole"), + landmarks[2].getAttributeValue("AXSubrole"), + "Found correct third landmark" + ); + is( + contentinfo.getAttributeValue("AXSubrole"), + landmarks[3].getAttributeValue("AXSubrole"), + "Found correct fourth landmark" + ); + } +); + +/** + * Test rotor with buttons + */ +addAccessibleTask( + ` +
+ +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXButtonSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const buttonCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, buttonCount, "Found two buttons"); + + const buttons = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const button = getNativeInterface(accDoc, "button"); + const input = getNativeInterface(accDoc, "input"); + + is( + button.getAttributeValue("AXRole"), + buttons[0].getAttributeValue("AXRole"), + "Found correct button" + ); + is( + input.getAttributeValue("AXRole"), + buttons[1].getAttributeValue("AXRole"), + "Found correct input button" + ); + } +); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `

hello


world


goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, headingCount, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + hello.getAttributeValue("AXTitle"), + headings[0].getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + world.getAttributeValue("AXTitle"), + headings[1].getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with buttons + */ +addAccessibleTask( + ` +
+

input[type=button]

+ + +

input[type=submit]

+ + +

input[type=image]

+ + +

input[type=reset]

+ + +

button element

+ +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(5, controlsCount, "Found 5 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const button1 = getNativeInterface(accDoc, "button1"); + const submit = getNativeInterface(accDoc, "submit"); + const image = getNativeInterface(accDoc, "image"); + const reset = getNativeInterface(accDoc, "reset"); + const button2 = getNativeInterface(accDoc, "button2"); + + is( + button1.getAttributeValue("AXTitle"), + controls[0].getAttributeValue("AXTitle"), + "Found correct first control" + ); + is( + submit.getAttributeValue("AXTitle"), + controls[1].getAttributeValue("AXTitle"), + "Found correct second control" + ); + is( + image.getAttributeValue("AXTitle"), + controls[2].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + reset.getAttributeValue("AXTitle"), + controls[3].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + button2.getAttributeValue("AXTitle"), + controls[4].getAttributeValue("AXTitle"), + "Found correct third control" + ); + } +); + +/** + * Test rotor with inputs + */ +addAccessibleTask( + ` +
+
+
+
+
+
+
+
+
+
+
+
+
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(13, controlsCount, "Found 13 controls"); + // the extra controls here come from our time control + // we can't filter out its internal buttons/incrementors + // like we do with the date entry because the time entry + // doesn't have its own specific role -- its just a grouping. + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const text = getNativeInterface(accDoc, "text"); + const implText = getNativeInterface(accDoc, "implText"); + const textarea = getNativeInterface(accDoc, "textarea"); + const tel = getNativeInterface(accDoc, "tel"); + const url = getNativeInterface(accDoc, "url"); + const email = getNativeInterface(accDoc, "email"); + const password = getNativeInterface(accDoc, "password"); + const month = getNativeInterface(accDoc, "month"); + const week = getNativeInterface(accDoc, "week"); + const number = getNativeInterface(accDoc, "number"); + const range = getNativeInterface(accDoc, "range"); + + const toCheck = [ + text, + implText, + textarea, + tel, + url, + email, + password, + month, + week, + number, + range, + ]; + + for (let i = 0; i < toCheck.length; i++) { + is( + toCheck[i].getAttributeValue("AXValue"), + controls[i].getAttributeValue("AXValue"), + "Found correct input control" + ); + } + + const date = getNativeInterface(accDoc, "date"); + const time = getNativeInterface(accDoc, "time"); + + is( + date.getAttributeValue("AXRole"), + controls[11].getAttributeValue("AXRole"), + "Found corrent date editor" + ); + + is( + time.getAttributeValue("AXRole"), + controls[12].getAttributeValue("AXRole"), + "Found corrent time editor" + ); + } +); + +/** + * Test rotor with groupings + */ +addAccessibleTask( + ` +
+ Radios +
+ Radio 1 + Radio 2 +
+
+ +
+ Checkboxes + Checkbox 1 + Checkbox 2 +
+ +
+ Switches + Switch 1 + Switch 2 +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(9, controlsCount, "Found 9 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const radios = getNativeInterface(accDoc, "radios"); + const radio1 = getNativeInterface(accDoc, "radio1"); + const radio2 = getNativeInterface(accDoc, "radio2"); + + is( + radios.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct group of radios" + ); + is( + radio1.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct radio 1" + ); + is( + radio2.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct radio 2" + ); + + const checkboxes = getNativeInterface(accDoc, "checkboxes"); + const checkbox1 = getNativeInterface(accDoc, "checkbox1"); + const checkbox2 = getNativeInterface(accDoc, "checkbox2"); + + is( + checkboxes.getAttributeValue("AXRole"), + controls[3].getAttributeValue("AXRole"), + "Found correct group of checkboxes" + ); + is( + checkbox1.getAttributeValue("AXRole"), + controls[4].getAttributeValue("AXRole"), + "Found correct checkbox 1" + ); + is( + checkbox2.getAttributeValue("AXRole"), + controls[5].getAttributeValue("AXRole"), + "Found correct checkbox 2" + ); + + const switches = getNativeInterface(accDoc, "switches"); + const switch1 = getNativeInterface(accDoc, "switch1"); + const switch2 = getNativeInterface(accDoc, "switch2"); + + is( + switches.getAttributeValue("AXRole"), + controls[6].getAttributeValue("AXRole"), + "Found correct group of switches" + ); + is( + switch1.getAttributeValue("AXRole"), + controls[7].getAttributeValue("AXRole"), + "Found correct switch 1" + ); + is( + switch2.getAttributeValue("AXRole"), + controls[8].getAttributeValue("AXRole"), + "Found correct switch 2" + ); + } +); + +/** + * Test rotor with misc controls + */ +addAccessibleTask( + ` + + +
+ Hello + world +
+ + + + Click Me + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, controlsCount, "Found 4 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const spin = getNativeInterface(accDoc, "spinbutton"); + const details = getNativeInterface(accDoc, "details"); + const tree = getNativeInterface(accDoc, "tree"); + const buttonMenu = getNativeInterface(accDoc, "buttonMenu"); + + is( + spin.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct spinbutton" + ); + is( + details.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct details element" + ); + is( + tree.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct tree" + ); + is( + buttonMenu.getAttributeValue("AXRole"), + controls[3].getAttributeValue("AXRole"), + "Found correct button menu" + ); + } +); + +/** + * Test rotor with links + */ +addAccessibleTask( + ` + empty link + Example link + link without href + `, + async (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, linkCount, "Found two links"); + + let links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const empty = getNativeInterface(accDoc, "empty"); + const href = getNativeInterface(accDoc, "href"); + + is( + empty.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct first link" + ); + is( + href.getAttributeValue("AXTitle"), + links[1].getAttributeValue("AXTitle"), + "Found correct second link" + ); + + // unvisited links + + searchPred = { + AXSearchKey: "AXUnvisitedLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, linkCount, "Found two links"); + + links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + empty.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct first link" + ); + is( + href.getAttributeValue("AXTitle"), + links[1].getAttributeValue("AXTitle"), + "Found correct second link" + ); + + // visited links + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "href"); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await PlacesTestUtils.addVisits(["http://www.example.com/"]); + + await stateChanged; + + searchPred = { + AXSearchKey: "AXVisitedLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(1, linkCount, "Found one link"); + + links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + href.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct visited link" + ); + + // Ensure history is cleared before running again + await PlacesUtils.history.clear(); + } +); + +/* + * Test AXAnyTypeSearchKey with root group + */ +addAccessibleTask( + `

hello


world


goodbye`, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(results.length, 1, "One result for root group"); + is( + results[0].getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + searchPred.AXStartElement = results[0]; + results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(results.length, 0, "No more results past root group"); + + searchPred.AXDirection = "AXDirectionPrevious"; + results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is( + results.length, + 0, + "Searching backwards from root group should yield no results" + ); + + const rootGroup = webArea.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + results = rootGroup.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + results[0].getAttributeValue("AXRole"), + "AXHeading", + "Is first heading child" + ); + } +); + +/** + * Test rotor with checkboxes + */ +addAccessibleTask( + ` +
+ Checkboxes + Checkbox 1 + Checkbox 2 + + +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXCheckBoxSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const checkboxCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, checkboxCount, "Found 4 checkboxes"); + + const checkboxes = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const checkbox1 = getNativeInterface(accDoc, "checkbox1"); + const checkbox2 = getNativeInterface(accDoc, "checkbox2"); + const checkbox3 = getNativeInterface(accDoc, "checkbox3"); + const checkbox4 = getNativeInterface(accDoc, "checkbox4"); + + is( + checkbox1.getAttributeValue("AXValue"), + checkboxes[0].getAttributeValue("AXValue"), + "Found correct checkbox 1" + ); + is( + checkbox2.getAttributeValue("AXValue"), + checkboxes[1].getAttributeValue("AXValue"), + "Found correct checkbox 2" + ); + is( + checkbox3.getAttributeValue("AXValue"), + checkboxes[2].getAttributeValue("AXValue"), + "Found correct checkbox 3" + ); + is( + checkbox4.getAttributeValue("AXValue"), + checkboxes[3].getAttributeValue("AXValue"), + "Found correct checkbox 4" + ); + } +); + +/** + * Test rotor with radiogroups + */ +addAccessibleTask( + ` +
+

some radio buttons

+ + +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXRadioGroupSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const radiogroupCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(1, radiogroupCount, "Found 1 radio group"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const radios = getNativeInterface(accDoc, "radios"); + + is( + radios.getAttributeValue("AXDescription"), + controls[0].getAttributeValue("AXDescription"), + "Found correct group of radios" + ); + } +); + +/* + * Test rotor with inputs + */ +addAccessibleTask( + ` +
+
+
+
+
+
+
+
+
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXTextFieldSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textfieldCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(9, textfieldCount, "Found 9 fields"); + + const fields = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const text = getNativeInterface(accDoc, "text"); + const implText = getNativeInterface(accDoc, "implText"); + const textarea = getNativeInterface(accDoc, "textarea"); + const tel = getNativeInterface(accDoc, "tel"); + const url = getNativeInterface(accDoc, "url"); + const email = getNativeInterface(accDoc, "email"); + const password = getNativeInterface(accDoc, "password"); + const month = getNativeInterface(accDoc, "month"); + const week = getNativeInterface(accDoc, "week"); + + const toCheck = [ + text, + implText, + textarea, + tel, + url, + email, + password, + month, + week, + ]; + + for (let i = 0; i < toCheck.length; i++) { + is( + toCheck[i].getAttributeValue("AXValue"), + fields[i].getAttributeValue("AXValue"), + "Found correct input control" + ); + } + } +); + +/** + * Test rotor with static text + */ +addAccessibleTask( + ` +

Hello I am a heading

+ This is some regular text.

this is some paragraph text


+ This is a list: + + This is a link + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXStaticTextSearchKey", + AXImmediateDescendants: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(7, textCount, "Found 7 pieces of text"); + + const text = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + "Hello I am a heading", + text[0].getAttributeValue("AXValue"), + "Found correct text node for heading" + ); + is( + "This is some regular text.", + text[1].getAttributeValue("AXValue"), + "Found correct text node" + ); + is( + "this is some paragraph text", + text[2].getAttributeValue("AXValue"), + "Found correct text node for paragraph" + ); + is( + "This is a list:", + text[3].getAttributeValue("AXValue"), + "Found correct text node for pre-list text node" + ); + is( + "List item one", + text[4].getAttributeValue("AXValue"), + "Found correct text node for list item one" + ); + is( + "List item two", + text[5].getAttributeValue("AXValue"), + "Found correct text node for list item two" + ); + is( + "This is a link", + text[6].getAttributeValue("AXValue"), + "Found correct text node for link" + ); + } +); + +/** + * Test rotor with lists + */ +addAccessibleTask( + ` + + +
    +
  1. item one
  2. +
  3. item two
  4. +
+ `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXListSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const listCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, listCount, "Found 2 lists"); + + const lists = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const ordered = getNativeInterface(accDoc, "ordered"); + const unordered = getNativeInterface(accDoc, "unordered"); + + is( + unordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + lists[0].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + "Found correct unordered list" + ); + is( + ordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + lists[1].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + "Found correct ordered list" + ); + } +); + +/* + * Test rotor with images + */ +addAccessibleTask( + ` + image one
+ + image two + + + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXImageSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let images = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(images.length, 3, "Found three images"); + + const img1 = getNativeInterface(accDoc, "img1"); + const img2 = getNativeInterface(accDoc, "img2"); + const img3 = getNativeInterface(accDoc, "img3"); + + is( + img1.getAttributeValue("AXDescription"), + images[0].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img2.getAttributeValue("AXDescription"), + images[1].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img3.getAttributeValue("AXDescription"), + images[2].getAttributeValue("AXDescription"), + "Found correct image" + ); + } +); + +/** + * Test rotor with frames + */ +addAccessibleTask( + ` + + + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXFrameSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const frameCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, frameCount, "Found 3 frames"); + } +); + +/** + * Test rotor with static text + */ +addAccessibleTask( + ` +

Hello I am a heading

+ This is some regular text.

this is some paragraph text


+ This is a list: + + This is a link + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXStaticTextSearchKey", + AXImmediateDescendants: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(7, textCount, "Found 7 pieces of text"); + + const text = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + "Hello I am a heading", + text[0].getAttributeValue("AXValue"), + "Found correct text node for heading" + ); + is( + "This is some regular text.", + text[1].getAttributeValue("AXValue"), + "Found correct text node" + ); + is( + "this is some paragraph text", + text[2].getAttributeValue("AXValue"), + "Found correct text node for paragraph" + ); + is( + "This is a list:", + text[3].getAttributeValue("AXValue"), + "Found correct text node for pre-list text node" + ); + is( + "List item one", + text[4].getAttributeValue("AXValue"), + "Found correct text node for list item one" + ); + is( + "List item two", + text[5].getAttributeValue("AXValue"), + "Found correct text node for list item two" + ); + is( + "This is a link", + text[6].getAttributeValue("AXValue"), + "Found correct text node for link" + ); + } +); + +/** + * Test search with non-webarea root + */ +addAccessibleTask( + ` +

hello

world

+

goodybe

+ `, + async (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const searchRoot = getNativeInterface(accDoc, "searchroot"); + const resultCount = searchRoot.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(resultCount, 2, "Found 2 items"); + + const p1 = getNativeInterface(accDoc, "p1"); + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXStartElement: p1, + }; + + let results = searchRoot.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["p2"], + "Result is next group sibling" + ); + + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionPrevious", + }; + + results = searchRoot.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["p2", "p1"], + "A reverse search should return groups in reverse" + ); + } +); + +/** + * Test search text + */ +addAccessibleTask( + ` +

It's about the future, isn't it?

+

Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.

+ + `, + async (browser, f, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXSearchText: "could", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textSearchCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(textSearchCount, 2, "Found 2 matching items in text search"); + + const results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + info(results.map(r => r.getAttributeValue("AXMozDebugDescription"))); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXValue")), + [ + "Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.", + "I could hang out, you could show me around.", + ], + "Correct text search results" + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js new file mode 100644 index 0000000000..331cd7d21c --- /dev/null +++ b/accessible/tests/browser/mac/browser_selectables.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 */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getSelectedIds(selectable) { + return selectable + .getAttributeValue("AXSelectedChildren") + .map(c => c.getAttributeValue("AXDOMIdentifier")); +} + +/** + * Test aria tabs + */ +addAccessibleTask("mac/doc_aria_tabs.html", async (browser, accDoc) => { + let tablist = getNativeInterface(accDoc, "tablist"); + is( + tablist.getAttributeValue("AXRole"), + "AXTabGroup", + "Correct role for tablist" + ); + + let tabMacAccs = tablist.getAttributeValue("AXTabs"); + is(tabMacAccs.length, 3, "3 items in AXTabs"); + + let selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + + let tab = selectedTabs[0]; + is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab"); + is( + tab.getAttributeValue("AXSubrole"), + "AXTabButton", + "Correct subrole for tab" + ); + is(tab.getAttributeValue("AXTitle"), "First Tab", "Correct title for tab"); + + let tabToSelect = tabMacAccs[1]; + is( + tabToSelect.getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); + + let actions = tabToSelect.actionNames; + ok(true, actions); + ok(actions.includes("AXPress"), "Has switch action"); + + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + tabToSelect.performAction("AXPress"); + await evt; + + selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + is( + selectedTabs[0].getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); +}); + +addAccessibleTask('

hello

', async (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + ok( + p.attributeNames.includes("AXSelected"), + "html element includes 'AXSelected' attribute" + ); + is(p.getAttributeValue("AXSelected"), 0, "AX selected is 'false'"); +}); + +addAccessibleTask( + ``, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + let three = getNativeInterface(accDoc, "three"); + let four = getNativeInterface(accDoc, "four"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a number", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + + is(one.getAttributeValue("AXTitle"), "", "Option should not have a title"); + is( + one.getAttributeValue("AXValue"), + "One", + "Option should have correct value" + ); + is( + one.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set"); + + is(select.getAttributeValue("AXSelectedChildren").length, 1); + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + one.setAttributeValue("AXSelected", false); + await evt; + is(select.getAttributeValue("AXSelectedChildren").length, 0); + evt = waitForMacEvent("AXSelectedChildrenChanged"); + three.setAttributeValue("AXSelected", true); + await evt; + is(select.getAttributeValue("AXSelectedChildren").length, 1); + ok(getSelectedIds(select).includes("three"), "'three' is selected"); + evt = waitForMacEvent("AXSelectedChildrenChanged"); + select.setAttributeValue("AXSelectedChildren", [one, two]); + await evt; + await untilCacheOk(() => { + let ids = getSelectedIds(select); + return ids[0] == "one" && ids[1] == "two"; + }, "Got correct selected children"); + + evt = waitForMacEvent("AXSelectedChildrenChanged"); + select.setAttributeValue("AXSelectedChildren", [three, two, four]); + await evt; + await untilCacheOk(() => { + let ids = getSelectedIds(select); + return ids[0] == "two" && ids[1] == "three"; + }, "Got correct selected children"); + + ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled"); + } +); + +addAccessibleTask( + ``, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a thing", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + let childValueSelectablePairs = select + .getAttributeValue("AXChildren") + .map(c => [ + c.getAttributeValue("AXValue"), + c.isAttributeSettable("AXSelected"), + c.getAttributeValue("AXEnabled"), + ]); + Assert.deepEqual( + childValueSelectablePairs, + [ + ["Fruits", false, false], + ["Banana", true, true], + ["Apple", true, true], + ["Orange", true, true], + ["Vegetables", false, false], + ["Lettuce", true, true], + ["Tomato", true, true], + ["Onion", true, true], + ["Spices", false, false], + ["Cumin", true, true], + ["Coriander", true, true], + ["Allspice", true, true], + ["Everything", true, true], + ], + "Options are selectable, group labels are not" + ); + + let allspice = getNativeInterface(accDoc, "allspice"); + is( + allspice.getAttributeValue("AXTitle"), + "", + "Option should not have a title" + ); + is( + allspice.getAttributeValue("AXValue"), + "Allspice", + "Option should have a value" + ); + is( + allspice.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok( + allspice.isAttributeSettable("AXSelected"), + "Option can have AXSelected set" + ); + is( + allspice + .getAttributeValue("AXParent") + .getAttributeValue("AXDOMIdentifier"), + "select", + "Select is direct parent of nested option" + ); + + let groupLabel = select.getAttributeValue("AXChildren")[0]; + ok( + !groupLabel.isAttributeSettable("AXSelected"), + "Group label should not be selectable" + ); + is( + groupLabel.getAttributeValue("AXValue"), + "Fruits", + "Group label should have a value" + ); + is( + groupLabel.getAttributeValue("AXTitle"), + null, + "Group label should not have a title" + ); + is( + groupLabel.getAttributeValue("AXRole"), + "AXStaticText", + "Group label should have AXStaticText role" + ); + is( + groupLabel + .getAttributeValue("AXParent") + .getAttributeValue("AXDOMIdentifier"), + "select", + "Select is direct parent of group label" + ); + + Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]); + } +); + +addAccessibleTask( + `
+
One
+
Two
+
Three
+
Four
+
`, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + let three = getNativeInterface(accDoc, "three"); + let four = getNativeInterface(accDoc, "four"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a number", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + + is(one.getAttributeValue("AXTitle"), "", "Option should not have a title"); + is( + one.getAttributeValue("AXValue"), + "One", + "Option should have correct value" + ); + is( + one.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set"); + + is(select.getAttributeValue("AXSelectedChildren").length, 1); + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + // Change selection from content. + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("one").removeAttribute("aria-selected"); + }); + await evt; + is(select.getAttributeValue("AXSelectedChildren").length, 0); + evt = waitForMacEvent("AXSelectedChildrenChanged"); + three.setAttributeValue("AXSelected", true); + await evt; + is(select.getAttributeValue("AXSelectedChildren").length, 1); + ok(getSelectedIds(select).includes("three"), "'three' is selected"); + evt = waitForMacEvent("AXSelectedChildrenChanged"); + select.setAttributeValue("AXSelectedChildren", [one, two]); + await evt; + await untilCacheOk(() => { + let ids = getSelectedIds(select); + return ids[0] == "one" && ids[1] == "two"; + }, "Got correct selected children"); + + evt = waitForMacEvent("AXSelectedChildrenChanged"); + select.setAttributeValue("AXSelectedChildren", [three, two, four]); + await evt; + await untilCacheOk(() => { + let ids = getSelectedIds(select); + return ids[0] == "two" && ids[1] == "three"; + }, "Got correct selected children"); + } +); diff --git a/accessible/tests/browser/mac/browser_table.js b/accessible/tests/browser/mac/browser_table.js new file mode 100644 index 0000000000..50ae697deb --- /dev/null +++ b/accessible/tests/browser/mac/browser_table.js @@ -0,0 +1,629 @@ +/* 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/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +/** + * Helper function to test table consistency. + */ +function testTableConsistency(table, expectedRowCount, expectedColumnCount) { + is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table"); + + let tableChildren = table.getAttributeValue("AXChildren"); + // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers + // if we're trying to match Safari. + is( + tableChildren.length, + expectedRowCount + expectedColumnCount, + "Table has children = rows (4) + cols (3)" + ); + for (let i = 0; i < tableChildren.length; i++) { + let currChild = tableChildren[i]; + if (i < expectedRowCount) { + is( + currChild.getAttributeValue("AXRole"), + "AXRow", + "Correct role for row" + ); + } else { + is( + currChild.getAttributeValue("AXRole"), + "AXColumn", + "Correct role for col" + ); + is( + currChild.getAttributeValue("AXRoleDescription"), + "column", + "Correct role desc for col" + ); + } + } + + is( + table.getAttributeValue("AXColumnCount"), + expectedColumnCount, + "Table has correct column count." + ); + is( + table.getAttributeValue("AXRowCount"), + expectedRowCount, + "Table has correct row count." + ); + + let cols = table.getAttributeValue("AXColumns"); + is(cols.length, expectedColumnCount, "Table has col list of correct length"); + for (let i = 0; i < cols.length; i++) { + let currCol = cols[i]; + let currChildren = currCol.getAttributeValue("AXChildren"); + is( + currChildren.length, + expectedRowCount, + "Column has correct number of cells" + ); + for (let j = 0; j < currChildren.length; j++) { + let currChild = currChildren[j]; + is( + currChild.getAttributeValue("AXRole"), + "AXCell", + "Column child is cell" + ); + } + } + + let rows = table.getAttributeValue("AXRows"); + is(rows.length, expectedRowCount, "Table has row list of correct length"); + for (let i = 0; i < rows.length; i++) { + let currRow = rows[i]; + let currChildren = currRow.getAttributeValue("AXChildren"); + is( + currChildren.length, + expectedColumnCount, + "Row has correct number of cells" + ); + for (let j = 0; j < currChildren.length; j++) { + let currChild = currChildren[j]; + is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell"); + } + } +} + +/** + * Test table, columns, rows + */ +addAccessibleTask( + ` + + + + + + +
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
Ernst HandelRoland MendelAustria
`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "customers"); + testTableConsistency(table, 4, 3); + + const rowText = [ + "Madrigal Electromotive GmbH", + "Lydia Rodarte-Quayle", + "Germany", + ]; + let reorder = waitForEvent(EVENT_REORDER, "customers"); + await SpecialPowers.spawn(browser, [rowText], _rowText => { + let tr = content.document.createElement("tr"); + for (let t of _rowText) { + let td = content.document.createElement("td"); + td.textContent = t; + tr.appendChild(td); + } + content.document.getElementById("customers").appendChild(tr); + }); + await reorder; + + let cols = table.getAttributeValue("AXColumns"); + is(cols.length, 3, "Table has col list of correct length"); + for (let i = 0; i < cols.length; i++) { + let currCol = cols[i]; + let currChildren = currCol.getAttributeValue("AXChildren"); + is(currChildren.length, 5, "Column has correct number of cells"); + let lastCell = currChildren[currChildren.length - 1]; + let cellChildren = lastCell.getAttributeValue("AXChildren"); + is(cellChildren.length, 1, "Cell has a single text child"); + is( + cellChildren[0].getAttributeValue("AXRole"), + "AXStaticText", + "Correct role for cell child" + ); + is( + cellChildren[0].getAttributeValue("AXValue"), + rowText[i], + "Correct text for cell" + ); + } + + reorder = waitForEvent(EVENT_REORDER, "firstrow"); + await SpecialPowers.spawn(browser, [], () => { + let td = content.document.createElement("td"); + td.textContent = "Ticker"; + content.document.getElementById("firstrow").appendChild(td); + }); + await reorder; + + cols = table.getAttributeValue("AXColumns"); + is(cols.length, 4, "Table has col list of correct length"); + is( + cols[cols.length - 1].getAttributeValue("AXChildren").length, + 1, + "Last column has single child" + ); + + reorder = waitForEvent( + EVENT_REORDER, + e => e.accessible.role == ROLE_DOCUMENT + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("customers").remove(); + }); + await reorder; + + try { + cols[0].getAttributeValue("AXChildren"); + ok(false, "Getting children from column of expired table should fail"); + } catch (e) { + ok(true, "Getting children from column of expired table should fail"); + } + } +); + +addAccessibleTask( + ` + + + + + + + + + + + + + +
Header 1Header 2
onetwothree
fourfive
`, + (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + + let getCellAt = (col, row) => + table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]); + + function testCell(cell, expectedId, expectedColRange, expectedRowRange) { + is( + cell.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct DOM Identifier" + ); + Assert.deepEqual( + cell.getAttributeValue("AXColumnIndexRange"), + expectedColRange, + "Correct column range" + ); + Assert.deepEqual( + cell.getAttributeValue("AXRowIndexRange"), + expectedRowRange, + "Correct row range" + ); + } + + testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]); + testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]); + testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]); + + testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]); + testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]); + testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]); + + testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]); + testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]); + testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]); + + let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements"); + Assert.deepEqual( + colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")), + ["header1", "header1", "header2"], + "Correct column headers" + ); + } +); + +addAccessibleTask( + ` + + + +
Foo
`, + (browser, accDoc) => { + // Make sure we guess this table to be a layout table. + testAttrs( + findAccessibleChildByID(accDoc, "table"), + { "layout-guess": "true" }, + true + ); + + let table = getNativeInterface(accDoc, "table"); + is( + table.getAttributeValue("AXRole"), + "AXGroup", + "Correct role (AXGroup) for layout table" + ); + + let children = table.getAttributeValue("AXChildren"); + is( + children.length, + 1, + "Layout table has single child (no additional columns)" + ); + } +); + +addAccessibleTask( + `
+ +
+
Cell 1
+
Cell 2
+
+
+ +
+ +
Cell 3
+
Cell 4
+
+
+
+
`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + testTableConsistency(table, 2, 2); + } +); + +/* + * After executing function 'change' which operates on 'elem', verify the specified + * 'event' (if not null) is fired on elem. After the event, check if the given + * native accessible 'table' is a layout or data table by role using 'isLayout'. + */ +async function testIsLayout(table, elem, event, change, isLayout) { + info( + "Changing " + + elem + + ", expecting table change to " + + (isLayout ? "AXGroup" : "AXTable") + ); + const toWait = event ? waitForEvent(event, elem) : null; + await change(); + if (toWait) { + await toWait; + } + let intendedRole = isLayout ? "AXGroup" : "AXTable"; + await untilCacheIs( + () => table.getAttributeValue("AXRole"), + intendedRole, + "Table role correct after change" + ); +} + +/* + * The following attributes should fire an attribute changed + * event, which in turn invalidates the layout-table cache + * associated with the given table. After adding and removing + * each attr, verify the table is a data or layout table, + * appropriately. Attrs: summary, abbr, scope, headers + */ +addAccessibleTask( + ` + + + + + + + + +
cell1cell2
cell3cell4
`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + // summary attr should take precedence over role="presentation" to make this + // a data table + is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table"); + + info("Removing summary attr"); + // after summary is removed, we should have a layout table + await testIsLayout( + table, + "table", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("table").removeAttribute("summary"); + }); + }, + true + ); + + info("Setting abbr attr"); + // after abbr is set we should have a data table again + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellThree") + .setAttribute("abbr", "hello world"); + }); + }, + false + ); + + info("Removing abbr attr"); + // after abbr is removed we should have a layout table again + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("cellThree").removeAttribute("abbr"); + }); + }, + true + ); + + info("Setting scope attr"); + // after scope is set we should have a data table again + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellThree") + .setAttribute("scope", "col"); + }); + }, + false + ); + + info("Removing scope attr"); + // remove scope should give layout + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("cellThree").removeAttribute("scope"); + }); + }, + true + ); + + info("Setting headers attr"); + // add headers attr should give data + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellThree") + .setAttribute("headers", "cellOne"); + }); + }, + false + ); + + info("Removing headers attr"); + // remove headers attr should give layout + await testIsLayout( + table, + "cellThree", + EVENT_OBJECT_ATTRIBUTE_CHANGED, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellThree") + .removeAttribute("headers"); + }); + }, + true + ); + } +); + +/* + * The following style changes should fire a table style changed + * event, which in turn invalidates the layout-table cache + * associated with the given table. + */ +addAccessibleTask( + ` + + + + + + + + +
cell1cell2
cell3cell4
`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + // we should start as a layout table + is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table"); + + info("Adding cell border"); + // after cell border added, we should have a data table + await testIsLayout( + table, + "cellOne", + null, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellOne") + .style.setProperty("border", "5px solid green"); + }); + }, + false + ); + + info("Removing cell border"); + // after cell border removed, we should have a layout table + await testIsLayout( + table, + "cellOne", + null, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("cellOne") + .style.removeProperty("border"); + }); + }, + true + ); + + info("Adding row background"); + // after row background added, we should have a data table + await testIsLayout( + table, + "rowOne", + null, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("rowOne") + .style.setProperty("background-color", "green"); + }); + }, + false + ); + + info("Removing row background"); + // after row background removed, we should have a layout table + await testIsLayout( + table, + "rowOne", + null, + async () => { + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("rowOne") + .style.removeProperty("background-color"); + }); + }, + true + ); + } +); + +/* + * thead/tbody elements with click handlers should: + * (a) render as AXGroup elements + * (b) expose their rows as part of their parent table's AXRows array + */ +addAccessibleTask( + ` + + + + + + + +
head row
body row
another body row
`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + + // No click handlers present on thead/tbody + let tableChildren = table.getAttributeValue("AXChildren"); + let tableRows = table.getAttributeValue("AXRows"); + + is(tableChildren.length, 4, "Table has four children (3 row + 1 col)"); + is(tableRows.length, 3, "Table has three rows"); + + for (let i = 0; i < tableChildren.length; i++) { + const child = tableChildren[i]; + if (i < 3) { + is( + child.getAttributeValue("AXRole"), + "AXRow", + "Table's first 3 children are rows" + ); + } else { + is( + child.getAttributeValue("AXRole"), + "AXColumn", + "Table's last child is a column" + ); + } + } + const reorder = waitForEvent(EVENT_REORDER); + await invokeContentTask(browser, [], () => { + const head = content.document.getElementById("thead"); + const body = content.document.getElementById("tbody"); + + head.addEventListener("click", function () {}); + body.addEventListener("click", function () {}); + }); + await reorder; + + // Click handlers present + tableChildren = table.getAttributeValue("AXChildren"); + + is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)"); + is( + tableChildren[0].getAttributeValue("AXRole"), + "AXGroup", + "Child one is a group" + ); + is( + tableChildren[0].getAttributeValue("AXChildren").length, + 1, + "Child one has one child" + ); + + is( + tableChildren[1].getAttributeValue("AXRole"), + "AXGroup", + "Child two is a group" + ); + is( + tableChildren[1].getAttributeValue("AXChildren").length, + 2, + "Child two has two children" + ); + + is( + tableChildren[2].getAttributeValue("AXRole"), + "AXColumn", + "Child three is a col" + ); + + tableRows = table.getAttributeValue("AXRows"); + is(tableRows.length, 3, "Table has three rows"); + } +); diff --git a/accessible/tests/browser/mac/browser_text_basics.js b/accessible/tests/browser/mac/browser_text_basics.js new file mode 100644 index 0000000000..e4f0bbfa18 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_basics.js @@ -0,0 +1,380 @@ +/* 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 testRangeAtMarker(macDoc, marker, attribute, expected, msg) { + let range = macDoc.getParameterizedAttributeValue(attribute, marker); + is(stringForRange(macDoc, range), expected, msg); +} + +function testUIElement( + macDoc, + marker, + msg, + expectedRole, + expectedValue, + expectedRange +) { + let elem = macDoc.getParameterizedAttributeValue( + "AXUIElementForTextMarker", + marker + ); + is( + elem.getAttributeValue("AXRole"), + expectedRole, + `${msg}: element role matches` + ); + is(elem.getAttributeValue("AXValue"), expectedValue, `${msg}: element value`); + let elemRange = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + elem + ); + is( + stringForRange(macDoc, elemRange), + expectedRange, + `${msg}: element range matches element value` + ); +} + +function testStyleRun(macDoc, marker, msg, expectedStyleRun) { + testRangeAtMarker( + macDoc, + marker, + "AXStyleTextMarkerRangeForTextMarker", + expectedStyleRun, + `${msg}: style run matches` + ); +} + +function testParagraph(macDoc, marker, msg, expectedParagraph) { + testRangeAtMarker( + macDoc, + marker, + "AXParagraphTextMarkerRangeForTextMarker", + expectedParagraph, + `${msg}: paragraph matches` + ); +} + +function testWords(macDoc, marker, msg, expectedLeft, expectedRight) { + testRangeAtMarker( + macDoc, + marker, + "AXLeftWordTextMarkerRangeForTextMarker", + expectedLeft, + `${msg}: left word matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXRightWordTextMarkerRangeForTextMarker", + expectedRight, + `${msg}: right word matches` + ); +} + +function testLines( + macDoc, + marker, + msg, + expectedLine, + expectedLeft, + expectedRight +) { + testRangeAtMarker( + macDoc, + marker, + "AXLineTextMarkerRangeForTextMarker", + expectedLine, + `${msg}: line matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXLeftLineTextMarkerRangeForTextMarker", + expectedLeft, + `${msg}: left line matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXRightLineTextMarkerRangeForTextMarker", + expectedRight, + `${msg}: right line matches` + ); +} + +function* markerIterator(macDoc, reverse = false) { + let m = macDoc.getAttributeValue( + reverse ? "AXEndTextMarker" : "AXStartTextMarker" + ); + let c = 0; + while (m) { + yield [m, c++]; + m = macDoc.getParameterizedAttributeValue( + reverse + ? "AXPreviousTextMarkerForTextMarker" + : "AXNextTextMarkerForTextMarker", + m + ); + } +} + +// Tests consistency in text markers between: +// 1. "Linked list" forward navagation +// 2. Getting markers by index +// 3. "Linked list" reverse navagation +// For each iteration method check that the returned index is consistent +function testMarkerIntegrity(accDoc, expectedMarkerValues) { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + // Iterate forward with "AXNextTextMarkerForTextMarker" + let prevMarker; + let count = 0; + for (let [marker, index] of markerIterator(macDoc)) { + count++; + let markerIndex = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is( + markerIndex, + index, + `Correct index in "AXNextTextMarkerForTextMarker": ${index}` + ); + if (prevMarker) { + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [prevMarker, marker] + ); + is( + macDoc.getParameterizedAttributeValue( + "AXLengthForTextMarkerRange", + range + ), + 1, + `[${index}] marker moved one character` + ); + } + prevMarker = marker; + + testWords( + macDoc, + marker, + `At index ${index}`, + ...expectedMarkerValues[index].words + ); + testLines( + macDoc, + marker, + `At index ${index}`, + ...expectedMarkerValues[index].lines + ); + testUIElement( + macDoc, + marker, + `At index ${index}`, + ...expectedMarkerValues[index].element + ); + testParagraph( + macDoc, + marker, + `At index ${index}`, + expectedMarkerValues[index].paragraph + ); + testStyleRun( + macDoc, + marker, + `At index ${index}`, + expectedMarkerValues[index].style + ); + } + + is(expectedMarkerValues.length, count, `Correct marker count: ${count}`); + + // Use "AXTextMarkerForIndex" to retrieve all text markers + for (let i = 0; i < count; i++) { + let marker = macDoc.getParameterizedAttributeValue( + "AXTextMarkerForIndex", + i + ); + let index = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`); + + if (i == count - 1) { + ok( + !macDoc.getParameterizedAttributeValue( + "AXNextTextMarkerForTextMarker", + marker + ), + "Iterated through all markers" + ); + } + } + + count = expectedMarkerValues.length; + + // Iterate backward with "AXPreviousTextMarkerForTextMarker" + for (let [marker] of markerIterator(macDoc, true)) { + if (count <= 0) { + ok(false, "Exceeding marker count"); + break; + } + count--; + let index = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is( + index, + count, + `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}` + ); + } + + is(count, 0, "Iterated backward through all text markers"); +} + +addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => { + const expectedValues = await SpecialPowers.spawn(browser, [], async () => { + return content.wrappedJSObject.EXPECTED; + }); + + testMarkerIntegrity(accDoc, expectedValues); +}); + +// Test text marker lesser-than operator +addAccessibleTask( + `

hello goodbye world

`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let start = macDoc.getParameterizedAttributeValue( + "AXTextMarkerForIndex", + 1 + ); + let end = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", 10); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [end, start] + ); + is(stringForRange(macDoc, range), "ello good"); + } +); + +addAccessibleTask( + `goodbye`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let input = getNativeInterface(accDoc, "input"); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + input + ); + + is(stringForRange(macDoc, range), "", "string value is correct"); + } +); + +addAccessibleTask( + `
+ + +
`, + async (browser, accDoc) => { + let box = getNativeInterface(accDoc, "box"); + const children = box.getAttributeValue("AXChildren"); + is(children.length, 2, "Listbox contains two items"); + is(children[0].getAttributeValue("AXValue"), "First item"); + is(children[1].getAttributeValue("AXValue"), "Second item"); + } +); + +addAccessibleTask( + `
+ A link should explain clearly what information the reader will get by clicking on that link. +
`, + async (browser, accDoc) => { + let t = getNativeInterface(accDoc, "t"); + const children = t.getAttributeValue("AXChildren"); + const expectedTitles = [ + "A link ", + "should", + " explain ", + "clearly", + " what information the ", + "reader", + " will get by clicking on that link. ", + ]; + is(children.length, 7, "container has seven children"); + children.forEach((child, index) => { + is(child.getAttributeValue("AXValue"), expectedTitles[index]); + }); + } +); + +addAccessibleTask( + `link `, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let input = getNativeInterface(accDoc, "input"); + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + input + ); + + let firstMarkerInInput = macDoc.getParameterizedAttributeValue( + "AXStartTextMarkerForTextMarkerRange", + range + ); + + let leftWordRange = macDoc.getParameterizedAttributeValue( + "AXLeftWordTextMarkerRangeForTextMarker", + firstMarkerInInput + ); + let str = macDoc.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + leftWordRange + ); + is(str, "hello", "Left word at start of input should be right word"); + } +); + +addAccessibleTask(`

hello world

`, async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let p = getNativeInterface(accDoc, "p"); + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + p + ); + + let bounds = macDoc.getParameterizedAttributeValue( + "AXBoundsForTextMarkerRange", + range + ); + + ok(bounds.origin && bounds.size, "Returned valid bounds"); +}); diff --git a/accessible/tests/browser/mac/browser_text_input.js b/accessible/tests/browser/mac/browser_text_input.js new file mode 100644 index 0000000000..11a9dc25f1 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_input.js @@ -0,0 +1,657 @@ +/* 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 testValueChangedEventData( + macIface, + data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft +) { + is( + data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct AXTextChangeElement" + ); + is( + data.AXTextStateChangeType, + AXTextStateChangeTypeEdit, + "Correct AXTextStateChangeType" + ); + + let changeValues = data.AXTextChangeValues; + is(changeValues.length, 1, "One element in AXTextChangeValues"); + is( + changeValues[0].AXTextChangeValue, + expectedChangeValue, + "Correct AXTextChangeValue" + ); + is( + changeValues[0].AXTextEditType, + expectedEditType, + "Correct AXTextEditType" + ); + + let textMarker = changeValues[0].AXTextChangeValueStartMarker; + ok(textMarker, "There is a AXTextChangeValueStartMarker"); + let range = macIface.getParameterizedAttributeValue( + "AXLeftWordTextMarkerRangeForTextMarker", + textMarker + ); + let str = macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range, + "correct word before caret" + ); + is(str, expectedWordAtLeft); +} + +// Return true if the first given object a subset of the second +function isSubset(subset, superset) { + if (typeof subset != "object" || typeof superset != "object") { + return superset == subset; + } + + for (let [prop, val] of Object.entries(subset)) { + if (!isSubset(val, superset[prop])) { + return false; + } + } + + return true; +} + +function matchWebArea(expectedId, expectedInfo) { + return (iface, data) => { + if (!data) { + return false; + } + + let textChangeElemID = + data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"); + + return ( + iface.getAttributeValue("AXRole") == "AXWebArea" && + textChangeElemID == expectedId && + isSubset(expectedInfo, data) + ); + }; +} + +function matchInput(expectedId, expectedInfo) { + return (iface, data) => { + if (!data) { + return false; + } + + return ( + iface.getAttributeValue("AXDOMIdentifier") == expectedId && + isSubset(expectedInfo, data) + ); + }; +} + +async function synthKeyAndTestSelectionChanged( + synthKey, + synthEvent, + expectedId, + expectedSelectionString, + expectedSelectionInfo +) { + let selectionChangedEvents = Promise.all([ + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchWebArea(expectedId, expectedSelectionInfo) + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchInput(expectedId, expectedSelectionInfo) + ), + ]); + + EventUtils.synthesizeKey(synthKey, synthEvent); + let [webareaEvent, inputEvent] = await selectionChangedEvents; + is( + inputEvent.data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct AXTextChangeElement" + ); + + let rangeString = inputEvent.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + inputEvent.data.AXSelectedTextMarkerRange + ); + is( + rangeString, + expectedSelectionString, + `selection has correct value (${expectedSelectionString})` + ); + + is( + webareaEvent.macIface.getAttributeValue("AXDOMIdentifier"), + "body", + "Input event target is top-level WebArea" + ); + rangeString = webareaEvent.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + inputEvent.data.AXSelectedTextMarkerRange + ); + is( + rangeString, + expectedSelectionString, + `selection has correct value (${expectedSelectionString}) via top document` + ); + + return inputEvent; +} + +function testSelectionEventLeftChar(event, expectedChar) { + const selStart = event.macIface.getParameterizedAttributeValue( + "AXStartTextMarkerForTextMarkerRange", + event.data.AXSelectedTextMarkerRange + ); + const selLeft = event.macIface.getParameterizedAttributeValue( + "AXPreviousTextMarkerForTextMarker", + selStart + ); + const leftCharRange = event.macIface.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [selLeft, selStart] + ); + const leftCharString = event.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + leftCharRange + ); + is(leftCharString, expectedChar, "Left character is correct"); +} + +function testSelectionEventLine(event, expectedLine) { + const selStart = event.macIface.getParameterizedAttributeValue( + "AXStartTextMarkerForTextMarkerRange", + event.data.AXSelectedTextMarkerRange + ); + const lineRange = event.macIface.getParameterizedAttributeValue( + "AXLineTextMarkerRangeForTextMarker", + selStart + ); + const lineString = event.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + lineRange + ); + is(lineString, expectedLine, "Line is correct"); +} + +async function synthKeyAndTestValueChanged( + synthKey, + synthEvent, + expectedId, + expectedTextSelectionId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft +) { + let valueChangedEvents = Promise.all([ + waitForMacEvent( + "AXSelectedTextChanged", + matchWebArea(expectedTextSelectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEvent( + "AXSelectedTextChanged", + matchInput(expectedTextSelectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEventWithInfo( + "AXValueChanged", + matchWebArea(expectedId, { + AXTextStateChangeType: AXTextStateChangeTypeEdit, + AXTextChangeValues: [ + { + AXTextChangeValue: expectedChangeValue, + AXTextEditType: expectedEditType, + }, + ], + }) + ), + waitForMacEventWithInfo( + "AXValueChanged", + matchInput(expectedId, { + AXTextStateChangeType: AXTextStateChangeTypeEdit, + AXTextChangeValues: [ + { + AXTextChangeValue: expectedChangeValue, + AXTextEditType: expectedEditType, + }, + ], + }) + ), + ]); + + EventUtils.synthesizeKey(synthKey, synthEvent); + let [, , webareaEvent, inputEvent] = await valueChangedEvents; + + testValueChangedEventData( + webareaEvent.macIface, + webareaEvent.data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft + ); + testValueChangedEventData( + inputEvent.macIface, + inputEvent.data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft + ); +} + +async function focusIntoInput(accDoc, inputId, innerContainerId) { + let selectionId = innerContainerId ? innerContainerId : inputId; + let input = getNativeInterface(accDoc, inputId); + ok(!input.getAttributeValue("AXFocused"), "input is not focused"); + ok(input.isAttributeSettable("AXFocused"), "input is focusable"); + let events = Promise.all([ + waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == inputId + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchWebArea(selectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchInput(selectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + ]); + input.setAttributeValue("AXFocused", true); + await events; +} + +async function focusIntoInputAndType(accDoc, inputId, innerContainerId) { + let selectionId = innerContainerId ? innerContainerId : inputId; + await focusIntoInput(accDoc, inputId, innerContainerId); + + async function testTextInput( + synthKey, + expectedChangeValue, + expectedWordAtLeft + ) { + await synthKeyAndTestValueChanged( + synthKey, + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeTyping, + expectedWordAtLeft + ); + } + + await testTextInput("h", "h", "h"); + await testTextInput("e", "e", "he"); + await testTextInput("l", "l", "hel"); + await testTextInput("l", "l", "hell"); + await testTextInput("o", "o", "hello"); + await testTextInput(" ", " ", "hello"); + // You would expect this to be useless but this is what VO + // consumes. I guess it concats the inserted text data to the + // word to the left of the marker. + await testTextInput("w", "w", " "); + await testTextInput("o", "o", "wo"); + await testTextInput("r", "r", "wor"); + await testTextInput("l", "l", "worl"); + await testTextInput("d", "d", "world"); + + async function testTextDelete(expectedChangeValue, expectedWordAtLeft) { + await synthKeyAndTestValueChanged( + "KEY_Backspace", + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeDelete, + expectedWordAtLeft + ); + } + + await testTextDelete("d", "worl"); + await testTextDelete("l", "wor"); + + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "o", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "wo", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true, metaKey: true }, + selectionId, + "hello ", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionBeginning, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + { shiftKey: true, altKey: true }, + selectionId, + "hello", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityWord, + } + ); +} + +// Test text input +addAccessibleTask( + `link `, + async (browser, accDoc) => { + await focusIntoInputAndType(accDoc, "input"); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test content editable +addAccessibleTask( + `

`, + async (browser, accDoc) => { + const inner = getNativeInterface(accDoc, "inner"); + const editableAncestor = inner.getAttributeValue("AXEditableAncestor"); + is( + editableAncestor.getAttributeValue("AXDOMIdentifier"), + "input", + "Editable ancestor is input" + ); + await focusIntoInputAndType(accDoc, "input"); + } +); + +// Test input that gets role::EDITCOMBOBOX +addAccessibleTask(``, async (browser, accDoc) => { + const box = getNativeInterface(accDoc, "box"); + const editableAncestor = box.getAttributeValue("AXEditableAncestor"); + is( + editableAncestor.getAttributeValue("AXDOMIdentifier"), + "box", + "Editable ancestor is box itself" + ); + await focusIntoInputAndType(accDoc, "box"); +}); + +// Test multiline caret control in a text area +addAccessibleTask( + ``, + async (browser, accDoc) => { + await focusIntoInput(accDoc, "input"); + + await synthKeyAndTestSelectionChanged("KEY_ArrowRight", null, "input", "", { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + }); + + await synthKeyAndTestSelectionChanged("KEY_ArrowDown", null, "input", "", { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + }); + + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { metaKey: true }, + "input", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionBeginning, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + + await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + { metaKey: true }, + "input", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionEnd, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test that the caret returns the correct marker when it is positioned after + * the last character (to facilitate appending text). + */ +addAccessibleTask( + ``, + async function (browser, docAcc) { + await focusIntoInput(docAcc, "input"); + + let event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + null, + "input", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + testSelectionEventLeftChar(event, "a"); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + null, + "input", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + testSelectionEventLeftChar(event, "b"); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + null, + "input", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + testSelectionEventLeftChar(event, "c"); + }, + { chrome: true, topLevel: true } +); + +/** + * Test that the caret returns the correct line when the caret is at the start + * of the line. + */ +addAccessibleTask( + ` + +
a b c
+ `, + async function (browser, docAcc) { + let hard = getNativeInterface(docAcc, "hard"); + await focusIntoInput(docAcc, "hard"); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 0); + let event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "hard", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, "cd"); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 1); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "hard", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, "ef"); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 2); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "hard", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, ""); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 3); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "hard", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, "gh"); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 4); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "hard", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, ""); + is(hard.getAttributeValue("AXInsertionPointLineNumber"), 5); + + let wrapped = getNativeInterface(docAcc, "wrapped"); + await focusIntoInput(docAcc, "wrapped"); + is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 0); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "wrapped", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, "b "); + is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 1); + event = await synthKeyAndTestSelectionChanged( + "KEY_ArrowDown", + null, + "wrapped", + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityLine, + } + ); + testSelectionEventLine(event, "c"); + is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 2); + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/tests/browser/mac/browser_text_leaf.js b/accessible/tests/browser/mac/browser_text_leaf.js new file mode 100644 index 0000000000..21deed6212 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_leaf.js @@ -0,0 +1,83 @@ +/* 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 accessibles aren't created for linebreaks. + */ +addAccessibleTask( + `hello
world`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + let children = rootGroup.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The root group contains 2 children"); + + // verify first child is correct + is( + children[0].getAttributeValue("AXRole"), + "AXStaticText", + "First child is a text node" + ); + is( + children[0].getAttributeValue("AXValue"), + "hello", + "First child is hello text" + ); + + // verify second child is correct + is( + children[1].getAttributeValue("AXRole"), + "AXStaticText", + "Second child is a text node" + ); + + is( + children[1].getAttributeValue("AXValue"), + gIsIframe && !gIsRemoteIframe ? "world" : "world ", + "Second child is world text" + ); + // we have a trailing space in here due to bug 1577028 + // but this appears fixed in non-remote iframes + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +addAccessibleTask( + `

hello, this is a test

`, + async (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + let textLeaf = p.getAttributeValue("AXChildren")[0]; + ok(textLeaf, "paragraph has a text leaf"); + + let str = textLeaf.getParameterizedAttributeValue( + "AXStringForRange", + NSRange(3, 6) + ); + + is(str, "lo, th", "AXStringForRange matches."); + + let smallBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 6) + ); + + let largeBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 8) + ); + + ok(smallBounds.size[0] < largeBounds.size[0], "longer range is wider"); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/mac/browser_text_selection.js b/accessible/tests/browser/mac/browser_text_selection.js new file mode 100644 index 0000000000..a914adba8e --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_selection.js @@ -0,0 +1,187 @@ +/* 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 simple text selection + */ +addAccessibleTask(`

Hello World

`, async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let startMarker = macDoc.getAttributeValue("AXStartTextMarker"); + let endMarker = macDoc.getAttributeValue("AXEndTextMarker"); + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [startMarker, endMarker] + ); + is(stringForRange(macDoc, range), "Hello World"); + + let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let p = content.document.getElementById("p"); + let r = new content.Range(); + r.setStart(p.firstChild, 1); + r.setEnd(p.firstChild, 8); + + let s = content.getSelection(); + s.addRange(r); + }); + await evt; + + range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "ello Wo"); + + let firstWordRange = macDoc.getParameterizedAttributeValue( + "AXRightWordTextMarkerRangeForTextMarker", + startMarker + ); + is(stringForRange(macDoc, firstWordRange), "Hello"); + + evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + macDoc.setAttributeValue("AXSelectedTextMarkerRange", firstWordRange); + await evt; + range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "Hello"); + + // Collapse selection + evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionMove && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let s = content.getSelection(); + s.collapseToEnd(); + }); + await evt; +}); + +/** + * Test text selection events caused by focus change + */ +addAccessibleTask( + `

+ Hello World, + I love +

`, + async (browser, accDoc) => { + // Set up an AXSelectedTextChanged listener here. It will get resolved + // on the first non-root event it encounters, so if we test its data at the end + // of this test it will show us the first text-selectable object that was focused, + // which is "link". + let selTextChanged = waitForMacEvent( + "AXSelectedTextChanged", + e => e.getAttributeValue("AXDOMIdentifier") != "body" + ); + + let focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("unselectable_link").focus(); + }); + let focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "unselectable_link", + "Correct event target" + ); + + focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("button").focus(); + }); + focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "button", + "Correct event target" + ); + + focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link").focus(); + }); + focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "link", + "Correct event target" + ); + + let selTextChangedTarget = await selTextChanged; + is( + selTextChangedTarget.getAttributeValue("AXDOMIdentifier"), + "link", + "Correct event target" + ); + } +); + +/** + * Test text selection with focus change + */ +addAccessibleTask( + `

Hello

`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let p = content.document.getElementById("p"); + let r = new content.Range(); + r.setStart(p.firstChild, 1); + r.setEnd(p.firstChild, 3); + + let s = content.getSelection(); + s.addRange(r); + }); + await evt; + + let range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "el"); + + let events = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged"), + waitForMacEventWithInfo("AXSelectedTextChanged"), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + let [, { data }] = await events; + ok( + data.AXTextSelectionChangedFocus, + "have AXTextSelectionChangedFocus in event info" + ); + ok(!data.AXTextStateSync, "no AXTextStateSync in editables"); + is( + data.AXTextSelectionDirection, + AXTextSelectionDirectionDiscontiguous, + "discontigous direction" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_toggle_radio_check.js b/accessible/tests/browser/mac/browser_toggle_radio_check.js new file mode 100644 index 0000000000..1695d73b0d --- /dev/null +++ b/accessible/tests/browser/mac/browser_toggle_radio_check.js @@ -0,0 +1,304 @@ +/* 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 input[type=checkbox] + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let checkbox = getNativeInterface(accDoc, "vehicle"); + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 0, + "Correct initial value" + ); + + let actions = checkbox.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 1, + "Correct checked value" + ); + + evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 0, + "Correct checked value" + ); + } +); + +/** + * Test aria-pressed toggle buttons + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + // Set up a callback to change the toggle value + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("toggle").onclick = e => { + let curVal = e.target.getAttribute("aria-pressed"); + let nextVal = curVal == "false" ? "true" : "false"; + e.target.setAttribute("aria-pressed", nextVal); + }; + }); + + let toggle = getNativeInterface(accDoc, "toggle"); + await untilCacheIs( + () => toggle.getAttributeValue("AXValue"), + 0, + "Correct initial value" + ); + + let actions = toggle.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "toggle"); + toggle.performAction("AXPress"); + await evt; + await untilCacheIs( + () => toggle.getAttributeValue("AXValue"), + 1, + "Correct checked value" + ); + + evt = waitForMacEvent("AXValueChanged", "toggle"); + toggle.performAction("AXPress"); + await evt; + await untilCacheIs( + () => toggle.getAttributeValue("AXValue"), + 0, + "Correct checked value" + ); + } +); + +/** + * Test aria-checked with tri state + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + // Set up a callback to change the toggle value + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("checkbox").onclick = e => { + const states = ["false", "true", "mixed"]; + let currState = e.target.getAttribute("aria-checked"); + let nextState = states[(states.indexOf(currState) + 1) % states.length]; + e.target.setAttribute("aria-checked", nextState); + }; + }); + let checkbox = getNativeInterface(accDoc, "checkbox"); + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 0, + "Correct initial value" + ); + + let actions = checkbox.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "checkbox"); + checkbox.performAction("AXPress"); + await evt; + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 1, + "Correct checked value" + ); + + // Changing from checked to mixed fires two events. Make sure we wait until + // the second so we're asserting based on the latest state. + evt = waitForMacEvent("AXValueChanged", (iface, data) => { + return ( + iface.getAttributeValue("AXDOMIdentifier") == "checkbox" && + iface.getAttributeValue("AXValue") == 2 + ); + }); + checkbox.performAction("AXPress"); + await evt; + is(checkbox.getAttributeValue("AXValue"), 2, "Correct checked value"); + } +); + +/** + * Test input[type=radio] + */ +addAccessibleTask( + ` + + + `, + async (browser, accDoc) => { + let huey = getNativeInterface(accDoc, "huey"); + await untilCacheIs( + () => huey.getAttributeValue("AXValue"), + 1, + "Correct initial value for huey" + ); + + let dewey = getNativeInterface(accDoc, "dewey"); + await untilCacheIs( + () => dewey.getAttributeValue("AXValue"), + 0, + "Correct initial value for dewey" + ); + + let actions = dewey.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = Promise.all([ + waitForMacEvent("AXValueChanged", "huey"), + waitForMacEvent("AXValueChanged", "dewey"), + ]); + dewey.performAction("AXPress"); + await evt; + await untilCacheIs( + () => dewey.getAttributeValue("AXValue"), + 1, + "Correct checked value for dewey" + ); + await untilCacheIs( + () => huey.getAttributeValue("AXValue"), + 0, + "Correct checked value for huey" + ); + } +); + +/** + * Test role=switch + */ +addAccessibleTask( + `
hello
`, + async (browser, accDoc) => { + let sw = getNativeInterface(accDoc, "sw"); + await untilCacheIs( + () => sw.getAttributeValue("AXValue"), + 0, + "Initially switch is off" + ); + is(sw.getAttributeValue("AXRole"), "AXCheckBox", "Has correct role"); + is(sw.getAttributeValue("AXSubrole"), "AXSwitch", "Has correct subrole"); + + let stateChanged = Promise.all([ + waitForMacEvent("AXValueChanged", "sw"), + waitForStateChange("sw", STATE_CHECKED, true), + ]); + + // We should get a state change event, and a value change. + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("sw") + .setAttribute("aria-checked", "true"); + }); + + await stateChanged; + + await untilCacheIs( + () => sw.getAttributeValue("AXValue"), + 1, + "Switch is now on" + ); + } +); + +/** + * Test input[type=checkbox] with role=menuitemcheckbox + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + let checkbox = getNativeInterface(accDoc, "vehicle"); + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 0, + "Correct initial value" + ); + + let actions = checkbox.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 1, + "Correct checked value" + ); + + evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + await untilCacheIs( + () => checkbox.getAttributeValue("AXValue"), + 0, + "Correct checked value" + ); + } +); + +/** + * Test input[type=radio] with role=menuitemradio + */ +addAccessibleTask( + ` + + + `, + async (browser, accDoc) => { + let huey = getNativeInterface(accDoc, "huey"); + await untilCacheIs( + () => huey.getAttributeValue("AXValue"), + 1, + "Correct initial value for huey" + ); + + let dewey = getNativeInterface(accDoc, "dewey"); + await untilCacheIs( + () => dewey.getAttributeValue("AXValue"), + 0, + "Correct initial value for dewey" + ); + + let actions = dewey.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = Promise.all([ + waitForMacEvent("AXValueChanged", "huey"), + waitForMacEvent("AXValueChanged", "dewey"), + ]); + dewey.performAction("AXPress"); + await evt; + await untilCacheIs( + () => dewey.getAttributeValue("AXValue"), + 1, + "Correct checked value for dewey" + ); + await untilCacheIs( + () => huey.getAttributeValue("AXValue"), + 0, + "Correct checked value for huey" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_webarea.js b/accessible/tests/browser/mac/browser_webarea.js new file mode 100644 index 0000000000..ac6122de14 --- /dev/null +++ b/accessible/tests/browser/mac/browser_webarea.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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Test web area role and AXLoadComplete event +addAccessibleTask(``, async (browser, accDoc) => { + let evt = waitForMacEvent("AXLoadComplete", (iface, data) => { + return iface.getAttributeValue("AXDescription") == "webarea test"; + }); + await SpecialPowers.spawn(browser, [], () => { + content.location = "data:text/html,webarea test"; + }); + let doc = await evt; + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "document has AXWebArea role" + ); + is(doc.getAttributeValue("AXValue"), "", "document has no AXValue"); + is(doc.getAttributeValue("AXTitle"), null, "document has no AXTitle"); + + is(doc.getAttributeValue("AXLoaded"), 1, "document has finished loading"); +}); + +// Test iframe web area role and AXLayoutComplete event +addAccessibleTask(`webarea test`, async (browser, accDoc) => { + // If the iframe loads before the top level document finishes loading, we'll + // get both an AXLayoutComplete event for the iframe and an AXLoadComplete + // event for the document. Otherwise, if the iframe loads after the + // document, we'll get one AXLoadComplete event. + let eventPromise = Promise.race([ + waitForMacEvent("AXLayoutComplete", (iface, data) => { + return iface.getAttributeValue("AXDescription") == "iframe document"; + }), + waitForMacEvent("AXLoadComplete", (iface, data) => { + return iface.getAttributeValue("AXDescription") == "webarea test"; + }), + ]); + await SpecialPowers.spawn(browser, [], () => { + const iframe = content.document.createElement("iframe"); + iframe.src = "data:text/html,iframe documenthello world"; + content.document.body.appendChild(iframe); + }); + let doc = await eventPromise; + + if (doc.getAttributeValue("AXTitle")) { + // iframe should have no title, so if we get a title here + // we've got the main document and need to get the iframe from + // the main doc + doc = doc.getAttributeValue("AXChildren")[0]; + } + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "iframe document has AXWebArea role" + ); + is(doc.getAttributeValue("AXValue"), "", "iframe document has no AXValue"); + is(doc.getAttributeValue("AXTitle"), null, "iframe document has no AXTitle"); + is( + doc.getAttributeValue("AXDescription"), + "iframe document", + "test has correct label" + ); + + is( + doc.getAttributeValue("AXLoaded"), + 1, + "iframe document has finished loading" + ); +}); diff --git a/accessible/tests/browser/mac/doc_aria_tabs.html b/accessible/tests/browser/mac/doc_aria_tabs.html new file mode 100644 index 0000000000..0c8f2afd6f --- /dev/null +++ b/accessible/tests/browser/mac/doc_aria_tabs.html @@ -0,0 +1,95 @@ + + + + + + + + ARIA: tab role - Example - code sample + + + +
+
+ + + +
+
+

Content for the first panel

+
+ + +
+ diff --git a/accessible/tests/browser/mac/doc_menulist.xhtml b/accessible/tests/browser/mac/doc_menulist.xhtml new file mode 100644 index 0000000000..d6751bc8f4 --- /dev/null +++ b/accessible/tests/browser/mac/doc_menulist.xhtml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/accessible/tests/browser/mac/doc_rich_listbox.xhtml b/accessible/tests/browser/mac/doc_rich_listbox.xhtml new file mode 100644 index 0000000000..3acaf3bff8 --- /dev/null +++ b/accessible/tests/browser/mac/doc_rich_listbox.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/accessible/tests/browser/mac/doc_textmarker_test.html b/accessible/tests/browser/mac/doc_textmarker_test.html new file mode 100644 index 0000000000..10b68b5114 --- /dev/null +++ b/accessible/tests/browser/mac/doc_textmarker_test.html @@ -0,0 +1,2424 @@ + + + + + + + +

Bob Loblaw Lobs Law Bomb

+

I love all of my children equally

+

This is the best free scrapbooking class I have ever taken

+ + +

These are my awards, Mother. From Army.

+

I , mom.

+ + + diff --git a/accessible/tests/browser/mac/doc_tree.xhtml b/accessible/tests/browser/mac/doc_tree.xhtml new file mode 100644 index 0000000000..d043fa8923 --- /dev/null +++ b/accessible/tests/browser/mac/doc_tree.xhtml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accessible/tests/browser/mac/head.js b/accessible/tests/browser/mac/head.js new file mode 100644 index 0000000000..f33f86288b --- /dev/null +++ b/accessible/tests/browser/mac/head.js @@ -0,0 +1,133 @@ +/* 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 getNativeInterface, waitForMacEventWithInfo, waitForMacEvent, waitForStateChange, + NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit, + AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove, + AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown, + AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext, + AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, + AXTextSelectionDirectionBeginning, AXTextSelectionDirectionEnd, + AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord, + AXTextSelectionGranularityLine */ + +// 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. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +// AXTextStateChangeType enum values +const AXTextStateChangeTypeEdit = 1; +const AXTextStateChangeTypeSelectionMove = 2; +const AXTextStateChangeTypeSelectionExtend = 3; + +// AXTextEditType enum values +const AXTextEditTypeDelete = 1; +const AXTextEditTypeTyping = 3; + +// AXTextSelectionDirection enum values +const AXTextSelectionDirectionUnknown = 0; +const AXTextSelectionDirectionBeginning = 1; +const AXTextSelectionDirectionEnd = 2; +const AXTextSelectionDirectionPrevious = 3; +const AXTextSelectionDirectionNext = 4; +const AXTextSelectionDirectionDiscontiguous = 5; + +// AXTextSelectionGranularity enum values +const AXTextSelectionGranularityUnknown = 0; +const AXTextSelectionGranularityCharacter = 1; +const AXTextSelectionGranularityWord = 2; +const AXTextSelectionGranularityLine = 3; + +function getNativeInterface(accDoc, id) { + return findAccessibleChildByID(accDoc, id).nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); +} + +function waitForMacEventWithInfo(notificationType, filter) { + let filterFunc = (macIface, data) => { + if (!filter) { + return true; + } + + if (typeof filter == "function") { + return filter(macIface, data); + } + + return macIface.getAttributeValue("AXDOMIdentifier") == filter; + }; + + return new Promise(resolve => { + let eventObserver = { + observe(subject, topic, data) { + let macEvent = subject.QueryInterface(Ci.nsIAccessibleMacEvent); + if ( + data === notificationType && + filterFunc(macEvent.macIface, macEvent.data) + ) { + Services.obs.removeObserver(this, "accessible-mac-event"); + resolve(macEvent); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-mac-event"); + }); +} + +function waitForMacEvent(notificationType, filter) { + return waitForMacEventWithInfo(notificationType, filter).then( + e => e.macIface + ); +} + +function NSRange(location, length) { + return { + valueType: "NSRange", + value: [location, length], + }; +} + +function NSDictionary(dict) { + return { + objectType: "NSDictionary", + object: dict, + }; +} + +function stringForRange(macDoc, range) { + if (!range) { + return ""; + } + + let str = macDoc.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range + ); + + let attrStr = macDoc.getParameterizedAttributeValue( + "AXAttributedStringForTextMarkerRange", + range + ); + + // This is a fly-by test to make sure our attributed strings + // always match our flat strings. + is( + attrStr.map(({ string }) => string).join(""), + str, + "attributed text matches non-attributed text" + ); + + return str; +} -- cgit v1.2.3