summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser/mac
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--accessible/tests/browser/mac/browser.toml95
-rw-r--r--accessible/tests/browser/mac/browser_app.js355
-rw-r--r--accessible/tests/browser/mac/browser_aria_busy.js44
-rw-r--r--accessible/tests/browser/mac/browser_aria_controls_flowto.js92
-rw-r--r--accessible/tests/browser/mac/browser_aria_current.js58
-rw-r--r--accessible/tests/browser/mac/browser_aria_expanded.js45
-rw-r--r--accessible/tests/browser/mac/browser_aria_haspopup.js320
-rw-r--r--accessible/tests/browser/mac/browser_aria_setsize.js47
-rw-r--r--accessible/tests/browser/mac/browser_attributed_text.js165
-rw-r--r--accessible/tests/browser/mac/browser_bounds.js77
-rw-r--r--accessible/tests/browser/mac/browser_details_summary.js69
-rw-r--r--accessible/tests/browser/mac/browser_focus.js44
-rw-r--r--accessible/tests/browser/mac/browser_heading.js77
-rw-r--r--accessible/tests/browser/mac/browser_hierarchy.js75
-rw-r--r--accessible/tests/browser/mac/browser_input.js225
-rw-r--r--accessible/tests/browser/mac/browser_label_title.js111
-rw-r--r--accessible/tests/browser/mac/browser_link.js222
-rw-r--r--accessible/tests/browser/mac/browser_live_regions.js165
-rw-r--r--accessible/tests/browser/mac/browser_mathml.js146
-rw-r--r--accessible/tests/browser/mac/browser_menulist.js103
-rw-r--r--accessible/tests/browser/mac/browser_navigate.js394
-rw-r--r--accessible/tests/browser/mac/browser_outline.js566
-rw-r--r--accessible/tests/browser/mac/browser_outline_xul.js274
-rw-r--r--accessible/tests/browser/mac/browser_popupbutton.js166
-rw-r--r--accessible/tests/browser/mac/browser_radio_position.js321
-rw-r--r--accessible/tests/browser/mac/browser_range.js190
-rw-r--r--accessible/tests/browser/mac/browser_required.js175
-rw-r--r--accessible/tests/browser/mac/browser_rich_listbox.js73
-rw-r--r--accessible/tests/browser/mac/browser_roles_elements.js334
-rw-r--r--accessible/tests/browser/mac/browser_rootgroup.js246
-rw-r--r--accessible/tests/browser/mac/browser_rotor.js1752
-rw-r--r--accessible/tests/browser/mac/browser_selectables.js363
-rw-r--r--accessible/tests/browser/mac/browser_table.js629
-rw-r--r--accessible/tests/browser/mac/browser_text_basics.js426
-rw-r--r--accessible/tests/browser/mac/browser_text_input.js657
-rw-r--r--accessible/tests/browser/mac/browser_text_leaf.js83
-rw-r--r--accessible/tests/browser/mac/browser_text_selection.js187
-rw-r--r--accessible/tests/browser/mac/browser_toggle_radio_check.js304
-rw-r--r--accessible/tests/browser/mac/browser_webarea.js77
-rw-r--r--accessible/tests/browser/mac/doc_aria_tabs.html95
-rw-r--r--accessible/tests/browser/mac/doc_menulist.xhtml19
-rw-r--r--accessible/tests/browser/mac/doc_rich_listbox.xhtml22
-rw-r--r--accessible/tests/browser/mac/doc_textmarker_test.html2427
-rw-r--r--accessible/tests/browser/mac/doc_tree.xhtml59
-rw-r--r--accessible/tests/browser/mac/head.js133
45 files changed, 12507 insertions, 0 deletions
diff --git a/accessible/tests/browser/mac/browser.toml b/accessible/tests/browser/mac/browser.toml
new file mode 100644
index 0000000000..b180a42ab7
--- /dev/null
+++ b/accessible/tests/browser/mac/browser.toml
@@ -0,0 +1,95 @@
+[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_busy.js"]
+
+["browser_aria_controls_flowto.js"]
+
+["browser_aria_current.js"]
+
+["browser_aria_expanded.js"]
+
+["browser_aria_haspopup.js"]
+
+["browser_aria_setsize.js"]
+
+["browser_attributed_text.js"]
+
+["browser_bounds.js"]
+
+["browser_details_summary.js"]
+
+["browser_focus.js"]
+
+["browser_heading.js"]
+
+["browser_hierarchy.js"]
+
+["browser_input.js"]
+
+["browser_label_title.js"]
+
+["browser_link.js"]
+
+["browser_live_regions.js"]
+
+["browser_mathml.js"]
+
+["browser_menulist.js"]
+
+["browser_navigate.js"]
+
+["browser_outline.js"]
+
+["browser_outline_xul.js"]
+
+["browser_popupbutton.js"]
+
+["browser_radio_position.js"]
+
+["browser_range.js"]
+
+["browser_required.js"]
+
+["browser_rich_listbox.js"]
+
+["browser_roles_elements.js"]
+
+["browser_rootgroup.js"]
+
+["browser_rotor.js"]
+
+["browser_selectables.js"]
+
+["browser_table.js"]
+
+["browser_text_basics.js"]
+
+["browser_text_input.js"]
+skip-if = ["os == 'mac'"] # Bug 1778821
+
+["browser_text_leaf.js"]
+
+["browser_text_selection.js"]
+
+["browser_toggle_radio_check.js"]
+
+["browser_webarea.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..bedefae440
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_app.js
@@ -0,0 +1,355 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+/* 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"),
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ UrlbarTestUtils.trimURL("http://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_aria_setsize.js b/accessible/tests/browser/mac/browser_aria_setsize.js
new file mode 100644
index 0000000000..e37afb4e63
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_setsize.js
@@ -0,0 +1,47 @@
+/* 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-setsize and aria-posinset
+ */
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number">
+ <div role="option" id="two" aria-setsize="5" aria-posinset="2">Two</div>
+ <div role="option" id="three" aria-setsize="5" aria-posinset="3">Three</div>
+ <div role="option" id="four" aria-setsize="5" aria-posinset="4">Four</div>
+ </div>
+ <p id="p">Hello</p>`,
+ async (browser, accDoc) => {
+ function testPosInSet(id, setsize, posinset) {
+ let elem = getNativeInterface(accDoc, id);
+ is(
+ elem.getAttributeValue("AXARIASetSize"),
+ setsize,
+ `${id}: Correct set size`
+ );
+
+ is(
+ elem.getAttributeValue("AXARIAPosInSet"),
+ posinset,
+ `${id}: Correct position in set`
+ );
+ }
+
+ testPosInSet("two", 5, 2);
+ testPosInSet("three", 5, 3);
+ testPosInSet("four", 5, 4);
+
+ let p = getNativeInterface(accDoc, "p");
+ ok(
+ !p.attributeNames.includes("AXARIASetSize"),
+ "Paragraph should not have AXARIASetSize"
+ );
+ ok(
+ !p.attributeNames.includes("AXARIAPosInSet"),
+ "Paragraph should not have AXARIAPosInSet"
+ );
+ }
+);
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..afd4d2c4dc
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_attributed_text.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 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<button></button>`,
+ 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,
+ 4,
+ "Should be 4 attribute runs for 2 texts, input and button"
+ );
+ is(attributedText[0].string, `hello `, "Attributed string is correct");
+ ok(
+ !attributedText[0].AXAttachment,
+ "Regular string attributes run doesn't have attachment"
+ );
+ is(
+ attributedText[1].AXAttachment.getAttributeValue("AXRole"),
+ "AXTextField",
+ "Entry text attribute run has correct attachment"
+ );
+ is(
+ attributedText[3].AXAttachment.getAttributeValue("AXRole"),
+ "AXButton",
+ "Button text attribute run has correct attachment"
+ );
+ is(text, `hello world${kEmbedChar}`, "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..b4597e10f4
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_link.js
@@ -0,0 +1,222 @@
+/* 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();
+ }
+);
+
+/**
+ * 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 onRecreation = waitForEvent(EVENT_SHOW, "link1");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link1").removeAttribute("href");
+ });
+ await onRecreation;
+ link1 = getNativeInterface(accDoc, "link1");
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from href gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link2").removeAttribute("onclick");
+ });
+ await onRecreation;
+ link2 = getNativeInterface(accDoc, "link2");
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from onclick gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link3");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("link3")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("href", "http://example.com");
+ });
+ await onRecreation;
+ link3 = getNativeInterface(accDoc, "link3");
+ 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"),
+ "",
+ "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..d4cf499810
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_mathml.js
@@ -0,0 +1,146 @@
+/* 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">
+ <mn>2</mn>
+ </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", "AXMathNumber", "2");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mroot id="root">
+ <mi>sin</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", "sin");
+ testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mfrac id="fraction">
+ <mn>2</mn>
+ <mn>3</mn>
+ </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", "AXMathNumber", "2");
+ testMathAttr(fraction, "AXMathFractionDenominator", "AXMathNumber", "3");
+ }
+);
+
+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..791598fed6
--- /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",
+ "AXMeter",
+ "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..af88c2e136
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_selectables.js
@@ -0,0 +1,363 @@
+/* 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"),
+ ]);
+ [
+ ["​", false, 0],
+ ["Fruits", false, 0],
+ ["Banana", true, 1],
+ ["Apple", true, 1],
+ ["Orange", true, 1],
+ ["​", false, 0],
+ ["Vegetables", false, 0],
+ ["Lettuce", true, 1],
+ ["Tomato", true, 1],
+ ["Onion", true, 1],
+ ["​", false, 0],
+ ["Spices", false, 0],
+ ["Cumin", true, 1],
+ ["Coriander", true, 1],
+ ["Allspice", true, 1],
+ ["Everything", true, 1],
+ ];
+ Assert.deepEqual(
+ childValueSelectablePairs,
+ [
+ ["​", false, false],
+ ["Fruits", false, false],
+ ["Banana", true, true],
+ ["Apple", true, true],
+ ["Orange", true, true],
+ ["​", false, false],
+ ["Vegetables", false, false],
+ ["Lettuce", true, true],
+ ["Tomato", true, true],
+ ["Onion", true, true],
+ ["​", false, false],
+ ["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")[1];
+ 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..21637d0524
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_basics.js
@@ -0,0 +1,426 @@
+/* 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");
+}
+
+// Run tests with old word segmenter
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", false],
+ ["layout.word_select.stop_at_punctuation", true], // This is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(false, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter without stop_at_punctuation.
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", false],
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, false);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter with stop_at_punctuation
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", true], // this is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// 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 <u>clearly</u> 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..9584d28e09
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_textmarker_test.html
@@ -0,0 +1,2427 @@
+<!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";
+ // eslint-disable-next-line no-unused-vars
+ function getExpected(useNewSegmenter, stopAtPunctuation) {
+ return [
+ { 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: useNewSegmenter && !stopAtPunctuation ? [" ", "Skip's"] : [" ", "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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["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: useNewSegmenter && !stopAtPunctuation ? ["Skip's", " "] : ["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;
+}