diff options
Diffstat (limited to '')
44 files changed, 12342 insertions, 0 deletions
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,<title>Two</title>" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<title>Three</title>" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<title>Four</title>" + ), + ]); + + // 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,<a id="exampleLink" href="https://example.com">link</a>', + }, + 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( + `<div id="section" role="group">Hello</div>`, + 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( + `<button aria-controls="info" id="info-button">Show info</button> + <div id="info">Information.</div> + <div id="more-info">More information.</div>`, + 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( + `<button aria-flowto="info" id="info-button">Show info</button> + <div id="info">Information.</div> + <div id="more-info">More information.</div>`, + 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( + `<input type="radio" id="cat-radio" name="animal"><label for="cat">Cat</label> + <input type="radio" id="dog-radio" name="animal" aria-flowto="info"><label for="dog">Dog</label> + <div id="info">Information.</div>`, + 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( + `<a id="one" href="%23" aria-current="page">One</a><a id="two" href="%23">Two</a>`, + 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<br> + <button aria-expanded="false" id="b">I am a button</button><br> + 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( + ` + <button aria-haspopup="false" id="false">action</button> + + <button aria-haspopup="menu" id="menu">action</button> + + <button aria-haspopup="listbox" id="listbox">action</button> + + <button aria-haspopup="tree" id="tree">action</button> + + <button aria-haspopup="grid" id="grid">action</button> + + <button aria-haspopup="dialog" id="dialog">action</button> + + `, + 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( + `<h1>hello <a href="#" id="a1">world</a></h1> + <p>this <b style="color: red; background-color: yellow;" aria-invalid="spelling">is</b> <span style="text-decoration: underline dotted green;">a</span> <a href="#" id="a2">test</a></p>`, + 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( + `<textarea id="t">hello worlf, i love you</textarea>`, + 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 <input id="input"> 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<br> + <div id="hello" style="display:inline;">hello</div><br> + <div id="world" style="display:inline;">hello world<br>I am some text</div>`, + 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<br> + <div id="hello" style="display:inline; position:absolute; left:-2000px;">hello</div><br> + <div id="world" style="display:inline; position:absolute; left:-2000px;">hello world<br>I am some text</div>`, + 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( + `<details id="details"><summary id="summary">Foo</summary><p>Bar</p></details>`, + 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( + ` + <div role="button" id="ariabutton">hello</div> <button id="button">world</button> + `, + 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( + ` + <h1 id="single-line-content">We’re building a richer search experience</h1> + <h1 id="multi-lines-content"> +We’re building a +richest +search experience + </h1> + `, + 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( + ` + <h1 id="a">Hello <a href="#">world</a></h1> + <h1 id="b">Hello</h1> + <h1 id="c" aria-label="Goodbye">Hello</h1> + `, + 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( + `<p id="p">Hello <a href="#" id="link">strange</a> 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( + `<div id="textbox" role="textbox">Hello <a href="#">strange</a> world</div>`, + (browser, accDoc) => { + let textbox = getNativeInterface(accDoc, "textbox"); + + is( + textbox.getAttributeValue("AXChildren").length, + 3, + "textbox has 3 children" + ); + } +); + +/** + * Test textbox with one child + */ +addAccessibleTask( + `<div id="textbox" role="textbox">Hello </div>`, + 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( + `<input aria-label="Name" id="input" value="Elmer Fudd">`, + testInput +); + +/** + * contenteditable selection test + */ +addAccessibleTask( + `<div aria-label="Name" tabindex="0" role="textbox" aria-multiline="true" id="input" contenteditable> + <p>Elmer Fudd</p> + </div>`, + testInput +); + +/** + * test contenteditable with selection that extends past editable part + */ +addAccessibleTask( + `<span aria-label="Name" + tabindex="0" + role="textbox" + id="input" + contenteditable>Elmer Fudd</span> <span id="notinput">is the name</span>`, + 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( + `<div id="outer" role="textbox" contenteditable="true"> + <p id="p">Bob <a href="#" id="link">Loblaw's</a></p> + <div id="inner" role="textbox" contenteditable="true"> + Law <a href="#" id="inner_link">Blog</a> + </div> + </div>`, + (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( + `<label for="n1">Label</label> <input id="n1"> + <label for="n2">Two</label> <label for="n2">Labels</label> <input id="n2"> + <input aria-label="ARIA Label" id="n3">`, + (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( + `<fieldset id="fieldset"><legend>Fields</legend><input aria-label="hello"></fieldset>`, + (browser, accDoc) => { + let fieldset = getNativeInterface(accDoc, "fieldset"); + is(fieldset.getAttributeValue("AXDescription"), "Fields"); + } +); + +/** + * Test to see that list items don't get titled groups + */ +addAccessibleTask( + `<ul style="list-style: none;"><li id="unstyled-item">Hello</li></ul> + <ul><li id="styled-item">World</li></ul>`, + (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( + `<div id="elem" aria-label="Hello world"></div>`, + 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( + `<article id="article" aria-label="Hello world"></article>`, + 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( + `<label for="input">Your favorite number?</label><input type="text" name="input" value="11" id="input" aria-label="The best number you know of">`, + 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( + ` + <a id="link" href="http://www.example.com/">I am a non-visited link</a><br> + `, + 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( + ` + <a id="link1" href="#">I am a link link</a> + <a id="link2" onclick="console.log('hi')">I am a link-ish link</a> + <a id="link3">I am a non-link link</a> + `, + 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 <a> 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", + "<a> 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", + "<a> 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( + ` + <a id="link0" href="http://example.com">I am a link</a> + <a id="link1" href="#">I am a link with an empty anchor</a> + <a id="link2" href="#hello">I am a link with no corresponding element</a> + <a id="link3" href="#world">I am a link with a corresponding element</a> + <a id="link4" href="#empty">I jump to an empty element</a> + <a id="link5" href="#namedElem">I jump to a named element</a> + <a id="link6" href="#emptyNamed">I jump to an empty named element</a> + <h1 id="world">I am that element</h1> + <h2 id="empty"></h2> + <a name="namedElem">I have a name</a> + <a name="emptyNamed"></a> + <h3>I have no name and no ID</h3> + <h4></h4> + `, + 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( + ` + <div id="polite" aria-relevant="removals">Polite region</div> + <div id="assertive" aria-live="assertive">Assertive region</div> + `, + 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, + <div id="region-1" aria-live="polite"></div> + <div id="region-2" aria-live="assertive"></div> + <div id="region-3" aria-live="off"></div> + <div id="alert" role="alert"></div> + <div id="status" role="status"></div> + <output id="output"></output>`; + }); + 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( + ` + <div id="live" aria-live="polite"> + The time is <span id="time">4:55pm</span> + <p id="p" style="display: none">Georgia on my mind</p> + <button id="button" aria-label="Start"></button> + </div> + `, + 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( + `<math id="math"> + <msqrt id="sqrt"> + <mi>-1</mi> + </msqrt> + </math>`, + 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( + `<math> + <mroot id="root"> + <mi>x</mi> + <mn>3</mn> + </mroot> + </math>`, + 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( + `<math> + <mfrac id="fraction"> + <mi>a</mi> + <mi>b</mi> + </mfrac> + </math>`, + 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( + `<math> + <msubsup id="subsup"> + <mo>∫</mo> + <mn>0</mn> + <mn>1</mn> + </msubsup> + </math>`, + 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( + `<math> + <munderover id="underover"> + <mo>∫</mo> + <mn>0</mn> + <mi>∞</mi> + </munderover> + </math>`, + 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( + `<h1 id="hello">hello</h1> + world<br> + <a href="example.com" id="link">I am a link</a> + <h1 id="goodbye">goodbye</h1>`, + 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( + ` + <h1 id="a">a</h1> + <h2 id="b">b</h2> + <h3 id="c">c</h3> + <h4 id="d">d</h4> + <h5 id="e">e</h5> + <h6 id="f">f</h5> + <h1 id="g">g</h1> + <h2 id="h">h</h2> + <h3 id="i">i</h3> + <h4 id="j">j</h4> + <h5 id="k">k</h5> + <h6 id="l">l</h5> + 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( + ` + <blockquote id="first">hello I am a blockquote</blockquote> + <blockquote id="second"> + I am also a blockquote of the same level + <br> + <blockquote id="third">but I have a different level</blockquote> + </blockquote> + `, + (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( + ` + <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br> + <a href="http://example.com"> + <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + </a> + <img src="" id="img3"> + `, + (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( + ` + <h3 id="tree1"> + Foods + </h3> + <ul role="tree" aria-labelledby="tree1" id="outline"> + <li role="treeitem" aria-expanded="false"> + <span> + Fruits + </span> + <ul> + <li role="none">Oranges</li> + <li role="treeitem" aria-expanded="true"> + <span> + Apples + </span> + <ul role="group"> + <li role="none">Honeycrisp</li> + <li role="none">Granny Smith</li> + </ul> + </li> + </ul> + </li> + <li id="vegetables" role="treeitem" aria-expanded="false"> + <span> + Vegetables + </span> + <ul role="group"> + <li role="treeitem" aria-expanded="true"> + <span> + Podded Vegetables + </span> + <ul role="group"> + <li role="none">Lentil</li> + <li role="none">Pea</li> + </ul> + </li> + </ul> + </li> + </ul> + `, + 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( + ` + <h3 id="tree1"> + Foods + </h3> + <ul role="tree" aria-labelledby="tree1" id="outline"> + <li role="treeitem" + aria-level="1" + aria-setsize="2" + aria-posinset="1" + aria-expanded="false"> + <span> + Fruits + </span> + <ul> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Oranges + </li> + <li role="treeitem" + aria-level="2" + aria-setsize="2" + aria-posinset="2" + aria-expanded="true"> + <span> + Apples + </span> + <ul role="group"> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Honeycrisp + </li> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="2"> + Granny Smith + </li> + </ul> + </li> + </ul> + </li> + <li role="treeitem" + aria-level="1" + aria-setsize="2" + aria-posinset="2" + aria-expanded="false"> + <span> + Vegetables + </span> + <ul role="group"> + <li role="treeitem" + aria-level="2" + aria-setsize="1" + aria-posinset="1" + aria-expanded="true"> + <span> + Podded Vegetables + </span> + <ul role="group"> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Lentil + </li> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="2"> + Pea + </li> + </ul> + </li> + </ul> + </li> + </ul> + `, + 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( + ` + <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles"> + <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div> + <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="false">Shared items</div> + </div> + `, + 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( + ` + <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles"> + <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div> + <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="true">Shared items</div> + </div> + `, + 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( + ` + <div role="tree" id="tree"> + <div role="treeitem" aria-checked="false" id="l1"> + Leaf 1 + </div> + <div role="treeitem" aria-checked="true" id="l2"> + Leaf 2 + </div> + <div role="treeitem" id="l3"> + Leaf 3 + </div> + <div role="treeitem" aria-checked="mixed" id="l4"> + Leaf 4 + </div> + </div> + + `, + 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( + `<select id="select" aria-label="Choose a number"> + <option id="one" selected>One</option> + <option id="two">Two</option> + <option id="three">Three</option> + <option id="four" disabled>Four</option> + </select>`, + 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( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Regular crust + </div> + <div role="radio" + id="radioGroupItem2"> + Deep dish + </div> + <div role="radio" + id="radioGroupItem3"> + Thin crust + </div> + </div>`, + 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( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Option One + </div> + </div>`, + 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( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>`, + 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( + `<input type="radio" id="cat" name="one"><label for="cat">Cat</label> + <input type="radio" id="dog" name="two"><label for="dog">Dog</label> + <input type="radio" id="catdog"><label for="catdog">CatDog</label>`, + 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( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <div> + <span> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + </span> + </div> + <div> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label> + </div>`, + 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( + `<div id="container"><input type="radio" id="cat" name="animal"></div>`, + 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( + `<input id="range" type="range" min="1" max="100" value="1" step="10">`, + 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( + `<input id="range" type="range" min="1" max="100" value="1" step="10">`, + 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( + `<input type="number" value="11" id="number" step=".05">`, + 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( + `<input type="number" value="11" id="number">`, + 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( + `<div aria-valuetext="High" id="slider" aria-orientation="horizontal" role="slider" aria-valuenow="2" aria-valuemin="0" aria-valuemax="3"></div>`, + 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( + ` + <form> + <input type="checkbox" id="checkbox" required> + <br> + <input type="radio" id="radio" required> + <br> + <input type="checkbox" id="ariaCheckbox" aria-required="true"> + <br> + <input type="radio" id="ariaRadio" aria-required="true"> + </form> + `, + 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( + ` + <!-- WAI-ARIA landmark roles --> + <div id="application" role="application"></div> + <div id="banner" role="banner"></div> + <div id="complementary" role="complementary"></div> + <div id="contentinfo" role="contentinfo"></div> + <div id="form" role="form"></div> + <div id="main" role="main"></div> + <div id="navigation" role="navigation"></div> + <div id="search" role="search"></div> + <div id="searchbox" role="searchbox"></div> + + <!-- DPub landmarks --> + <div id="dPubNavigation" role="doc-index"></div> + <div id="dPubRegion" role="doc-introduction"></div> + + <!-- Other WAI-ARIA widget roles --> + <div id="alert" role="alert"></div> + <div id="alertdialog" role="alertdialog"></div> + <div id="article" role="article"></div> + <div id="code" role="code"></div> + <div id="dialog" role="dialog"></div> + <div id="ariaDocument" role="document"></div> + <div id="log" role="log"></div> + <div id="marquee" role="marquee"></div> + <div id="ariaMath" role="math"></div> + <div id="note" role="note"></div> + <div id="ariaRegion" aria-label="region" role="region"></div> + <div id="ariaStatus" role="status"></div> + <div id="switch" role="switch"></div> + <div id="timer" role="timer"></div> + <div id="tooltip" role="tooltip"></div> + <input type="radio" role="menuitemradio" id="menuitemradio"> + <input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox"> + <input type="datetime-local" id="datetime"> + + <!-- text entries --> + <div id="textbox_multiline" role="textbox" aria-multiline="true"></div> + <div id="textbox_singleline" role="textbox" aria-multiline="false"></div> + <textarea id="textArea"></textarea> + <input id="textInput"> + + <!-- True HTML5 search box --> + <input type="search" id="htmlSearch" /> + + <!-- A button morphed into a toggle via ARIA --> + <button id="toggle" aria-pressed="false"></button> + + <!-- A button with a 'banana' role description --> + <button id="banana" aria-roledescription="banana"></button> + + <!-- Other elements --> + <del id="deletion">Deleted text</del> + <dl id="dl"><dt id="dt">term</dt><dd id="dd">definition</dd></dl> + <hr id="hr" /> + <ins id="insertion">Inserted text</ins> + <meter id="meter" min="0" max="100" value="24">meter text here</meter> + <sub id="sub">sub text here</sub> + <sup id="sup">sup text here</sup> + + <!-- Some SVG stuff --> + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="g"> + <title>g</title> + </g> + <rect width="300" height="100" id="rect" + style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)"> + <title>rect</title> + </rect> + <circle cx="100" cy="50" r="40" stroke="black" id="circle" + stroke-width="2" fill="red"> + <title>circle</title> + </circle> + <ellipse cx="300" cy="80" rx="100" ry="50" id="ellipse" + style="fill:yellow;stroke:purple;stroke-width:2"> + <title>ellipse</title> + </ellipse> + <line x1="0" y1="0" x2="200" y2="200" id="line" + style="stroke:rgb(255,0,0);stroke-width:2"> + <title>line</title> + </line> + <polygon points="200,10 250,190 160,210" id="polygon" + style="fill:lime;stroke:purple;stroke-width:1"> + <title>polygon</title> + </polygon> + <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" id="polyline" + style="fill:none;stroke:black;stroke-width:3" > + <title>polyline</title> + </polyline> + <path d="M150 0 L75 200 L225 200 Z" id="path"> + <title>path</title> + </path> + <image x1="25" y1="80" width="50" height="20" id="image" + xlink:href="../moz.png"> + <title>image</title> + </image> + </svg>`, + (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( + ` + <figure id="figure"> + <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" alt="Logo"> + <p>Non-image figure content</p> + <figcaption id="figcaption">Old Mozilla logo</figcaption> + </figure>`, + (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(`<button>hello world</button>`, 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( + `<p id="p1">hello</p><p>world</p>`, + 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( + `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`, + 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( + `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`, + 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( + `<button id="button">I am a button</button>`, + 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( + `<body role="dialog" aria-labelledby="h"> + <h1 id="h"> + We're building a richer search experience + </h1> + </body>`, + 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( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>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( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>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( + `<article id="google"> + <h2>Google Chrome</h2> + <p>Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!</p> + </article> + + <article id="moz"> + <h2>Mozilla Firefox</h2> + <p>Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.</p> + </article> + + <article id="microsoft"> + <h2>Microsoft Edge</h2> + <p>Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.</p> + </article> `, + 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( + ` + <table id="shapes"> + <tr> + <th>Shape</th> + <th>Color</th> + <th>Do I like it?</th> + </tr> + <tr> + <td>Triangle</td> + <td>Green</td> + <td>No</td> + </tr> + <tr> + <td>Square</td> + <td>Red</td> + <td>Yes</td> + </tr> + </table> + <br> + <table id="food"> + <tr> + <th>Grocery Item</th> + <th>Quantity</th> + </tr> + <tr> + <td>Onions</td> + <td>2</td> + </tr> + <tr> + <td>Yogurt</td> + <td>1</td> + </tr> + <tr> + <td>Spinach</td> + <td>1</td> + </tr> + <tr> + <td>Cherries</td> + <td>12</td> + </tr> + <tr> + <td>Carrots</td> + <td>5</td> + </tr> + </table> + <br> + <div role="table" id="ariaTable"> + <div role="row"> + <div role="cell"> + I am a tiny aria table + </div> + </div> + </div> + <br> + <table role="grid" id="grid"> + <tr> + <th>A</th> + <th>B</th> + <th>C</th> + <th>D</th> + <th>E</th> + </tr> + <tr> + <th>F</th> + <th>G</th> + <th>H</th> + <th>I</th> + <th>J</th> + </tr> + </table> + `, + 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( + ` + <header id="header"> + <h1>This is a heading within a header</h1> + </header> + + <nav id="nav"> + <a href="example.com">I am a link in a nav</a> + </nav> + + <main id="main"> + I am some text in a main element + </main> + + <footer id="footer"> + <h2>Heading in footer</h2> + </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( + ` + <div id="banner" role="banner"> + <h1>This is a heading within a banner</h1> + </div> + + <div id="nav" role="navigation"> + <a href="example.com">I am a link in a nav</a> + </div> + + <div id="main" role="main"> + I am some text in a main element + </div> + + <div id="contentinfo" role="contentinfo"> + <h2>Heading in contentinfo</h2> + </div> + `, + 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( + ` + <button id="button">hello world</button><br> + + <input type="button" value="another kinda button" id="input"><br> + `, + 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( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>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( + ` + <form> + <h2>input[type=button]</h2> + <input type="button" value="apply" id="button1"> + + <h2>input[type=submit]</h2> + <input type="submit" value="submit now" id="submit"> + + <h2>input[type=image]</h2> + <input type="image" src="sample.jpg" alt="submit image" id="image"> + + <h2>input[type=reset]</h2> + <input type="reset" value="reset now" id="reset"> + + <h2>button element</h2> + <button id="button2">Submit button</button> + </form> + `, + 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( + ` + <input type="text" value="I'm a text field." id="text"><br> + <input type="text" value="me too" id="implText"><br> + <textarea id="textarea">this is some text in a text area</textarea><br> + <input type="tel" value="0000000000" id="tel"><br> + <input type="url" value="https://example.com" id="url"><br> + <input type="email" value="hi@example.com" id="email"><br> + <input type="password" value="blah" id="password"><br> + <input type="month" value="2020-01" id="month"><br> + <input type="week" value="2020-W01" id="week"><br> + <input type="number" value="12" id="number"><br> + <input type="range" value="12" min="0" max="20" id="range"><br> + <input type="date" value="2020-01-01" id="date"><br> + <input type="time" value="10:10:10" id="time"><br> + `, + 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( + ` + <fieldset> + <legend>Radios</legend> + <div role="radiogroup" id="radios"> + <input id="radio1" type="radio" name="g1" checked="checked"> Radio 1 + <input id="radio2" type="radio" name="g1"> Radio 2 + </div> + </fieldset> + + <fieldset id="checkboxes"> + <legend>Checkboxes</legend> + <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1 + <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2 + </fieldset> + + <fieldset id="switches"> + <legend>Switches</legend> + <input id="switch1" name="g3" role="switch" type="checkbox">Switch 1 + <input checked="checked" id="switch2" name="g3" role="switch" type="checkbox">Switch 2 + </fieldset> + `, + 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( + ` + <input role="spinbutton" id="spinbutton" type="number" value="25"> + + <details id="details"> + <summary>Hello</summary> + world + </details> + + <ul role="tree" id="tree"> + <li role="treeitem">item1</li> + <li role="treeitem">item1</li> + </ul> + + <a id="buttonMenu" role="button">Click Me</a> + `, + 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( + ` + <a href="" id="empty">empty link</a> + <a href="http://www.example.com/" id="href">Example link</a> + <a id="noHref">link without href</a> + `, + 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( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>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( + ` + <fieldset id="checkboxes"> + <legend>Checkboxes</legend> + <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1 + <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2 + <div id="checkbox3" role="checkbox">Checkbox 3</div> + <div id="checkbox4" role="checkbox" aria-checked="true">Checkbox 4</div> + </fieldset> + `, + 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( + ` + <div role="radiogroup" id="radios" aria-labelledby="desc"> + <h1 id="desc">some radio buttons</h1> + <div id="radio1" role="radio"> Radio 1</div> + <div id="radio2" role="radio"> Radio 2</div> + </div> + `, + 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( + ` + <input type="text" value="I'm a text field." id="text"><br> + <input type="text" value="me too" id="implText"><br> + <textarea id="textarea">this is some text in a text area</textarea><br> + <input type="tel" value="0000000000" id="tel"><br> + <input type="url" value="https://example.com" id="url"><br> + <input type="email" value="hi@example.com" id="email"><br> + <input type="password" value="blah" id="password"><br> + <input type="month" value="2020-01" id="month"><br> + <input type="week" value="2020-W01" id="week"><br> + `, + 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( + ` + <h1>Hello I am a heading</h1> + This is some regular text.<p>this is some paragraph text</p><br> + This is a list:<ul> + <li>List item one</li> + <li>List item two</li> + </ul> + + <a href="http://example.com">This is a link</a> + `, + 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( + ` + <ul id="unordered"> + <li>hello</li> + <li>world</li> + </ul> + + <ol id="ordered"> + <li>item one</li> + <li>item two</li> + </ol> + `, + 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( + ` + <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br> + <a href="http://example.com"> + <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + </a> + <img src="" id="img3"> + `, + (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( + ` + <iframe id="frame1" src="data:text/html,<h1>hello</h1>world"></iframe> + <iframe id="frame2" src="data:text/html,<iframe id='frame3' src='data:text/html,<h1>goodbye</h1>'>"></iframe> + `, + 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( + ` + <h1>Hello I am a heading</h1> + This is some regular text.<p>this is some paragraph text</p><br> + This is a list:<ul> + <li>List item one</li> + <li>List item two</li> + </ul> + + <a href="http://example.com">This is a link</a> + `, + 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( + ` + <div id="searchroot"><p id="p1">hello</p><p id="p2">world</p></div> + <div><p>goodybe</p></div> + `, + 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( + ` + <p>It's about the future, isn't it?</p> + <p>Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.</p> + <ul> + <li>I could hang out, you could show me around.</li> + <li>There's that word again, heavy.</li> + </ul> + `, + 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('<p id="p">hello</p>', 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( + `<select id="select" aria-label="Choose a number" multiple> + <option id="one" selected>One</option> + <option id="two">Two</option> + <option id="three">Three</option> + <option id="four" disabled>Four</option> + </select>`, + 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( + `<select id="select" aria-label="Choose a thing" multiple> + <optgroup label="Fruits"> + <option id="banana" selected>Banana</option> + <option id="apple">Apple</option> + <option id="orange">Orange</option> + </optgroup> + <optgroup label="Vegetables"> + <option id="lettuce" selected>Lettuce</option> + <option id="tomato">Tomato</option> + <option id="onion">Onion</option> + </optgroup> + <optgroup label="Spices"> + <option id="cumin">Cumin</option> + <option id="coriander">Coriander</option> + <option id="allspice" selected>Allspice</option> + </optgroup> + <option id="everything">Everything</option> + </select>`, + 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( + `<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true"> + <div role="option" id="one" aria-selected="true">One</div> + <div role="option" id="two">Two</div> + <div role="option" id="three">Three</div> + <div role="option" id="four" aria-disabled="true">Four</div> +</div>`, + 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( + `<table id="customers"> + <tbody> + <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr> + <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr> + <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr> + <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr> + </tbody> + </table>`, + 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( + `<table id="table"> + <tr> + <th colspan="2" id="header1">Header 1</th> + <th id="header2">Header 2</th> + </tr> + <tr> + <td id="cell1">one</td> + <td id="cell2" rowspan="2">two</td> + <td id="cell3">three</td> + </tr> + <tr> + <td id="cell4">four</td> + <td id="cell5">five</td> + </tr> + </table>`, + (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( + `<table id="table"> + <tr> + <td>Foo</td> + </tr> + </table>`, + (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( + `<div id="table" role="table"> + <span style="display: block;"> + <div role="row"> + <div role="cell">Cell 1</div> + <div role="cell">Cell 2</div> + </div> + </span> + <span style="display: block;"> + <div role="row"> + <span style="display: block;"> + <div role="cell">Cell 3</div> + <div role="cell">Cell 4</div> + </span> + </div> + </span> + </div>`, + 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( + `<table id="table" summary="example summary"> + <tr role="presentation"> + <td id="cellOne">cell1</td> + <td>cell2</td> + </tr> + <tr> + <td id="cellThree">cell3</td> + <td>cell4</td> + </tr> + </table>`, + 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( + `<table id="table"> + <tr id="rowOne"> + <td id="cellOne">cell1</td> + <td>cell2</td> + </tr> + <tr> + <td>cell3</td> + <td>cell4</td> + </tr> + </table>`, + 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( + `<table id="table"> + <thead id="thead"> + <tr><td>head row</td></tr> + </thead> + <tbody id="tbody"> + <tr><td>body row</td></tr> + <tr><td>another body row</td></tr> + </tbody> + </table>`, + 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( + `<p id="p">hello <a id="a" href="#">goodbye</a> world</p>`, + 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( + `<input id="input" value=""><a href="#">goodbye</a>`, + 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( + `<div role="listbox" id="box"> + <input type="radio" name="test" role="option" title="First item"/> + <input type="radio" name="test" role="option" title="Second item"/> + </div>`, + 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( + `<div id="t"> + A link <b>should</b> explain <em>clearly</em> what information the <i>reader</i> will get by clicking on that link. + </div>`, + 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( + `<a href="#">link</a> <input id="input" value="hello">`, + 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(`<p id="p">hello world</p>`, 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( + `<a href="#">link</a> <input id="input">`, + async (browser, accDoc) => { + await focusIntoInputAndType(accDoc, "input"); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test content editable +addAccessibleTask( + `<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`, + 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(`<input type="text" id="box">`, 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( + `<textarea id="input" cols="15">one two three four five six seven eight</textarea>`, + 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( + `<input id="input" value="abc">`, + 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( + ` +<textarea id="hard">ab +cd +ef + +gh +</textarea> +<div role="textbox" id="wrapped" contenteditable style="width: 1ch;">a b c</div> + `, + 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<br>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( + `<p id="p">hello, this is a test</p>`, + 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(`<p id="p">Hello World</p>`, 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( + `<p> + Hello <a href="#" id="link">World</a>, + I <a href="#" style="user-select: none;" id="unselectable_link">love</a> + <button id="button">you</button></p>`, + 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( + `<p id="p">Hello <input id="input"></p>`, + 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( + `<input type="checkbox" id="vehicle"><label for="vehicle"> Bike</label>`, + 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( + `<button id="toggle" aria-pressed="false">toggle</button>`, + 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( + `<button role="checkbox" id="checkbox" aria-checked="false">toggle</button>`, + 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( + `<input type="radio" id="huey" name="drone" value="huey" checked> + <label for="huey">Huey</label> + <input type="radio" id="dewey" name="drone" value="dewey"> + <label for="dewey">Dewey</label>`, + 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( + `<div role="switch" aria-checked="false" id="sw">hello</div>`, + 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( + `<input type="checkbox" role="menuitemcheckbox" id="vehicle"><label for="vehicle"> Bike</label>`, + 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( + `<input type="radio" role="menuitemradio" id="huey" name="drone" value="huey" checked> + <label for="huey">Huey</label> + <input type="radio" role="menuitemradio" id="dewey" name="drone" value="dewey"> + <label for="dewey">Dewey</label>`, + 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,<title>webarea test</title>"; + }); + 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(`<title>webarea test</title>`, 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,<title>iframe document</title>hello 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 @@ +<!DOCTYPE html> +<html><head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta charset="utf-8"> + + <style type="text/css"> + .tabs { + padding: 1em; + } + + [role="tablist"] { + margin-bottom: -1px; + } + + [role="tab"] { + position: relative; + z-index: 1; + background: white; + border-radius: 5px 5px 0 0; + border: 1px solid grey; + border-bottom: 0; + padding: 0.2em; + } + + [role="tab"][aria-selected="true"] { + z-index: 3; + } + + [role="tabpanel"] { + position: relative; + padding: 0 0.5em 0.5em 0.7em; + border: 1px solid grey; + border-radius: 0 0 5px 5px; + background: white; + z-index: 2; + } + + [role="tabpanel"]:focus { + border-color: orange; + outline: 1px solid orange; + } + </style> + <script> + 'use strict'; + /* exported changeTabs */ + function changeTabs(target) { + const parent = target.parentNode; + const grandparent = parent.parentNode; + + // Remove all current selected tabs + parent + .querySelectorAll('[aria-selected="true"]') + .forEach(t => t.setAttribute("aria-selected", false)); + + // Set this tab as selected + target.setAttribute("aria-selected", true); + + // Hide all tab panels + grandparent + .querySelectorAll('[role="tabpanel"]') + .forEach(p => (p.hidden = true)); + + // Show the selected panel + grandparent.parentNode + .querySelector(`#${target.getAttribute("aria-controls")}`) + .removeAttribute("hidden"); + } + </script> + <title>ARIA: tab role - Example - code sample</title> +</head> +<body id="body"> + + <div class="tabs"> + <div id="tablist" role="tablist" aria-label="Sample Tabs"> + <button onclick="changeTabs(this)" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1"> + First Tab + </button> + <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2"> + Second Tab + </button> + <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3"> + Third Tab + </button> + </div> + <div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1"> + <p>Content for the first panel</p> + </div> + <div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden=""> + <p>Content for the second panel</p> + </div> + <div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden=""> + <p>Content for the third panel</p> + </div> + </div> +</body></html> 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 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <hbox> + <label control="defaultZoom" value="Zoom"/> + <hbox> + <menulist id="defaultZoom"> + <menupopup> + <menuitem label="50%" value="50"/> + <menuitem label="100%" value="100"/> + <menuitem label="150%" value="150"/> + <menuitem label="200%" value="200"/> + </menupopup> + </menulist> + </hbox> + </hbox> +</window> 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 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <richlistbox id="categories"> + <richlistitem id="general"> + <label value="general"/> + </richlistitem> + + <richlistitem id="home"> + <label value="home"/> + </richlistitem> + + <richlistitem id="search"> + <label value="search"/> + </richlistitem> + + <richlistitem id="privacy"> + <label value="privacy"/> + </richlistitem> + </richlistbox> +</window> 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 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta charset="utf-8"> + </head> + <body id="body"> + <p>Bob Loblaw Lobs Law Bomb</p> + <p>I love all of my <a href="#">children</a> equally</p> + <p>This is the <b>best</b> free scr<a href="#">apbook</a>ing class I have ever taken</p> + <ul> + <li>Fried cheese with club sauce</li> + <li>Popcorn shrimp with club sauce</li> + <li>Chicken fingers with <i>spicy</i> club sauce</li> + </ul> + <ul style="list-style: none;"><li>Do not order the Skip's Scramble</li></ul> + <p style="width: 1rem">These are my awards, Mother. From Army.</p> + <p>I <input value="deceived you">, mom.</p> + <script> + "use strict"; + window.EXPECTED = [ + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "Bob Loblaw Lobs Law Bomb", + "I love all of my children equally"], + words: ["Bomb", ""], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["I", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "of"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["of", "of"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["of", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "my"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["my", "my"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["my", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "children"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", " "], + element: ["AXStaticText", "children", "children"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "I love all of my children equally", + "This is the best free scrapbooking class I have ever taken"], + words: ["equally", ""], + element: ["AXStaticText", " equally", " equally"] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "is"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["is", "is"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["is", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "best"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", " "], + element: ["AXStaticText", "best", "best"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", " "], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "I"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["I", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "This is the best free scrapbooking class I have ever taken", + "\u2022 Fried cheese with club sauce"], + words: ["taken", ""], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", ""], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", ""], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "spicy"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", " "], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", " "], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "\u2022 Chicken fingers with spicy club sauce", + "Do not order the Skip's Scramble"], + words: ["sauce", ""], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Do", "Do"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Do", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "s"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["s", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", + "Do not order the Skip's Scramble", + "These "], + words: ["Scramble", ""], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "These ", "are "], + words: [" ", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "are ", "my "], + words: [" ", "my"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "my ", "my "], + words: ["my", "my"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "my ", "my "], + words: ["my", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "my ", "awards, "], + words: [" ", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "awards, ", "Mother. "], + words: [" ", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "Mother. ", "From "], + words: [" ", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "From ", "Army."], + words: [" ", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "Army.", "I deceived you, mom."], + words: ["Army.", ""], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "I ", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["I", " "], + element: ["AXStaticText", "I ", "I "] }, + { style: "I ", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXStaticText", "I ", "I "] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", " "], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: [" ", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["you", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["you", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["", ""], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: [",", " "], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: [" ", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", ""], + element: ["AXStaticText", ", mom.", ", mom."] }]; + </script> + </body> +</html> 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 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <tree id="tree" hidecolumnpicker="true"> + <treecols> + <treecol primary="true" label="Groceries"/> + </treecols> + <treechildren id="internalTree"> + <treeitem id="fruits" container="true" open="true"> + <treerow> + <treecell label="Fruits"/> + </treerow> + <treechildren> + <treeitem id="apple"> + <treerow> + <treecell label="Apple"/> + </treerow> + </treeitem> + <treeitem id="orange"> + <treerow> + <treecell label="Orange"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem id="veggies" container="true" open="true"> + <treerow> + <treecell label="Veggies"/> + </treerow> + <treechildren> + <treeitem id="greenVeggies" container="true" open="true"> + <treerow> + <treecell label="Green Veggies"/> + </treerow> + <treechildren> + <treeitem id="spinach"> + <treerow> + <treecell label="Spinach"/> + </treerow> + </treeitem> + <treeitem id="peas"> + <treerow> + <treecell label="Peas"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem id="squash"> + <treerow> + <treecell label="Squash"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + </treechildren> + </tree> +</window> 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; +} |