summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--accessible/tests/browser/.eslintrc.js28
-rw-r--r--accessible/tests/browser/Common.jsm456
-rw-r--r--accessible/tests/browser/Layout.jsm181
-rw-r--r--accessible/tests/browser/bounds/browser.ini13
-rw-r--r--accessible/tests/browser/bounds/browser_test_resolution.js72
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom.js69
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom_text.js76
-rw-r--r--accessible/tests/browser/bounds/head.js21
-rw-r--r--accessible/tests/browser/browser.ini34
-rw-r--r--accessible/tests/browser/browser_shutdown_acc_reference.js64
-rw-r--r--accessible/tests/browser/browser_shutdown_doc_acc_reference.js56
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js76
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js76
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js90
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js90
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_reference.js58
-rw-r--r--accessible/tests/browser/browser_shutdown_parent_own_reference.js104
-rw-r--r--accessible/tests/browser/browser_shutdown_pref.js72
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_acc_reference.js76
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js78
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_no_reference.js151
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_only.js56
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_own_reference.js191
-rw-r--r--accessible/tests/browser/browser_shutdown_scope_lifecycle.js28
-rw-r--r--accessible/tests/browser/browser_shutdown_start_restart.js51
-rw-r--r--accessible/tests/browser/e10s/browser.ini60
-rw-r--r--accessible/tests/browser/e10s/browser_caching_attributes.js134
-rw-r--r--accessible/tests/browser/e10s/browser_caching_description.js214
-rw-r--r--accessible/tests/browser/e10s/browser_caching_name.js539
-rw-r--r--accessible/tests/browser/e10s/browser_caching_relations.js96
-rw-r--r--accessible/tests/browser/e10s/browser_caching_states.js154
-rw-r--r--accessible/tests/browser/e10s/browser_caching_uniqueid.js30
-rw-r--r--accessible/tests/browser/e10s/browser_caching_value.js195
-rw-r--r--accessible/tests/browser/e10s/browser_events_announcement.js30
-rw-r--r--accessible/tests/browser/e10s/browser_events_caretmove.js22
-rw-r--r--accessible/tests/browser/e10s/browser_events_hide.js44
-rw-r--r--accessible/tests/browser/e10s/browser_events_show.js22
-rw-r--r--accessible/tests/browser/e10s/browser_events_statechange.js71
-rw-r--r--accessible/tests/browser/e10s/browser_events_textchange.js114
-rw-r--r--accessible/tests/browser/e10s/browser_events_vcchange.js87
-rw-r--r--accessible/tests/browser/e10s/browser_text_paragraph_boundary.js22
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js45
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js294
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_canvas.js28
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js60
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_doc.js320
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_gencontent.js94
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_hidden.js32
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_imagemap.js187
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_list.js52
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js48
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_listener.js38
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_optgroup.js103
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_removal.js58
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js77
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_table.js48
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_textleaf.js38
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_visibility.js342
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_whitespace.js68
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html23
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html44
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_imagemap.html21
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml11
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_visibility.html78
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_whitespace.html10
-rw-r--r--accessible/tests/browser/e10s/head.js19
-rw-r--r--accessible/tests/browser/events/browser.ini17
-rw-r--r--accessible/tests/browser/events/browser_test_A11yUtils_announce.js57
-rw-r--r--accessible/tests/browser/events/browser_test_docload.js120
-rw-r--r--accessible/tests/browser/events/browser_test_focus_browserui.js52
-rw-r--r--accessible/tests/browser/events/browser_test_focus_dialog.js76
-rw-r--r--accessible/tests/browser/events/browser_test_focus_urlbar.js410
-rw-r--r--accessible/tests/browser/events/browser_test_scrolling.js96
-rw-r--r--accessible/tests/browser/events/browser_test_selection_urlbar.js58
-rw-r--r--accessible/tests/browser/events/browser_test_textcaret.js58
-rw-r--r--accessible/tests/browser/events/head.js19
-rw-r--r--accessible/tests/browser/fission/browser.ini14
-rw-r--r--accessible/tests/browser/fission/browser_content_tree.js75
-rw-r--r--accessible/tests/browser/fission/browser_hidden_iframe.js81
-rw-r--r--accessible/tests/browser/fission/browser_nested_iframe.js153
-rw-r--r--accessible/tests/browser/fission/browser_reframe_root.js95
-rw-r--r--accessible/tests/browser/fission/browser_reframe_visibility.js116
-rw-r--r--accessible/tests/browser/fission/browser_src_change.js61
-rw-r--r--accessible/tests/browser/fission/browser_take_focus.js26
-rw-r--r--accessible/tests/browser/fission/head.js19
-rw-r--r--accessible/tests/browser/general/browser.ini9
-rw-r--r--accessible/tests/browser/general/browser_test_doc_creation.js55
-rw-r--r--accessible/tests/browser/general/browser_test_urlbar.js40
-rw-r--r--accessible/tests/browser/general/head.js72
-rw-r--r--accessible/tests/browser/head.js147
-rw-r--r--accessible/tests/browser/hittest/browser.ini15
-rw-r--r--accessible/tests/browser/hittest/browser_test_browser.js68
-rw-r--r--accessible/tests/browser/hittest/browser_test_canvas_hitregion.js92
-rw-r--r--accessible/tests/browser/hittest/browser_test_general.js123
-rw-r--r--accessible/tests/browser/hittest/browser_test_shadowroot.js61
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom.js38
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom_text.js76
-rw-r--r--accessible/tests/browser/hittest/head.js89
-rw-r--r--accessible/tests/browser/mac/browser.ini50
-rw-r--r--accessible/tests/browser/mac/browser_app.js292
-rw-r--r--accessible/tests/browser/mac/browser_aria_busy.js44
-rw-r--r--accessible/tests/browser/mac/browser_aria_controls_flowto.js84
-rw-r--r--accessible/tests/browser/mac/browser_aria_current.js60
-rw-r--r--accessible/tests/browser/mac/browser_aria_haspopup.js330
-rw-r--r--accessible/tests/browser/mac/browser_details_summary.js60
-rw-r--r--accessible/tests/browser/mac/browser_focus.js44
-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.js79
-rw-r--r--accessible/tests/browser/mac/browser_link.js125
-rw-r--r--accessible/tests/browser/mac/browser_live_regions.js165
-rw-r--r--accessible/tests/browser/mac/browser_mathml.js151
-rw-r--r--accessible/tests/browser/mac/browser_menulist.js52
-rw-r--r--accessible/tests/browser/mac/browser_navigate.js394
-rw-r--r--accessible/tests/browser/mac/browser_outline.js459
-rw-r--r--accessible/tests/browser/mac/browser_outline_xul.js274
-rw-r--r--accessible/tests/browser/mac/browser_popupbutton.js153
-rw-r--r--accessible/tests/browser/mac/browser_radio_position.js321
-rw-r--r--accessible/tests/browser/mac/browser_range.js92
-rw-r--r--accessible/tests/browser/mac/browser_required.js145
-rw-r--r--accessible/tests/browser/mac/browser_rich_listbox.js73
-rw-r--r--accessible/tests/browser/mac/browser_roles_elements.js309
-rw-r--r--accessible/tests/browser/mac/browser_rootgroup.js196
-rw-r--r--accessible/tests/browser/mac/browser_rotor.js1697
-rw-r--r--accessible/tests/browser/mac/browser_selectables.js336
-rw-r--r--accessible/tests/browser/mac/browser_table.js281
-rw-r--r--accessible/tests/browser/mac/browser_text_basics.js282
-rw-r--r--accessible/tests/browser/mac/browser_text_input.js412
-rw-r--r--accessible/tests/browser/mac/browser_text_leaf.js75
-rw-r--r--accessible/tests/browser/mac/browser_text_selection.js187
-rw-r--r--accessible/tests/browser/mac/browser_toggle_radio_check.js140
-rw-r--r--accessible/tests/browser/mac/browser_webarea.js76
-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.html2424
-rw-r--r--accessible/tests/browser/mac/doc_tree.xhtml59
-rw-r--r--accessible/tests/browser/mac/head.js114
-rw-r--r--accessible/tests/browser/scroll/browser.ini9
-rw-r--r--accessible/tests/browser/scroll/browser_test_zoom_text.js150
-rw-r--r--accessible/tests/browser/scroll/head.js19
-rw-r--r--accessible/tests/browser/shared-head.js779
-rw-r--r--accessible/tests/browser/states/browser.ini17
-rw-r--r--accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js133
-rw-r--r--accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js98
-rw-r--r--accessible/tests/browser/states/browser_test_link.js47
-rw-r--r--accessible/tests/browser/states/browser_test_visibility.js52
-rw-r--r--accessible/tests/browser/states/head.js81
-rw-r--r--accessible/tests/browser/states/target.html3
-rw-r--r--accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml10
-rw-r--r--accessible/tests/browser/telemetry/browser.ini3
-rw-r--r--accessible/tests/browser/telemetry/browser_HCM_telemetry.js177
-rw-r--r--accessible/tests/browser/tree/browser.ini11
-rw-r--r--accessible/tests/browser/tree/browser_aria_owns.js278
-rw-r--r--accessible/tests/browser/tree/browser_searchbar.js52
-rw-r--r--accessible/tests/browser/tree/browser_shadowdom.js64
-rw-r--r--accessible/tests/browser/tree/head.js34
157 files changed, 21215 insertions, 0 deletions
diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..528797cb91
--- /dev/null
+++ b/accessible/tests/browser/.eslintrc.js
@@ -0,0 +1,28 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ "mozilla/no-aArgs": "error",
+ "mozilla/reject-importGlobalProperties": ["error", "everything"],
+ "mozilla/var-only-at-top-level": "error",
+
+ "block-scoped-var": "error",
+ camelcase: ["error", { properties: "never" }],
+ complexity: ["error", 20],
+
+ "handle-callback-err": ["error", "er"],
+ "max-nested-callbacks": ["error", 4],
+ "new-cap": ["error", { capIsNew: false }],
+ "no-fallthrough": "error",
+ "no-multi-str": "error",
+ "no-proto": "error",
+ "no-return-assign": "error",
+ "no-shadow": "error",
+ "no-unused-vars": ["error", { vars: "all", args: "none" }],
+ "one-var": ["error", "never"],
+ radix: "error",
+ strict: ["error", "global"],
+ yoda: "error",
+ "no-undef-init": "error",
+ },
+};
diff --git a/accessible/tests/browser/Common.jsm b/accessible/tests/browser/Common.jsm
new file mode 100644
index 0000000000..527e301be9
--- /dev/null
+++ b/accessible/tests/browser/Common.jsm
@@ -0,0 +1,456 @@
+/* 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 EXPORTED_SYMBOLS = ["CommonUtils"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+const MAX_TRIM_LENGTH = 100;
+
+const CommonUtils = {
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if
+ * there is no accessible.
+ */
+ DONOTFAIL_IF_NO_ACC: 1,
+
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if it
+ * does not support an interface.
+ */
+ DONOTFAIL_IF_NO_INTERFACE: 2,
+
+ /**
+ * nsIAccessibilityService service.
+ */
+ get accService() {
+ if (!this._accService) {
+ this._accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ }
+
+ return this._accService;
+ },
+
+ clearAccService() {
+ this._accService = null;
+ Cu.forceGC();
+ },
+
+ /**
+ * Adds an observer for an 'a11y-consumers-changed' event.
+ */
+ addAccConsumersChangedObserver() {
+ const deferred = {};
+ this._accConsumersChanged = new Promise(resolve => {
+ deferred.resolve = resolve;
+ });
+ const observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-consumers-changed");
+ deferred.resolve(JSON.parse(data));
+ };
+ Services.obs.addObserver(observe, "a11y-consumers-changed");
+ },
+
+ /**
+ * Returns a promise that resolves when 'a11y-consumers-changed' event is
+ * fired.
+ *
+ * @return {Promise}
+ * event promise evaluating to event's data
+ */
+ observeAccConsumersChanged() {
+ return this._accConsumersChanged;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "1"
+ * which indicates that an accessibility service is initialized in the current
+ * process.
+ */
+ addAccServiceInitializedObserver() {
+ const deferred = {};
+ this._accServiceInitialized = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "1") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is shutdown unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is
+ * initialized in the current process. Otherwise (if the service is shutdown)
+ * the promise is rejected.
+ */
+ observeAccServiceInitialized() {
+ return this._accServiceInitialized;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "0"
+ * which indicates that an accessibility service is shutdown in the current
+ * process.
+ */
+ addAccServiceShutdownObserver() {
+ const deferred = {};
+ this._accServiceShutdown = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is initialized unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is shutdown
+ * in the current process. Otherwise (if the service is initialized) the
+ * promise is rejected.
+ */
+ observeAccServiceShutdown() {
+ return this._accServiceShutdown;
+ },
+
+ /**
+ * Extract DOMNode id from an accessible. If the accessible is in the remote
+ * process, DOMNode is not present in parent process. However, if specified by
+ * the author, DOMNode id will be attached to an accessible object.
+ *
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} DOMNode id if available
+ */
+ getAccessibleDOMNodeID(accessible) {
+ if (accessible instanceof Ci.nsIAccessibleDocument) {
+ // If accessible is a document, trying to find its document body id.
+ try {
+ return accessible.DOMNode.body.id;
+ } catch (e) {
+ /* This only works if accessible is not a proxy. */
+ }
+ }
+ try {
+ return accessible.DOMNode.id;
+ } catch (e) {
+ /* This will fail if DOMNode is in different process. */
+ }
+ try {
+ // When e10s is enabled, accessible will have an "id" property if its
+ // corresponding DOMNode has an id. If accessible is a document, its "id"
+ // property corresponds to the "id" of its body element.
+ return accessible.id;
+ } catch (e) {
+ /* This will fail if accessible is not a proxy. */
+ }
+
+ return null;
+ },
+
+ getObjAddress(obj) {
+ const exp = /native\s*@\s*(0x[a-f0-9]+)/g;
+ const match = exp.exec(obj.toString());
+ if (match) {
+ return match[1];
+ }
+
+ return obj.toString();
+ },
+
+ getNodePrettyName(node) {
+ try {
+ let tag = "";
+ if (node.nodeType == Node.DOCUMENT_NODE) {
+ tag = "document";
+ } else {
+ tag = node.localName;
+ if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) {
+ tag += `@id="${node.getAttribute("id")}"`;
+ }
+ }
+
+ return `"${tag} node", address: ${this.getObjAddress(node)}`;
+ } catch (e) {
+ return `" no node info "`;
+ }
+ },
+
+ /**
+ * Convert role to human readable string.
+ */
+ roleToString(role) {
+ return this.accService.getStringRole(role);
+ },
+
+ /**
+ * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
+ *
+ * @param aString the string to shorten.
+ *
+ * @returns the shortened string.
+ */
+ shortenString(str) {
+ if (str.length <= MAX_TRIM_LENGTH) {
+ return str;
+ }
+
+ // Trim the string if its length is > MAX_TRIM_LENGTH characters.
+ const trimOffset = MAX_TRIM_LENGTH / 2;
+
+ return `${str.substring(0, trimOffset - 1)}…${str.substring(
+ str.length - trimOffset,
+ str.length
+ )}`;
+ },
+
+ normalizeAccTreeObj(obj) {
+ const key = Object.keys(obj)[0];
+ const roleName = `ROLE_${key}`;
+ if (roleName in Ci.nsIAccessibleRole) {
+ return {
+ role: Ci.nsIAccessibleRole[roleName],
+ children: obj[key],
+ };
+ }
+
+ return obj;
+ },
+
+ stringifyTree(obj) {
+ let text = this.roleToString(obj.role) + ": [ ";
+ if ("children" in obj) {
+ for (let i = 0; i < obj.children.length; i++) {
+ const c = this.normalizeAccTreeObj(obj.children[i]);
+ text += this.stringifyTree(c);
+ if (i < obj.children.length - 1) {
+ text += ", ";
+ }
+ }
+ }
+
+ return `${text}] `;
+ },
+
+ /**
+ * Return pretty name for identifier, it may be ID, DOM node or accessible.
+ */
+ prettyName(identifier) {
+ if (identifier instanceof Array) {
+ let msg = "";
+ for (let idx = 0; idx < identifier.length; idx++) {
+ if (msg != "") {
+ msg += ", ";
+ }
+
+ msg += this.prettyName(identifier[idx]);
+ }
+ return msg;
+ }
+
+ if (identifier instanceof Ci.nsIAccessible) {
+ const acc = this.getAccessible(identifier);
+ const domID = this.getAccessibleDOMNodeID(acc);
+ let msg = "[";
+ try {
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ if (domID) {
+ msg += `DOM node id: ${domID}, `;
+ }
+ } else {
+ msg += `${this.getNodePrettyName(acc.DOMNode)}, `;
+ }
+ msg += `role: ${this.roleToString(acc.role)}`;
+ if (acc.name) {
+ msg += `, name: "${this.shortenString(acc.name)}"`;
+ }
+ } catch (e) {
+ msg += "defunct";
+ }
+
+ if (acc) {
+ msg += `, address: ${this.getObjAddress(acc)}`;
+ }
+ msg += "]";
+
+ return msg;
+ }
+
+ if (Node.isInstance(identifier)) {
+ return `[ ${this.getNodePrettyName(identifier)} ]`;
+ }
+
+ if (identifier && typeof identifier === "object") {
+ const treeObj = this.normalizeAccTreeObj(identifier);
+ if ("role" in treeObj) {
+ return `{ ${this.stringifyTree(treeObj)} }`;
+ }
+
+ return JSON.stringify(identifier);
+ }
+
+ return ` "${identifier}" `;
+ },
+
+ /**
+ * Return accessible for the given identifier (may be ID attribute or DOM
+ * element or accessible object) or null.
+ *
+ * @param accOrElmOrID
+ * identifier to get an accessible implementing the given interfaces
+ * @param aInterfaces
+ * [optional] the interface or an array interfaces to query it/them
+ * from obtained accessible
+ * @param elmObj
+ * [optional] object to store DOM element which accessible is obtained
+ * for
+ * @param doNotFailIf
+ * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC,
+ * DONOTFAIL_IF_NO_INTERFACE)
+ * @param doc
+ * [optional] document for when accOrElmOrID is an ID.
+ */
+ getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) {
+ if (!accOrElmOrID) {
+ return null;
+ }
+
+ let elm = null;
+ if (accOrElmOrID instanceof Ci.nsIAccessible) {
+ try {
+ elm = accOrElmOrID.DOMNode;
+ } catch (e) {}
+ } else if (Node.isInstance(accOrElmOrID)) {
+ elm = accOrElmOrID;
+ } else {
+ elm = doc.getElementById(accOrElmOrID);
+ if (!elm) {
+ Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`);
+ return null;
+ }
+ }
+
+ if (elmObj && typeof elmObj == "object") {
+ elmObj.value = elm;
+ }
+
+ let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null;
+ if (!acc) {
+ try {
+ acc = this.accService.getAccessibleFor(elm);
+ } catch (e) {}
+
+ if (!acc) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) {
+ Assert.ok(
+ false,
+ `Can't get accessible for ${this.prettyName(accOrElmOrID)}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ if (!interfaces) {
+ return acc;
+ }
+
+ if (!(interfaces instanceof Array)) {
+ interfaces = [interfaces];
+ }
+
+ for (let index = 0; index < interfaces.length; index++) {
+ if (acc instanceof interfaces[index]) {
+ continue;
+ }
+
+ try {
+ acc.QueryInterface(interfaces[index]);
+ } catch (e) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) {
+ Assert.ok(
+ false,
+ `Can't query ${interfaces[index]} for ${accOrElmOrID}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ return acc;
+ },
+
+ /**
+ * Return the DOM node by identifier (may be accessible, DOM node or ID).
+ */
+ getNode(accOrNodeOrID, doc) {
+ if (!accOrNodeOrID) {
+ return null;
+ }
+
+ if (Node.isInstance(accOrNodeOrID)) {
+ return accOrNodeOrID;
+ }
+
+ if (accOrNodeOrID instanceof Ci.nsIAccessible) {
+ return accOrNodeOrID.DOMNode;
+ }
+
+ const node = doc.getElementById(accOrNodeOrID);
+ if (!node) {
+ Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`);
+ return null;
+ }
+
+ return node;
+ },
+
+ /**
+ * Return root accessible.
+ *
+ * @param {DOMNode} doc
+ * Chrome document.
+ *
+ * @return {nsIAccessible}
+ * Accessible object for chrome window.
+ */
+ getRootAccessible(doc) {
+ const acc = this.getAccessible(doc);
+ return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null;
+ },
+
+ /**
+ * Analogy of SimpleTest.is function used to compare objects.
+ */
+ isObject(obj, expectedObj, msg) {
+ if (obj == expectedObj) {
+ Assert.ok(true, msg);
+ return;
+ }
+
+ Assert.ok(
+ false,
+ `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName(
+ expectedObj
+ )}"`
+ );
+ },
+};
diff --git a/accessible/tests/browser/Layout.jsm b/accessible/tests/browser/Layout.jsm
new file mode 100644
index 0000000000..d51644ec57
--- /dev/null
+++ b/accessible/tests/browser/Layout.jsm
@@ -0,0 +1,181 @@
+/* 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 EXPORTED_SYMBOLS = ["Layout"];
+
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+);
+
+const Layout = {
+ /**
+ * Zoom the given document.
+ */
+ zoomDocument(doc, zoom) {
+ const bc = BrowsingContext.getFromWindow(doc.defaultView);
+ bc.fullZoom = zoom;
+ },
+
+ /**
+ * Set the relative resolution of this document. This is what apz does.
+ * On non-mobile platforms you won't see a visible change.
+ */
+ setResolution(doc, zoom) {
+ const windowUtils = doc.defaultView.windowUtils;
+ windowUtils.setResolutionAndScaleTo(zoom);
+ },
+
+ /**
+ * Assert.is() function checking the expected value is within the range.
+ */
+ isWithin(expected, got, within, msg) {
+ if (Math.abs(got - expected) <= within) {
+ Assert.ok(true, `${msg} - Got ${got}`);
+ } else {
+ Assert.ok(
+ false,
+ `${msg} - Got ${got}, expected ${expected} with error of ${within}`
+ );
+ }
+ },
+
+ /**
+ * Return the accessible coordinates relative to the screen in device pixels.
+ */
+ getPos(id) {
+ const accessible = CommonUtils.getAccessible(id);
+ const x = {};
+ const y = {};
+ accessible.getBounds(x, y, {}, {});
+
+ return [x.value, y.value];
+ },
+
+ /**
+ * Return the accessible coordinates and size relative to the screen in device
+ * pixels. This methods also retrieves coordinates in CSS pixels and ensures that they
+ * match Dev pixels with a given device pixel ratio.
+ */
+ getBounds(id, dpr) {
+ const accessible = CommonUtils.getAccessible(id);
+ const x = {};
+ const y = {};
+ const width = {};
+ const height = {};
+ const xInCSS = {};
+ const yInCSS = {};
+ const widthInCSS = {};
+ const heightInCSS = {};
+ accessible.getBounds(x, y, width, height);
+ accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS);
+
+ this.isWithin(
+ x.value / dpr,
+ xInCSS.value,
+ 1,
+ "X in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ y.value / dpr,
+ yInCSS.value,
+ 1,
+ "Y in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ width.value / dpr,
+ widthInCSS.value,
+ 1,
+ "Width in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ height.value / dpr,
+ heightInCSS.value,
+ 1,
+ "Height in CSS pixels is calculated correctly"
+ );
+
+ return [x.value, y.value, width.value, height.value];
+ },
+
+ getRangeExtents(id, startOffset, endOffset, coordOrigin) {
+ const hyperText = CommonUtils.getAccessible(id, [Ci.nsIAccessibleText]);
+ const x = {};
+ const y = {};
+ const width = {};
+ const height = {};
+ hyperText.getRangeExtents(
+ startOffset,
+ endOffset,
+ x,
+ y,
+ width,
+ height,
+ coordOrigin
+ );
+
+ return [x.value, y.value, width.value, height.value];
+ },
+
+ CSSToDevicePixels(win, x, y, width, height) {
+ const winUtil = win.windowUtils;
+ const ratio = winUtil.screenPixelsPerCSSPixel;
+
+ // CSS pixels and ratio can be not integer. Device pixels are always integer.
+ // Do our best and hope it works.
+ return [
+ Math.round(x * ratio),
+ Math.round(y * ratio),
+ Math.round(width * ratio),
+ Math.round(height * ratio),
+ ];
+ },
+
+ /**
+ * Return DOM node coordinates relative the screen and its size in device
+ * pixels.
+ */
+ getBoundsForDOMElm(id, doc) {
+ let x = 0;
+ let y = 0;
+ let width = 0;
+ let height = 0;
+
+ const elm = CommonUtils.getNode(id, doc);
+ const elmWindow = elm.ownerGlobal;
+ if (elm.localName == "area") {
+ const mapName = elm.parentNode.getAttribute("name");
+ const selector = `[usemap="#${mapName}"]`;
+ const img = elm.ownerDocument.querySelector(selector);
+
+ const areaCoords = elm.coords.split(",");
+ const areaX = parseInt(areaCoords[0], 10);
+ const areaY = parseInt(areaCoords[1], 10);
+ const areaWidth = parseInt(areaCoords[2], 10) - areaX;
+ const areaHeight = parseInt(areaCoords[3], 10) - areaY;
+
+ const rect = img.getBoundingClientRect();
+ x = rect.left + areaX;
+ y = rect.top + areaY;
+ width = areaWidth;
+ height = areaHeight;
+ } else {
+ const rect = elm.getBoundingClientRect();
+ x = rect.left;
+ y = rect.top;
+ width = rect.width;
+ height = rect.height;
+ }
+
+ return this.CSSToDevicePixels(
+ elmWindow,
+ x + elmWindow.mozInnerScreenX,
+ y + elmWindow.mozInnerScreenY,
+ width,
+ height
+ );
+ },
+};
diff --git a/accessible/tests/browser/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini
new file mode 100644
index 0000000000..1997e92ff1
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/browser/*.jsm
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/mochitest/letters.gif
+
+[browser_test_resolution.js]
+skip-if = e10s && os == 'win' # bug 1372296
+[browser_test_zoom.js]
+[browser_test_zoom_text.js]
+skip-if = e10s && os == 'win' # bug 1372296
diff --git a/accessible/tests/browser/bounds/browser_test_resolution.js b/accessible/tests/browser/bounds/browser_test_resolution.js
new file mode 100644
index 0000000000..bdcb3e1792
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_resolution.js
@@ -0,0 +1,72 @@
+/* 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/layout.js */
+
+async function testScaledBounds(browser, accDoc, scale, id, type = "object") {
+ let acc = findAccessibleChildByID(accDoc, id);
+
+ // Get document offset
+ let [docX, docY] = getBounds(accDoc);
+
+ // Get the unscaled bounds of the accessible
+ let [x, y, width, height] =
+ type == "text"
+ ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE)
+ : getBounds(acc);
+
+ await invokeContentTask(browser, [scale], _scale => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+ Layout.setResolution(content.document, _scale);
+ });
+
+ let [scaledX, scaledY, scaledWidth, scaledHeight] =
+ type == "text"
+ ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE)
+ : getBounds(acc);
+
+ let name = prettyName(acc);
+ isWithin(scaledWidth, width * scale, 2, "Wrong scaled width of " + name);
+ isWithin(scaledHeight, height * scale, 2, "Wrong scaled height of " + name);
+ isWithin(scaledX - docX, (x - docX) * scale, 2, "Wrong scaled x of " + name);
+ isWithin(scaledY - docY, (y - docY) * scale, 2, "Wrong scaled y of " + name);
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+ Layout.setResolution(content.document, 1.0);
+ });
+}
+
+async function runTests(browser, accDoc) {
+ // The scrollbars get in the way of container bounds calculation.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.useOverlayScrollbars", 1]],
+ });
+
+ await testScaledBounds(browser, accDoc, 2.0, "p1");
+ await testScaledBounds(browser, accDoc, 0.5, "p2");
+ await testScaledBounds(browser, accDoc, 3.5, "b1");
+
+ await testScaledBounds(browser, accDoc, 2.0, "p1", "text");
+ await testScaledBounds(browser, accDoc, 0.75, "p2", "text");
+}
+
+/**
+ * Test accessible boundaries when page is zoomed
+ */
+addAccessibleTask(
+ `
+<p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+<p id="p2">para 2</p>
+<button id="b1">Hello</button>
+`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_zoom.js b/accessible/tests/browser/bounds/browser_test_zoom.js
new file mode 100644
index 0000000000..6600a17130
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom.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/layout.js */
+
+async function testContentBounds(browser, acc) {
+ let [
+ expectedX,
+ expectedY,
+ expectedWidth,
+ expectedHeight,
+ ] = await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc));
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(acc, contentDPR);
+ let prettyAccName = prettyName(acc);
+ is(x, expectedX, "Wrong x coordinate of " + prettyAccName);
+ is(y, expectedY, "Wrong y coordinate of " + prettyAccName);
+ is(width, expectedWidth, "Wrong width of " + prettyAccName);
+ ok(height >= expectedHeight, "Wrong height of " + prettyAccName);
+}
+
+async function runTests(browser, accDoc) {
+ let p1 = findAccessibleChildByID(accDoc, "p1");
+ let p2 = findAccessibleChildByID(accDoc, "p2");
+ let imgmap = findAccessibleChildByID(accDoc, "imgmap");
+ if (!imgmap.childCount) {
+ // An image map may not be available even after the doc and image load
+ // is complete. We don't recieve any DOM events for this change either,
+ // so we need to wait for a REORDER.
+ await waitForEvent(EVENT_REORDER, "imgmap");
+ }
+ let area = imgmap.firstChild;
+
+ await testContentBounds(browser, p1);
+ await testContentBounds(browser, p2);
+ await testContentBounds(browser, area);
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+ Layout.zoomDocument(content.document, 2.0);
+ });
+
+ await testContentBounds(browser, p1);
+ await testContentBounds(browser, p2);
+ await testContentBounds(browser, area);
+}
+
+/**
+ * Test accessible boundaries when page is zoomed
+ */
+addAccessibleTask(
+ `
+<p id="p1">para 1</p><p id="p2">para 2</p>
+<map name="atoz_map" id="map">
+ <area id="area1" href="http://mozilla.org"
+ coords=17,0,30,14" alt="mozilla.org" shape="rect">
+</map>
+<img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_zoom_text.js b/accessible/tests/browser/bounds/browser_test_zoom_text.js
new file mode 100644
index 0000000000..8abbff025d
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom_text.js
@@ -0,0 +1,76 @@
+/* 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/layout.js */
+
+async function runTests(browser, accDoc) {
+ async function testTextNode(id) {
+ let hyperTextNode = findAccessibleChildByID(accDoc, id);
+ let textNode = hyperTextNode.firstChild;
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(textNode, contentDPR);
+ testTextBounds(
+ hyperTextNode,
+ 0,
+ -1,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ }
+
+ async function testEmptyInputNode(id) {
+ let inputNode = findAccessibleChildByID(accDoc, id);
+
+ let [x, y, width, height] = getBounds(inputNode);
+ testTextBounds(
+ inputNode,
+ 0,
+ -1,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ testTextBounds(
+ inputNode,
+ 0,
+ 0,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ }
+
+ await testTextNode("p1");
+ await testTextNode("p2");
+ await testEmptyInputNode("i1");
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+ Layout.zoomDocument(content.document, 2.0);
+ });
+
+ await testTextNode("p1");
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+ Layout.zoomDocument(content.document, 1.0);
+ });
+}
+
+/**
+ * Test the text range boundary when page is zoomed
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2'>ل</p>
+ <form><input id='i1' /></form>`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/head.js b/accessible/tests/browser/bounds/head.js
new file mode 100644
index 0000000000..f4d20e636c
--- /dev/null
+++ b/accessible/tests/browser/bounds/head.js
@@ -0,0 +1,21 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+
+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 events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "layout.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/browser.ini b/accessible/tests/browser/browser.ini
new file mode 100644
index 0000000000..99acf3c0b0
--- /dev/null
+++ b/accessible/tests/browser/browser.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+skip-if = a11y_checks || (os == 'win' && processor == 'aarch64') # 1534855
+support-files =
+ !/accessible/tests/mochitest/*.js
+ *.jsm
+ head.js
+ shared-head.js
+
+[browser_shutdown_acc_reference.js]
+skip-if = (os == 'linux' && debug && bits == 64) #Bug 1421307
+[browser_shutdown_doc_acc_reference.js]
+[browser_shutdown_multi_acc_reference_obj.js]
+[browser_shutdown_multi_acc_reference_doc.js]
+[browser_shutdown_multi_reference.js]
+[browser_shutdown_parent_own_reference.js]
+skip-if = !e10s || (verify && debug && (os == 'win')) # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_pref.js]
+[browser_shutdown_proxy_acc_reference.js]
+skip-if = !e10s || (os == 'win') # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_proxy_doc_acc_reference.js]
+skip-if = !e10s || (os == 'win') || (verify && debug) # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_multi_proxy_acc_reference_doc.js]
+skip-if = !e10s || (os == 'win') || (verify && debug && (os == 'linux')) # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_multi_proxy_acc_reference_obj.js]
+skip-if = !e10s || (os == 'win') || (verify && debug && (os == 'linux')) # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_remote_no_reference.js]
+skip-if = !e10s || (verify && debug && (os == 'win')) # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_remote_only.js]
+skip-if = !e10s # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_remote_own_reference.js]
+skip-if = !e10s # e10s specific test for a11y start/shutdown between parent and content.
+[browser_shutdown_scope_lifecycle.js]
+[browser_shutdown_start_restart.js]
+skip-if = (verify && debug)
diff --git a/accessible/tests/browser/browser_shutdown_acc_reference.js b/accessible/tests/browser/browser_shutdown_acc_reference.js
new file mode 100644
index 0000000000..68c07ba2b6
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_acc_reference.js
@@ -0,0 +1,64 @@
+/* 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";
+
+add_task(async function() {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible object.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js
new file mode 100644
index 0000000000..baf2b898e5
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js
@@ -0,0 +1,56 @@
+/* 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";
+
+add_task(async function() {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ // Accessible document reference will live longer than the scope of this
+ // function.
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js
new file mode 100644
index 0000000000..b67b2f46f7
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js
@@ -0,0 +1,76 @@
+/* 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";
+
+add_task(async function() {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there are
+ // references to accessible objects.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js
new file mode 100644
index 0000000000..18160a8db7
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js
@@ -0,0 +1,76 @@
+/* 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";
+
+add_task(async function() {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there are
+ // references to accessible objects.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible object.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js
new file mode 100644
index 0000000000..8763327bae
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js
@@ -0,0 +1,90 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"><div id="div"></div></body>
+ </html>`,
+ },
+ async function(browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let acc = docAcc.getChildAt(0);
+ ok(acc, "Accessible proxy is created");
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible proxy.
+ acc = null;
+ ok(!acc, "Accessible proxy is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js
new file mode 100644
index 0000000000..5134901355
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js
@@ -0,0 +1,90 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"><div id="div"></div></body>
+ </html>`,
+ },
+ async function(browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let acc = docAcc.getChildAt(0);
+ ok(acc, "Accessible proxy is created");
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible proxy.
+ acc = null;
+ ok(!acc, "Accessible proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_reference.js b/accessible/tests/browser/browser_shutdown_multi_reference.js
new file mode 100644
index 0000000000..cd0bc0d103
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_reference.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";
+
+add_task(async function() {
+ info("Creating a service");
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService1 = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService1, "Service initialized");
+
+ // Add another reference to a11y service. This will not trigger
+ // 'a11y-init-or-shutdown' event
+ let accService2 = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService2, "Service initialized");
+
+ info("Removing all service references");
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+ // Remove first a11y service reference.
+ accService1 = null;
+ ok(!accService1, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // another reference.
+ forceGC();
+
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove last a11y service reference.
+ accService2 = null;
+ ok(!accService2, "Service is removed");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_parent_own_reference.js b/accessible/tests/browser/browser_shutdown_parent_own_reference.js
new file mode 100644
index 0000000000..86115fdaad
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_parent_own_reference.js
@@ -0,0 +1,104 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function(browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, "Common.jsm");
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] = initAccService(
+ browser
+ );
+
+ await Promise.all([parentA11yInitObserver, contentA11yInitObserver]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+
+ info(
+ "Adding additional reference to accessibility service in content " +
+ "process"
+ );
+ // Add a new reference to the a11y service inside the content process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+
+ info(
+ "Trying to shut down a service in content and making sure it stays " +
+ "alive as it was started by parent"
+ );
+ let contentCanShutdown = false;
+ // This promise will resolve only if contentCanShutdown flag is set to true.
+ // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before
+ // it can be shut down, the promise will reject.
+ const [
+ contentA11yShutdownObserver,
+ contentA11yShutdownPromise,
+ ] = shutdownAccService(browser);
+ await contentA11yShutdownObserver;
+ const contentA11yShutdown = new Promise((resolve, reject) =>
+ contentA11yShutdownPromise.then(flag =>
+ contentCanShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+ // Remove a11y service reference in content and force garbage collection.
+ // This should not trigger shutdown since a11y was originally initialized by
+ // the main process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+
+ // Have some breathing room between a11y service shutdowns.
+ await TestUtils.waitForTick();
+
+ info("Removing a service in parent");
+ // Now allow a11y service to shutdown in content.
+ contentCanShutdown = true;
+ // Remove the a11y service reference in the main process.
+ const [
+ parentA11yShutdownObserver,
+ parentA11yShutdown,
+ ] = shutdownAccService();
+ await parentA11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should trigger shutdown in both parent and
+ // content.
+ forceGC();
+ await Promise.all([parentA11yShutdown, contentA11yShutdown]);
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_pref.js b/accessible/tests/browser/browser_shutdown_pref.js
new file mode 100644
index 0000000000..74cef28b03
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_pref.js
@@ -0,0 +1,72 @@
+/* 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 PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
+
+add_task(async function testForceDisable() {
+ ok(
+ !Services.appinfo.accessibilityEnabled,
+ "Accessibility is disabled by default"
+ );
+
+ info("Reset force disabled preference");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+
+ info("Enable accessibility service via XPCOM");
+ let [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled");
+
+ info("Force disable a11y service via preference");
+ let [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
+ await a11yShutdown;
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+
+ info("Attempt to get an instance of a11y service and call its method.");
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ try {
+ accService.getAccesssibleFor(document);
+ ok(false, "getAccesssibleFor should've triggered an exception.");
+ } catch (e) {
+ ok(
+ true,
+ "getAccesssibleFor triggers an exception as a11y service is shutdown."
+ );
+ }
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+
+ info("Reset force disabled preference");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+
+ info("Create a11y service again");
+ [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled");
+
+ info("Remove all references to a11y service");
+ [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ forceGC();
+ await a11yShutdown;
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+});
diff --git a/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js
new file mode 100644
index 0000000000..d6fa715cf3
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js
@@ -0,0 +1,76 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body><div id="div" style="visibility: hidden;"></div></body>
+ </html>`,
+ },
+ async function(browser) {
+ let onShow = waitForEvent(Ci.nsIAccessibleEvent.EVENT_SHOW, "div");
+ await invokeSetStyle(browser, "div", "visibility", "visible");
+ let showEvent = await onShow;
+ let divAcc = showEvent.accessible;
+ ok(divAcc, "Accessible proxy is created");
+ // Remove unnecessary dangling references
+ onShow = null;
+ showEvent = null;
+ forceGC();
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible proxy.
+ divAcc = null;
+ ok(!divAcc, "Accessible proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+});
diff --git a/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js
new file mode 100644
index 0000000000..1dc2344acb
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js
@@ -0,0 +1,78 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"></body>
+ </html>`,
+ },
+ async function(browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_no_reference.js b/accessible/tests/browser/browser_shutdown_remote_no_reference.js
new file mode 100644
index 0000000000..cce695579d
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_no_reference.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function(browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, "Common.jsm");
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] = initAccService(
+ browser
+ );
+ let [
+ parentConsumersChangedObserver,
+ parentConsumersChanged,
+ ] = accConsumersChanged();
+ let [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yInitObserver,
+ contentA11yInitObserver,
+ parentConsumersChangedObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+ await parentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ )
+ );
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ Assert.deepEqual(
+ JSON.parse(accService.getConsumers()),
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ );
+
+ info(
+ "Removing a service in parent and waiting for service to be shut " +
+ "down in content"
+ );
+ // Remove a11y service reference in the main process.
+ const [
+ parentA11yShutdownObserver,
+ parentA11yShutdown,
+ ] = shutdownAccService();
+ const [
+ contentA11yShutdownObserver,
+ contentA11yShutdown,
+ ] = shutdownAccService(browser);
+ [
+ parentConsumersChangedObserver,
+ parentConsumersChanged,
+ ] = accConsumersChanged();
+ [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yShutdownObserver,
+ contentA11yShutdownObserver,
+ parentConsumersChangedObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should trigger shutdown in both main and
+ // content process.
+ forceGC();
+ await Promise.all([parentA11yShutdown, contentA11yShutdown]);
+ await parentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers are correct."
+ )
+ );
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers are correct."
+ )
+ );
+ }
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_only.js b/accessible/tests/browser/browser_shutdown_remote_only.js
new file mode 100644
index 0000000000..8798170c05
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_only.js
@@ -0,0 +1,56 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function(browser) {
+ info("Creating a service in content");
+ await loadContentScripts(browser, "Common.jsm");
+ // Create a11y service in the content process.
+ const [a11yInitObserver, a11yInit] = initAccService(browser);
+ await a11yInitObserver;
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+ await a11yInit;
+ ok(
+ true,
+ "Accessibility service is started in content process correctly."
+ );
+
+ info("Removing a service in content");
+ // Remove a11y service reference from the content process.
+ const [a11yShutdownObserver, a11yShutdown] = shutdownAccService(browser);
+ await a11yShutdownObserver;
+ // Force garbage collection that should trigger shutdown.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+ await a11yShutdown;
+ ok(
+ true,
+ "Accessibility service is shutdown in content process correctly."
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_own_reference.js b/accessible/tests/browser/browser_shutdown_remote_own_reference.js
new file mode 100644
index 0000000000..2d88e5ed85
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_own_reference.js
@@ -0,0 +1,191 @@
+/* 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";
+
+add_task(async function() {
+ // Making sure that the e10s is enabled on Windows for testing.
+ await setE10sPrefs();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function(browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, "Common.jsm");
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] = initAccService(
+ browser
+ );
+ let [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yInitObserver,
+ contentA11yInitObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ info(
+ "Adding additional reference to accessibility service in content " +
+ "process"
+ );
+ [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+ await contentConsumersChangedObserver;
+ // Add a new reference to the a11y service inside the content process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ const contentConsumers = await SpecialPowers.spawn(browser, [], () =>
+ content.CommonUtils.accService.getConsumers()
+ );
+ Assert.deepEqual(
+ JSON.parse(contentConsumers),
+ {
+ XPCOM: true,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ );
+
+ info(
+ "Shutting down a service in parent and making sure the one in " +
+ "content stays alive"
+ );
+ let contentCanShutdown = false;
+ const [
+ parentA11yShutdownObserver,
+ parentA11yShutdown,
+ ] = shutdownAccService();
+ [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+ // This promise will resolve only if contentCanShutdown flag is set to true.
+ // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before
+ // it can be shut down, the promise will reject.
+ const [
+ contentA11yShutdownObserver,
+ contentA11yShutdownPromise,
+ ] = shutdownAccService(browser);
+ const contentA11yShutdown = new Promise((resolve, reject) =>
+ contentA11yShutdownPromise.then(flag =>
+ contentCanShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ await Promise.all([
+ parentA11yShutdownObserver,
+ contentA11yShutdownObserver,
+ contentConsumersChangedObserver,
+ ]);
+ // Remove a11y service reference in the main process and force garbage
+ // collection. This should not trigger shutdown in content since a11y
+ // service is used by XPCOM.
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference in a content process.
+ forceGC();
+ await SpecialPowers.spawn(browser, [], () => {
+ SpecialPowers.Cu.forceGC();
+ });
+ await parentA11yShutdown;
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ // Have some breathing room between a11y service shutdowns.
+ await TestUtils.waitForTick();
+
+ info("Removing a service in content");
+ // Now allow a11y service to shutdown in content.
+ contentCanShutdown = true;
+ [
+ contentConsumersChangedObserver,
+ contentConsumersChanged,
+ ] = accConsumersChanged(browser);
+ await contentConsumersChangedObserver;
+ // Remove last reference to a11y service in content and force garbage
+ // collection that should trigger shutdown.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+ await contentA11yShutdown;
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ // Unsetting e10s related preferences.
+ await unsetE10sPrefs();
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_scope_lifecycle.js b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js
new file mode 100644
index 0000000000..b4dad44de8
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js
@@ -0,0 +1,28 @@
+/* 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";
+
+add_task(async function() {
+ // Create a11y service inside of the function scope. Its reference should be
+ // released once the anonimous function is called.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+ const a11yInitThenShutdown = a11yInit.then(async () => {
+ const [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+ return a11yShutdown;
+ });
+
+ (function() {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ })();
+
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yInitThenShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_start_restart.js b/accessible/tests/browser/browser_shutdown_start_restart.js
new file mode 100644
index 0000000000..bac7a61da7
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_start_restart.js
@@ -0,0 +1,51 @@
+/* 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";
+
+add_task(async function() {
+ info("Creating a service");
+ // Create a11y service.
+ let [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ info("Removing a service");
+ // Remove the only reference to an a11y service.
+ let [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+
+ info("Recreating a service");
+ // Re-create a11y service.
+ [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService, "Service initialized again");
+
+ info("Removing a service again");
+ // Remove the only reference to an a11y service again.
+ [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed again");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/e10s/browser.ini b/accessible/tests/browser/e10s/browser.ini
new file mode 100644
index 0000000000..a582b9b8bb
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser.ini
@@ -0,0 +1,60 @@
+[DEFAULT]
+support-files =
+ head.js
+ doc_treeupdate_ariadialog.html
+ doc_treeupdate_ariaowns.html
+ doc_treeupdate_imagemap.html
+ doc_treeupdate_removal.xhtml
+ doc_treeupdate_visibility.html
+ doc_treeupdate_whitespace.html
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/browser/*.jsm
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/mochitest/letters.gif
+ !/accessible/tests/mochitest/moz.png
+
+# Caching tests
+[browser_caching_attributes.js]
+[browser_caching_description.js]
+[browser_caching_name.js]
+skip-if = (os == "linux" && bits == 64) || (debug && os == "mac") || (debug && os == "win") #Bug 1388256
+[browser_caching_relations.js]
+[browser_caching_states.js]
+[browser_caching_value.js]
+[browser_caching_uniqueid.js]
+
+# Events tests
+[browser_events_announcement.js]
+skip-if = e10s && os == 'win' # Bug 1288839
+[browser_events_caretmove.js]
+[browser_events_hide.js]
+[browser_events_show.js]
+[browser_events_statechange.js]
+[browser_events_textchange.js]
+[browser_events_vcchange.js]
+
+# Text tests
+[browser_text_paragraph_boundary.js]
+
+# Tree update tests
+[browser_treeupdate_ariadialog.js]
+[browser_treeupdate_ariaowns.js]
+[browser_treeupdate_canvas.js]
+skip-if = (os == 'win' && os_version == '10.0' && bits == 64 && !debug) #Bug 1462638 - Disabled on Win10 opt/pgo for frequent failures
+[browser_treeupdate_cssoverflow.js]
+[browser_treeupdate_doc.js]
+skip-if = e10s && os == 'win' # Bug 1288839
+[browser_treeupdate_gencontent.js]
+[browser_treeupdate_hidden.js]
+[browser_treeupdate_imagemap.js]
+[browser_treeupdate_list.js]
+[browser_treeupdate_list_editabledoc.js]
+[browser_treeupdate_listener.js]
+[browser_treeupdate_optgroup.js]
+[browser_treeupdate_removal.js]
+[browser_treeupdate_select_dropdown.js]
+[browser_treeupdate_table.js]
+[browser_treeupdate_textleaf.js]
+[browser_treeupdate_visibility.js]
+[browser_treeupdate_whitespace.js]
+skip-if = true # Failing due to incorrect index of test container children on document load.
diff --git a/accessible/tests/browser/e10s/browser_caching_attributes.js b/accessible/tests/browser/e10s/browser_caching_attributes.js
new file mode 100644
index 0000000000..b8e157b068
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_attributes.js
@@ -0,0 +1,134 @@
+/* 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 });
+
+/**
+ * Default textbox accessible attributes.
+ */
+const defaultAttributes = {
+ "margin-top": "0px",
+ "margin-right": "0px",
+ "margin-bottom": "0px",
+ "margin-left": "0px",
+ "text-align": "start",
+ "text-indent": "0px",
+ id: "textbox",
+ tag: "input",
+ display: "inline-block",
+};
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {Object} expected attributes for given accessibles
+ * unexpected {Object} unexpected attributes for given accessibles
+ *
+ * action {?AsyncFunction} an optional action that awaits a change in
+ * attributes
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Number} an optional event to wait for
+ * }
+ */
+const attributesTests = [
+ {
+ desc: "Initiall accessible attributes",
+ expected: defaultAttributes,
+ unexpected: {
+ "line-number": "1",
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ },
+ },
+ {
+ desc: "@line-number attribute is present when textbox is focused",
+ async action(browser) {
+ await invokeFocus(browser, "textbox");
+ },
+ waitFor: EVENT_FOCUS,
+ expected: Object.assign({}, defaultAttributes, { "line-number": "1" }),
+ unexpected: {
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ },
+ },
+ {
+ desc: "@aria-live sets container-live and live attributes",
+ attrs: [
+ {
+ attr: "aria-live",
+ value: "polite",
+ },
+ ],
+ expected: Object.assign({}, defaultAttributes, {
+ "line-number": "1",
+ "container-live": "polite",
+ live: "polite",
+ }),
+ unexpected: {
+ "explicit-name": "true",
+ },
+ },
+ {
+ desc: "@title attribute sets explicit-name attribute to true",
+ attrs: [
+ {
+ attr: "title",
+ value: "textbox",
+ },
+ ],
+ expected: Object.assign({}, defaultAttributes, {
+ "line-number": "1",
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ }),
+ unexpected: {},
+ },
+];
+
+/**
+ * Test caching of accessible object attributes
+ */
+addAccessibleTask(
+ `
+ <input id="textbox" value="hello">`,
+ async function(browser, accDoc) {
+ let textbox = findAccessibleChildByID(accDoc, "textbox");
+ for (let {
+ desc,
+ action,
+ attrs,
+ expected,
+ waitFor,
+ unexpected,
+ } of attributesTests) {
+ info(desc);
+ let onUpdate;
+
+ if (waitFor) {
+ onUpdate = waitForEvent(waitFor, "textbox");
+ }
+
+ if (action) {
+ await action(browser);
+ } else if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, "textbox", attr, value);
+ }
+ }
+
+ await onUpdate;
+ testAttrs(textbox, expected);
+ testAbsentAttrs(textbox, unexpected);
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_description.js b/accessible/tests/browser/e10s/browser_caching_description.js
new file mode 100644
index 0000000000..5b9e70ce07
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_description.js
@@ -0,0 +1,214 @@
+/* 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/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {String} expected description value for a given accessible
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Array} an optional list of accessible events to wait for when
+ * attributes are updated
+ * }
+ */
+const tests = [
+ {
+ desc: "No description when there are no @alt, @title and @aria-describedby",
+ expected: "",
+ },
+ {
+ desc: "Description from @aria-describedby attribute",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "aria description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt attribute which is used as the name",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_REORDER, matchContentDoc]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @alt and " +
+ "@aria-describedby are not the same",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @title (used for " +
+ "name) and @aria-describedby are not the same",
+ attrs: [
+ {
+ attr: "alt",
+ },
+ {
+ attr: "title",
+ value: "title",
+ },
+ ],
+ waitFor: [[EVENT_REORDER, matchContentDoc]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@title attribute which is used as the name",
+ attrs: [
+ {
+ attr: "title",
+ value: "another description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc: "No description with only @title attribute which is used as the name",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @title attribute when @alt and @atitle are not the " +
+ "same",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_REORDER, matchContentDoc]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "No description from @title since it is the same as the @alt " +
+ "attribute which is used as the name",
+ attrs: [
+ {
+ attr: "alt",
+ value: "another description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt (used for name) and @title attributes",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when it is different " +
+ "from @alt (used for name) and @title attributes",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "aria description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt attribute (used for name) but different from title",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @alt (used for " +
+ "name) and @aria-describedby are not the same but @title and " +
+ "aria-describedby are",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "another description",
+ },
+];
+
+/**
+ * Test caching of accessible object description
+ */
+addAccessibleTask(
+ `
+ <p id="description">aria description</p>
+ <p id="description2">another description</p>
+ <img id="image" />`,
+ async function(browser, accDoc) {
+ let imgAcc = findAccessibleChildByID(accDoc, "image");
+
+ for (let { desc, waitFor, attrs, expected } of tests) {
+ info(desc);
+ let onUpdate;
+ if (waitFor) {
+ onUpdate = waitForOrderedEvents(waitFor);
+ }
+ if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, "image", attr, value);
+ }
+ }
+ await onUpdate;
+ // When attribute change (alt) triggers reorder event, accessible will
+ // become defunct.
+ if (isDefunct(imgAcc)) {
+ imgAcc = findAccessibleChildByID(accDoc, "image");
+ }
+ testDescr(imgAcc, expected);
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_name.js b/accessible/tests/browser/e10s/browser_caching_name.js
new file mode 100644
index 0000000000..cac05a04de
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_name.js
@@ -0,0 +1,539 @@
+/* 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/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Rules for name tests that are inspired by
+ * accessible/tests/mochitest/name/markuprules.xul
+ *
+ * Each element in the list of rules represents a name calculation rule for a
+ * particular test case.
+ *
+ * The rules have the following format:
+ * { attr } - calculated from attribute
+ * { elm } - calculated from another element
+ * { fromsubtree } - calculated from element's subtree
+ *
+ *
+ * Options include:
+ * * waitFor - changes in the subtree will result in an accessible event
+ * being fired, the test must only continue after the event
+ * is receieved.
+ */
+const ARIARule = [
+ { attr: "aria-labelledby" },
+ { attr: "aria-label", waitFor: EVENT_NAME_CHANGE },
+];
+const HTMLControlHeadRule = [...ARIARule, { elm: "label", isSibling: true }];
+const rules = {
+ CSSContent: [{ elm: "style", isSibling: true }, { fromsubtree: true }],
+ HTMLARIAGridCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLControl: [
+ ...HTMLControlHeadRule,
+ { fromsubtree: true },
+ { attr: "title" },
+ ],
+ HTMLElm: [...ARIARule, { attr: "title" }],
+ HTMLImg: [
+ ...ARIARule,
+ { attr: "alt", waitFor: EVENT_NAME_CHANGE },
+ { attr: "title" },
+ ],
+ HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }],
+ HTMLInputButton: [
+ ...HTMLControlHeadRule,
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImage: [
+ ...HTMLControlHeadRule,
+ { attr: "alt", waitFor: EVENT_NAME_CHANGE },
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImageNoValidSrc: [
+ ...HTMLControlHeadRule,
+ { attr: "alt", waitFor: EVENT_NAME_CHANGE },
+ { attr: "value" },
+ ],
+ HTMLInputReset: [
+ ...HTMLControlHeadRule,
+ { attr: "value", waitFor: EVENT_TEXT_INSERTED },
+ ],
+ HTMLInputSubmit: [
+ ...HTMLControlHeadRule,
+ { attr: "value", waitFor: EVENT_TEXT_INSERTED },
+ ],
+ HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLLinkImage: [...ARIARule, { elm: "img" }, { attr: "title" }],
+ HTMLOption: [
+ ...ARIARule,
+ { attr: "label" },
+ { fromsubtree: true },
+ { attr: "title" },
+ ],
+ HTMLTable: [
+ ...ARIARule,
+ { elm: "caption" },
+ { attr: "summary" },
+ { attr: "title" },
+ ],
+};
+
+const markupTests = [
+ {
+ id: "btn",
+ ruleset: "HTMLControl",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn">test4</label>
+ <button id="btn"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5">press me</button>`,
+ expected: ["test2 test3", "test1", "test4", "press me", "test5"],
+ },
+ {
+ id: "btn",
+ ruleset: "HTMLInputButton",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn">test4</label>
+ <input id="btn"
+ type="button"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from al"
+ src="no name from src"
+ data="no name from data"
+ title="name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from value",
+ "name from title",
+ ],
+ },
+ {
+ id: "btn-submit",
+ ruleset: "HTMLInputSubmit",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-submit">test4</label>
+ <input id="btn-submit"
+ type="submit"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from atl"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: ["test2 test3", "test1", "test4", "name from value"],
+ },
+ {
+ id: "btn-reset",
+ ruleset: "HTMLInputReset",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-reset">test4</label>
+ <input id="btn-reset"
+ type="reset"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from alt"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: ["test2 test3", "test1", "test4", "name from value"],
+ },
+ {
+ id: "btn-image",
+ ruleset: "HTMLInputImage",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-image">test4</label>
+ <input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ data="no name from data"
+ title="name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from alt",
+ "name from value",
+ "name from title",
+ ],
+ },
+ {
+ id: "btn-image",
+ ruleset: "HTMLInputImageNoValidSrc",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-image">test4</label>
+ <input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from alt",
+ "name from value",
+ ],
+ },
+ {
+ id: "opt",
+ ruleset: "HTMLOption",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <select>
+ <option id="opt"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ label="test4"
+ title="test5">option1</option>
+ <option>option2</option>
+ </select>`,
+ expected: ["test2 test3", "test1", "test4", "option1", "test5"],
+ },
+ {
+ id: "img",
+ ruleset: "HTMLImg",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <img id="img"
+ aria-label="Logo of Mozilla"
+ aria-labelledby="l1 l2"
+ alt="Mozilla logo"
+ title="This is a logo"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`,
+ expected: [
+ "test2 test3",
+ "Logo of Mozilla",
+ "Mozilla logo",
+ "This is a logo",
+ ],
+ },
+ {
+ id: "imgemptyalt",
+ ruleset: "HTMLImgEmptyAlt",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <img id="imgemptyalt"
+ aria-label="Logo of Mozilla"
+ aria-labelledby="l1 l2"
+ title="This is a logo"
+ alt=""
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`,
+ expected: ["test2 test3", "Logo of Mozilla", "This is a logo", ""],
+ },
+ {
+ id: "tc",
+ ruleset: "HTMLElm",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="tc">test4</label>
+ <table>
+ <tr>
+ <td id="tc"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5">
+ <p>This is a paragraph</p>
+ <a href="#">This is a link</a>
+ <ul>
+ <li>This is a list</li>
+ </ul>
+ </td>
+ </tr>
+ </table>`,
+ expected: ["test2 test3", "test1", "test5"],
+ },
+ {
+ id: "gc",
+ ruleset: "HTMLARIAGridCell",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="gc">test4</label>
+ <table>
+ <tr>
+ <td id="gc"
+ role="gridcell"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="This is a paragraph This is a link This is a list">
+ <p>This is a paragraph</p>
+ <a href="#">This is a link</a>
+ <ul>
+ <li>Listitem1</li>
+ <li>Listitem2</li>
+ </ul>
+ </td>
+ </tr>
+ </table>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "This is a paragraph This is a link \u2022 Listitem1 \u2022 Listitem2",
+ "This is a paragraph This is a link This is a list",
+ ],
+ },
+ {
+ id: "t",
+ ruleset: "HTMLTable",
+ markup: `
+ <span id="l1">lby_tst6_1</span>
+ <span id="l2">lby_tst6_2</span>
+ <label for="t">label_tst6</label>
+ <table id="t"
+ aria-label="arialabel_tst6"
+ aria-labelledby="l1 l2"
+ summary="summary_tst6"
+ title="title_tst6">
+ <caption>caption_tst6</caption>
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>`,
+ expected: [
+ "lby_tst6_1 lby_tst6_2",
+ "arialabel_tst6",
+ "caption_tst6",
+ "summary_tst6",
+ "title_tst6",
+ ],
+ },
+ {
+ id: "btn",
+ ruleset: "CSSContent",
+ markup: `
+ <div role="main">
+ <style>
+ button::before {
+ content: "do not ";
+ }
+ </style>
+ <button id="btn">press me</button>
+ </div>`,
+ expected: ["do not press me", "press me"],
+ },
+ {
+ // TODO: uncomment when Bug-1256382 is resoved.
+ // id: 'li',
+ // ruleset: 'CSSContent',
+ // markup: `
+ // <style>
+ // ul {
+ // list-style-type: decimal;
+ // }
+ // </style>
+ // <ul id="ul">
+ // <li id="li">Listitem</li>
+ // </ul>`,
+ // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`]
+ // }, {
+ id: "a",
+ ruleset: "HTMLLink",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <a id="a"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test4">test5</a>`,
+ expected: ["test2 test3", "test1", "test5", "test4"],
+ },
+ {
+ id: "a-img",
+ ruleset: "HTMLLinkImage",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <a id="a-img"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test4"><img alt="test5"/></a>`,
+ expected: ["test2 test3", "test1", "test5", "test4"],
+ },
+];
+
+/**
+ * Test accessible name that is calculated from an attribute, remove the
+ * attribute before proceeding to the next name test. If attribute removal
+ * results in a reorder or text inserted event - wait for it. If accessible
+ * becomes defunct, update its reference using the one that is attached to one
+ * of the above events.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, parent, id } structure that contains an
+ * accessible, its parent and its content element
+ * id.
+ * @param {Object} rule current attr rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testAttrRule(browser, target, rule, expected) {
+ let { id, parent, acc } = target;
+ let { waitFor, attr } = rule;
+
+ testName(acc, expected);
+
+ if (waitFor) {
+ let [event] = await contentSpawnMutation(
+ browser,
+ {
+ expected: [[waitFor, waitFor === EVENT_REORDER ? parent : id]],
+ },
+ (contentId, contentAttr) =>
+ content.document.getElementById(contentId).removeAttribute(contentAttr),
+ [id, attr]
+ );
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+ } else {
+ await invokeSetAttribute(browser, id, attr);
+ }
+}
+
+/**
+ * Test accessible name that is calculated from an element name, remove the
+ * element before proceeding to the next name test. If element removal results
+ * in a reorder event - wait for it. If accessible becomes defunct, update its
+ * reference using the one that is attached to a possible reorder event.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, parent, id } structure that contains an
+ * accessible, its parent and its content element
+ * id.
+ * @param {Object} rule current elm rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testElmRule(browser, target, rule, expected) {
+ let { id, parent, acc } = target;
+ let { isSibling, elm } = rule;
+
+ testName(acc, expected);
+ let [event] = await contentSpawnMutation(
+ browser,
+ {
+ expected: [[EVENT_REORDER, isSibling ? parent : id]],
+ },
+ contentElm => content.document.querySelector(`${contentElm}`).remove(),
+ [elm]
+ );
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * Test accessible name that is calculated from its subtree, remove the subtree
+ * and wait for a reorder event before proceeding to the next name test. If
+ * accessible becomes defunct, update its reference using the one that is
+ * attached to a reorder event.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, parent, id } structure that contains an
+ * accessible, its parent and its content element
+ * id.
+ * @param {Object} rule current subtree rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testSubtreeRule(browser, target, rule, expected) {
+ let { id, acc } = target;
+
+ testName(acc, expected);
+ let [event] = await contentSpawnMutation(
+ browser,
+ {
+ expected: [[EVENT_REORDER, id]],
+ },
+ contentId => {
+ let elm = content.document.getElementById(contentId);
+ while (elm.firstChild) {
+ elm.firstChild.remove();
+ }
+ },
+ [id]
+ );
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * Iterate over a list of rules and test accessible names for each one of the
+ * rules.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, parent, id } structure that contains an
+ * accessible, its parent and its content element
+ * id.
+ * @param {Array} ruleset A list of rules to test a target with
+ * @param {Array} expected A list of expected name value for each rule
+ */
+async function testNameRule(browser, target, ruleset, expected) {
+ for (let i = 0; i < ruleset.length; ++i) {
+ let rule = ruleset[i];
+ let testFn;
+ if (rule.attr) {
+ testFn = testAttrRule;
+ } else if (rule.elm) {
+ testFn = testElmRule;
+ } else if (rule.fromsubtree) {
+ testFn = testSubtreeRule;
+ }
+ await testFn(browser, target, rule, expected[i]);
+ }
+}
+
+markupTests.forEach(({ id, ruleset, markup, expected }) =>
+ addAccessibleTask(
+ markup,
+ async function(browser, accDoc) {
+ const observer = {
+ observe(subject, topic, data) {
+ const event = subject.QueryInterface(nsIAccessibleEvent);
+ console.log(eventToString(event));
+ },
+ };
+ Services.obs.addObserver(observer, "accessible-event");
+ // Find a target accessible from an accessible subtree.
+ let acc = findAccessibleChildByID(accDoc, id);
+ // Find target's parent accessible from an accessible subtree.
+ let parent = getAccessibleDOMNodeID(acc.parent);
+ let target = { id, parent, acc };
+ await testNameRule(browser, target, rules[ruleset], expected);
+ Services.obs.removeObserver(observer, "accessible-event");
+ },
+ { iframe: true, remoteIframe: true }
+ )
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_relations.js b/accessible/tests/browser/e10s/browser_caching_relations.js
new file mode 100644
index 0000000000..38d3a0c63b
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_relations.js
@@ -0,0 +1,96 @@
+/* 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/relations.js */
+loadScripts({ name: "relations.js", dir: MOCHITESTS_DIR });
+
+/**
+ * A test specification that has the following format:
+ * [
+ * attr relevant aria attribute
+ * hostRelation corresponding host relation type
+ * dependantRelation corresponding dependant relation type
+ * ]
+ */
+const attrRelationsSpec = [
+ ["aria-labelledby", RELATION_LABELLED_BY, RELATION_LABEL_FOR],
+ ["aria-describedby", RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR],
+ ["aria-controls", RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY],
+ ["aria-flowto", RELATION_FLOWS_TO, RELATION_FLOWS_FROM],
+];
+
+async function testRelated(
+ browser,
+ accDoc,
+ attr,
+ hostRelation,
+ dependantRelation
+) {
+ let host = findAccessibleChildByID(accDoc, "host");
+ let dependant1 = findAccessibleChildByID(accDoc, "dependant1");
+ let dependant2 = findAccessibleChildByID(accDoc, "dependant2");
+
+ /**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * attrs {?Array} an optional list of attributes to update
+ * expected {Array} expected relation values for dependant1, dependant2
+ * and host respectively.
+ * }
+ */
+ const tests = [
+ {
+ desc: "No attribute",
+ expected: [null, null, null],
+ },
+ {
+ desc: "Set attribute",
+ attrs: [{ key: attr, value: "dependant1" }],
+ expected: [host, null, dependant1],
+ },
+ {
+ desc: "Change attribute",
+ attrs: [{ key: attr, value: "dependant2" }],
+ expected: [null, host, dependant2],
+ },
+ {
+ desc: "Remove attribute",
+ attrs: [{ key: attr }],
+ expected: [null, null, null],
+ },
+ ];
+
+ for (let { desc, attrs, expected } of tests) {
+ info(desc);
+
+ if (attrs) {
+ for (let { key, value } of attrs) {
+ await invokeSetAttribute(browser, "host", key, value);
+ }
+ }
+
+ testRelation(dependant1, dependantRelation, expected[0]);
+ testRelation(dependant2, dependantRelation, expected[1]);
+ testRelation(host, hostRelation, expected[2]);
+ }
+}
+
+/**
+ * Test caching of relations between accessible objects.
+ */
+addAccessibleTask(
+ `
+ <div id="dependant1">label</div>
+ <div id="dependant2">label2</div>
+ <div role="checkbox" id="host"></div>`,
+ async function(browser, accDoc) {
+ for (let spec of attrRelationsSpec) {
+ await testRelated(browser, accDoc, ...spec);
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js
new file mode 100644
index 0000000000..e6dbc33d65
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_states.js
@@ -0,0 +1,154 @@
+/* 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 data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {Array} expected states for a given accessible that have the
+ * following format:
+ * [
+ * expected state,
+ * expected extra state,
+ * absent state,
+ * absent extra state
+ * ]
+ * attrs {?Array} an optional list of attributes to update
+ * }
+ */
+
+// State caching tests for attribute changes
+const attributeTests = [
+ {
+ desc:
+ "Checkbox with @checked attribute set to true should have checked " +
+ "state",
+ attrs: [
+ {
+ attr: "checked",
+ value: "true",
+ },
+ ],
+ expected: [STATE_CHECKED, 0],
+ },
+ {
+ desc: "Checkbox with no @checked attribute should not have checked state",
+ attrs: [
+ {
+ attr: "checked",
+ },
+ ],
+ expected: [0, 0, STATE_CHECKED],
+ },
+];
+
+// State caching tests for ARIA changes
+const ariaTests = [
+ {
+ desc: "File input has busy state when @aria-busy attribute is set to true",
+ attrs: [
+ {
+ attr: "aria-busy",
+ value: "true",
+ },
+ ],
+ expected: [STATE_BUSY, 0, STATE_REQUIRED | STATE_INVALID],
+ },
+ {
+ desc:
+ "File input has required state when @aria-required attribute is set " +
+ "to true",
+ attrs: [
+ {
+ attr: "aria-required",
+ value: "true",
+ },
+ ],
+ expected: [STATE_REQUIRED, 0, STATE_INVALID],
+ },
+ {
+ desc:
+ "File input has invalid state when @aria-invalid attribute is set to " +
+ "true",
+ attrs: [
+ {
+ attr: "aria-invalid",
+ value: "true",
+ },
+ ],
+ expected: [STATE_INVALID, 0],
+ },
+];
+
+// Extra state caching tests
+const extraStateTests = [
+ {
+ desc:
+ "Input has no extra enabled state when aria and native disabled " +
+ "attributes are set at once",
+ attrs: [
+ {
+ attr: "aria-disabled",
+ value: "true",
+ },
+ {
+ attr: "disabled",
+ value: "true",
+ },
+ ],
+ expected: [0, 0, 0, EXT_STATE_ENABLED],
+ },
+ {
+ desc:
+ "Input has an extra enabled state when aria and native disabled " +
+ "attributes are unset at once",
+ attrs: [
+ {
+ attr: "aria-disabled",
+ },
+ {
+ attr: "disabled",
+ },
+ ],
+ expected: [0, EXT_STATE_ENABLED],
+ },
+];
+
+async function runStateTests(browser, accDoc, id, tests) {
+ let acc = findAccessibleChildByID(accDoc, id);
+ for (let { desc, attrs, expected } of tests) {
+ info(desc);
+ let onUpdate = waitForEvent(EVENT_STATE_CHANGE, id);
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, id, attr, value);
+ }
+ await onUpdate;
+ testStates(acc, ...expected);
+ }
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ `
+ <input id="checkbox" type="checkbox">
+ <input id="file" type="file">
+ <input id="text">`,
+ async function(browser, accDoc) {
+ await runStateTests(browser, accDoc, "checkbox", attributeTests);
+ await runStateTests(browser, accDoc, "file", ariaTests);
+ await runStateTests(browser, accDoc, "text", extraStateTests);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_uniqueid.js b/accessible/tests/browser/e10s/browser_caching_uniqueid.js
new file mode 100644
index 0000000000..287f896c36
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_uniqueid.js
@@ -0,0 +1,30 @@
+/* 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 UniqueID property.
+ */
+addAccessibleTask(
+ '<div id="div"></div>',
+ async function(browser, accDoc) {
+ const div = findAccessibleChildByID(accDoc, "div");
+ const accUniqueID = await invokeContentTask(browser, [], () => {
+ const accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ return accService.getAccessibleFor(content.document.getElementById("div"))
+ .uniqueID;
+ });
+
+ is(
+ accUniqueID,
+ div.uniqueID,
+ "Both proxy and the accessible return correct unique ID."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_value.js b/accessible/tests/browser/e10s/browser_caching_value.js
new file mode 100644
index 0000000000..c727fc5252
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_value.js
@@ -0,0 +1,195 @@
+/* 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/value.js */
+loadScripts({ name: "value.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * id {String} given accessible DOMNode ID
+ * expected {String} expected value for a given accessible
+ * action {?AsyncFunction} an optional action that awaits a value change
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Number} an optional value change event to wait for
+ * }
+ */
+const valueTests = [
+ {
+ desc: "Initially value is set to 1st element of select",
+ id: "select",
+ expected: "1st",
+ },
+ {
+ desc: "Value should update to 3rd when 3 is pressed",
+ id: "select",
+ async action(browser) {
+ await invokeFocus(browser, "select");
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("3", {}, content);
+ });
+ },
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "3rd",
+ },
+ {
+ desc: "Initially value is set to @aria-valuenow for slider",
+ id: "slider",
+ expected: ["5", 5, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuenow is updated",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuenow",
+ value: "6",
+ },
+ ],
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: ["6", 6, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuetext is set",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuetext",
+ value: "plain",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: ["plain", 6, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuetext is updated",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuetext",
+ value: "hey!",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: ["hey!", 6, 0, 7, 0],
+ },
+ {
+ desc:
+ "Value should change to @aria-valuetext when @aria-valuenow is removed",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuenow",
+ },
+ ],
+ expected: ["hey!", 3.5, 0, 7, 0],
+ },
+ {
+ desc: "Initially value is not set for combobox",
+ id: "combobox",
+ expected: "",
+ },
+ {
+ desc: "Value should change when @value attribute is updated",
+ id: "combobox",
+ attrs: [
+ {
+ attr: "value",
+ value: "hello",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "hello",
+ },
+ {
+ desc: "Initially value corresponds to @value attribute for progress",
+ id: "progress",
+ expected: "22%",
+ },
+ {
+ desc: "Value should change when @value attribute is updated",
+ id: "progress",
+ attrs: [
+ {
+ attr: "value",
+ value: "50",
+ },
+ ],
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "50%",
+ },
+ {
+ desc: "Initially value corresponds to @value attribute for range",
+ id: "range",
+ expected: "6",
+ },
+ {
+ desc: "Value should change when slider is moved",
+ id: "range",
+ async action(browser) {
+ await invokeFocus(browser, "range");
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_LEFT", {}, content);
+ });
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "5",
+ },
+];
+
+/**
+ * Test caching of accessible object values
+ */
+addAccessibleTask(
+ `
+ <div id="slider" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="7">slider</div>
+ <select id="select">
+ <option>1st</option>
+ <option>2nd</option>
+ <option>3rd</option>
+ </select>
+ <input id="combobox" role="combobox" aria-autocomplete="inline">
+ <progress id="progress" value="22" max="100"></progress>
+ <input type="range" id="range" min="0" max="10" value="6">`,
+ async function(browser, accDoc) {
+ for (let { desc, id, action, attrs, expected, waitFor } of valueTests) {
+ info(desc);
+ let acc = findAccessibleChildByID(accDoc, id);
+ let onUpdate;
+
+ if (waitFor) {
+ onUpdate = waitForEvent(waitFor, id);
+ }
+
+ if (action) {
+ await action(browser);
+ } else if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, id, attr, value);
+ }
+ }
+
+ await onUpdate;
+ if (Array.isArray(expected)) {
+ acc.QueryInterface(nsIAccessibleValue);
+ testValue(acc, ...expected);
+ } else {
+ is(acc.value, expected, `Correct value for ${prettyName(acc)}`);
+ }
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_announcement.js b/accessible/tests/browser/e10s/browser_events_announcement.js
new file mode 100644
index 0000000000..2de6d4b005
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_announcement.js
@@ -0,0 +1,30 @@
+/* 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";
+
+addAccessibleTask(
+ `<p id="p">abc</p>`,
+ async function(browser, accDoc) {
+ let acc = findAccessibleChildByID(accDoc, "p");
+ let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE);
+ let evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "please", "announcement matches.");
+ is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches");
+
+ onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE);
+ evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "do it", "announcement matches.");
+ is(
+ evt.priority,
+ nsIAccessibleAnnouncementEvent.ASSERTIVE,
+ "priority matches"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_caretmove.js b/accessible/tests/browser/e10s/browser_events_caretmove.js
new file mode 100644
index 0000000000..a39d16e710
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_caretmove.js
@@ -0,0 +1,22 @@
+/* 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 caret move event and its interface:
+ * - caretOffset
+ */
+addAccessibleTask(
+ '<input id="textbox" value="hello"/>',
+ async function(browser) {
+ let onCaretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textbox");
+ await invokeFocus(browser, "textbox");
+ let event = await onCaretMoved;
+
+ let caretMovedEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent);
+ is(caretMovedEvent.caretOffset, 5, "Correct caret offset.");
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_hide.js b/accessible/tests/browser/e10s/browser_events_hide.js
new file mode 100644
index 0000000000..d46921d051
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_hide.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 hide event and its interface:
+ * - targetParent
+ * - targetNextSibling
+ * - targetPrevSibling
+ */
+addAccessibleTask(
+ `
+ <div id="parent">
+ <div id="previous"></div>
+ <div id="to-hide"></div>
+ <div id="next"></div>
+ </div>`,
+ async function(browser, accDoc) {
+ let acc = findAccessibleChildByID(accDoc, "to-hide");
+ let onHide = waitForEvent(EVENT_HIDE, acc);
+ await invokeSetStyle(browser, "to-hide", "visibility", "hidden");
+ let event = await onHide;
+ let hideEvent = event.QueryInterface(Ci.nsIAccessibleHideEvent);
+
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetParent),
+ "parent",
+ "Correct target parent."
+ );
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetNextSibling),
+ "next",
+ "Correct target next sibling."
+ );
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetPrevSibling),
+ "previous",
+ "Correct target previous sibling."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_show.js b/accessible/tests/browser/e10s/browser_events_show.js
new file mode 100644
index 0000000000..d464d8fb9d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_show.js
@@ -0,0 +1,22 @@
+/* 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 show event
+ */
+addAccessibleTask(
+ '<div id="div" style="visibility: hidden;"></div>',
+ async function(browser) {
+ let onShow = waitForEvent(EVENT_SHOW, "div");
+ await invokeSetStyle(browser, "div", "visibility", "visible");
+ let showEvent = await onShow;
+ ok(
+ showEvent.accessibleDocument instanceof nsIAccessibleDocument,
+ "Accessible document not present."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_statechange.js b/accessible/tests/browser/e10s/browser_events_statechange.js
new file mode 100644
index 0000000000..a027a974e4
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_statechange.js
@@ -0,0 +1,71 @@
+/* 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 checkStateChangeEvent(event, state, isExtraState, isEnabled) {
+ let scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ is(scEvent.state, state, "Correct state of the statechange event.");
+ is(
+ scEvent.isExtraState,
+ isExtraState,
+ "Correct extra state bit of the statechange event."
+ );
+ is(scEvent.isEnabled, isEnabled, "Correct state of statechange event state");
+}
+
+// Insert mock source into the iframe to be able to verify the right document
+// body id.
+let iframeSrc = `data:text/html,
+ <html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Inner Iframe</title>
+ </head>
+ <body id='iframe'></body>
+ </html>`;
+
+/**
+ * Test state change event and its interface:
+ * - state
+ * - isExtraState
+ * - isEnabled
+ */
+addAccessibleTask(
+ `
+ <iframe id="iframe" src="${iframeSrc}"></iframe>
+ <input id="checkbox" type="checkbox" />`,
+ async function(browser) {
+ // Test state change
+ let onStateChange = waitForEvent(EVENT_STATE_CHANGE, "checkbox");
+ // Set checked for a checkbox.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("checkbox").checked = true;
+ });
+ let event = await onStateChange;
+
+ checkStateChangeEvent(event, STATE_CHECKED, false, true);
+ testStates(event.accessible, STATE_CHECKED, 0);
+
+ // Test extra state
+ onStateChange = waitForEvent(EVENT_STATE_CHANGE, "iframe");
+ // Set design mode on.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("iframe").contentDocument.designMode =
+ "on";
+ });
+ event = await onStateChange;
+
+ checkStateChangeEvent(event, EXT_STATE_EDITABLE, true, true);
+ testStates(event.accessible, 0, EXT_STATE_EDITABLE);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_textchange.js b/accessible/tests/browser/e10s/browser_events_textchange.js
new file mode 100644
index 0000000000..1e822dc65a
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_textchange.js
@@ -0,0 +1,114 @@
+/* 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 checkTextChangeEvent(
+ event,
+ id,
+ text,
+ start,
+ end,
+ isInserted,
+ isFromUserInput
+) {
+ let tcEvent = event.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(tcEvent.start, start, `Correct start offset for ${prettyName(id)}`);
+ is(tcEvent.length, end - start, `Correct length for ${prettyName(id)}`);
+ is(
+ tcEvent.isInserted,
+ isInserted,
+ `Correct isInserted flag for ${prettyName(id)}`
+ );
+ is(tcEvent.modifiedText, text, `Correct text for ${prettyName(id)}`);
+ is(
+ tcEvent.isFromUserInput,
+ isFromUserInput,
+ `Correct value of isFromUserInput for ${prettyName(id)}`
+ );
+ ok(
+ tcEvent.accessibleDocument instanceof nsIAccessibleDocument,
+ "Accessible document not present."
+ );
+}
+
+async function changeText(browser, id, value, events) {
+ let onEvents = waitForOrderedEvents(
+ events.map(({ isInserted }) => {
+ let eventType = isInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED;
+ return [eventType, id];
+ })
+ );
+ // Change text in the subtree.
+ await invokeContentTask(browser, [id, value], (contentId, contentValue) => {
+ content.document.getElementById(
+ contentId
+ ).firstChild.textContent = contentValue;
+ });
+ let resolvedEvents = await onEvents;
+
+ events.forEach(({ isInserted, str, offset }, idx) =>
+ checkTextChangeEvent(
+ resolvedEvents[idx],
+ id,
+ str,
+ offset,
+ offset + str.length,
+ isInserted,
+ false
+ )
+ );
+}
+
+async function removeTextFromInput(browser, id, value, start, end) {
+ let onTextRemoved = waitForEvent(EVENT_TEXT_REMOVED, id);
+ // Select text and delete it.
+ await invokeContentTask(
+ browser,
+ [id, start, end],
+ (contentId, contentStart, contentEnd) => {
+ let el = content.document.getElementById(contentId);
+ el.focus();
+ el.setSelectionRange(contentStart, contentEnd);
+ }
+ );
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.sendChar("VK_DELETE", content);
+ });
+
+ let event = await onTextRemoved;
+ checkTextChangeEvent(event, id, value, start, end, false, true);
+}
+
+/**
+ * Test text change event and its interface:
+ * - start
+ * - length
+ * - isInserted
+ * - modifiedText
+ * - isFromUserInput
+ */
+addAccessibleTask(
+ `
+ <p id="p">abc</p>
+ <input id="input" value="input" />`,
+ async function(browser) {
+ let events = [
+ { isInserted: false, str: "abc", offset: 0 },
+ { isInserted: true, str: "def", offset: 0 },
+ ];
+ await changeText(browser, "p", "def", events);
+
+ events = [{ isInserted: true, str: "DEF", offset: 2 }];
+ await changeText(browser, "p", "deDEFf", events);
+
+ // Test isFromUserInput property.
+ await removeTextFromInput(browser, "input", "n", 1, 2);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_vcchange.js b/accessible/tests/browser/e10s/browser_events_vcchange.js
new file mode 100644
index 0000000000..bf649fe045
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_vcchange.js
@@ -0,0 +1,87 @@
+/* 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";
+
+addAccessibleTask(
+ `
+ <p id="p1">abc</p>
+ <input id="input1" value="input" />`,
+ async function(browser) {
+ let onVCChanged = waitForEvent(
+ EVENT_VIRTUALCURSOR_CHANGED,
+ matchContentDoc
+ );
+ await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+ );
+ let vc = CommonUtils.getAccessible(
+ content.document,
+ Ci.nsIAccessibleDocument
+ ).virtualCursor;
+ vc.position = CommonUtils.getAccessible(
+ "p1",
+ null,
+ null,
+ null,
+ content.document
+ );
+ });
+ let vccEvent = (await onVCChanged).QueryInterface(
+ nsIAccessibleVirtualCursorChangeEvent
+ );
+ is(vccEvent.newAccessible.id, "p1", "New position is correct");
+ is(vccEvent.newStartOffset, -1, "New start offset is correct");
+ is(vccEvent.newEndOffset, -1, "New end offset is correct");
+ ok(!vccEvent.isFromUserInput, "not user initiated");
+
+ onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc);
+ await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+ );
+ let vc = CommonUtils.getAccessible(
+ content.document,
+ Ci.nsIAccessibleDocument
+ ).virtualCursor;
+ vc.moveNextByText(Ci.nsIAccessiblePivot.CHAR_BOUNDARY);
+ });
+ vccEvent = (await onVCChanged).QueryInterface(
+ nsIAccessibleVirtualCursorChangeEvent
+ );
+ is(vccEvent.newAccessible.id, vccEvent.oldAccessible.id, "Same position");
+ is(vccEvent.newStartOffset, 0, "New start offset is correct");
+ is(vccEvent.newEndOffset, 1, "New end offset is correct");
+ ok(vccEvent.isFromUserInput, "user initiated");
+
+ onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc);
+ await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+ );
+ let vc = CommonUtils.getAccessible(
+ content.document,
+ Ci.nsIAccessibleDocument
+ ).virtualCursor;
+ vc.position = CommonUtils.getAccessible(
+ "input1",
+ null,
+ null,
+ null,
+ content.document
+ );
+ });
+ vccEvent = (await onVCChanged).QueryInterface(
+ nsIAccessibleVirtualCursorChangeEvent
+ );
+ isnot(vccEvent.oldAccessible, vccEvent.newAccessible, "positions differ");
+ is(vccEvent.oldAccessible.id, "p1", "Old position is correct");
+ is(vccEvent.newAccessible.id, "input1", "New position is correct");
+ is(vccEvent.newStartOffset, -1, "New start offset is correct");
+ is(vccEvent.newEndOffset, -1, "New end offset is correct");
+ ok(!vccEvent.isFromUserInput, "not user initiated");
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js b/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js
new file mode 100644
index 0000000000..04e64520e8
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js
@@ -0,0 +1,22 @@
+/* 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 that we don't crash the parent process when querying the paragraph
+// boundary on an Accessible which has remote ProxyAccessible descendants.
+addAccessibleTask(
+ `test`,
+ async function testParagraphBoundaryWithRemoteDescendants(browser, accDoc) {
+ const root = getRootAccessible(document).QueryInterface(
+ Ci.nsIAccessibleText
+ );
+ let start = {};
+ let end = {};
+ // The offsets will change as the Firefox UI changes. We don't really care
+ // what they are, just that we don't crash.
+ root.getTextAtOffset(0, nsIAccessibleText.BOUNDARY_PARAGRAPH, start, end);
+ ok(true, "Getting paragraph boundary succeeded");
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js
new file mode 100644
index 0000000000..8b4a575d75
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.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/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Test ARIA Dialog
+addAccessibleTask(
+ "e10s/doc_treeupdate_ariadialog.html",
+ async function(browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ role: ROLE_DOCUMENT,
+ children: [],
+ });
+
+ // Make dialog visible and update its inner content.
+ let onShow = waitForEvent(EVENT_SHOW, "dialog");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("dialog").style.display = "block";
+ });
+ await onShow;
+
+ testAccessibleTree(accDoc, {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_DIALOG,
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ children: [{ role: ROLE_TEXT_LEAF }],
+ },
+ {
+ role: ROLE_ENTRY,
+ },
+ ],
+ },
+ ],
+ });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js
new file mode 100644
index 0000000000..dfd6401c48
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js
@@ -0,0 +1,294 @@
+/* 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 });
+
+async function testContainer1(browser, accDoc) {
+ const id = "t1_container";
+ const docID = getAccessibleDOMNodeID(accDoc);
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree test ==================================== */
+ // children are swapped by ARIA owns
+ let tree = {
+ SECTION: [{ CHECKBUTTON: [{ SECTION: [] }] }, { PUSHBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Change ARIA owns ====================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv");
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // checkbox, native order
+ { PUSHBUTTON: [] }, // button, rearranged by ARIA own
+ { SECTION: [] }, // subdiv from the subtree, ARIA owned
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove ARIA owns ====================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns");
+ await onReorder;
+
+ // children follow the DOM order
+ tree = {
+ SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Set ARIA owns ========================================= */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv");
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // checkbox
+ { PUSHBUTTON: [] }, // button, rearranged by ARIA own
+ { SECTION: [] }, // subdiv from the subtree, ARIA owned
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Add ID to ARIA owns =================================== */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(
+ browser,
+ id,
+ "aria-owns",
+ "t1_button t1_subdiv t1_group"
+ );
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // t1_checkbox
+ { PUSHBUTTON: [] }, // button, t1_button
+ { SECTION: [] }, // subdiv from the subtree, t1_subdiv
+ { GROUPING: [] }, // group from outside, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Append element ======================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let div = content.document.createElement("div");
+ div.setAttribute("id", "t1_child3");
+ div.setAttribute("role", "radio");
+ content.document.getElementById(contentId).appendChild(div);
+ });
+ await onReorder;
+
+ // children are invalidated, they includes aria-owns swapped kids and
+ // newly inserted child.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // existing explicit, t1_checkbox
+ { RADIOBUTTON: [] }, // new explicit, t1_child3
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { SECTION: [] }, // ARIA owned, t1_subdiv
+ { GROUPING: [] }, // ARIA owned, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove element ======================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("t1_span").remove();
+ });
+ await onReorder;
+
+ // subdiv should go away
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // explicit, t1_checkbox
+ { RADIOBUTTON: [] }, // explicit, t1_child3
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { GROUPING: [] }, // ARIA owned, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove ID ============================================= */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(browser, "t1_group", "id");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] },
+ { RADIOBUTTON: [] },
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Set ID ================================================ */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(browser, "t1_grouptmp", "id", "t1_group");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] },
+ { RADIOBUTTON: [] },
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { GROUPING: [] }, // ARIA owned, t1_group, previously t1_grouptmp
+ ],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function removeContainer(browser, accDoc) {
+ const id = "t2_container1";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // ARIA owned, 't2_owned'
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("t2_container2")
+ .removeChild(content.document.getElementById("t2_container3"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function stealAndRecacheChildren(browser, accDoc) {
+ const id1 = "t3_container1";
+ const id2 = "t3_container2";
+ const acc1 = findAccessibleChildByID(accDoc, id1);
+ const acc2 = findAccessibleChildByID(accDoc, id2);
+
+ /* ================ Attempt to steal from other ARIA owns ================= */
+ let onReorder = waitForEvent(EVENT_REORDER, id2);
+ await invokeSetAttribute(browser, id2, "aria-owns", "t3_child");
+ await invokeContentTask(browser, [id2], id => {
+ let div = content.document.createElement("div");
+ div.setAttribute("role", "radio");
+ content.document.getElementById(id).appendChild(div);
+ });
+ await onReorder;
+
+ let tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // ARIA owned
+ ],
+ };
+ testAccessibleTree(acc1, tree);
+
+ tree = {
+ SECTION: [{ RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc2, tree);
+}
+
+async function showHiddenElement(browser, accDoc) {
+ const id = "t4_container1";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [{ RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetStyle(browser, "t4_child1", "display", "block");
+ await onReorder;
+
+ tree = {
+ SECTION: [{ CHECKBUTTON: [] }, { RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function rearrangeARIAOwns(browser, accDoc) {
+ const id = "t5_container";
+ const acc = findAccessibleChildByID(accDoc, id);
+ const tests = [
+ {
+ val: "t5_checkbox t5_radio t5_button",
+ roleList: ["CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON"],
+ },
+ {
+ val: "t5_radio t5_button t5_checkbox",
+ roleList: ["RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON"],
+ },
+ ];
+
+ for (let { val, roleList } of tests) {
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", val);
+ await onReorder;
+
+ let tree = { SECTION: [] };
+ for (let role of roleList) {
+ let ch = {};
+ ch[role] = [];
+ tree.SECTION.push(ch);
+ }
+ testAccessibleTree(acc, tree);
+ }
+}
+
+async function removeNotARIAOwnedEl(browser, accDoc) {
+ const id = "t6_container";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [{ TEXT_LEAF: [] }, { GROUPING: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ content.document
+ .getElementById(contentId)
+ .removeChild(content.document.getElementById("t6_span"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GROUPING: [] }],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_ariaowns.html",
+ async function(browser, accDoc) {
+ await testContainer1(browser, accDoc);
+ await removeContainer(browser, accDoc);
+ await stealAndRecacheChildren(browser, accDoc);
+ await showHiddenElement(browser, accDoc);
+ await rearrangeARIAOwns(browser, accDoc);
+ await removeNotARIAOwnedEl(browser, accDoc);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_canvas.js b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js
new file mode 100644
index 0000000000..5fcd1eb773
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js
@@ -0,0 +1,28 @@
+/* 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 });
+
+addAccessibleTask(
+ `
+ <canvas id="canvas">
+ <div id="dialog" role="dialog" style="display: none;"></div>
+ </canvas>`,
+ async function(browser, accDoc) {
+ let canvas = findAccessibleChildByID(accDoc, "canvas");
+ let dialog = findAccessibleChildByID(accDoc, "dialog");
+
+ testAccessibleTree(canvas, { CANVAS: [] });
+
+ let onShow = waitForEvent(EVENT_SHOW, "dialog");
+ await invokeSetStyle(browser, "dialog", "display", "block");
+ await onShow;
+
+ testAccessibleTree(dialog, { DIALOG: [] });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js
new file mode 100644
index 0000000000..629f9fb89f
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js
@@ -0,0 +1,60 @@
+/* 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 });
+
+addAccessibleTask(
+ `
+ <div id="container"><div id="scrollarea" style="overflow:auto;"><input>`,
+ async function(browser, accDoc) {
+ const id1 = "container";
+ const container = findAccessibleChildByID(accDoc, id1);
+
+ /* ================= Change scroll range ================================== */
+ let tree = {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // scroll area
+ ENTRY: [], // child content
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id1);
+ await invokeContentTask(browser, [id1], id => {
+ let doc = content.document;
+ doc.getElementById("scrollarea").style.width = "20px";
+ doc.getElementById(id).appendChild(doc.createElement("input"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // scroll area
+ ENTRY: [], // child content
+ },
+ ],
+ },
+ {
+ ENTRY: [], // inserted input
+ },
+ ],
+ };
+ testAccessibleTree(container, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_doc.js b/accessible/tests/browser/e10s/browser_treeupdate_doc.js
new file mode 100644
index 0000000000..98f399695c
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_doc.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 */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const iframeSrc = `data:text/html,
+ <html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Inner Iframe</title>
+ </head>
+ <body id='inner-iframe'></body>
+ </html>`;
+
+addAccessibleTask(
+ `
+ <iframe id="iframe" src="${iframeSrc}"></iframe>`,
+ async function(browser, accDoc) {
+ // ID of the iframe that is being tested
+ const id = "inner-iframe";
+
+ let iframe = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree check =================================== */
+ let tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Write iframe document ================================ */
+ let reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newHTMLNode = docNode.createElement("html");
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Wave");
+ newBodyNode.id = contentId;
+ newBodyNode.appendChild(newTextNode);
+ newHTMLNode.appendChild(newBodyNode);
+ docNode.replaceChild(newHTMLNode, docNode.documentElement);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Wave",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Replace iframe HTML element ========================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ // We can't use open/write/close outside of iframe document because of
+ // security error.
+ let script = docNode.createElement("script");
+ script.textContent = `
+ document.open();
+ document.write('<body id="${contentId}">hello</body>');
+ document.close();`;
+ docNode.body.appendChild(script);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Replace iframe body ================================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.id = contentId;
+ newBodyNode.appendChild(newTextNode);
+ newBodyNode.setAttribute("role", "application");
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_APPLICATION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Open iframe document ================================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Open document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let script = docNode.createElement("script");
+ script.textContent = `
+ function closeMe() {
+ document.write('Works?');
+ document.close();
+ }
+ window.closeMe = closeMe;
+ document.open();
+ document.write('<body id="${contentId}"></body>');`;
+ docNode.body.appendChild(script);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Close iframe document ================================ */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ // Write and close document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.write("Works?");
+ docNode.close();
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Works?",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Remove HTML from iframe document ===================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ // Remove HTML element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.firstChild.remove();
+ });
+ let event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Insert HTML to iframe document ======================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Insert HTML element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let html = docNode.createElement("html");
+ let body = docNode.createElement("body");
+ let text = docNode.createTextNode("Haha");
+ body.appendChild(text);
+ body.id = contentId;
+ html.appendChild(body);
+ docNode.appendChild(html);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Haha",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Remove body from iframe document ===================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ // Remove body element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.documentElement.removeChild(docNode.body);
+ });
+ event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================ Insert element under document element while body missed */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let inputNode = (content.window.inputNode = docNode.createElement(
+ "input"
+ ));
+ docNode.documentElement.appendChild(inputNode);
+ });
+ event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ DOCUMENT: [{ ENTRY: [] }],
+ };
+ testAccessibleTree(iframe, tree);
+
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ let docEl = content.document.getElementById("iframe").contentDocument
+ .documentElement;
+ // Remove aftermath of this test before next test starts.
+ docEl.firstChild.remove();
+ });
+ // Make sure reorder event was fired and that the input was removed.
+ await reorderEventPromise;
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Insert body to iframe document ======================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Write and close document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ // Insert body element.
+ let body = docNode.createElement("body");
+ let text = docNode.createTextNode("Yo ho ho i butylka roma!");
+ body.appendChild(text);
+ body.id = contentId;
+ docNode.documentElement.appendChild(body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Yo ho ho i butylka roma!",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Change source ======================================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, "iframe");
+ await invokeSetAttribute(
+ browser,
+ "iframe",
+ "src",
+ `data:text/html,<html><body id="${id}"><input></body></html>`
+ );
+ event = await reorderEventPromise;
+
+ tree = {
+ INTERNAL_FRAME: [{ DOCUMENT: [{ ENTRY: [] }] }],
+ };
+ testAccessibleTree(event.accessible, tree);
+ iframe = findAccessibleChildByID(event.accessible, id);
+
+ /* ================= Replace iframe body on ARIA role body ================ */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.appendChild(newTextNode);
+ newBodyNode.setAttribute("role", "application");
+ newBodyNode.id = contentId;
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_APPLICATION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js
new file mode 100644
index 0000000000..ca1150f9dd
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js
@@ -0,0 +1,94 @@
+/* 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 });
+
+addAccessibleTask(
+ `
+ <style>
+ .gentext:before {
+ content: "START"
+ }
+ .gentext:after {
+ content: "END"
+ }
+ </style>
+ <div id="container1"></div>
+ <div id="container2"><div id="container2_child">text</div></div>`,
+ async function(browser, accDoc) {
+ const id1 = "container1";
+ const id2 = "container2";
+ let container1 = findAccessibleChildByID(accDoc, id1);
+ let container2 = findAccessibleChildByID(accDoc, id2);
+
+ let tree = {
+ SECTION: [], // container
+ };
+ testAccessibleTree(container1, tree);
+
+ tree = {
+ SECTION: [
+ {
+ // container2
+ SECTION: [
+ {
+ // container2 child
+ TEXT_LEAF: [], // primary text
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container2, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id1);
+ // Create and add an element with CSS generated content to container1
+ await invokeContentTask(browser, [id1], id => {
+ let node = content.document.createElement("div");
+ node.textContent = "text";
+ node.setAttribute("class", "gentext");
+ content.document.getElementById(id).appendChild(node);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // inserted node
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container1, tree);
+
+ onReorder = waitForEvent(EVENT_REORDER, "container2_child");
+ // Add CSS generated content to an element in container2's subtree
+ await invokeSetAttribute(browser, "container2_child", "class", "gentext");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container2
+ {
+ SECTION: [
+ // container2 child
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container2, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_hidden.js b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js
new file mode 100644
index 0000000000..725999db36
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js
@@ -0,0 +1,32 @@
+/* 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 });
+
+async function setHidden(browser, value) {
+ let onReorder = waitForEvent(EVENT_REORDER, "container");
+ await invokeSetAttribute(browser, "child", "hidden", value);
+ await onReorder;
+}
+
+addAccessibleTask(
+ '<div id="container"><input id="child"></div>',
+ async function(browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] });
+
+ // Set @hidden attribute
+ await setHidden(browser, "true");
+ testAccessibleTree(container, { SECTION: [] });
+
+ // Remove @hidden attribute
+ await setHidden(browser);
+ testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
new file mode 100644
index 0000000000..e206b83276
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.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";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function testImageMap(browser, accDoc) {
+ const id = "imgmap";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree test ==================================== */
+ let tree = {
+ IMAGE_MAP: [{ role: ROLE_LINK, name: "b", children: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Insert area ========================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let areaElm = content.document.createElement("area");
+ let mapNode = content.document.getElementById("map");
+ areaElm.setAttribute(
+ "href",
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ );
+ areaElm.setAttribute("coords", "0,0,13,14");
+ areaElm.setAttribute("alt", "a");
+ areaElm.setAttribute("shape", "rect");
+ mapNode.insertBefore(areaElm, mapNode.firstChild);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "a", children: [] },
+ { role: ROLE_LINK, name: "b", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Append area ========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let areaElm = content.document.createElement("area");
+ let mapNode = content.document.getElementById("map");
+ areaElm.setAttribute(
+ "href",
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#c"
+ );
+ areaElm.setAttribute("coords", "34,0,47,14");
+ areaElm.setAttribute("alt", "c");
+ areaElm.setAttribute("shape", "rect");
+ mapNode.appendChild(areaElm);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "a", children: [] },
+ { role: ROLE_LINK, name: "b", children: [] },
+ { role: ROLE_LINK, name: "c", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Remove area ========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let mapNode = content.document.getElementById("map");
+ mapNode.removeChild(mapNode.firstElementChild);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "b", children: [] },
+ { role: ROLE_LINK, name: "c", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function testContainer(browser) {
+ const id = "container";
+ /* ================= Remove name on map =================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, "map", "name");
+ let event = await onReorder;
+ const acc = event.accessible;
+
+ let tree = {
+ SECTION: [{ GRAPHIC: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Restore name on map ================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, "map", "name", "atoz_map");
+ // XXX: force repainting of the image (see bug 745788 for details).
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeMouse(
+ content.document.getElementById("imgmap"),
+ 10,
+ 10,
+ { type: "mousemove" },
+ content
+ );
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ IMAGE_MAP: [{ LINK: [] }, { LINK: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Remove map =========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let mapNode = content.document.getElementById("map");
+ mapNode.remove();
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GRAPHIC: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Insert map =========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let map = content.document.createElement("map");
+ let area = content.document.createElement("area");
+
+ map.setAttribute("name", "atoz_map");
+ map.setAttribute("id", "map");
+
+ area.setAttribute("href", "http://www.bbc.co.uk/radio4/atoz/index.shtml#b");
+ area.setAttribute("coords", "17,0,30,14");
+ area.setAttribute("alt", "b");
+ area.setAttribute("shape", "rect");
+
+ map.appendChild(area);
+ content.document.getElementById(contentId).appendChild(map);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ IMAGE_MAP: [{ LINK: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Hide image map ======================================= */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetStyle(browser, "imgmap", "display", "none");
+ await onReorder;
+
+ tree = {
+ SECTION: [],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_imagemap.html",
+ async function(browser, accDoc) {
+ await waitForImageMap(browser, accDoc);
+ await testImageMap(browser, accDoc);
+ await testContainer(browser);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list.js b/accessible/tests/browser/e10s/browser_treeupdate_list.js
new file mode 100644
index 0000000000..d14b983c10
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_list.js
@@ -0,0 +1,52 @@
+/* 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 });
+
+async function setDisplayAndWaitForReorder(browser, value) {
+ let onReorder = waitForEvent(EVENT_REORDER, "ul");
+ await invokeSetStyle(browser, "li", "display", value);
+ return onReorder;
+}
+
+addAccessibleTask(
+ `
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>`,
+ async function(browser, accDoc) {
+ let li = findAccessibleChildByID(accDoc, "li");
+ let bullet = li.firstChild;
+ let accTree = {
+ role: ROLE_LISTITEM,
+ children: [
+ {
+ role: ROLE_LISTITEM_MARKER,
+ children: [],
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+ testAccessibleTree(li, accTree);
+
+ await setDisplayAndWaitForReorder(browser, "none");
+
+ ok(isDefunct(li), "Check that li is defunct.");
+ ok(isDefunct(bullet), "Check that bullet is defunct.");
+
+ let event = await setDisplayAndWaitForReorder(browser, "list-item");
+
+ testAccessibleTree(
+ findAccessibleChildByID(event.accessible, "li"),
+ accTree
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js
new file mode 100644
index 0000000000..9c672f3c7c
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js
@@ -0,0 +1,48 @@
+/* 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 });
+
+addAccessibleTask(
+ '<ol id="list"></ol>',
+ async function(browser, accDoc) {
+ let list = findAccessibleChildByID(accDoc, "list");
+
+ testAccessibleTree(list, {
+ role: ROLE_LIST,
+ children: [],
+ });
+
+ await invokeSetAttribute(
+ browser,
+ currentContentDoc(),
+ "contentEditable",
+ "true"
+ );
+ let onReorder = waitForEvent(EVENT_REORDER, "list");
+ await invokeContentTask(browser, [], () => {
+ let li = content.document.createElement("li");
+ li.textContent = "item";
+ content.document.getElementById("list").appendChild(li);
+ });
+ await onReorder;
+
+ testAccessibleTree(list, {
+ role: ROLE_LIST,
+ children: [
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER, name: "1. ", children: [] },
+ { role: ROLE_TEXT_LEAF, children: [] },
+ ],
+ },
+ ],
+ });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_listener.js b/accessible/tests/browser/e10s/browser_treeupdate_listener.js
new file mode 100644
index 0000000000..35baf28667
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_listener.js
@@ -0,0 +1,38 @@
+/* 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 });
+
+addAccessibleTask(
+ '<span id="parent"><span id="child"></span></span>',
+ async function(browser, accDoc) {
+ is(
+ findAccessibleChildByID(accDoc, "parent"),
+ null,
+ "Check that parent is not accessible."
+ );
+ is(
+ findAccessibleChildByID(accDoc, "child"),
+ null,
+ "Check that child is not accessible."
+ );
+
+ let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc);
+ // Add an event listener to parent.
+ await invokeContentTask(browser, [], () => {
+ content.window.dummyListener = () => {};
+ content.document
+ .getElementById("parent")
+ .addEventListener("click", content.window.dummyListener);
+ });
+ await onReorder;
+
+ let tree = { TEXT: [] };
+ testAccessibleTree(findAccessibleChildByID(accDoc, "parent"), tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js
new file mode 100644
index 0000000000..15fed8112d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.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/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ '<select id="select"></select>',
+ async function(browser, accDoc) {
+ let select = findAccessibleChildByID(accDoc, "select");
+
+ let onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Create a combobox with grouping and 2 standalone options
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ let contentSelect = doc.getElementById("select");
+ let optGroup = doc.createElement("optgroup");
+
+ for (let i = 0; i < 2; i++) {
+ let opt = doc.createElement("option");
+ opt.value = i;
+ opt.text = "Option: Value " + i;
+ optGroup.appendChild(opt);
+ }
+ contentSelect.add(optGroup, null);
+
+ for (let i = 0; i < 2; i++) {
+ let opt = doc.createElement("option");
+ contentSelect.add(opt, null);
+ }
+ contentSelect.firstChild.firstChild.id = "option1Node";
+ });
+ let event = await onEvent;
+ let option1Node = findAccessibleChildByID(event.accessible, "option1Node");
+
+ let tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [
+ {
+ GROUPING: [
+ { COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
+ { COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] },
+ ],
+ },
+ {
+ COMBOBOX_OPTION: [],
+ },
+ {
+ COMBOBOX_OPTION: [],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ ok(!isDefunct(option1Node), "option shouldn't be defunct");
+
+ onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Remove grouping from combobox
+ await invokeContentTask(browser, [], () => {
+ let contentSelect = content.document.getElementById("select");
+ contentSelect.firstChild.remove();
+ });
+ await onEvent;
+
+ tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ ok(
+ isDefunct(option1Node),
+ "removed option shouldn't be accessible anymore!"
+ );
+
+ onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Remove all options from combobox
+ await invokeContentTask(browser, [], () => {
+ let contentSelect = content.document.getElementById("select");
+ while (contentSelect.length) {
+ contentSelect.remove(0);
+ }
+ });
+ await onEvent;
+
+ tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_removal.js b/accessible/tests/browser/e10s/browser_treeupdate_removal.js
new file mode 100644
index 0000000000..eb791525b3
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_removal.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 */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_removal.xhtml",
+ async function(browser, accDoc) {
+ ok(
+ isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table should be accessible"
+ );
+
+ // Move the_table element into hidden subtree.
+ let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("the_displaynone")
+ .appendChild(content.document.getElementById("the_table"));
+ });
+ await onReorder;
+
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table in display none tree shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_row")),
+ "row shouldn't be accessible"
+ );
+
+ // Remove the_row element (since it did not have accessible, no event needed).
+ await invokeContentTask(browser, [], () => {
+ content.document.body.removeChild(
+ content.document.getElementById("the_row")
+ );
+ });
+
+ // make sure no accessibles have stuck around.
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_row")),
+ "row shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_displayNone")),
+ "display none things shouldn't be accessible"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js
new file mode 100644
index 0000000000..add454ceac
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.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 });
+
+const snippet = `
+<select id="select">
+ <option>o1</option>
+ <optgroup label="g1">
+ <option>g1o1</option>
+ <option>g1o2</option>
+ </optgroup>
+ <optgroup label="g2">
+ <option>g2o1</option>
+ <option>g2o2</option>
+ </optgroup>
+ <option>o2</option>
+</select>
+`;
+
+addAccessibleTask(
+ snippet,
+ async function(browser, accDoc) {
+ await invokeFocus(browser, "select");
+ // Expand the select. A dropdown item should get focus.
+ // Note that the dropdown is rendered in the parent process.
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_COMBOBOX_OPTION,
+ "Dropdown item focused after select expanded"
+ );
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content);
+ });
+ let event = await focused;
+ let dropdown = event.accessible.parent;
+
+ let selectedOptionChildren = [];
+ if (MAC) {
+ // Checkmark is part of the Mac menu styling.
+ selectedOptionChildren = [{ STATICTEXT: [] }];
+ }
+ let tree = {
+ COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: selectedOptionChildren },
+ { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
+ { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
+ { COMBOBOX_OPTION: [] },
+ ],
+ };
+ testAccessibleTree(dropdown, tree);
+
+ // Collapse the select. Focus should return to the select.
+ focused = waitForEvent(
+ EVENT_FOCUS,
+ "select",
+ "select focused after collapsed"
+ );
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, content);
+ });
+ await focused;
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_table.js b/accessible/tests/browser/e10s/browser_treeupdate_table.js
new file mode 100644
index 0000000000..5c2903225a
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_table.js
@@ -0,0 +1,48 @@
+/* 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 });
+
+addAccessibleTask(
+ `
+ <table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>`,
+ async function(browser, accDoc) {
+ let table = findAccessibleChildByID(accDoc, "table");
+
+ let tree = {
+ TABLE: [
+ { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] },
+ ],
+ };
+ testAccessibleTree(table, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "table");
+ await invokeContentTask(browser, [], () => {
+ // append a caption, it should appear as a first element in the
+ // accessible tree.
+ let doc = content.document;
+ let caption = doc.createElement("caption");
+ caption.textContent = "table caption";
+ doc.getElementById("table").appendChild(caption);
+ });
+ await onReorder;
+
+ tree = {
+ TABLE: [
+ { CAPTION: [{ TEXT_LEAF: [] }] },
+ { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] },
+ ],
+ };
+ testAccessibleTree(table, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js
new file mode 100644
index 0000000000..6f89105b86
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js
@@ -0,0 +1,38 @@
+/* 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 });
+
+async function removeTextData(browser, accessible, id, role) {
+ let tree = {
+ role,
+ children: [{ role: ROLE_TEXT_LEAF, name: "text" }],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ content.document.getElementById(contentId).firstChild.textContent = "";
+ });
+ await onReorder;
+
+ tree = { role, children: [] };
+ testAccessibleTree(accessible, tree);
+}
+
+addAccessibleTask(
+ `
+ <p id="p">text</p>
+ <pre id="pre">text</pre>`,
+ async function(browser, accDoc) {
+ let p = findAccessibleChildByID(accDoc, "p");
+ let pre = findAccessibleChildByID(accDoc, "pre");
+ await removeTextData(browser, p, "p", ROLE_PARAGRAPH);
+ await removeTextData(browser, pre, "pre", ROLE_TEXT_CONTAINER);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_visibility.js b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js
new file mode 100644
index 0000000000..4583056586
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js
@@ -0,0 +1,342 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function testTreeOnHide(browser, accDoc, containerID, id, before, after) {
+ let acc = findAccessibleChildByID(accDoc, containerID);
+ testAccessibleTree(acc, before);
+
+ let onReorder = waitForEvent(EVENT_REORDER, containerID);
+ await invokeSetStyle(browser, id, "visibility", "hidden");
+ await onReorder;
+
+ testAccessibleTree(acc, after);
+}
+
+async function test3(browser, accessible) {
+ let tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // parent
+ {
+ SECTION: [
+ // child
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ // parent2
+ {
+ SECTION: [
+ // child2
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "t3_container");
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("t3_container").style.color = "red";
+ doc.getElementById("t3_parent").style.visibility = "hidden";
+ doc.getElementById("t3_parent2").style.visibility = "hidden";
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // child
+ { TEXT_LEAF: [] },
+ ],
+ },
+ {
+ SECTION: [
+ // child2
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+}
+
+async function test4(browser, accessible) {
+ let tree = {
+ SECTION: [{ TABLE: [{ ROW: [{ CELL: [] }] }] }],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "t4_parent");
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("t4_container").style.color = "red";
+ doc.getElementById("t4_child").style.visibility = "visible";
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_visibility.html",
+ async function(browser, accDoc) {
+ let t3Container = findAccessibleChildByID(accDoc, "t3_container");
+ let t4Container = findAccessibleChildByID(accDoc, "t4_container");
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t1_container",
+ "t1_parent",
+ {
+ SECTION: [
+ {
+ SECTION: [
+ {
+ SECTION: [{ TEXT_LEAF: [] }],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ SECTION: [{ TEXT_LEAF: [] }],
+ },
+ ],
+ }
+ );
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t2_container",
+ "t2_grandparent",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // grand parent
+ SECTION: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+
+ await test3(browser, t3Container);
+ await test4(browser, t4Container);
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t5_container",
+ "t5_subcontainer",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // subcontainer
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t6_container",
+ "t6_subcontainer",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // subcontainer
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ TABLE: [
+ {
+ // nested table
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js
new file mode 100644
index 0000000000..7f7e4ba97d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js
@@ -0,0 +1,68 @@
+/* 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 });
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_whitespace.html",
+ async function(browser, accDoc) {
+ let container1 = findAccessibleChildByID(accDoc, "container1");
+ let container2Parent = findAccessibleChildByID(accDoc, "container2-parent");
+
+ let tree = {
+ SECTION: [
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ ],
+ };
+ testAccessibleTree(container1, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "container1");
+ // Remove img1 from container1
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("container1").removeChild(doc.getElementById("img1"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GRAPHIC: [] }, { TEXT_LEAF: [] }, { GRAPHIC: [] }],
+ };
+ testAccessibleTree(container1, tree);
+
+ tree = {
+ SECTION: [{ LINK: [] }, { LINK: [{ GRAPHIC: [] }] }],
+ };
+ testAccessibleTree(container2Parent, tree);
+
+ onReorder = waitForEvent(EVENT_REORDER, "container2-parent");
+ // Append an img with valid src to container2
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ let img = doc.createElement("img");
+ img.setAttribute(
+ "src",
+ "http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ );
+ doc.getElementById("container2").appendChild(img);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { LINK: [{ GRAPHIC: [] }] },
+ { TEXT_LEAF: [] },
+ { LINK: [{ GRAPHIC: [] }] },
+ ],
+ };
+ testAccessibleTree(container2Parent, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html
new file mode 100644
index 0000000000..9d08854b9a
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update ARIA Dialog Test</title>
+ </head>
+ <body id="body">
+ <div id="dialog" role="dialog" style="display: none;">
+ <table id="table" role="presentation"
+ style="display: block; position: fixed; top: 88px; left: 312.5px; z-index: 10010;">
+ <tbody>
+ <tr>
+ <td role="presentation">
+ <div role="presentation">
+ <a id="a" role="button">text</a>
+ </div>
+ <input id="input">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html
new file mode 100644
index 0000000000..38b5c333a1
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html
@@ -0,0 +1,44 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update ARIA Owns Test</title>
+ </head>
+ <body id="body">
+ <div id="t1_container" aria-owns="t1_checkbox t1_button">
+ <div role="button" id="t1_button"></div>
+ <div role="checkbox" id="t1_checkbox">
+ <span id="t1_span">
+ <div id="t1_subdiv"></div>
+ </span>
+ </div>
+ </div>
+ <div id="t1_group" role="group"></div>
+ <div id="t1_grouptmp" role="group"></div>
+
+ <div id="t2_container1" aria-owns="t2_owned"></div>
+ <div id="t2_container2">
+ <div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div>
+ </div>
+
+ <div id="t3_container1" aria-owns="t3_child"></div>
+ <div id="t3_child" role="checkbox"></div>
+ <div id="t3_container2"></div>
+
+ <div id="t4_container1" aria-owns="t4_child1 t4_child2"></div>
+ <div id="t4_container2">
+ <div id="t4_child1" style="display:none" role="checkbox"></div>
+ <div id="t4_child2" role="radio"></div>
+ </div>
+
+ <div id="t5_container">
+ <div role="button" id="t5_button"></div>
+ <div role="checkbox" id="t5_checkbox"></div>
+ <div role="radio" id="t5_radio"></div>
+ </div>
+
+ <div id="t6_container" aria-owns="t6_fake">
+ <span id="t6_span">hey</span>
+ </div>
+ <div id="t6_fake" role="group"></div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html
new file mode 100644
index 0000000000..4dd230fc28
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html
@@ -0,0 +1,21 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Imagemap Test</title>
+ </head>
+ <body id="body">
+ <map name="atoz_map" id="map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"><!--
+ Important: no whitespace between the <img> and the </div>, so we
+ don't end up with textframes there, because those would be reflected
+ in our accessible tree in some cases.
+ --></div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml
new file mode 100644
index 0000000000..9c59fb9d11
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Removal Test</title>
+ </head>
+ <body id="body">
+ <div id="the_displaynone" style="display: none;"></div>
+ <table id="the_table"></table>
+ <tr id="the_row"></tr>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_visibility.html b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html
new file mode 100644
index 0000000000..00213b2b70
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html
@@ -0,0 +1,78 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Visibility Test</title>
+ </head>
+ <body id="body">
+ <!-- hide parent while child stays visible -->
+ <div id="t1_container">
+ <div id="t1_parent">
+ <div id="t1_child" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- hide grandparent while its children stay visible -->
+ <div id="t2_container">
+ <div id="t2_grandparent">
+ <div id="t2_parent">
+ <div id="t2_child" style="visibility: visible">text</div>
+ <div id="t2_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- change container style, hide parents while their children stay visible -->
+ <div id="t3_container">
+ <div id="t3_parent">
+ <div id="t3_child" style="visibility: visible">text</div>
+ </div>
+ <div id="t3_parent2">
+ <div id="t3_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- change container style, show child inside the table -->
+ <div id="t4_container">
+ <table>
+ <tr>
+ <td id="t4_parent">
+ <div id="t4_child" style="visibility: hidden;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <!-- hide subcontainer while child inside the table stays visible -->
+ <div id="t5_container">
+ <div id="t5_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <div id="t5_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <!-- hide subcontainer while its child and child inside the nested table stays visible -->
+ <div id="t6_container">
+ <div id="t6_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <td>
+ <div id="t6_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <div id="t6_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html
new file mode 100644
index 0000000000..f17dbbd60e
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Whitespace text accessible creation/destruction</title>
+ </head>
+ <body id="body">
+ <div id="container1"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img id="img1" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> </div>
+ <div id="container2-parent"> <a id="container2"></a> <a><img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js
new file mode 100644
index 0000000000..672aa46171
--- /dev/null
+++ b/accessible/tests/browser/e10s/head.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
diff --git a/accessible/tests/browser/events/browser.ini b/accessible/tests/browser/events/browser.ini
new file mode 100644
index 0000000000..fe1d15917d
--- /dev/null
+++ b/accessible/tests/browser/events/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/browser/*.jsm
+
+[browser_test_docload.js]
+skip-if = e10s
+[browser_test_scrolling.js]
+[browser_test_textcaret.js]
+[browser_test_focus_browserui.js]
+skip-if = sessionHistoryInParent
+[browser_test_focus_dialog.js]
+[browser_test_focus_urlbar.js]
+[browser_test_A11yUtils_announce.js]
+[browser_test_selection_urlbar.js]
diff --git a/accessible/tests/browser/events/browser_test_A11yUtils_announce.js b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js
new file mode 100644
index 0000000000..b2848f35c2
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Check that the browser A11yUtils.announce() function works correctly.
+// Note that this does not use mozilla::a11y::Accessible::Announce and a11y
+// announcement events, as these aren't yet supported on desktop.
+async function runTests() {
+ const alert = document.getElementById("a11y-announcement");
+ let alerted = waitForEvent(EVENT_ALERT, alert);
+ A11yUtils.announce({ raw: "first" });
+ let event = await alerted;
+ const alertAcc = event.accessible;
+ is(alertAcc.role, ROLE_ALERT);
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, "first");
+
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ A11yUtils.announce({ raw: "second" });
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, "second");
+
+ info("Testing Fluent message");
+ // We need a simple Fluent message here without arguments or attributes.
+ const fluentId = "search-one-offs-with-title";
+ const fluentMessage = await document.l10n.formatValue(fluentId);
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ A11yUtils.announce({ id: fluentId });
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, fluentMessage);
+
+ info("Ensuring Fluent message is cancelled if announce is re-entered");
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ // This call runs async.
+ let asyncAnnounce = A11yUtils.announce({ id: fluentId });
+ // Before the async call finishes, call announce again.
+ A11yUtils.announce({ raw: "third" });
+ // Wait for the async call to complete.
+ await asyncAnnounce;
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ // The async call should have been cancelled. If it wasn't, we would get
+ // fluentMessage here instead of "third".
+ is(alertAcc.firstChild.name, "third");
+}
+
+addAccessibleTask(``, runTests);
diff --git a/accessible/tests/browser/events/browser_test_docload.js b/accessible/tests/browser/events/browser_test_docload.js
new file mode 100644
index 0000000000..53722a582a
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_docload.js
@@ -0,0 +1,120 @@
+/* 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 busyChecker(isBusy) {
+ return function(event) {
+ let scEvent;
+ try {
+ scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ } catch (e) {
+ return false;
+ }
+
+ return scEvent.state == STATE_BUSY && scEvent.isEnabled == isBusy;
+ };
+}
+
+function inIframeChecker(iframeId) {
+ return function(event) {
+ return getAccessibleDOMNodeID(event.accessibleDocument.parent) == iframeId;
+ };
+}
+
+function urlChecker(url) {
+ return function(event) {
+ info(`${event.accessibleDocument.URL} == ${url}`);
+ return event.accessibleDocument.URL == url;
+ };
+}
+
+async function runTests(browser, accDoc) {
+ let onLoadEvents = waitForEvents({
+ expected: [
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_DOCUMENT_LOAD_COMPLETE, "body2"],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ],
+ unexpected: [
+ [EVENT_DOCUMENT_LOAD_COMPLETE, inIframeChecker("iframe1")],
+ [EVENT_STATE_CHANGE, inIframeChecker("iframe1")],
+ ],
+ });
+
+ BrowserTestUtils.loadURI(
+ browser,
+ `data:text/html;charset=utf-8,
+ <html><body id="body2">
+ <iframe id="iframe1" src="http://example.com"></iframe>
+ </body></html>`
+ );
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:about")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.loadURI(browser, "about:about");
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_RELOAD, evt => evt.isFromUserInput],
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ]);
+
+ EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal);
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:mozilla")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.loadURI(browser, "about:mozilla");
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_RELOAD, evt => !evt.isFromUserInput],
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ]);
+
+ browser.reload();
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("http://www.wronguri.wronguri/")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.loadURI(browser, "http://www.wronguri.wronguri/");
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("https://nocert.example.com/")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.loadURI(browser, "https://nocert.example.com:443/");
+
+ await onLoadEvents;
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask("", runTests);
diff --git a/accessible/tests/browser/events/browser_test_focus_browserui.js b/accessible/tests/browser/events/browser_test_focus_browserui.js
new file mode 100644
index 0000000000..98fc735269
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_browserui.js
@@ -0,0 +1,52 @@
+/* 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 */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTests(browser, accDoc) {
+ let onFocus = waitForEvent(EVENT_FOCUS, "input");
+ EventUtils.synthesizeKey("VK_TAB", {}, browser.ownerGlobal);
+ let evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "buttonInputDoc");
+ let url = snippetToURL(`<input id="input" type="button" value="button">`, {
+ contentDocBodyAttrs: { id: "buttonInputDoc" },
+ });
+ browser.loadURI(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "input");
+ browser.goBack();
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.DOMNode == gURLBar.inputField
+ );
+ EventUtils.synthesizeKey("t", { accelKey: true }, browser.ownerGlobal);
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "input");
+ EventUtils.synthesizeKey("w", { accelKey: true }, browser.ownerGlobal);
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+}
+
+/**
+ * Accessibility loading document events test.
+ */
+addAccessibleTask(`<input id="input">`, runTests);
diff --git a/accessible/tests/browser/events/browser_test_focus_dialog.js b/accessible/tests/browser/events/browser_test_focus_dialog.js
new file mode 100644
index 0000000000..71485a678d
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_dialog.js
@@ -0,0 +1,76 @@
+/* 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 */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTests(browser, accDoc) {
+ let onFocus = waitForEvent(EVENT_FOCUS, "button");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("button").focus();
+ });
+ let button = (await onFocus).accessible;
+ testStates(button, STATE_FOCUSED);
+
+ // Bug 1377942 - The target of the focus event changes under different
+ // circumstances.
+ // In e10s the focus event is the new window, in non-e10s it's the doc.
+ onFocus = waitForEvent(EVENT_FOCUS, () => true);
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ // button should be blurred
+ await onFocus;
+ testStates(button, 0, 0, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "button");
+ await BrowserTestUtils.closeWindow(newWin);
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "body2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("editabledoc")
+ .contentWindow.document.body.focus();
+ });
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "body2");
+ newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.closeWindow(newWin);
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ let onShow = waitForEvent(EVENT_SHOW, "alertdialog");
+ onFocus = waitForEvent(EVENT_FOCUS, "alertdialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ let alertDialog = content.document.getElementById("alertdialog");
+ alertDialog.style.display = "block";
+ alertDialog.focus();
+ });
+ await onShow;
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+}
+
+/**
+ * Accessible dialog focus testing
+ */
+addAccessibleTask(
+ `
+ <button id="button">button</button>
+ <iframe id="editabledoc"
+ src="${snippetToURL("", {
+ contentDocBodyAttrs: { id: "body2", contentEditable: "true" },
+ })}">
+ </iframe>
+ <div id="alertdialog" style="display: none" tabindex="-1" role="alertdialog" aria-labelledby="title2" aria-describedby="desc2">
+ <div id="title2">Blah blah</div>
+ <div id="desc2">Woof woof woof.</div>
+ <button>Close</button>
+ </div>`,
+ runTests
+);
diff --git a/accessible/tests/browser/events/browser_test_focus_urlbar.js b/accessible/tests/browser/events/browser_test_focus_urlbar.js
new file mode 100644
index 0000000000..874a0e239b
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_urlbar.js
@@ -0,0 +1,410 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+function isEventForAutocompleteItem(event) {
+ return event.accessible.role == ROLE_COMBOBOX_OPTION;
+}
+
+function isEventForButton(event) {
+ return event.accessible.role == ROLE_PUSHBUTTON;
+}
+
+function isEventForOneOffEngine(event) {
+ let parent = event.accessible.parent;
+ return (
+ event.accessible.role == ROLE_PUSHBUTTON &&
+ parent &&
+ parent.role == ROLE_GROUPING &&
+ parent.name
+ );
+}
+
+function isEventForMenuPopup(event) {
+ return event.accessible.role == ROLE_MENUPOPUP;
+}
+
+function isEventForMenuItem(event) {
+ return event.accessible.role == ROLE_MENUITEM;
+}
+
+function isEventForTipButton(event) {
+ let parent = event.accessible.parent;
+ return (
+ event.accessible.role == ROLE_PUSHBUTTON &&
+ parent &&
+ parent.role == ROLE_GROUPING &&
+ parent.name
+ );
+}
+
+/**
+ * A test provider.
+ */
+class TipTestProvider extends UrlbarProvider {
+ constructor(matches) {
+ super();
+ this._matches = matches;
+ }
+ get name() {
+ return "TipTestProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ isRestricting(context) {
+ return true;
+ }
+ async startQuery(context, addCallback) {
+ this._context = context;
+ for (const match of this._matches) {
+ addCallback(this, match);
+ }
+ }
+}
+
+// Check that the URL bar manages accessibility focus appropriately.
+async function runTests() {
+ registerCleanupFunction(async function() {
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ });
+
+ await PlacesTestUtils.addVisits([
+ "http://example1.com/blah",
+ "http://example2.com/blah",
+ "http://example1.com/",
+ "http://example2.com/",
+ ]);
+
+ // Ensure initial state.
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ gURLBar.focus();
+ let event = await focused;
+ let textBox = event.accessible;
+ // Ensure the URL bar is ready for a new URL to be typed.
+ // Sometimes, when this test runs, the existing text isn't selected when the
+ // URL bar is focused. Pressing escape twice ensures that the popup is
+ // closed and that the existing text is selected.
+ EventUtils.synthesizeKey("KEY_Escape");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Ensuring no focus change when first text is typed");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring no focus change on backspace");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring no focus change on text selection and delete");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Delete");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (1)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on down arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring previous arrow selection state doesn't get stale on input");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.sendString("z");
+ await focused;
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on down arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ if (AppConstants.platform == "macosx") {
+ info("Ensuring focus of another autocomplete item on ctrl-n");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on ctrl-p");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+ }
+
+ info("Ensuring focus of another autocomplete item on up arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on left arrow");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+
+ gURLBar.view.close();
+ // On Mac, down arrow when not at the end of the field moves to the end.
+ // Move back to the end so the next press of down arrow opens the popup.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+
+ info("Ensuring autocomplete focus on down arrow (2)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on arrow up for search settings button");
+ focused = waitForEvent(EVENT_FOCUS, isEventForButton);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus when text is typed");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.sendString("z");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ info("Ensuring autocomplete focus on down arrow (3)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on backspace");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ info("Ensuring autocomplete focus on arrow down (4)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ // Arrow down to the last result.
+ const resultCount = UrlbarTestUtils.getResultCount(window);
+ while (UrlbarTestUtils.getSelectedRowIndex(window) != resultCount - 1) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ info("Ensuring one-off search button focus on arrow down");
+ focused = waitForEvent(EVENT_FOCUS, isEventForOneOffEngine);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on arrow up");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on text selection");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+
+ if (AppConstants.platform == "macosx") {
+ // On Mac, ctrl-n after arrow left/right does not re-open the popup.
+ // Type some text so the next press of ctrl-n opens the popup.
+ EventUtils.sendString("ple");
+
+ info("Ensuring autocomplete focus on ctrl-n");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+ }
+
+ info(
+ "Ensuring context menu gets menu event on launch, item focus on down, and address bar focus on escape."
+ );
+ let menuEvent = waitForEvent(
+ nsIAccessibleEvent.EVENT_MENUPOPUP_START,
+ isEventForMenuPopup
+ );
+ await EventUtils.sendMouseEvent(
+ { type: "contextmenu" },
+ gURLBar.querySelector("moz-input-box")
+ );
+ await menuEvent;
+
+ focused = waitForEvent(EVENT_FOCUS, isEventForMenuItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ let closed = waitForEvent(
+ nsIAccessibleEvent.EVENT_MENUPOPUP_END,
+ isEventForMenuPopup
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await closed;
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+}
+
+// We test TIP results in their own test so the spoofed results don't interfere
+// with the main test.
+async function runTipTests() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ icon: "",
+ text: "This is a test intervention.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "about:blank",
+ buttonUrl: "about:mozilla",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ // Ensure the tip appears in the expected position.
+ matches[1].suggestedIndex = 2;
+
+ let provider = new TipTestProvider(matches);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ registerCleanupFunction(async function() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ gURLBar.focus();
+ let event = await focused;
+ let textBox = event.accessible;
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Ensuring no focus change when first text is typed");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (1)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the tip button is focused on down arrow");
+ info("Also ensuring that the tip button is a part of a labelled group");
+ focused = waitForEvent(EVENT_FOCUS, isEventForTipButton);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the help button is focused on down arrow");
+ info("Also ensuring that the help button is a part of a labelled group");
+ focused = waitForEvent(EVENT_FOCUS, isEventForTipButton);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (2)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the help button is focused on up arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForTipButton);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on left arrow, and not back to the tip button");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+}
+
+addAccessibleTask(``, runTests);
+addAccessibleTask(``, runTipTests);
diff --git a/accessible/tests/browser/events/browser_test_scrolling.js b/accessible/tests/browser/events/browser_test_scrolling.js
new file mode 100644
index 0000000000..d9a4a0a73f
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_scrolling.js
@@ -0,0 +1,96 @@
+/* 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";
+
+addAccessibleTask(
+ `
+ <div style="height: 100vh" id="one">one</div>
+ <div style="height: 100vh" id="two">two</div>
+ <div style="height: 100vh; width: 200vw; overflow: auto;" id="three">
+ <div style="height: 300%;">three</div>
+ </div>`,
+ async function(browser, accDoc) {
+ let onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#two";
+ });
+ let [scrollEvent1, scrollEndEvent1] = await onScrolling;
+ scrollEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent1.maxScrollY >= scrollEvent1.scrollY,
+ "scrollY is within max"
+ );
+ scrollEndEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent1.maxScrollY >= scrollEndEvent1.scrollY,
+ "scrollY is within max"
+ );
+
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#three";
+ });
+ let [scrollEvent2, scrollEndEvent2] = await onScrolling;
+ scrollEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent2.scrollY > scrollEvent1.scrollY,
+ `${scrollEvent2.scrollY} > ${scrollEvent1.scrollY}`
+ );
+ scrollEndEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent2.maxScrollY >= scrollEndEvent2.scrollY,
+ "scrollY is within max"
+ );
+
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(10, 0);
+ });
+ let [scrollEvent3, scrollEndEvent3] = await onScrolling;
+ scrollEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent3.maxScrollX >= scrollEvent3.scrollX,
+ "scrollX is within max"
+ );
+ scrollEndEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent3.maxScrollX >= scrollEndEvent3.scrollX,
+ "scrollY is within max"
+ );
+ ok(
+ scrollEvent3.scrollX > scrollEvent2.scrollX,
+ `${scrollEvent3.scrollX} > ${scrollEvent2.scrollX}`
+ );
+
+ // non-doc scrolling
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, "three"],
+ [EVENT_SCROLLING_END, "three"],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#three").scrollTo(0, 10);
+ });
+ let [scrollEvent4, scrollEndEvent4] = await onScrolling;
+ scrollEvent4.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent4.maxScrollY >= scrollEvent4.scrollY,
+ "scrollY is within max"
+ );
+ scrollEndEvent4.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent4.maxScrollY >= scrollEndEvent4.scrollY,
+ "scrollY is within max"
+ );
+ }
+);
diff --git a/accessible/tests/browser/events/browser_test_selection_urlbar.js b/accessible/tests/browser/events/browser_test_selection_urlbar.js
new file mode 100644
index 0000000000..0a773ce896
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_selection_urlbar.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+// Check that the URL bar manages accessibility
+// selection notifications appropriately on startup (new window).
+async function runTests() {
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ info("Creating new window");
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await PlacesTestUtils.addVisits("http://addons.mozilla.org");
+
+ registerCleanupFunction(async function() {
+ await BrowserTestUtils.closeWindow(newWin);
+ await PlacesUtils.history.clear();
+ });
+ info("Focusing window");
+ newWin.focus();
+ await focused;
+
+ // Ensure the URL bar is ready for a new URL to be typed.
+ // Sometimes, when this test runs, the existing text isn't selected when the
+ // URL bar is focused. Pressing escape twice ensures that the popup is
+ // closed and that the existing text is selected.
+ EventUtils.synthesizeKey("KEY_Escape", {}, newWin);
+ EventUtils.synthesizeKey("KEY_Escape", {}, newWin);
+ let caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+
+ info("Autofilling after typing `a` in new window URL bar.");
+ EventUtils.synthesizeKey("a", {}, newWin);
+ await UrlbarTestUtils.promiseSearchComplete(newWin);
+ Assert.equal(
+ newWin.gURLBar.inputField.value,
+ "addons.mozilla.org/",
+ "autofilled value as expected"
+ );
+
+ info("Ensuring caret moved on text selection");
+ await caretMoved;
+}
+
+addAccessibleTask(``, runTests);
diff --git a/accessible/tests/browser/events/browser_test_textcaret.js b/accessible/tests/browser/events/browser_test_textcaret.js
new file mode 100644
index 0000000000..d4e0f11a0f
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_textcaret.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Caret move events checker.
+ */
+function caretMoveChecker(target, caretOffset) {
+ return function(event) {
+ let cmEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent);
+ return (
+ cmEvent.accessible == getAccessible(target) &&
+ cmEvent.caretOffset == caretOffset
+ );
+ };
+}
+
+async function checkURLBarCaretEvents() {
+ const kURL = "about:mozilla";
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, kURL);
+ newWin.gBrowser.selectedBrowser.focus();
+
+ await waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => {
+ try {
+ return event.accessible.QueryInterface(nsIAccessibleDocument).URL == kURL;
+ } catch (e) {
+ return false;
+ }
+ });
+ info("Loaded " + kURL);
+
+ let urlbarInputEl = newWin.gURLBar.inputField;
+ let urlbarInput = getAccessible(urlbarInputEl, [nsIAccessibleText]);
+
+ let onCaretMove = waitForEvents([
+ [EVENT_TEXT_CARET_MOVED, caretMoveChecker(urlbarInput, kURL.length)],
+ [EVENT_FOCUS, urlbarInput],
+ ]);
+
+ urlbarInput.caretOffset = -1;
+ await onCaretMove;
+ ok(true, "Caret move in URL bar #1");
+
+ onCaretMove = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ caretMoveChecker(urlbarInput, 0)
+ );
+
+ urlbarInput.caretOffset = 0;
+ await onCaretMove;
+ ok(true, "Caret move in URL bar #2");
+
+ await BrowserTestUtils.closeWindow(newWin);
+}
+
+add_task(checkURLBarCaretEvents);
diff --git a/accessible/tests/browser/events/head.js b/accessible/tests/browser/events/head.js
new file mode 100644
index 0000000000..672aa46171
--- /dev/null
+++ b/accessible/tests/browser/events/head.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
diff --git a/accessible/tests/browser/fission/browser.ini b/accessible/tests/browser/fission/browser.ini
new file mode 100644
index 0000000000..a3fc7aa839
--- /dev/null
+++ b/accessible/tests/browser/fission/browser.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/browser/*.jsm
+ !/accessible/tests/mochitest/*.js
+
+[browser_content_tree.js]
+[browser_hidden_iframe.js]
+[browser_nested_iframe.js]
+[browser_reframe_root.js]
+[browser_reframe_visibility.js]
+[browser_src_change.js]
+[browser_take_focus.js]
diff --git a/accessible/tests/browser/fission/browser_content_tree.js b/accessible/tests/browser/fission/browser_content_tree.js
new file mode 100644
index 0000000000..54df06c7f4
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_content_tree.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";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ (gIsRemoteIframe ? isnot : is)(
+ browser.browsingContext.currentWindowGlobal.osPid,
+ browser.browsingContext.children[0].currentWindowGlobal.osPid,
+ `Content and IFRAME documents are in ${
+ gIsRemoteIframe ? "separate processes" : "same process"
+ }.`
+ );
+
+ const tree = {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ { CELL: [{ TEXT_LEAF: [] }] },
+ { CELL: [{ TEXT_LEAF: [] }] },
+ ],
+ },
+ ],
+ },
+ {
+ LIST: [
+ {
+ LISTITEM: [{ LISTITEM_MARKER: [] }, { TEXT_LEAF: [] }],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(contentDocAcc, tree);
+
+ const iframeAcc = contentDocAcc.getChildAt(0);
+ is(
+ iframeAcc.getChildAt(0),
+ iframeDocAcc,
+ "Document for the IFRAME matches IFRAME's first child."
+ );
+
+ is(
+ iframeDocAcc.parent,
+ iframeAcc,
+ "IFRAME document's parent matches the IFRAME."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_hidden_iframe.js b/accessible/tests/browser/fission/browser_hidden_iframe.js
new file mode 100644
index 0000000000..94b3015424
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_hidden_iframe.js
@@ -0,0 +1,81 @@
+/* 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 */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function(browser, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are not accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ let iframeDocAcc = findAccessibleChildByID(
+ contentDocAcc,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+ ok(!iframeAcc, "IFRAME is hidden and should not be accessible");
+ ok(!iframeDocAcc, "IFRAME document is hidden and should be accessible");
+
+ info(
+ "Show the IFRAME and check that it's now available in the accessibility tree."
+ );
+
+ const events = [[EVENT_REORDER, contentDocAcc]];
+ // Until this event is fired, IFRAME accessible has no children attached.
+ events.push([
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ const id = getAccessibleDOMNodeID(event.accessible);
+ return (
+ id === DEFAULT_IFRAME_DOC_BODY_ID &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === false
+ );
+ },
+ ]);
+
+ const onEvents = waitForEvents(events);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "";
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ iframeDocAcc = findAccessibleChildByID(
+ contentDocAcc,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(iframeDocAcc, "IFRAME document exists");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+ is(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "An accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ is(
+ iframeDocAcc.parent,
+ iframeAcc,
+ "IFRAME document's parent matches the IFRAME."
+ );
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: {
+ style: "display: none;",
+ },
+ skipFissionDocLoad: true,
+ }
+);
diff --git a/accessible/tests/browser/fission/browser_nested_iframe.js b/accessible/tests/browser/fission/browser_nested_iframe.js
new file mode 100644
index 0000000000..0e48d767f8
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_nested_iframe.js
@@ -0,0 +1,153 @@
+/* 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 });
+
+const NESTED_IFRAME_DOC_BODY_ID = "nested-iframe-body";
+const NESTED_IFRAME_ID = "nested-iframe";
+const nestedURL = new URL(`http://example.com/document-builder.sjs`);
+nestedURL.searchParams.append(
+ "html",
+ `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Nested Iframe Frame Test</title>
+ </head>
+ <body id="${NESTED_IFRAME_DOC_BODY_ID}">
+ <table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>
+ </body>
+ </html>`
+);
+
+function getOsPid(browsingContext) {
+ return browsingContext.currentWindowGlobal.osPid;
+}
+
+addAccessibleTask(
+ `<iframe id="${NESTED_IFRAME_ID}" src="${nestedURL.href}"/>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ let nestedDocAcc = findAccessibleChildByID(
+ iframeDocAcc,
+ NESTED_IFRAME_DOC_BODY_ID
+ );
+
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ ok(nestedDocAcc, "Nested IFRAME document accessible is present");
+
+ const state = {};
+ nestedDocAcc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ nestedDocAcc = (
+ await waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ NESTED_IFRAME_DOC_BODY_ID
+ )
+ ).accessible;
+ }
+
+ if (gIsRemoteIframe) {
+ isnot(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0]),
+ `Content and IFRAME documents are in separate processes.`
+ );
+ isnot(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `Content and nested IFRAME documents are in separate processes.`
+ );
+ isnot(
+ getOsPid(browser.browsingContext.children[0]),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `IFRAME and nested IFRAME documents are in separate processes.`
+ );
+ } else {
+ is(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0]),
+ `Content and IFRAME documents are in same processes.`
+ );
+ if (gFissionBrowser) {
+ isnot(
+ getOsPid(browser.browsingContext.children[0]),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `IFRAME and nested IFRAME documents are in separate processes.`
+ );
+ } else {
+ is(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `Content and nested IFRAME documents are in same processes.`
+ );
+ }
+ }
+
+ const tree = {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ { CELL: [{ TEXT_LEAF: [] }] },
+ { CELL: [{ TEXT_LEAF: [] }] },
+ ],
+ },
+ ],
+ },
+ {
+ LIST: [
+ {
+ LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(contentDocAcc, tree);
+
+ const nestedIframeAcc = iframeDocAcc.getChildAt(0);
+ is(
+ nestedIframeAcc.getChildAt(0),
+ nestedDocAcc,
+ "Document for nested IFRAME matches."
+ );
+
+ is(
+ nestedDocAcc.parent,
+ nestedIframeAcc,
+ "Nested IFRAME document's parent matches the nested IFRAME."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_reframe_root.js b/accessible/tests/browser/fission/browser_reframe_root.js
new file mode 100644
index 0000000000..66dcf249bf
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_reframe_root.js
@@ -0,0 +1,95 @@
+/* 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 */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+
+ info("Move the IFRAME under a new hidden root.");
+ let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => {
+ const doc = content.document;
+ const root = doc.createElement("div");
+ root.style.display = "none";
+ doc.body.appendChild(root);
+ root.appendChild(doc.getElementById(id));
+ });
+ await onEvents;
+
+ ok(
+ isDefunct(iframeAcc),
+ "IFRAME accessible should be defunct when hidden."
+ );
+ ok(
+ isDefunct(iframeDocAcc),
+ "IFRAME document's accessible should be defunct when the IFRAME is hidden."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID),
+ "No accessible for an IFRAME present."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID),
+ "No accessible for the IFRAME document present."
+ );
+
+ info("Move the IFRAME back under the content document's body.");
+ onEvents = waitForEvents([
+ [EVENT_REORDER, contentDocAcc],
+ [
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ const id = getAccessibleDOMNodeID(event.accessible);
+ return (
+ id === DEFAULT_IFRAME_DOC_BODY_ID &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === false
+ );
+ },
+ ],
+ ]);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => {
+ content.document.body.appendChild(content.document.getElementById(id));
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ const newiframeDocAcc = iframeAcc.firstChild;
+
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible");
+ ok(
+ isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should be defunct."
+ );
+ isnot(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "A new accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_reframe_visibility.js b/accessible/tests/browser/fission/browser_reframe_visibility.js
new file mode 100644
index 0000000000..bddb651f91
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_reframe_visibility.js
@@ -0,0 +1,116 @@
+/* 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 */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+
+ info(
+ "Hide the IFRAME and check that it's gone along with the IFRAME document."
+ );
+ let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "none";
+ });
+ await onEvents;
+
+ ok(
+ isDefunct(iframeAcc),
+ "IFRAME accessible should be defunct when hidden."
+ );
+ if (gIsRemoteIframe) {
+ ok(
+ !isDefunct(iframeDocAcc),
+ "IFRAME document's accessible is not defunct when the IFRAME is hidden and fission is enabled."
+ );
+ } else {
+ ok(
+ isDefunct(iframeDocAcc),
+ "IFRAME document's accessible is defunct when the IFRAME is hidden and fission is not enabled."
+ );
+ }
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID),
+ "No accessible for an IFRAME present."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID),
+ "No accessible for the IFRAME document present."
+ );
+
+ info(
+ "Show the IFRAME and check that a new accessible is created for it as " +
+ "well as the IFRAME document."
+ );
+
+ const events = [[EVENT_REORDER, contentDocAcc]];
+ if (!gIsRemoteIframe) {
+ events.push([
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ const id = getAccessibleDOMNodeID(event.accessible);
+ return (
+ id === DEFAULT_IFRAME_DOC_BODY_ID &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === false
+ );
+ },
+ ]);
+ }
+ onEvents = waitForEvents(events);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "block";
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ const newiframeDocAcc = iframeAcc.firstChild;
+
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(newiframeDocAcc, "IFRAME document exists");
+ ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible");
+ if (gIsRemoteIframe) {
+ ok(
+ !isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should not be defunct when fission is enabled."
+ );
+ is(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "Existing accessible is used for a IFRAME document."
+ );
+ } else {
+ ok(
+ isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should be defunct when fission is not enabled."
+ );
+ isnot(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ }
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "A new accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_src_change.js b/accessible/tests/browser/fission/browser_src_change.js
new file mode 100644
index 0000000000..c490468b15
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_src_change.js
@@ -0,0 +1,61 @@
+/* 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 });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(isAccessible(iframeAcc), "IFRAME should be accessible");
+ ok(isAccessible(iframeDocAcc), "IFRAME document should be accessible");
+
+ info("Replace src URL for the IFRAME with one with different origin.");
+ const onDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [DEFAULT_IFRAME_ID, CURRENT_CONTENT_DIR],
+ (id, olddir) => {
+ const { src } = content.document.getElementById(id);
+ content.document.getElementById(id).src = src.replace(
+ olddir,
+ "http://example.net/browser/accessible/tests/browser/"
+ );
+ }
+ );
+ const newiframeDocAcc = (await onDocLoad).accessible;
+
+ ok(isAccessible(iframeAcc), "IFRAME should be accessible");
+ ok(
+ isAccessible(newiframeDocAcc),
+ "new IFRAME document should be accessible"
+ );
+ isnot(
+ iframeDocAcc,
+ newiframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "An IFRAME has a new accessible for a IFRAME document as a child."
+ );
+ is(
+ newiframeDocAcc.parent,
+ iframeAcc,
+ "A new accessible for a IFRAME document has an IFRAME as a parent."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_take_focus.js b/accessible/tests/browser/fission/browser_take_focus.js
new file mode 100644
index 0000000000..6e6eba662e
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_take_focus.js
@@ -0,0 +1,26 @@
+/* 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: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function(browser, iframeDocAcc, contentDocAcc) {
+ const textbox = findAccessibleChildByID(iframeDocAcc, "textbox");
+ testStates(textbox, STATE_FOCUSABLE, 0, STATE_FOCUSED);
+
+ let onFocus = waitForEvent(EVENT_FOCUS, textbox);
+ textbox.takeFocus();
+ await onFocus;
+
+ testStates(textbox, STATE_FOCUSABLE | STATE_FOCUSED, 0);
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/head.js b/accessible/tests/browser/fission/head.js
new file mode 100644
index 0000000000..672aa46171
--- /dev/null
+++ b/accessible/tests/browser/fission/head.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
diff --git a/accessible/tests/browser/general/browser.ini b/accessible/tests/browser/general/browser.ini
new file mode 100644
index 0000000000..93ff094995
--- /dev/null
+++ b/accessible/tests/browser/general/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ !/accessible/tests/browser/shared-head.js
+ head.js
+ !/accessible/tests/mochitest/*.js
+skip-if = a11y_checks || (os == 'win' && processor == 'aarch64') # windows-aarch64, 1534811
+
+[browser_test_doc_creation.js]
+[browser_test_urlbar.js]
diff --git a/accessible/tests/browser/general/browser_test_doc_creation.js b/accessible/tests/browser/general/browser_test_doc_creation.js
new file mode 100644
index 0000000000..7ee07f63fd
--- /dev/null
+++ b/accessible/tests/browser/general/browser_test_doc_creation.js
@@ -0,0 +1,55 @@
+/* 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 tab1URL = `data:text/html,
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>First tab to be loaded</title>
+ </head>
+ <body>
+ <butotn>JUST A BUTTON</butotn>
+ </body>
+ </html>`;
+
+const tab2URL = `data:text/html,
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Second tab to be loaded</title>
+ </head>
+ <body>
+ <butotn>JUST A BUTTON</butotn>
+ </body>
+ </html>`;
+
+// Checking that, if there are open windows before accessibility was started,
+// root accessibles for open windows are created so that all root accessibles
+// are stored in application accessible children array.
+add_task(async function testDocumentCreation() {
+ let tab1 = await openNewTab(tab1URL);
+ let tab2 = await openNewTab(tab2URL);
+ let accService = await initAccessibilityService();
+
+ info("Verifying that each tab content document is in accessible cache.");
+ for (const browser of [...gBrowser.browsers]) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let accServiceContent = Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(Ci.nsIAccessibilityService);
+ Assert.ok(
+ !!accServiceContent.getAccessibleFromCache(content.document),
+ "Document accessible is in cache."
+ );
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ accService = null; // eslint-disable-line no-unused-vars
+ await shutdownAccessibilityService();
+});
diff --git a/accessible/tests/browser/general/browser_test_urlbar.js b/accessible/tests/browser/general/browser_test_urlbar.js
new file mode 100644
index 0000000000..8c0c7797b5
--- /dev/null
+++ b/accessible/tests/browser/general/browser_test_urlbar.js
@@ -0,0 +1,40 @@
+/* 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.import(
+ "resource://testing-common/UrlbarTestUtils.jsm"
+);
+
+// Checking that the awesomebar popup gets COMBOBOX_LIST role instead of
+// LISTBOX, since its parent is a <panel> (see Bug 1422465)
+add_task(async function testAutocompleteRichResult() {
+ let tab = await openNewTab("data:text/html;charset=utf-8,");
+ let accService = await initAccessibilityService();
+
+ info("Opening the URL bar and entering a key to show the urlbar panel");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "a",
+ });
+
+ info("Waiting for accessibility to be created for the results list");
+ let resultsView;
+ resultsView = gURLBar.view.panel.querySelector(".urlbarView-results");
+ await TestUtils.waitForCondition(() =>
+ accService.getAccessibleFor(resultsView)
+ );
+
+ info("Confirming that the special case is handled in XULListboxAccessible");
+ let accessible = accService.getAccessibleFor(resultsView);
+ is(accessible.role, ROLE_COMBOBOX_LIST, "Right role");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+registerCleanupFunction(async function() {
+ await shutdownAccessibilityService();
+});
diff --git a/accessible/tests/browser/general/head.js b/accessible/tests/browser/general/head.js
new file mode 100644
index 0000000000..cd03a441f3
--- /dev/null
+++ b/accessible/tests/browser/general/head.js
@@ -0,0 +1,72 @@
+/* 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 initAccessibilityService, openNewTab, shutdownAccessibilityService */
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+const nsIAccessibleRole = Ci.nsIAccessibleRole; // eslint-disable-line no-unused-vars
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function openNewTab(url) {
+ const forceNewProcess = true;
+
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url,
+ forceNewProcess,
+ });
+}
+
+async function initAccessibilityService() {
+ info("Create accessibility service.");
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await new Promise(resolve => {
+ if (Services.appinfo.accessibilityEnabled) {
+ resolve();
+ return;
+ }
+
+ let observe = (subject, topic, data) => {
+ if (data === "1") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+
+ return accService;
+}
+
+function shutdownAccessibilityService() {
+ forceGC();
+
+ return new Promise(resolve => {
+ if (!Services.appinfo.accessibilityEnabled) {
+ resolve();
+ return;
+ }
+
+ let observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+}
diff --git a/accessible/tests/browser/head.js b/accessible/tests/browser/head.js
new file mode 100644
index 0000000000..915d502905
--- /dev/null
+++ b/accessible/tests/browser/head.js
@@ -0,0 +1,147 @@
+/* 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 initAccService, shutdownAccService, waitForEvent, setE10sPrefs,
+ unsetE10sPrefs, accConsumersChanged */
+
+// Load the shared-head file first.
+/* import-globals-from shared-head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+);
+
+/**
+ * Set e10s related preferences in the test environment.
+ * @return {Promise} promise that resolves when preferences are set.
+ */
+function setE10sPrefs() {
+ return new Promise(resolve =>
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["browser.tabs.remote.autostart", true]],
+ },
+ resolve
+ )
+ );
+}
+
+/**
+ * Unset e10s related preferences in the test environment.
+ * @return {Promise} promise that resolves when preferences are unset.
+ */
+function unsetE10sPrefs() {
+ return new Promise(resolve => {
+ SpecialPowers.popPrefEnv(resolve);
+ });
+}
+
+/**
+ * Capture when 'a11y-consumers-changed' event is fired.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function accConsumersChanged(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccConsumersChangedObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccConsumersChanged()
+ ),
+ ]
+ : [
+ CommonUtils.addAccConsumersChangedObserver(),
+ CommonUtils.observeAccConsumersChanged(),
+ ];
+}
+
+/**
+ * Capture when accessibility service is initialized.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is expected to be initialized in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function initAccService(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccServiceInitializedObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccServiceInitialized()
+ ),
+ ]
+ : [
+ CommonUtils.addAccServiceInitializedObserver(),
+ CommonUtils.observeAccServiceInitialized(),
+ ];
+}
+
+/**
+ * Capture when accessibility service is shutdown.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is expected to be shutdown in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function shutdownAccService(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccServiceShutdownObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccServiceShutdown()
+ ),
+ ]
+ : [
+ CommonUtils.addAccServiceShutdownObserver(),
+ CommonUtils.observeAccServiceShutdown(),
+ ];
+}
+
+/**
+ * Simpler verions of waitForEvent defined in
+ * accessible/tests/browser/events.js
+ */
+function waitForEvent(eventType, expectedId) {
+ return new Promise(resolve => {
+ let eventObserver = {
+ observe(subject) {
+ let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
+ let id;
+ try {
+ id = event.accessible.id;
+ } catch (e) {
+ // This can throw NS_ERROR_FAILURE.
+ }
+ if (event.eventType === eventType && id === expectedId) {
+ Services.obs.removeObserver(this, "accessible-event");
+ resolve(event);
+ }
+ },
+ };
+ Services.obs.addObserver(eventObserver, "accessible-event");
+ });
+}
diff --git a/accessible/tests/browser/hittest/browser.ini b/accessible/tests/browser/hittest/browser.ini
new file mode 100644
index 0000000000..6b293b24bf
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/browser/*.jsm
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/mochitest/letters.gif
+
+[browser_test_browser.js]
+[browser_test_canvas_hitregion.js]
+skip-if = os == "android"
+[browser_test_general.js]
+[browser_test_shadowroot.js]
+[browser_test_zoom_text.js]
+[browser_test_zoom.js]
diff --git a/accessible/tests/browser/hittest/browser_test_browser.js b/accessible/tests/browser/hittest/browser_test_browser.js
new file mode 100644
index 0000000000..477af42fe9
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_browser.js
@@ -0,0 +1,68 @@
+/* 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";
+
+async function runTests(browser, accDoc) {
+ // Hit testing. See bug #726097
+ await invokeContentTask(browser, [], () =>
+ content.document.getElementById("hittest").scrollIntoView(true)
+ );
+
+ const dpr = await getContentDPR(browser);
+ const hititem = findAccessibleChildByID(accDoc, "hititem");
+ const hittest = findAccessibleChildByID(accDoc, "hittest");
+ const outerDocAcc = accDoc.parent;
+ const rootAcc = CommonUtils.getRootAccessible(document);
+
+ const [hitX, hitY, hitWidth, hitHeight] = Layout.getBounds(hititem, dpr);
+ // "hititem" node has the full screen width, so when we divide it by 2, we are
+ // still way outside the inline content.
+ const tgtX = hitX + hitWidth / 2;
+ const tgtY = hitY + hitHeight / 2;
+
+ let hitAcc = rootAcc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hititem,
+ `Hit match at ${tgtX},${tgtY} (root doc deepest child). Found: ${prettyName(
+ hitAcc
+ )}`
+ );
+
+ const hitAcc2 = accDoc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hitAcc2,
+ `Hit match at ${tgtX},${tgtY} (doc deepest child). Found: ${prettyName(
+ hitAcc2
+ )}`
+ );
+
+ hitAcc = outerDocAcc.getChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ accDoc,
+ `Hit match at ${tgtX},${tgtY} (outer doc child). Found: ${prettyName(
+ hitAcc
+ )}`
+ );
+
+ hitAcc = accDoc.getChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hittest,
+ `Hit match at ${tgtX},${tgtY} (doc child). Found: ${prettyName(hitAcc)}`
+ );
+}
+
+addAccessibleTask(
+ `
+ <div id="hittest">
+ <div id="hititem"><span role="img">img</span>item</div>
+ </div>
+ `,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_canvas_hitregion.js b/accessible/tests/browser/hittest/browser_test_canvas_hitregion.js
new file mode 100644
index 0000000000..dbd326f593
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_canvas_hitregion.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";
+
+function convertCSSToDevicePixels(browser, ...bounds) {
+ return invokeContentTask(browser, bounds, (x, y, width, height) => {
+ const { Layout: LayoutUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+
+ return LayoutUtils.CSSToDevicePixels(content, x, y, width, height);
+ });
+}
+
+async function runTests(browser, accDoc) {
+ Services.prefs.setBoolPref("canvas.hitregions.enabled", true);
+
+ const offsetX = 20;
+ const offsetY = 40;
+ await invokeContentTask(browser, [], () =>
+ content.document.getElementById("hitcanvas").scrollIntoView(true)
+ );
+ const dpr = await getContentDPR(browser);
+
+ await invokeContentTask(browser, [offsetX, offsetY], (x, y) => {
+ const doc = content.document;
+ const element = doc.getElementById("hitcheck");
+ const context = doc.getElementById("hitcanvas").getContext("2d");
+ context.save();
+ context.font = "10px sans-serif";
+ context.textAlign = "left";
+ context.textBaseline = "middle";
+ const metrics = context.measureText(element.parentNode.textContent);
+ context.beginPath();
+ context.strokeStyle = "black";
+ context.rect(x - 5, y - 5, 10, 10);
+ context.stroke();
+ if (element.checked) {
+ context.fillStyle = "black";
+ context.fill();
+ }
+
+ context.fillText(element.parentNode.textContent, x + 5, y);
+ context.beginPath();
+ context.rect(x - 7, y - 7, 12 + metrics.width + 2, 14);
+
+ if (doc.activeElement == element) {
+ context.drawFocusIfNeeded(element);
+ }
+
+ context.addHitRegion({ control: element });
+ context.restore();
+ });
+
+ const hitcanvas = findAccessibleChildByID(accDoc, "hitcanvas");
+ const hitcheck = findAccessibleChildByID(accDoc, "hitcheck");
+ const [hitX, hitY /* hitWidth, hitHeight */] = Layout.getBounds(
+ hitcanvas,
+ dpr
+ );
+ const [deltaX, deltaY] = await convertCSSToDevicePixels(
+ browser,
+ offsetX,
+ offsetY
+ );
+
+ info("Test if we hit the region associated with the shadow dom checkbox.");
+ const tgtX = hitX + deltaX;
+ let tgtY = hitY + deltaY;
+ let hitAcc = accDoc.getDeepestChildAtPoint(tgtX, tgtY);
+ CommonUtils.isObject(hitAcc, hitcheck, `Hit match at (${tgtX}, ${tgtY}`);
+
+ info(
+ "Test that we don't hit the region associated with the shadow dom checkbox."
+ );
+ tgtY = hitY + deltaY * 2;
+ hitAcc = accDoc.getDeepestChildAtPoint(tgtX, tgtY);
+ CommonUtils.isObject(hitAcc, hitcanvas, `Hit match at (${tgtX}, ${tgtY}`);
+
+ Services.prefs.clearUserPref("canvas.hitregions.enabled");
+}
+
+addAccessibleTask(
+ `
+ <canvas id="hitcanvas">
+ <input id="hitcheck" type="checkbox"><label for="showA"> Show A </label>
+ </canvas>`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_general.js b/accessible/tests/browser/hittest/browser_test_general.js
new file mode 100644
index 0000000000..08596529ee
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_general.js
@@ -0,0 +1,123 @@
+/* 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";
+
+async function runTests(browser, accDoc) {
+ await waitForImageMap(browser, accDoc);
+ const dpr = await getContentDPR(browser);
+
+ info("Not specific case, child and deepchild testing.");
+ testChildAtPoint(
+ dpr,
+ 3,
+ 3,
+ findAccessibleChildByID(accDoc, "list"),
+ findAccessibleChildByID(accDoc, "listitem"),
+ findAccessibleChildByID(accDoc, "inner").firstChild
+ );
+ todo(
+ false,
+ "Bug 746974 - children must match on all platforms. On Windows, " +
+ "ChildAtPoint with eDeepestChild is incorrectly ignoring MustPrune " +
+ "for the graphic."
+ );
+
+ info(
+ "::MustPrune case (in this case childAtPoint doesn't look inside a " +
+ "textbox), point is inside of textbox."
+ );
+ const txt = findAccessibleChildByID(accDoc, "txt");
+ testChildAtPoint(dpr, 1, 1, txt, txt, txt);
+
+ info(
+ "::MustPrune case, point is outside of textbox accessible but is in document."
+ );
+ testChildAtPoint(dpr, -1, -1, txt, null, null);
+
+ info("::MustPrune case, point is outside of root accessible.");
+ testChildAtPoint(dpr, -10000, -10000, txt, null, null);
+
+ info("Not specific case, point is inside of btn accessible.");
+ const btn = findAccessibleChildByID(accDoc, "btn");
+ testChildAtPoint(dpr, 1, 1, btn, btn, btn);
+
+ info("Not specific case, point is outside of btn accessible.");
+ testChildAtPoint(dpr, -1, -1, btn, null, null);
+
+ info(
+ "Out of flow accessible testing, do not return out of flow accessible " +
+ "because it's not a child of the accessible even though visually it is."
+ );
+ await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+ );
+
+ const doc = content.document;
+ const rectArea = CommonUtils.getNode("area", doc).getBoundingClientRect();
+ const outOfFlow = CommonUtils.getNode("outofflow", doc);
+ outOfFlow.style.left = rectArea.left + "px";
+ outOfFlow.style.top = rectArea.top + "px";
+ });
+
+ const area = findAccessibleChildByID(accDoc, "area");
+ testChildAtPoint(dpr, 1, 1, area, area, area);
+
+ info("Test image maps. Their children are not in the layout tree.");
+ const imgmap = findAccessibleChildByID(accDoc, "imgmap");
+ const theLetterA = imgmap.firstChild;
+ await hitTest(browser, imgmap, theLetterA, theLetterA);
+ await hitTest(
+ browser,
+ findAccessibleChildByID(accDoc, "container"),
+ imgmap,
+ theLetterA
+ );
+
+ info("hit testing for element contained by zero-width element");
+ const container2Input = findAccessibleChildByID(accDoc, "container2_input");
+ await hitTest(
+ browser,
+ findAccessibleChildByID(accDoc, "container2"),
+ container2Input,
+ container2Input
+ );
+}
+
+addAccessibleTask(
+ `
+ <div role="list" id="list">
+ <div role="listitem" id="listitem"><span title="foo" id="inner">inner</span>item</div>
+ </div>
+
+ <span role="button">button1</span><span role="button" id="btn">button2</span>
+
+ <span role="textbox">textbox1</span><span role="textbox" id="txt">textbox2</span>
+
+ <div id="outofflow" style="width: 10px; height: 10px; position: absolute; left: 0px; top: 0px; background-color: yellow;">
+ </div>
+ <div id="area" style="width: 100px; height: 100px; background-color: blue;"></div>
+
+ <map name="atoz_map">
+ <area id="thelettera" href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,15,15" alt="thelettera" shape="rect"/>
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15" usemap="#atoz_map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"/>
+ </div>
+
+ <div id="container2" style="width: 0px">
+ <input id="container2_input">
+ </div>
+ `,
+ runTests,
+ {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px;" },
+ }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_shadowroot.js b/accessible/tests/browser/hittest/browser_test_shadowroot.js
new file mode 100644
index 0000000000..cb499a4fd9
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_shadowroot.js
@@ -0,0 +1,61 @@
+/* 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";
+
+async function runTests(browser, accDoc) {
+ const dpr = await getContentDPR(browser);
+ let componentAcc = findAccessibleChildByID(accDoc, "component1");
+ testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ componentAcc,
+ componentAcc.firstChild,
+ componentAcc.firstChild
+ );
+
+ componentAcc = findAccessibleChildByID(accDoc, "component2");
+ testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ componentAcc,
+ componentAcc.firstChild,
+ componentAcc.firstChild
+ );
+}
+
+addAccessibleTask(
+ `
+ <div role="group" class="components" id="component1" style="display: inline-block;">
+ <!--
+ <div role="button" id="component-child"
+ style="width: 100px; height: 100px; background-color: pink;">
+ </div>
+ -->
+ </div>
+ <div role="group" class="components" id="component2" style="display: inline-block;">
+ <!--
+ <button>Hello world</button>
+ -->
+ </div>
+ <script>
+ // This routine adds the comment children of each 'component' to its
+ // shadow root.
+ var components = document.querySelectorAll(".components");
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var shadow = component.attachShadow({mode: "open"});
+ for (var child = component.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === 8)
+ // eslint-disable-next-line no-unsanitized/property
+ shadow.innerHTML = child.data;
+ }
+ }
+ </script>
+ `,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_zoom.js b/accessible/tests/browser/hittest/browser_test_zoom.js
new file mode 100644
index 0000000000..2ea330fb74
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_zoom.js
@@ -0,0 +1,38 @@
+/* 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";
+
+async function runTests(browser, accDoc) {
+ if (Services.appinfo.OS !== "Darwin") {
+ const p1 = findAccessibleChildByID(accDoc, "p1");
+ const p2 = findAccessibleChildByID(accDoc, "p2");
+ await hitTest(browser, accDoc, p1, p1.firstChild);
+ await hitTest(browser, accDoc, p2, p2.firstChild);
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+
+ Layout.zoomDocument(content.document, 2.0);
+ content.document.body.offsetTop; // getBounds doesn't flush layout on its own.
+ });
+
+ await hitTest(browser, accDoc, p1, p1.firstChild);
+ await hitTest(browser, accDoc, p2, p2.firstChild);
+ } else {
+ todo(
+ false,
+ "Bug 746974 - deepest child must be correct on all platforms, disabling on Mac!"
+ );
+ }
+}
+
+addAccessibleTask(`<p id="p1">para 1</p><p id="p2">para 2</p>`, runTests, {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "left: 100px; top: 100px;" },
+});
diff --git a/accessible/tests/browser/hittest/browser_test_zoom_text.js b/accessible/tests/browser/hittest/browser_test_zoom_text.js
new file mode 100644
index 0000000000..0d7eb336ca
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_zoom_text.js
@@ -0,0 +1,76 @@
+/* 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 if getOffsetAtPoint returns the given text offset at given coordinates.
+ */
+function testOffsetAtPoint(hyperText, x, y, coordType, expectedOffset) {
+ is(
+ hyperText.getOffsetAtPoint(x, y, coordType),
+ expectedOffset,
+ `Wrong offset at given point (${x}, ${y}) for ${prettyName(hyperText)}`
+ );
+}
+
+async function runTests(browser, accDoc) {
+ const expectedLength = await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+ );
+ const hyperText = CommonUtils.getNode("paragraph", content.document);
+
+ return hyperText.textContent.length / 2;
+ });
+ const hyperText = findAccessibleChildByID(accDoc, "paragraph", [
+ Ci.nsIAccessibleText,
+ ]);
+ const textNode = hyperText.firstChild;
+
+ let [x, y, width, height] = Layout.getBounds(
+ textNode,
+ await getContentDPR(browser)
+ );
+
+ testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+
+ Layout.zoomDocument(content.document, 2.0);
+ content.document.body.offsetTop; // getBounds doesn't flush layout on its own.
+ });
+
+ [x, y, width, height] = Layout.getBounds(
+ textNode,
+ await getContentDPR(browser)
+ );
+
+ testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+}
+
+addAccessibleTask(
+ `<p id="paragraph" style="font-family: monospace;">Болтали две сороки</p>`,
+ runTests,
+ {
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "width: 600px; height: 600px;" },
+ }
+);
diff --git a/accessible/tests/browser/hittest/head.js b/accessible/tests/browser/hittest/head.js
new file mode 100644
index 0000000000..f06467da7c
--- /dev/null
+++ b/accessible/tests/browser/hittest/head.js
@@ -0,0 +1,89 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+
+/* exported CommonUtils, testChildAtPoint, Layout, hitTest */
+
+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 }
+);
+
+const { CommonUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm"
+);
+
+const { Layout } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+);
+
+function getChildAtPoint(container, x, y, findDeepestChild) {
+ try {
+ return findDeepestChild
+ ? container.getDeepestChildAtPoint(x, y)
+ : container.getChildAtPoint(x, y);
+ } catch (e) {
+ // Failed to get child at point.
+ }
+
+ return null;
+}
+
+function testChildAtPoint(dpr, x, y, container, child, grandChild) {
+ const [containerX, containerY] = Layout.getBounds(container, dpr);
+ x += containerX;
+ y += containerY;
+
+ CommonUtils.isObject(
+ getChildAtPoint(container, x, y, false),
+ child,
+ `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}`
+ );
+
+ CommonUtils.isObject(
+ getChildAtPoint(container, x, y, true),
+ grandChild,
+ `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}`
+ );
+}
+
+/**
+ * Test if getChildAtPoint returns the given child and grand child accessibles
+ * at coordinates of child accessible (direct and deep hit test).
+ */
+async function hitTest(browser, container, child, grandChild) {
+ const [childX, childY] = await getContentBoundsForDOMElm(
+ browser,
+ getAccessibleDOMNodeID(child)
+ );
+ const x = childX + 1;
+ const y = childY + 1;
+
+ CommonUtils.isObject(
+ getChildAtPoint(container, x, y, false),
+ child,
+ `Wrong direct child of ${prettyName(container)}`
+ );
+
+ CommonUtils.isObject(
+ getChildAtPoint(container, x, y, true),
+ grandChild,
+ `Wrong deepest child of ${prettyName(container)}`
+ );
+}
diff --git a/accessible/tests/browser/mac/browser.ini b/accessible/tests/browser/mac/browser.ini
new file mode 100644
index 0000000000..ddcfe0e081
--- /dev/null
+++ b/accessible/tests/browser/mac/browser.ini
@@ -0,0 +1,50 @@
+[DEFAULT]
+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
+
+[browser_app.js]
+[browser_aria_current.js]
+[browser_details_summary.js]
+[browser_label_title.js]
+[browser_range.js]
+[browser_roles_elements.js]
+[browser_table.js]
+[browser_selectables.js]
+[browser_radio_position.js]
+[browser_toggle_radio_check.js]
+[browser_link.js]
+[browser_aria_haspopup.js]
+[browser_required.js]
+[browser_popupbutton.js]
+[browser_mathml.js]
+[browser_input.js]
+[browser_focus.js]
+[browser_text_leaf.js]
+[browser_webarea.js]
+skip-if = os == 'mac' && !debug #1648813
+[browser_text_basics.js]
+[browser_text_input.js]
+skip-if = os == 'mac' && debug # Bug 1664577
+[browser_rotor.js]
+[browser_rootgroup.js]
+[browser_text_selection.js]
+[browser_navigate.js]
+[browser_outline.js]
+[browser_outline_xul.js]
+[browser_hierarchy.js]
+[browser_menulist.js]
+[browser_rich_listbox.js]
+[browser_live_regions.js]
+[browser_aria_busy.js]
+[browser_aria_controls_flowto.js] \ No newline at end of file
diff --git a/accessible/tests/browser/mac/browser_app.js b/accessible/tests/browser/mac/browser_app.js
new file mode 100644
index 0000000000..e57555b556
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_app.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getMacAccessible(accOrElmOrID) {
+ return new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let acc = getAccessible(accOrElmOrID);
+ if (acc) {
+ clearInterval(intervalId);
+ resolve(
+ acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface)
+ );
+ }
+ }, 10);
+ });
+}
+
+/**
+ * Test 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)
+ is(rootChildCount(), 5, "Root with no popups has 5 children");
+
+ // Open a context menu
+ const menu = document.getElementById("contentAreaContextMenu");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await waitForMacEvent("AXMenuOpened");
+
+ // Now root has 6 children
+ is(rootChildCount(), 6, "Root has 6 children");
+
+ // Close context menu
+ let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ await closed;
+
+ // We're back to 5
+ is(rootChildCount(), 5, "Root has 5 children");
+
+ // Open site identity popup
+ document.getElementById("identity-box").click();
+ const identityPopup = document.getElementById("identity-popup");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
+
+ // Now root has 6 children
+ is(rootChildCount(), 6, "Root has 6 children");
+
+ // Close popup
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden");
+
+ // We're back to 5
+ is(rootChildCount(), 5, "Root has 5 children");
+ }
+ );
+});
+
+/**
+ * Tests for location bar
+ */
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://example.com",
+ },
+ async browser => {
+ let input = await getMacAccessible("urlbar-input");
+ is(
+ input.getAttributeValue("AXValue"),
+ "example.com",
+ "Location bar has correct value"
+ );
+ }
+ );
+});
+
+/**
+ * Test context menu
+ */
+add_task(async () => {
+ 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;
+
+ // 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");
+ // menu contains 14 items when containers disabled, 15 items otherwise
+ const expectedChildCount = hasContainers ? 15 : 14;
+ is(
+ menuChildren.length,
+ expectedChildCount,
+ "Context menu on link contains 14 or 15 items depending on release"
+ );
+ // items at indicies 4, 10, and 12 are the splitters when containers exist
+ // everything else should be a menu item, otherwise indicies of splitters are
+ // 3, 9, and 11
+ const splitterIndicies = hasContainers ? [4, 10, 12] : [3, 9, 11];
+ 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..c1b75b8318
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_controls_flowto.js
@@ -0,0 +1,84 @@
+/* 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 isARIAControls = (id, expectedIds) =>
+ Assert.deepEqual(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXARIAControls")
+ .map(e => e.getAttributeValue("AXDOMIdentifier")),
+ expectedIds,
+ `"${id}" has correct AXARIAControls`
+ );
+
+ isARIAControls("info-button", ["info"]);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-controls", "info more-info");
+ });
+
+ isARIAControls("info-button", ["info", "more-info"]);
+ }
+);
+
+/**
+ * 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) => {
+ const isLinkedUIElements = (id, expectedIds) =>
+ Assert.deepEqual(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXLinkedUIElements")
+ .map(e => e.getAttributeValue("AXDOMIdentifier")),
+ expectedIds,
+ `"${id}" has correct AXARIAControls`
+ );
+
+ isLinkedUIElements("info-button", ["info"]);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-flowto", "info more-info");
+ });
+
+ isLinkedUIElements("info-button", ["info", "more-info"]);
+ }
+);
+
+/**
+ * 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) => {
+ const isLinkedUIElements = (id, expectedIds) =>
+ Assert.deepEqual(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXLinkedUIElements")
+ .map(e => e.getAttributeValue("AXDOMIdentifier")),
+ expectedIds,
+ `"${id}" has correct AXARIAControls`
+ );
+
+ isLinkedUIElements("dog-radio", ["cat-radio", "dog-radio", "info"]);
+ }
+);
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..0c5ff08a0a
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_current.js
@@ -0,0 +1,60 @@
+/* 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"
+ );
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("one")
+ .setAttribute("aria-current", "step");
+ });
+ await stateChanged;
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ "step",
+ "Correct aria-current for #one"
+ );
+
+ 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_haspopup.js b/accessible/tests/browser/mac/browser_aria_haspopup.js
new file mode 100644
index 0000000000..36bff246db
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_haspopup.js
@@ -0,0 +1,330 @@
+/* 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"
+ );
+
+ attrChanged = waitForEvent(EVENT_STATE_CHANGE, "menu");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("menu")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ 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;
+
+ is(
+ 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"
+ );
+
+ attrChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("listbox")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ 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"
+ );
+
+ attrChanged = waitForEvent(EVENT_STATE_CHANGE, "tree");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("tree")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ 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"
+ );
+
+ attrChanged = waitForEvent(EVENT_STATE_CHANGE, "grid");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("grid")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ 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"
+ );
+
+ attrChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("dialog")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ 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_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js
new file mode 100644
index 0000000000..62183afef5
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_details_summary.js
@@ -0,0 +1,60 @@
+/* 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>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 reorder = waitForEvent(EVENT_REORDER, "details");
+ summary.performAction("AXPress");
+ is(summary.getAttributeValue("AXExpanded"), 1, "Summary is collapsed");
+ // The reorder gecko event notifies us of a tree change.
+ await reorder;
+
+ detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 2, "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_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..0ed45391e9
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_label_title.js
@@ -0,0 +1,79 @@
+/* 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(
+ `<article id="article" aria-label="Hello world"></article>`,
+ async (browser, accDoc) => {
+ let article = getNativeInterface(accDoc, "article");
+ is(article.getAttributeValue("AXTitle"), "Hello world");
+ let evt = waitForMacEvent("AXTitleChanged", "article");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("article")
+ .setAttribute("aria-label", "Hello universe");
+ });
+ await evt;
+ is(article.getAttributeValue("AXTitle"), "Hello universe");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_link.js b/accessible/tests/browser/mac/browser_link.js
new file mode 100644
index 0000000000..4ae128fbf6
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_link.js
@@ -0,0 +1,125 @@
+/* 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.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+/**
+ * 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");
+
+ 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 stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link1");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link1").removeAttribute("href");
+ });
+ await stateChanged;
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from href gets group role"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link2").removeAttribute("onclick");
+ });
+ await stateChanged;
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from onclick gets group role"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link3");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("link3")
+ .setAttribute("href", "http://example.com");
+ });
+ await stateChanged;
+ is(
+ link3.getAttributeValue("AXRole"),
+ "AXLink",
+ "href added to bare a gets link role"
+ );
+
+ ok(
+ link3.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js
new file mode 100644
index 0000000000..10a03120f8
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_live_regions.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test live region creation and removal.
+ */
+addAccessibleTask(
+ `
+ <div id="polite" aria-relevant="removals">Polite region</div>
+ <div id="assertive" aria-live="assertive">Assertive region</div>
+ `,
+ async (browser, accDoc) => {
+ let politeRegion = getNativeInterface(accDoc, "polite");
+ ok(
+ !politeRegion.attributeNames.includes("AXARIALive"),
+ "region is not live"
+ );
+
+ let liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "polite");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-atomic", "true");
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-live", "polite");
+ });
+ await liveRegionAdded;
+ is(
+ politeRegion.getAttributeValue("AXARIALive"),
+ "polite",
+ "region is now live"
+ );
+ ok(politeRegion.getAttributeValue("AXARIAAtomic"), "region is atomic");
+ is(
+ politeRegion.getAttributeValue("AXARIARelevant"),
+ "removals",
+ "region has defined aria-relevant"
+ );
+
+ let assertiveRegion = getNativeInterface(accDoc, "assertive");
+ is(
+ assertiveRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+ ok(
+ !assertiveRegion.getAttributeValue("AXARIAAtomic"),
+ "region is not atomic"
+ );
+ is(
+ assertiveRegion.getAttributeValue("AXARIARelevant"),
+ "additions text",
+ "region has default aria-relevant"
+ );
+
+ let liveRegionRemoved = waitForEvent(
+ EVENT_LIVE_REGION_REMOVED,
+ "assertive"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("assertive").removeAttribute("aria-live");
+ });
+ await liveRegionRemoved;
+ ok(!assertiveRegion.getAttributeValue("AXARIALive"), "region is not live");
+
+ liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "new-region");
+ await SpecialPowers.spawn(browser, [], () => {
+ let newRegionElm = content.document.createElement("div");
+ newRegionElm.id = "new-region";
+ newRegionElm.setAttribute("aria-live", "assertive");
+ content.document.body.appendChild(newRegionElm);
+ });
+ await liveRegionAdded;
+
+ let newRegion = getNativeInterface(accDoc, "new-region");
+ is(
+ newRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+
+ let loadComplete = Promise.all([
+ waitForMacEvent("AXLoadComplete"),
+ waitForMacEvent("AXLiveRegionCreated", "region-1"),
+ waitForMacEvent("AXLiveRegionCreated", "region-2"),
+ waitForMacEvent("AXLiveRegionCreated", "status"),
+ waitForMacEvent("AXLiveRegionCreated", "output"),
+ ]);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location = `data:text/html;charset=utf-8,
+ <div id="region-1" aria-live="polite"></div>
+ <div id="region-2" aria-live="assertive"></div>
+ <div id="region-3" aria-live="off"></div>
+ <div id="alert" role="alert"></div>
+ <div id="status" role="status"></div>
+ <output id="output"></output>`;
+ });
+ let webArea = (await loadComplete)[0];
+
+ is(webArea.getAttributeValue("AXRole"), "AXWebArea", "web area yeah");
+ const searchPred = {
+ AXSearchKey: "AXLiveRegionSearchKey",
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+ const liveRegions = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ Assert.deepEqual(
+ liveRegions.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["region-1", "region-2", "alert", "status", "output"],
+ "SearchPredicate returned all live regions"
+ );
+ }
+);
+
+/**
+ * Test live region changes
+ */
+addAccessibleTask(
+ `
+ <div id="live" aria-live="polite">
+ The time is <span id="time">4:55pm</span>
+ <p id="p" style="display: none">Georgia on my mind</p>
+ <button id="button" aria-label="Start"></button>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ let liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("time").textContent = "4:56pm";
+ });
+ await liveRegionChanged;
+ ok(true, "changed textContent");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "block";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to block");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "none";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to none");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Stop");
+ });
+ await liveRegionChanged;
+ ok(true, "changed aria-label");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_mathml.js b/accessible/tests/browser/mac/browser_mathml.js
new file mode 100644
index 0000000000..1afaa8399f
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_mathml.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testMathAttr(iface, attr, subrole, textLeafValue) {
+ ok(iface.attributeNames.includes(attr), `Object has ${attr} attribute`);
+ let value = iface.getAttributeValue(attr);
+ is(
+ value.getAttributeValue("AXSubrole"),
+ subrole,
+ `${attr} value has correct subrole`
+ );
+
+ if (textLeafValue) {
+ let children = value.getAttributeValue("AXChildren");
+ is(children.length, 1, `${attr} value has one child`);
+
+ is(
+ children[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ `${attr} value's child is static text`
+ );
+ is(
+ children[0].getAttributeValue("AXValue"),
+ textLeafValue,
+ `${attr} value has correct text`
+ );
+ }
+}
+
+addAccessibleTask(
+ `<math id="math">
+ <msqrt id="sqrt">
+ <mi>-1</mi>
+ </msqrt>
+ </math>`,
+ async (browser, accDoc) => {
+ let math = getNativeInterface(accDoc, "math");
+ is(
+ math.getAttributeValue("AXSubrole"),
+ "AXDocumentMath",
+ "Math element has correct subrole"
+ );
+
+ let sqrt = getNativeInterface(accDoc, "sqrt");
+ is(
+ sqrt.getAttributeValue("AXSubrole"),
+ "AXMathSquareRoot",
+ "msqrt has correct subrole"
+ );
+
+ testMathAttr(sqrt, "AXMathRootRadicand", "AXMathIdentifier", "-1");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mroot id="root">
+ <mi>x</mi>
+ <mn>3</mn>
+ </mroot>
+ </math>`,
+ async (browser, accDoc) => {
+ let root = getNativeInterface(accDoc, "root");
+ is(
+ root.getAttributeValue("AXSubrole"),
+ "AXMathRoot",
+ "mroot has correct subrole"
+ );
+
+ testMathAttr(root, "AXMathRootRadicand", "AXMathIdentifier", "x");
+ testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mfrac id="fraction">
+ <mi>a</mi>
+ <mi>b</mi>
+ </mfrac>
+ </math>`,
+ async (browser, accDoc) => {
+ let fraction = getNativeInterface(accDoc, "fraction");
+ is(
+ fraction.getAttributeValue("AXSubrole"),
+ "AXMathFraction",
+ "mfrac has correct subrole"
+ );
+ ok(fraction.attributeNames.includes("AXMathFractionNumerator"));
+ ok(fraction.attributeNames.includes("AXMathFractionDenominator"));
+ ok(fraction.attributeNames.includes("AXMathLineThickness"));
+
+ // Bug 1639745
+ todo_is(fraction.getAttributeValue("AXMathLineThickness"), 1);
+
+ testMathAttr(fraction, "AXMathFractionNumerator", "AXMathIdentifier", "a");
+ testMathAttr(
+ fraction,
+ "AXMathFractionDenominator",
+ "AXMathIdentifier",
+ "b"
+ );
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <msubsup id="subsup">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mn>1</mn>
+ </msubsup>
+ </math>`,
+ async (browser, accDoc) => {
+ let subsup = getNativeInterface(accDoc, "subsup");
+ is(
+ subsup.getAttributeValue("AXSubrole"),
+ "AXMathSubscriptSuperscript",
+ "msubsup has correct subrole"
+ );
+
+ testMathAttr(subsup, "AXMathSubscript", "AXMathNumber", "0");
+ testMathAttr(subsup, "AXMathSuperscript", "AXMathNumber", "1");
+ testMathAttr(subsup, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <munderover id="underover">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mi>∞</mi>
+ </munderover>
+ </math>`,
+ async (browser, accDoc) => {
+ let underover = getNativeInterface(accDoc, "underover");
+ is(
+ underover.getAttributeValue("AXSubrole"),
+ "AXMathUnderOver",
+ "munderover has correct subrole"
+ );
+
+ testMathAttr(underover, "AXMathUnder", "AXMathNumber", "0");
+ testMathAttr(underover, "AXMathOver", "AXMathIdentifier", "∞");
+ testMathAttr(underover, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_menulist.js b/accessible/tests/browser/mac/browser_menulist.js
new file mode 100644
index 0000000000..dd037c01ee
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_menulist.js
@@ -0,0 +1,52 @@
+/* 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 }
+);
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..1bb3c56bad
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_outline.js
@@ -0,0 +1,459 @@
+/* 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 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);
+ }
+);
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..9916a7cb12
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_popupbutton.js
@@ -0,0 +1,153 @@
+/* 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 three events happen in quick succession when select is pressed
+ let events = Promise.all([
+ waitForMacEvent("AXMenuOpened"),
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ ]);
+ 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"),
+ ]);
+ menuItem.performAction("AXPress");
+ let [, newFocus] = await events;
+ is(
+ newFocus.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "Newly focused element is AXPopupButton"
+ );
+ 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..7158dfcb30
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_range.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";
+
+/* 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=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"
+ );
+ }
+);
+
+/**
+ * Test input[type=range]
+ */
+addAccessibleTask(
+ `<input type="number" value="11" id="number">`,
+ 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"), "12", "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"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_required.js b/accessible/tests/browser/mac/browser_required.js
new file mode 100644
index 0000000000..5f552d44c3
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_required.js
@@ -0,0 +1,145 @@
+/* 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"
+ );
+
+ // 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"
+ );
+
+ // 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..5f07961486
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_roles_elements.js
@@ -0,0 +1,309 @@
+/* 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>
+
+ <!-- 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>
+
+ <!-- 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");
+
+ // Text boxes
+ testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea");
+ testRoleAndSubRole(accDoc, "textbox_singleline", "AXTextArea");
+ 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");
+
+ // 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..87f5e6c04f
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rootgroup.js
@@ -0,0 +1,196 @@
+/* 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"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js
new file mode 100644
index 0000000000..8b0d12de9d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rotor.js
@@ -0,0 +1,1697 @@
+/* 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.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+/**
+ * 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 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>
+ `,
+ 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(3, controlsCount, "Found 3 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const spin = getNativeInterface(accDoc, "spinbutton");
+ const details = getNativeInterface(accDoc, "details");
+ const tree = getNativeInterface(accDoc, "tree");
+
+ 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"
+ );
+ }
+);
+
+/**
+ * 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");
+
+ 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..29cd326bdd
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_selectables.js
@@ -0,0 +1,336 @@
+/* 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);
+ one.setAttributeValue("AXSelected", false);
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+
+ three.setAttributeValue("AXSelected", true);
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ Assert.deepEqual(
+ getSelectedIds(select),
+ ["one", "two"],
+ "one and two are selected"
+ );
+
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ Assert.deepEqual(
+ getSelectedIds(select),
+ ["two", "three"],
+ "two and three are selected, four is disabled so it's not"
+ );
+
+ ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled");
+ }
+);
+
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a thing" multiple>
+ <optgroup label="Fruits">
+ <option id="banana" selected>Banana</option>
+ <option id="apple">Apple</option>
+ <option id="orange">Orange</option>
+ </optgroup>
+ <optgroup label="Vegetables">
+ <option id="lettuce" selected>Lettuce</option>
+ <option id="tomato">Tomato</option>
+ <option id="onion">Onion</option>
+ </optgroup>
+ <optgroup label="Spices">
+ <option id="cumin">Cumin</option>
+ <option id="coriander">Coriander</option>
+ <option id="allspice" selected>Allspice</option>
+ </optgroup>
+ <option id="everything">Everything</option>
+ </select>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a thing",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+ let childValueSelectablePairs = select
+ .getAttributeValue("AXChildren")
+ .map(c => [
+ c.getAttributeValue("AXValue"),
+ c.isAttributeSettable("AXSelected"),
+ c.getAttributeValue("AXEnabled"),
+ ]);
+ Assert.deepEqual(
+ childValueSelectablePairs,
+ [
+ ["Fruits", false, false],
+ ["Banana", true, true],
+ ["Apple", true, true],
+ ["Orange", true, true],
+ ["Vegetables", false, false],
+ ["Lettuce", true, true],
+ ["Tomato", true, true],
+ ["Onion", true, true],
+ ["Spices", false, false],
+ ["Cumin", true, true],
+ ["Coriander", true, true],
+ ["Allspice", true, true],
+ ["Everything", true, true],
+ ],
+ "Options are selectable, group labels are not"
+ );
+
+ let allspice = getNativeInterface(accDoc, "allspice");
+ is(
+ allspice.getAttributeValue("AXTitle"),
+ "",
+ "Option should not have a title"
+ );
+ is(
+ allspice.getAttributeValue("AXValue"),
+ "Allspice",
+ "Option should have a value"
+ );
+ is(
+ allspice.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(
+ allspice.isAttributeSettable("AXSelected"),
+ "Option can have AXSelected set"
+ );
+ is(
+ allspice
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of nested option"
+ );
+
+ let groupLabel = select.getAttributeValue("AXChildren")[0];
+ ok(
+ !groupLabel.isAttributeSettable("AXSelected"),
+ "Group label should not be selectable"
+ );
+ is(
+ groupLabel.getAttributeValue("AXValue"),
+ "Fruits",
+ "Group label should have a value"
+ );
+ is(
+ groupLabel.getAttributeValue("AXTitle"),
+ null,
+ "Group label should not have a title"
+ );
+ is(
+ groupLabel.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Group label should have AXStaticText role"
+ );
+ is(
+ groupLabel
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of group label"
+ );
+
+ Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]);
+ }
+);
+
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true">
+ <div role="option" id="one" aria-selected="true">One</div>
+ <div role="option" id="two">Two</div>
+ <div role="option" id="three">Three</div>
+ <div role="option" id="four" aria-disabled="true">Four</div>
+</div>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+ let three = getNativeInterface(accDoc, "three");
+ let four = getNativeInterface(accDoc, "four");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a number",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+
+ is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
+ is(
+ one.getAttributeValue("AXValue"),
+ "One",
+ "Option should have correct value"
+ );
+ is(
+ one.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
+
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ // Change selection from content.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("one").removeAttribute("aria-selected");
+ });
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+
+ three.setAttributeValue("AXSelected", true);
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ Assert.deepEqual(
+ getSelectedIds(select),
+ ["one", "two"],
+ "one and two are selected"
+ );
+
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ Assert.deepEqual(
+ getSelectedIds(select),
+ ["two", "three"],
+ "two and three are selected, four is disabled so it's not"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_table.js b/accessible/tests/browser/mac/browser_table.js
new file mode 100644
index 0000000000..34aeb87a17
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_table.js
@@ -0,0 +1,281 @@
+/* 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"
+ );
+ }
+);
+
+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);
+ }
+);
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..de66ebf639
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_basics.js
@@ -0,0 +1,282 @@
+/* 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`
+ );
+}
+
+// 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
+ );
+
+ let count = 0;
+
+ // Iterate forward with "AXNextTextMarkerForTextMarker"
+ let marker = macDoc.getAttributeValue("AXStartTextMarker");
+ while (marker) {
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ index,
+ count,
+ `Correct index in "AXNextTextMarkerForTextMarker": ${count}`
+ );
+
+ testWords(
+ macDoc,
+ marker,
+ `At index ${count}`,
+ ...expectedMarkerValues[count].words
+ );
+ testLines(
+ macDoc,
+ marker,
+ `At index ${count}`,
+ ...expectedMarkerValues[count].lines
+ );
+ testUIElement(
+ macDoc,
+ marker,
+ `At index ${count}`,
+ ...expectedMarkerValues[count].element
+ );
+ testParagraph(
+ macDoc,
+ marker,
+ `At index ${count}`,
+ expectedMarkerValues[count].paragraph
+ );
+ testStyleRun(
+ macDoc,
+ marker,
+ `At index ${count}`,
+ expectedMarkerValues[count].style
+ );
+
+ let prevMarker = marker;
+ marker = macDoc.getParameterizedAttributeValue(
+ "AXNextTextMarkerForTextMarker",
+ marker
+ );
+
+ if (marker) {
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [prevMarker, marker]
+ );
+ is(
+ macDoc.getParameterizedAttributeValue(
+ "AXLengthForTextMarkerRange",
+ range
+ ),
+ 1,
+ "marker moved one character"
+ );
+ }
+
+ count++;
+ }
+
+ // Use "AXTextMarkerForIndex" to retrieve all text markers
+ for (let i = 0; i < count; i++) {
+ marker = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", i);
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`);
+ }
+
+ ok(
+ !macDoc.getParameterizedAttributeValue(
+ "AXNextTextMarkerForTextMarker",
+ marker
+ ),
+ "Iterated through all markers"
+ );
+
+ // Iterate backward with "AXPreviousTextMarkerForTextMarker"
+ marker = macDoc.getAttributeValue("AXEndTextMarker");
+ while (marker) {
+ count--;
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ index,
+ count,
+ `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}`
+ );
+ marker = macDoc.getParameterizedAttributeValue(
+ "AXPreviousTextMarkerForTextMarker",
+ marker
+ );
+ }
+
+ is(count, 0, "Iterated backward through all text markers");
+}
+
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ const expectedMarkerValues = await SpecialPowers.spawn(
+ browser,
+ [],
+ async () => {
+ return content.wrappedJSObject.EXPECTED;
+ }
+ );
+
+ testMarkerIntegrity(accDoc, expectedMarkerValues);
+});
+
+// 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");
+ }
+);
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..b5aa9511a1
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_input.js
@@ -0,0 +1,412 @@
+/* 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`
+ );
+}
+
+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 focusIntoInputAndType(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 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_Home",
+ { shiftKey: true },
+ selectionId,
+ "hello ",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityWord,
+ }
+ );
+ 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");
+ }
+);
+
+// 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 text input in iframe
+addAccessibleTask(
+ `<a href="#">link</a> <input id="input">`,
+ async (browser, accDoc) => {
+ await focusIntoInputAndType(accDoc, "input");
+ },
+ { iframe: true }
+);
+
+// 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");
+});
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..c7c7a5c319
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_leaf.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";
+
+/* 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"),
+ "world ",
+ "Second child is world text"
+ );
+ // we have a trailing space here due to bug 1577028
+});
+
+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, this ", "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");
+ }
+);
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..17c292fd87
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_toggle_radio_check.js
@@ -0,0 +1,140 @@
+/* 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");
+ is(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;
+ is(checkbox.getAttributeValue("AXValue"), 1, "Correct checked value");
+
+ evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ is(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");
+ is(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;
+ is(toggle.getAttributeValue("AXValue"), 1, "Correct checked value");
+
+ evt = waitForMacEvent("AXValueChanged", "toggle");
+ toggle.performAction("AXPress");
+ await evt;
+ is(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");
+ is(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;
+ is(checkbox.getAttributeValue("AXValue"), 1, "Correct checked value");
+
+ evt = waitForMacEvent("AXValueChanged", "checkbox");
+ 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");
+ is(huey.getAttributeValue("AXValue"), 1, "Correct initial value for huey");
+
+ let dewey = getNativeInterface(accDoc, "dewey");
+ is(
+ 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;
+ is(
+ dewey.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value for dewey"
+ );
+ is(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..c1e68dac1b
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_webarea.js
@@ -0,0 +1,76 @@
+/* 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");
+ 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("AXDescription"),
+ "webarea test",
+ "test has correct label"
+ );
+
+ 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"),
+ waitForMacEvent("AXLoadComplete"),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = "data:text/html,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"),
+ "data:text/html,hello world",
+ "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..9de6f8b81c
--- /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.setAttribute("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..8a73c95a35
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_textmarker_test.html
@@ -0,0 +1,2424 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ </head>
+ <body id="body">
+ <p>Bob Loblaw Lobs Law Bomb</p>
+ <p>I love all of my <a href="#">children</a> equally</p>
+ <p>This is the <b>best</b> free scr<a href="#">apbook</a>ing class I have ever taken</p>
+ <ul>
+ <li>Fried cheese with club sauce</li>
+ <li>Popcorn shrimp with club sauce</li>
+ <li>Chicken fingers with <i>spicy</i> club sauce</li>
+ </ul>
+ <ul style="list-style: none;"><li>Do not order the Skip's Scramble</li></ul>
+ <p style="width: 1rem">These are my awards, Mother. From Army.</p>
+ <p>I <input value="deceived you">, mom.</p>
+ <script>
+ "use strict";
+ window.EXPECTED = [
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "I love all of my children equally",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "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: ["I love all of my children equally",
+ "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: ["This is the best free scrapbooking class I have ever taken",
+ "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 Fried cheese 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 Popcorn shrimp with 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: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "Do not order the Skip's Scramble"],
+ words: ["sauce", ""],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", "Do"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Skip'", "'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Skip'", "s"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["s", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Do not order the Skip's Scramble",
+ "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 ", "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: ["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 ", "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: ["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, ", "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: ["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. ", "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: ["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 ", "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: ["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.", "Army.", "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: ["Army.", "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"],
+ 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: "deceived you",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["you", ""],
+ 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..7f70bd4a4e
--- /dev/null
+++ b/accessible/tests/browser/mac/head.js
@@ -0,0 +1,114 @@
+/* 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,
+ NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit,
+ AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove,
+ AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown,
+ AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext,
+ AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown,
+ AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord */
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 AXTextSelectionDirectionPrevious = 3;
+const AXTextSelectionDirectionNext = 4;
+const AXTextSelectionDirectionDiscontiguous = 5;
+
+// AXTextSelectionGranularity enum values
+const AXTextSelectionGranularityUnknown = 0;
+const AXTextSelectionGranularityCharacter = 1;
+const AXTextSelectionGranularityWord = 2;
+
+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 "";
+ }
+
+ return macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range
+ );
+}
diff --git a/accessible/tests/browser/scroll/browser.ini b/accessible/tests/browser/scroll/browser.ini
new file mode 100644
index 0000000000..1e24426535
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/browser/*.jsm
+ !/accessible/tests/mochitest/*.js
+
+[browser_test_zoom_text.js]
+skip-if = e10s && os == 'win' # bug 1372296
diff --git a/accessible/tests/browser/scroll/browser_test_zoom_text.js b/accessible/tests/browser/scroll/browser_test_zoom_text.js
new file mode 100644
index 0000000000..be58361c7b
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_zoom_text.js
@@ -0,0 +1,150 @@
+/* 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/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+async function waitForContentPaint(browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(function(r) {
+ content.requestAnimationFrame(() => content.setTimeout(r));
+ });
+ });
+}
+
+async function runTests(browser, accDoc) {
+ await loadContentScripts(browser, "Layout.jsm");
+
+ let paragraph = findAccessibleChildByID(accDoc, "paragraph", [
+ nsIAccessibleText,
+ ]);
+ let offset = 64; // beginning of 4th stanza
+
+ let [x /* ,y*/] = getPos(paragraph);
+ let [docX, docY] = getPos(accDoc);
+
+ paragraph.scrollSubstringToPoint(
+ offset,
+ offset,
+ COORDTYPE_SCREEN_RELATIVE,
+ docX,
+ docY
+ );
+
+ await waitForContentPaint(browser);
+ testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.Layout.zoomDocument(content.document, 2.0);
+ });
+
+ paragraph = findAccessibleChildByID(accDoc, "paragraph2", [
+ nsIAccessibleText,
+ ]);
+ offset = 52; // // beginning of 4th stanza
+ [x /* ,y*/] = getPos(paragraph);
+ paragraph.scrollSubstringToPoint(
+ offset,
+ offset,
+ COORDTYPE_SCREEN_RELATIVE,
+ docX,
+ docY
+ );
+
+ await waitForContentPaint(browser);
+ testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE);
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ `
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><hr>
+ <p id='paragraph'>
+ Пошел котик на торжок<br>
+ Купил котик пирожок<br>
+ Пошел котик на улочку<br>
+ Купил котик булочку<br>
+ </p>
+ <hr><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><hr>
+ <p id='paragraph2'>
+ Самому ли съесть<br>
+ Либо Сашеньке снесть<br>
+ Я и сам укушу<br>
+ Я и Сашеньке снесу<br>
+ </p>
+ <hr><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>`,
+ runTests
+);
diff --git a/accessible/tests/browser/scroll/head.js b/accessible/tests/browser/scroll/head.js
new file mode 100644
index 0000000000..672aa46171
--- /dev/null
+++ b/accessible/tests/browser/scroll/head.js
@@ -0,0 +1,19 @@
+/* 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";
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
diff --git a/accessible/tests/browser/shared-head.js b/accessible/tests/browser/shared-head.js
new file mode 100644
index 0000000000..b848e09932
--- /dev/null
+++ b/accessible/tests/browser/shared-head.js
@@ -0,0 +1,779 @@
+/* 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/common.js */
+/* import-globals-from ../mochitest/promisified-events.js */
+
+/* exported Logger, MOCHITESTS_DIR, invokeSetAttribute, invokeFocus,
+ invokeSetStyle, getAccessibleDOMNodeID, getAccessibleTagName,
+ addAccessibleTask, findAccessibleChildByID, isDefunct,
+ CURRENT_CONTENT_DIR, loadScripts, loadContentScripts, snippetToURL,
+ Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation,
+ DEFAULT_IFRAME_ID, DEFAULT_IFRAME_DOC_BODY_ID, invokeContentTask,
+ matchContentDoc, currentContentDoc, getContentDPR,
+ waitForImageMap, getContentBoundsForDOMElm */
+
+const CURRENT_FILE_DIR = "/browser/accessible/tests/browser/";
+
+/**
+ * Current browser test directory path used to load subscripts.
+ */
+const CURRENT_DIR = `chrome://mochitests/content${CURRENT_FILE_DIR}`;
+/**
+ * A11y mochitest directory where we find common files used in both browser and
+ * plain tests.
+ */
+const MOCHITESTS_DIR =
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/";
+/**
+ * A base URL for test files used in content.
+ */
+const CURRENT_CONTENT_DIR = `http://example.com${CURRENT_FILE_DIR}`;
+
+const LOADED_CONTENT_SCRIPTS = new Map();
+
+const DEFAULT_CONTENT_DOC_BODY_ID = "body";
+const DEFAULT_IFRAME_ID = "default-iframe-id";
+const DEFAULT_IFRAME_DOC_BODY_ID = "default-iframe-body-id";
+
+const HTML_MIME_TYPE = "text/html";
+const XHTML_MIME_TYPE = "application/xhtml+xml";
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ const testHTMLFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ const dirs = path.split("/");
+ for (let i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+
+ const testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ const testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+
+ return testHTML;
+}
+
+let gIsIframe = false;
+let gIsRemoteIframe = false;
+
+function currentContentDoc() {
+ return gIsIframe ? DEFAULT_IFRAME_DOC_BODY_ID : DEFAULT_CONTENT_DOC_BODY_ID;
+}
+
+/**
+ * Accessible event match criteria based on the id of the current document
+ * accessible in test.
+ *
+ * @param {nsIAccessibleEvent} event
+ * Accessible event to be tested for a match.
+ *
+ * @return {Boolean}
+ * True if accessible event's accessible object ID matches current
+ * document accessible ID.
+ */
+function matchContentDoc(event) {
+ return getAccessibleDOMNodeID(event.accessible) === currentContentDoc();
+}
+
+/**
+ * Used to dump debug information.
+ */
+let Logger = {
+ /**
+ * Set up this variable to dump log messages into console.
+ */
+ dumpToConsole: false,
+
+ /**
+ * Set up this variable to dump log messages into error console.
+ */
+ dumpToAppConsole: false,
+
+ /**
+ * Return true if dump is enabled.
+ */
+ get enabled() {
+ return this.dumpToConsole || this.dumpToAppConsole;
+ },
+
+ /**
+ * Dump information into console if applicable.
+ */
+ log(msg) {
+ if (this.enabled) {
+ this.logToConsole(msg);
+ this.logToAppConsole(msg);
+ }
+ },
+
+ /**
+ * Log message to console.
+ */
+ logToConsole(msg) {
+ if (this.dumpToConsole) {
+ dump(`\n${msg}\n`);
+ }
+ },
+
+ /**
+ * Log message to error console.
+ */
+ logToAppConsole(msg) {
+ if (this.dumpToAppConsole) {
+ Services.console.logStringMessage(`${msg}`);
+ }
+ },
+};
+
+/**
+ * Asynchronously set or remove content element's attribute (in content process
+ * if e10s is enabled).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @param {String} attr attribute name
+ * @param {String?} value optional attribute value, if not present, remove
+ * attribute
+ * @return {Promise} promise indicating that attribute is set/removed
+ */
+function invokeSetAttribute(browser, id, attr, value) {
+ if (value) {
+ Logger.log(`Setting ${attr} attribute to ${value} for node with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${attr} attribute from node with id: ${id}`);
+ }
+
+ return invokeContentTask(
+ browser,
+ [id, attr, value],
+ (contentId, contentAttr, contentValue) => {
+ let elm = content.document.getElementById(contentId);
+ if (contentValue) {
+ elm.setAttribute(contentAttr, contentValue);
+ } else {
+ elm.removeAttribute(contentAttr);
+ }
+ }
+ );
+}
+
+/**
+ * Asynchronously set or remove content element's style (in content process if
+ * e10s is enabled, or in fission process if fission is enabled and a fission
+ * frame is present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @param {String} aStyle style property name
+ * @param {String?} aValue optional style property value, if not present,
+ * remove style
+ * @return {Promise} promise indicating that style is set/removed
+ */
+function invokeSetStyle(browser, id, style, value) {
+ if (value) {
+ Logger.log(`Setting ${style} style to ${value} for node with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${style} style from node with id: ${id}`);
+ }
+
+ return invokeContentTask(
+ browser,
+ [id, style, value],
+ (contentId, contentStyle, contentValue) => {
+ const elm = content.document.getElementById(contentId);
+ if (contentValue) {
+ elm.style[contentStyle] = contentValue;
+ } else {
+ delete elm.style[contentStyle];
+ }
+ }
+ );
+}
+
+/**
+ * Asynchronously set focus on a content element (in content process if e10s is
+ * enabled, or in fission process if fission is enabled and a fission frame is
+ * present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @return {Promise} promise indicating that focus is set
+ */
+function invokeFocus(browser, id) {
+ Logger.log(`Setting focus on a node with id: ${id}`);
+
+ return invokeContentTask(browser, [id], contentId => {
+ const elm = content.document.getElementById(contentId);
+ if (elm.editor) {
+ elm.selectionStart = elm.selectionEnd = elm.value.length;
+ }
+
+ elm.focus();
+ });
+}
+
+/**
+ * Get DPR for a specific content window.
+ * @param browser
+ * Browser for which we want its content window's DPR reported.
+ *
+ * @return {Promise}
+ * Promise with the value that resolves to the devicePixelRatio of the
+ * content window of a given browser.
+ *
+ */
+function getContentDPR(browser) {
+ return invokeContentTask(browser, [], () => content.window.devicePixelRatio);
+}
+
+/**
+ * Asynchronously perform a task in content (in content process if e10s is
+ * enabled, or in fission process if fission is enabled and a fission frame is
+ * present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Array} args arguments for the content task
+ * @param {Function} task content task function
+ *
+ * @return {Promise} promise indicating that content task is complete
+ */
+function invokeContentTask(browser, args, task) {
+ return SpecialPowers.spawn(
+ browser,
+ [DEFAULT_IFRAME_ID, task.toString(), ...args],
+ (iframeId, contentTask, ...contentArgs) => {
+ // eslint-disable-next-line no-eval
+ const runnableTask = eval(`
+ (() => {
+ return (${contentTask});
+ })();`);
+ const frame = content.document.getElementById(iframeId);
+
+ return frame
+ ? SpecialPowers.spawn(frame, contentArgs, runnableTask)
+ : runnableTask.call(this, ...contentArgs);
+ }
+ );
+}
+
+/**
+ * Compare process ID's between the top level content process and possible
+ * remote/local iframe proccess.
+ * @param {Object} browser
+ * Top level browser object for a tab.
+ * @param {Boolean} isRemote
+ * Indicates if we expect the iframe content process to be remote or not.
+ */
+async function comparePIDs(browser, isRemote) {
+ function getProcessID() {
+ const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+
+ return Services.appinfo.processID;
+ }
+
+ const contentPID = await SpecialPowers.spawn(browser, [], getProcessID);
+ const iframePID = await invokeContentTask(browser, [], getProcessID);
+ is(
+ isRemote,
+ contentPID !== iframePID,
+ isRemote
+ ? "Remote IFRAME is in a different process."
+ : "IFRAME is in the same process."
+ );
+}
+
+/**
+ * Load a list of scripts into the test
+ * @param {Array} scripts a list of scripts to load
+ */
+function loadScripts(...scripts) {
+ for (let script of scripts) {
+ let path =
+ typeof script === "string"
+ ? `${CURRENT_DIR}${script}`
+ : `${script.dir}${script.name}`;
+ Services.scriptloader.loadSubScript(path, this);
+ }
+}
+
+/**
+ * Load a list of scripts into target's content.
+ * @param {Object} target
+ * target for loading scripts into
+ * @param {Array} scripts
+ * a list of scripts to load into content
+ */
+async function loadContentScripts(target, ...scripts) {
+ for (let script of scripts) {
+ let contentScript;
+ if (typeof script === "string") {
+ // If script string includes a .jsm extention, assume it is a module path.
+ contentScript = `${CURRENT_DIR}${script}`;
+ } else {
+ // Script is a object that has { dir, name } format.
+ contentScript = `${script.dir}${script.name}`;
+ }
+
+ let loadedScriptSet = LOADED_CONTENT_SCRIPTS.get(contentScript);
+ if (!loadedScriptSet) {
+ loadedScriptSet = new WeakSet();
+ LOADED_CONTENT_SCRIPTS.set(contentScript, loadedScriptSet);
+ } else if (loadedScriptSet.has(target)) {
+ continue;
+ }
+
+ await SpecialPowers.spawn(target, [contentScript], async _contentScript => {
+ ChromeUtils.import(_contentScript, content.window);
+ });
+ loadedScriptSet.add(target);
+ }
+}
+
+function attrsToString(attrs) {
+ return Object.entries(attrs)
+ .map(([attr, value]) => `${attr}=${JSON.stringify(value)}`)
+ .join(" ");
+}
+
+function wrapWithIFrame(doc, options = {}) {
+ let src;
+ let { iframeAttrs = {}, iframeDocBodyAttrs = {} } = options;
+ iframeDocBodyAttrs = {
+ id: DEFAULT_IFRAME_DOC_BODY_ID,
+ ...iframeDocBodyAttrs,
+ };
+ if (options.remoteIframe) {
+ const srcURL = new URL(`http://example.net/document-builder.sjs`);
+ if (doc.endsWith("html")) {
+ srcURL.searchParams.append("file", `${CURRENT_FILE_DIR}${doc}`);
+ } else {
+ srcURL.searchParams.append(
+ "html",
+ `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Fission Test</title>
+ </head>
+ <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>
+ </html>`
+ );
+ }
+ src = srcURL.href;
+ } else {
+ const mimeType = doc.endsWith("xhtml") ? XHTML_MIME_TYPE : HTML_MIME_TYPE;
+ if (doc.endsWith("html")) {
+ doc = loadHTMLFromFile(`${CURRENT_FILE_DIR}${doc}`);
+ doc = doc.replace(
+ /<body[.\s\S]*?>/,
+ `<body ${attrsToString(iframeDocBodyAttrs)}>`
+ );
+ } else {
+ doc = `<body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>`;
+ }
+
+ src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`;
+ }
+
+ iframeAttrs = {
+ id: DEFAULT_IFRAME_ID,
+ src,
+ ...iframeAttrs,
+ };
+
+ return `<iframe ${attrsToString(iframeAttrs)}/>`;
+}
+
+/**
+ * Takes an HTML snippet or HTML doc url and returns an encoded URI for a full
+ * document with the snippet or the URL as a source for the IFRAME.
+ * @param {String} doc
+ * a markup snippet or url.
+ * @param {Object} options (see options in addAccessibleTask).
+ *
+ * @return {String}
+ * a base64 encoded data url of the document container the snippet.
+ **/
+function snippetToURL(doc, options = {}) {
+ const { contentDocBodyAttrs = {} } = options;
+ const attrs = {
+ id: DEFAULT_CONTENT_DOC_BODY_ID,
+ ...contentDocBodyAttrs,
+ };
+
+ if (gIsIframe) {
+ doc = wrapWithIFrame(doc, options);
+ }
+
+ const encodedDoc = encodeURIComponent(
+ `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body ${attrsToString(attrs)}>${doc}</body>
+ </html>`
+ );
+
+ return `data:text/html;charset=utf-8,${encodedDoc}`;
+}
+
+function accessibleTask(doc, task, options = {}) {
+ return async function() {
+ gIsRemoteIframe = options.remoteIframe;
+ gIsIframe = options.iframe || gIsRemoteIframe;
+ let url;
+ if (options.chrome && doc.endsWith("html")) {
+ // Load with a chrome:// URL so this loads as a chrome document in the
+ // parent process.
+ url = `${CURRENT_DIR}${doc}`;
+ } else if (doc.endsWith("html") && !gIsIframe) {
+ url = `${CURRENT_CONTENT_DIR}${doc}`;
+ } else {
+ url = snippetToURL(doc, options);
+ }
+
+ registerCleanupFunction(() => {
+ for (let observer of Services.obs.enumerateObservers(
+ "accessible-event"
+ )) {
+ Services.obs.removeObserver(observer, "accessible-event");
+ }
+ });
+
+ let onContentDocLoad;
+ if (!options.chrome) {
+ onContentDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_CONTENT_DOC_BODY_ID
+ );
+ }
+
+ let onIframeDocLoad;
+ if (options.remoteIframe && !options.skipFissionDocLoad) {
+ onIframeDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function(browser) {
+ registerCleanupFunction(() => {
+ if (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab && !tab.closing && tab.linkedBrowser) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ });
+
+ await SimpleTest.promiseFocus(browser);
+ await loadContentScripts(browser, "Common.jsm");
+
+ if (options.chrome) {
+ ok(!browser.isRemoteBrowser, "Not remote browser");
+ } else if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(browser.isRemoteBrowser, "Actually remote browser");
+ }
+
+ let docAccessible;
+ if (options.chrome) {
+ // Chrome documents don't fire DOCUMENT_LOAD_COMPLETE. Instead, wait
+ // until we can get the DocAccessible and it doesn't have the busy
+ // state.
+ await BrowserTestUtils.waitForCondition(() => {
+ docAccessible = getAccessible(browser.contentWindow.document);
+ if (!docAccessible) {
+ return false;
+ }
+ const state = {};
+ docAccessible.getState(state, {});
+ return !(state.value & STATE_BUSY);
+ });
+ } else {
+ ({ accessible: docAccessible } = await onContentDocLoad);
+ }
+ let iframeDocAccessible;
+ if (gIsIframe) {
+ if (!options.skipFissionDocLoad) {
+ await comparePIDs(browser, options.remoteIframe);
+ iframeDocAccessible = onIframeDocLoad
+ ? (await onIframeDocLoad).accessible
+ : findAccessibleChildByID(docAccessible, DEFAULT_IFRAME_ID)
+ .firstChild;
+ }
+ }
+
+ await task(
+ browser,
+ iframeDocAccessible || docAccessible,
+ iframeDocAccessible && docAccessible
+ );
+ }
+ );
+ };
+}
+
+/**
+ * A wrapper around browser test add_task that triggers an accessible test task
+ * as a new browser test task with given document, data URL or markup snippet.
+ * @param {String} doc
+ * URL (relative to current directory) or data URL or markup snippet
+ * that is used to test content with
+ * @param {Function|AsyncFunction} task
+ * a generator or a function with tests to run
+ * @param {null|Object} options
+ * Options for running accessibility test tasks:
+ * - {Boolean} topLevel
+ * Flag to run the test with content in the top level content process.
+ * Default is true.
+ * - {Boolean} chrome
+ * Flag to run the test with content as a chrome document in the
+ * parent process. Default is false. This is only valid if url is a
+ * relative URL to a XUL document, not a markup snippet. topLevel
+ * should usually be set to false in this case, since XUL documents
+ * don't work in the content process.
+ * - {Boolean} iframe
+ * Flag to run the test with content wrapped in an iframe. Default is
+ * false.
+ * - {Boolean} remoteIframe
+ * Flag to run the test with content wrapped in a remote iframe.
+ * Default is false.
+ * - {Object} iframeAttrs
+ * A map of attribute/value pairs to be applied to IFRAME element.
+ * - {Boolean} skipFissionDocLoad
+ * If true, the test will not wait for iframe document document
+ * loaded event (useful for when IFRAME is initially hidden).
+ * - {Object} contentDocBodyAttrs
+ * a set of attributes to be applied to a top level content document
+ * body
+ * - {Object} iframeDocBodyAttrs
+ * a set of attributes to be applied to a iframe content document body
+ */
+function addAccessibleTask(doc, task, options = {}) {
+ const {
+ topLevel = true,
+ chrome = false,
+ iframe = false,
+ remoteIframe = false,
+ } = options;
+ if (topLevel) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ chrome: false,
+ iframe: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (chrome) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ iframe: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (iframe) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ chrome: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (gFissionBrowser && remoteIframe) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ chrome: false,
+ iframe: false,
+ })
+ );
+ }
+}
+
+/**
+ * Check if an accessible object has a defunct test.
+ * @param {nsIAccessible} accessible object to test defunct state for
+ * @return {Boolean} flag indicating defunct state
+ */
+function isDefunct(accessible) {
+ let defunct = false;
+ try {
+ let extState = {};
+ accessible.getState({}, extState);
+ defunct = extState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT;
+ } catch (x) {
+ defunct = true;
+ } finally {
+ if (defunct) {
+ Logger.log(`Defunct accessible: ${prettyName(accessible)}`);
+ }
+ }
+ return defunct;
+}
+
+/**
+ * Get the DOM tag name for a given accessible.
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} tag name of associated DOM node, or null.
+ */
+function getAccessibleTagName(acc) {
+ try {
+ return acc.attributes.getStringProperty("tag");
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Traverses the accessible tree starting from a given accessible as a root and
+ * looks for an accessible that matches based on its DOMNode id.
+ * @param {nsIAccessible} accessible root accessible
+ * @param {String} id id to look up accessible for
+ * @param {Array?} interfaces the interface or an array interfaces
+ * to query it/them from obtained accessible
+ * @return {nsIAccessible?} found accessible if any
+ */
+function findAccessibleChildByID(accessible, id, interfaces) {
+ if (getAccessibleDOMNodeID(accessible) === id) {
+ return queryInterfaces(accessible, interfaces);
+ }
+ for (let i = 0; i < accessible.children.length; ++i) {
+ let found = findAccessibleChildByID(accessible.getChildAt(i), id);
+ if (found) {
+ return queryInterfaces(found, interfaces);
+ }
+ }
+ return null;
+}
+
+function queryInterfaces(accessible, interfaces) {
+ if (!interfaces) {
+ return accessible;
+ }
+
+ for (let iface of interfaces.filter(i => !(accessible instanceof i))) {
+ try {
+ accessible.QueryInterface(iface);
+ } catch (e) {
+ ok(false, "Can't query " + iface);
+ }
+ }
+
+ return accessible;
+}
+
+function arrayFromChildren(accessible) {
+ return Array.from({ length: accessible.childCount }, (c, i) =>
+ accessible.getChildAt(i)
+ );
+}
+
+/**
+ * Force garbage collection.
+ */
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+}
+
+/*
+ * This function spawns a content task and awaits expected mutation events from
+ * various content changes. It's good at catching events we did *not* expect. We
+ * do this advancing the layout refresh to flush the relocations/insertions
+ * queue.
+ */
+async function contentSpawnMutation(browser, waitFor, func, args = []) {
+ let onReorders = waitForEvents({ expected: waitFor.expected || [] });
+ let unexpectedListener = new UnexpectedEvents(waitFor.unexpected || []);
+
+ function tick() {
+ // 100ms is an arbitrary positive number to advance the clock.
+ // We don't need to advance the clock for a11y mutations, but other
+ // tick listeners may depend on an advancing clock with each refresh.
+ content.windowUtils.advanceTimeAndRefresh(100);
+ }
+
+ // This stops the refreh driver from doing its regular ticks, and leaves
+ // us in control.
+ await invokeContentTask(browser, [], tick);
+
+ // Perform the tree mutation.
+ await invokeContentTask(browser, args, func);
+
+ // Do one tick to flush our queue (insertions, relocations, etc.)
+ await invokeContentTask(browser, [], tick);
+
+ let events = await onReorders;
+
+ unexpectedListener.stop();
+
+ // Go back to normal refresh driver ticks.
+ await invokeContentTask(browser, [], function() {
+ content.windowUtils.restoreNormalRefresh();
+ });
+
+ return events;
+}
+
+async function waitForImageMap(browser, accDoc, id = "imgmap") {
+ const acc = findAccessibleChildByID(accDoc, id);
+ if (acc.firstChild) {
+ return;
+ }
+
+ const onReorder = waitForEvent(EVENT_REORDER, id);
+ // Wave over image map
+ await invokeContentTask(browser, [id], contentId => {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeMouse(
+ content.document.getElementById(contentId),
+ 10,
+ 10,
+ { type: "mousemove" },
+ content
+ );
+ });
+ await onReorder;
+}
+
+async function getContentBoundsForDOMElm(browser, id) {
+ return invokeContentTask(browser, [id], contentId => {
+ const { Layout: LayoutUtils } = ChromeUtils.import(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm"
+ );
+
+ return LayoutUtils.getBoundsForDOMElm(contentId, content.document);
+ });
+}
diff --git a/accessible/tests/browser/states/browser.ini b/accessible/tests/browser/states/browser.ini
new file mode 100644
index 0000000000..726c7f4f03
--- /dev/null
+++ b/accessible/tests/browser/states/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/browser/*.jsm
+
+[browser_test_link.js]
+skip-if = verify
+[browser_test_visibility.js]
+[browser_deck_has_out_of_process_iframe.js]
+skip-if = (!debug && webrender && (os == 'win')) # bug 1584037
+support-files =
+ target.html
+ test_deck_has_out_of_process_iframe.xhtml
+[browser_offscreen_element_in_out_of_process_iframe.js]
+skip-if = (webrender && os == 'win') # bug 1580706
diff --git a/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js b/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js
new file mode 100644
index 0000000000..155ac4c954
--- /dev/null
+++ b/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.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";
+
+const { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+
+const DIRPATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ ""
+);
+const parentPATH = DIRPATH + "test_deck_has_out_of_process_iframe.xhtml";
+const iframePATH = DIRPATH + "target.html";
+
+// XXX: Using external files here since using data URL breaks something, e.g. it
+// makes querying the second iframe in a hidden deck failure for some reasons.
+const parentURL = `http://example.com/${parentPATH}`;
+const iframeURL = `http://example.org/${iframePATH}`;
+
+add_task(async function() {
+ if (Preferences.locked("fission.autostart")) {
+ ok(
+ true,
+ "fission.autostart pref is locked on this channel which means " +
+ "we don't need to run the following tests"
+ );
+ return;
+ }
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+
+ try {
+ const browser = win.gBrowser.selectedTab.linkedBrowser;
+
+ BrowserTestUtils.loadURI(browser, parentURL);
+ await BrowserTestUtils.browserLoaded(browser, false, parentURL);
+
+ async function setupIFrame(id, url) {
+ const iframe = content.document.getElementById(id);
+
+ iframe.contentWindow.location = url;
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+
+ return iframe.browsingContext;
+ }
+
+ async function spawnSelectDeck(index) {
+ async function selectDeck(i) {
+ const deck = content.document.getElementById("deck");
+
+ deck.setAttribute("selectedIndex", i);
+ await new Promise(resolve => {
+ content.window.addEventListener("MozAfterPaint", resolve, {
+ once: true,
+ });
+ });
+ return deck.selectedIndex;
+ }
+ await SpecialPowers.spawn(browser, [index], selectDeck);
+
+ await waitForIFrameUpdates();
+ }
+
+ const firstIFrame = await SpecialPowers.spawn(
+ browser,
+ ["first", iframeURL],
+ setupIFrame
+ );
+ const secondIFrame = await SpecialPowers.spawn(
+ browser,
+ ["second", iframeURL],
+ setupIFrame
+ );
+
+ await waitForIFrameUpdates();
+
+ await spawnTestStates(
+ firstIFrame,
+ "target",
+ 0,
+ nsIAccessibleStates.STATE_OFFSCREEN
+ );
+ // Disable the check for the target element in the unselected pane of the
+ // deck, this should be fixed by bug 1578932.
+ // Note: As of now we can't use todo in the script transfered into the
+ // out-of-process.
+ //await spawnTestStates(
+ // secondIFrame,
+ // "target",
+ // nsIAccessibleStates.STATE_OFFSCREEN,
+ // nsIAccessibleStates.STATE_INVISIBLE
+ //);
+
+ // Select the second panel.
+ await spawnSelectDeck(1);
+ await spawnTestStates(
+ firstIFrame,
+ "target",
+ nsIAccessibleStates.STATE_OFFSCREEN,
+ nsIAccessibleStates.STATE_INVISIBLE
+ );
+ await spawnTestStates(
+ secondIFrame,
+ "target",
+ 0,
+ nsIAccessibleStates.STATE_OFFSCREEN
+ );
+
+ // Select the first panel again.
+ await spawnSelectDeck(0);
+ await spawnTestStates(
+ firstIFrame,
+ "target",
+ 0,
+ nsIAccessibleStates.STATE_OFFSCREEN
+ );
+ await spawnTestStates(
+ secondIFrame,
+ "target",
+ nsIAccessibleStates.STATE_OFFSCREEN,
+ nsIAccessibleStates.STATE_INVISIBLE
+ );
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js b/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js
new file mode 100644
index 0000000000..289d3fec9a
--- /dev/null
+++ b/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js
@@ -0,0 +1,98 @@
+/* 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 parentURL =
+ "data:text/html;charset=utf-8," +
+ '<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden;">' +
+ ' <div style="width: 100%; height: 1000px;"></div>' +
+ ' <iframe frameborder="0"/>' +
+ "</div>";
+const iframeURL =
+ "data:text/html;charset=utf-8," +
+ "<style>" +
+ " html,body {" +
+ " /* Convenient for calculation of element positions */" +
+ " margin: 0;" +
+ " padding: 0;" +
+ " }" +
+ "</style>" +
+ '<div id="target" style="width: 100px; height: 100px;">target</div>';
+
+add_task(async function() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+
+ try {
+ const browser = win.gBrowser.selectedTab.linkedBrowser;
+
+ BrowserTestUtils.loadURI(browser, parentURL);
+ await BrowserTestUtils.browserLoaded(browser, false, parentURL);
+
+ async function setup(url) {
+ const iframe = content.document.querySelector("iframe");
+
+ iframe.contentWindow.location = url;
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+
+ return iframe.browsingContext;
+ }
+
+ async function scrollTo(x, y) {
+ await SpecialPowers.spawn(browser, [x, y], async (scrollX, scrollY) => {
+ const scroller = content.document.getElementById("scroller");
+ scroller.scrollTo(scrollX, scrollY);
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ });
+ });
+ await waitForIFrameUpdates();
+ }
+
+ // Setup an out-of-process iframe which is initially scrolled out.
+ const iframe = await SpecialPowers.spawn(browser, [iframeURL], setup);
+
+ await waitForIFrameA11yReady(iframe);
+ await spawnTestStates(
+ iframe,
+ "target",
+ nsIAccessibleStates.STATE_OFFSCREEN,
+ nsIAccessibleStates.STATE_INVISIBLE
+ );
+
+ // Scroll the iframe into view and the target element is also visible but
+ // the visible area height is 11px.
+ await scrollTo(0, 711);
+ await spawnTestStates(
+ iframe,
+ "target",
+ nsIAccessibleStates.STATE_OFFSCREEN,
+ nsIAccessibleStates.STATE_INVISIBLE
+ );
+
+ // Scroll to a position where the visible height is 13px.
+ await scrollTo(0, 713);
+ await spawnTestStates(
+ iframe,
+ "target",
+ 0,
+ nsIAccessibleStates.STATE_OFFSCREEN
+ );
+
+ // Scroll the iframe out again.
+ await scrollTo(0, 0);
+ await spawnTestStates(
+ iframe,
+ "target",
+ nsIAccessibleStates.STATE_OFFSCREEN,
+ nsIAccessibleStates.STATE_INVISIBLE
+ );
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/accessible/tests/browser/states/browser_test_link.js b/accessible/tests/browser/states/browser_test_link.js
new file mode 100644
index 0000000000..f747a69971
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_link.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";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTests(browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // a: no traversed state
+ testStates(getAcc("link_traversed"), 0, 0, STATE_TRAVERSED);
+
+ let onStateChanged = waitForEvent(EVENT_STATE_CHANGE, "link_traversed");
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#link_traversed",
+ 1,
+ 1,
+ { shiftKey: true },
+ browser
+ );
+
+ await onStateChanged;
+ testStates(getAcc("link_traversed"), STATE_TRAVERSED);
+
+ let newWin = await newWinOpened;
+ await BrowserTestUtils.closeWindow(newWin);
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ `
+ <a id="link_traversed" href="http://www.example.com" target="_top">
+ example.com
+ </a>`,
+ runTests
+);
diff --git a/accessible/tests/browser/states/browser_test_visibility.js b/accessible/tests/browser/states/browser_test_visibility.js
new file mode 100644
index 0000000000..572c6f57d3
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_visibility.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTest(browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ testStates(getAcc("div"), 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+
+ let input = getAcc("input_scrolledoff");
+ testStates(input, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+
+ // scrolled off item (twice)
+ let lastLi = getAcc("li_last");
+ testStates(lastLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+
+ // scroll into view the item
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("li_last").scrollIntoView(true);
+ });
+ testStates(lastLi, 0, 0, STATE_OFFSCREEN | STATE_INVISIBLE);
+
+ // first item is scrolled off now (testcase for bug 768786)
+ let firstLi = getAcc("li_first");
+ testStates(firstLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ // Accessibles in background tab should have offscreen state and no
+ // invisible state.
+ testStates(getAcc("div"), STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+ BrowserTestUtils.removeTab(newTab);
+}
+
+addAccessibleTask(
+ `
+ <div id="div" style="border:2px solid blue; width: 500px; height: 110vh;"></div>
+ <input id="input_scrolledoff">
+ <ul style="border:2px solid red; width: 100px; height: 50px; overflow: auto;">
+ <li id="li_first">item1</li><li>item2</li><li>item3</li>
+ <li>item4</li><li>item5</li><li id="li_last">item6</li>
+ </ul>`,
+ runTest
+);
diff --git a/accessible/tests/browser/states/head.js b/accessible/tests/browser/states/head.js
new file mode 100644
index 0000000000..f9d7393229
--- /dev/null
+++ b/accessible/tests/browser/states/head.js
@@ -0,0 +1,81 @@
+/* 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 waitForIFrameA11yReady, waitForIFrameUpdates, spawnTestStates */
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
+
+// This is another version of addA11yLoadEvent for fission.
+async function waitForIFrameA11yReady(iFrameBrowsingContext) {
+ await SimpleTest.promiseFocus(window);
+
+ await SpecialPowers.spawn(iFrameBrowsingContext, [], () => {
+ return new Promise(resolve => {
+ function waitForDocLoad() {
+ SpecialPowers.executeSoon(() => {
+ const acc = SpecialPowers.Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(SpecialPowers.Ci.nsIAccessibilityService);
+
+ const accDoc = acc.getAccessibleFor(content.document);
+ let state = {};
+ accDoc.getState(state, {});
+ if (state.value & SpecialPowers.Ci.nsIAccessibleStates.STATE_BUSY) {
+ SpecialPowers.executeSoon(waitForDocLoad);
+ return;
+ }
+ resolve();
+ }, 0);
+ }
+ waitForDocLoad();
+ });
+ });
+}
+
+// A utility function to make sure the information of scroll position or visible
+// area changes reach to out-of-process iframes.
+async function waitForIFrameUpdates() {
+ // Wait for two frames since the information is notified via asynchronous IPC
+ // calls.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+// A utility function to test the state of |elementId| element in out-of-process
+// |browsingContext|.
+async function spawnTestStates(browsingContext, elementId, expectedStates) {
+ function testStates(id, expected, unexpected) {
+ const acc = SpecialPowers.Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(SpecialPowers.Ci.nsIAccessibilityService);
+ const target = content.document.getElementById(id);
+ let state = {};
+ acc.getAccessibleFor(target).getState(state, {});
+ if (expected === 0) {
+ Assert.equal(state.value, expected);
+ } else {
+ Assert.ok(state.value & expected);
+ }
+ Assert.ok(!(state.value & unexpected));
+ }
+ await SpecialPowers.spawn(
+ browsingContext,
+ [elementId, expectedStates],
+ testStates
+ );
+}
diff --git a/accessible/tests/browser/states/target.html b/accessible/tests/browser/states/target.html
new file mode 100644
index 0000000000..0412c13d53
--- /dev/null
+++ b/accessible/tests/browser/states/target.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<div id="target">target</div>
diff --git a/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml b/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml
new file mode 100644
index 0000000000..72d93ca30a
--- /dev/null
+++ b/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml
@@ -0,0 +1,10 @@
+<?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"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="XUL elements visibility states testing">
+<deck id="deck" selectedIndex="0">
+ <iframe id="first" flex="1"/>
+ <iframe id="second" flex="1"/>
+</deck>
+</window>
diff --git a/accessible/tests/browser/telemetry/browser.ini b/accessible/tests/browser/telemetry/browser.ini
new file mode 100644
index 0000000000..8a6381f19f
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser.ini
@@ -0,0 +1,3 @@
+[browser_HCM_telemetry.js]
+support-files =
+ !/browser/components/preferences/tests/head.js
diff --git a/accessible/tests/browser/telemetry/browser_HCM_telemetry.js b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js
new file mode 100644
index 0000000000..d9c48f5942
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js
@@ -0,0 +1,177 @@
+/* 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/. */
+
+/* import-globals-from ../../../../browser/components/preferences/tests/head.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/preferences/tests/head.js",
+ this
+);
+
+ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm", this);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.display.document_color_use");
+ Services.prefs.clearUserPref("browser.display.permit_backplate");
+ Services.telemetry.clearEvents();
+});
+
+async function openColorsDialog() {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const colorsButton = gBrowser.selectedBrowser.contentDocument.getElementById(
+ "colors"
+ );
+
+ const dialogOpened = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/colors.xhtml"
+ );
+ colorsButton.doCommand();
+
+ return dialogOpened;
+}
+
+async function closeColorsDialog(dialogWin) {
+ const dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ dialogWin.document
+ .getElementById("ColorsDialog")
+ .getButton("accept")
+ .doCommand();
+ return dialogClosed;
+}
+
+function verifyBackplate(expectedValue) {
+ const snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok("a11y.backplate" in snapshot, "Backplate scalar must be present.");
+ is(
+ snapshot["a11y.backplate"],
+ expectedValue,
+ "Backplate scalar is logged as " + expectedValue
+ );
+}
+
+add_task(async function testInit() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+ if (AppConstants.platform == "win") {
+ is(
+ menulistHCM.value,
+ "0",
+ "HCM menulist should be set to only with HCM theme on startup for windows"
+ );
+
+ // Verify correct default value on windows
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "default",
+ false
+ );
+ } else {
+ is(
+ menulistHCM.value,
+ "1",
+ "HCM menulist should be set to never on startup for non-windows platforms"
+ );
+
+ // Verify correct default value on non-windows
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "always",
+ false
+ );
+ }
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetAlways() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorAlways");
+ newOption.click();
+
+ is(menulistHCM.value, "2", "HCM menulist should be set to always");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "never",
+ false
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetDefault() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorAutomatic");
+ newOption.click();
+
+ is(menulistHCM.value, "0", "HCM menulist should be set to default");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "default",
+ false
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetNever() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorNever");
+ newOption.click();
+
+ is(menulistHCM.value, "1", "HCM menulist should be set to never");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "always",
+ false
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// XXX We're uable to assert scalars that are false using assertScalar util,
+// so we directly query the scalar in the scalar list below. (bug 1633883)
+
+add_task(async function testBackplate() {
+ is(
+ Services.prefs.getBoolPref("browser.display.permit_backplate"),
+ true,
+ "Backplate is init'd to true"
+ );
+
+ Services.prefs.setBoolPref("browser.display.permit_backplate", false);
+ // Verify correct recorded value
+ verifyBackplate(false);
+
+ Services.prefs.setBoolPref("browser.display.permit_backplate", true);
+ // Verify correct recorded value
+ verifyBackplate(true);
+});
diff --git a/accessible/tests/browser/tree/browser.ini b/accessible/tests/browser/tree/browser.ini
new file mode 100644
index 0000000000..8072f55c37
--- /dev/null
+++ b/accessible/tests/browser/tree/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/accessible/tests/browser/shared-head.js
+ !/accessible/tests/mochitest/*.js
+ !/accessible/tests/browser/*.jsm
+
+[browser_aria_owns.js]
+skip-if = true || (verify && !debug && (os == 'linux')) #Bug 1445513
+[browser_searchbar.js]
+[browser_shadowdom.js]
diff --git a/accessible/tests/browser/tree/browser_aria_owns.js b/accessible/tests/browser/tree/browser_aria_owns.js
new file mode 100644
index 0000000000..084ac83fea
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_aria_owns.js
@@ -0,0 +1,278 @@
+/* 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";
+
+let NO_MOVE = { unexpected: [[EVENT_REORDER, "container"]] };
+let MOVE = { expected: [[EVENT_REORDER, "container"]] };
+
+// Set last ordinal child as aria-owned, should produce no reorder.
+addAccessibleTask(
+ `<ul id="container"><li id="a">Test</li></ul>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function() {
+ // aria-own ordinal child in place, should be a no-op.
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a");
+ });
+
+ testChildrenIds(containerAcc, ["a"]);
+ }
+);
+
+// Add a new ordinal child to a container with an aria-owned child.
+// Order should respect aria-owns.
+addAccessibleTask(
+ `<ul id="container"><li id="a">Test</li></ul>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, MOVE, function() {
+ let container = content.document.getElementById("container");
+ container.setAttribute("aria-owns", "a");
+
+ let aa = content.document.createElement("li");
+ aa.id = "aa";
+ container.appendChild(aa);
+ });
+
+ testChildrenIds(containerAcc, ["aa", "a"]);
+
+ await contentSpawnMutation(browser, MOVE, function() {
+ content.document.getElementById("container").removeAttribute("aria-owns");
+ });
+
+ testChildrenIds(containerAcc, ["a", "aa"]);
+ }
+);
+
+// Remove a no-move aria-owns attribute, should result in a no-move.
+addAccessibleTask(
+ `<ul id="container" aria-owns="a"><li id="a">Test</li></ul>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function() {
+ // remove aria-owned child that is already ordinal, should be no-op.
+ content.document.getElementById("container").removeAttribute("aria-owns");
+ });
+
+ testChildrenIds(containerAcc, ["a"]);
+ }
+);
+
+// Attempt to steal an aria-owned child. The attempt should fail.
+addAccessibleTask(
+ `
+ <ul>
+ <li id="a">Test</li>
+ </ul>
+ <ul aria-owns="a"></ul>
+ <ul id="container"></ul>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, []);
+
+ await contentSpawnMutation(browser, NO_MOVE, function() {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a");
+ });
+
+ testChildrenIds(containerAcc, []);
+ }
+);
+
+// Don't aria-own children of <select>
+addAccessibleTask(
+ `
+ <div id="container" role="group" aria-owns="b"></div>
+ <select id="select">
+ <option id="a"></option>
+ <option id="b"></option>
+ </select>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+ let selectAcc = findAccessibleChildByID(accDoc, "select");
+
+ testChildrenIds(containerAcc, []);
+ testChildrenIds(selectAcc.firstChild, ["a", "b"]);
+ }
+);
+
+// Don't allow <select> to aria-own
+addAccessibleTask(
+ `
+ <div id="container" role="group">
+ <div id="a"></div>
+ <div id="b"></div>
+ </div>
+ <select id="select" aria-owns="a">
+ <option id="c"></option>
+ </select>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+ let selectAcc = findAccessibleChildByID(accDoc, "select");
+
+ testChildrenIds(containerAcc, ["a", "b"]);
+ testChildrenIds(selectAcc.firstChild, ["c"]);
+ }
+);
+
+// Don't allow one <select> to aria-own an <option> from another <select>.
+addAccessibleTask(
+ `
+ <select id="select1" aria-owns="c">
+ <option id="a"></option>
+ <option id="b"></option>
+ </select>
+ <select id="select2">
+ <option id="c"></option>
+ </select>`,
+ async function(browser, accDoc) {
+ let selectAcc1 = findAccessibleChildByID(accDoc, "select1");
+ let selectAcc2 = findAccessibleChildByID(accDoc, "select2");
+
+ testChildrenIds(selectAcc1.firstChild, ["a", "b"]);
+ testChildrenIds(selectAcc2.firstChild, ["c"]);
+ }
+);
+
+// Don't allow a <select> to reorder its children with aria-owns.
+addAccessibleTask(
+ `
+ <select id="container" aria-owns="c b a">
+ <option id="a"></option>
+ <option id="b"></option>
+ <option id="c"></option>
+ </select>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function() {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a c b");
+ });
+
+ testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]);
+ }
+);
+
+// Don't crash if ID in aria-owns does not exist
+addAccessibleTask(
+ `
+ <select id="container" aria-owns="boom" multiple></select>`,
+ async function(browser, accDoc) {
+ ok(true, "Did not crash");
+ }
+);
+
+addAccessibleTask(
+ `
+ <ul id="one">
+ <li id="a">Test</li>
+ <li id="b">Test 2</li>
+ <li id="c">Test 3</li>
+ </ul>
+ <ul id="two"></ul>`,
+ async function(browser, accDoc) {
+ let one = findAccessibleChildByID(accDoc, "one");
+ let two = findAccessibleChildByID(accDoc, "two");
+
+ let waitfor = {
+ expected: [
+ [EVENT_REORDER, "one"],
+ [EVENT_REORDER, "two"],
+ ],
+ };
+
+ await contentSpawnMutation(browser, waitfor, function() {
+ // Put same id twice in aria-owns
+ content.document.getElementById("two").setAttribute("aria-owns", "a a");
+ });
+
+ testChildrenIds(one, ["b", "c"]);
+ testChildrenIds(two, ["a"]);
+
+ await contentSpawnMutation(browser, waitfor, function() {
+ // If the previous double-id aria-owns worked correctly, we should
+ // be in a good state and all is fine..
+ content.document.getElementById("two").setAttribute("aria-owns", "a b");
+ });
+
+ testChildrenIds(one, ["c"]);
+ testChildrenIds(two, ["a", "b"]);
+ }
+);
+
+addAccessibleTask(`<div id="a"></div><div id="b"></div>`, async function(
+ browser,
+ accDoc
+) {
+ testChildrenIds(accDoc, ["a", "b"]);
+
+ let waitFor = {
+ expected: [[EVENT_REORDER, e => e.accessible == accDoc]],
+ };
+
+ await contentSpawnMutation(browser, waitFor, function() {
+ content.document.documentElement.style.display = "none";
+ content.document.documentElement.getBoundingClientRect();
+ content.document.body.setAttribute("aria-owns", "b a");
+ content.document.documentElement.remove();
+ });
+
+ testChildrenIds(accDoc, []);
+});
+
+// Don't allow ordinal child to be placed after aria-owned child (bug 1405796)
+addAccessibleTask(
+ `<div id="container"><div id="a">Hello</div></div>
+ <div><div id="c">There</div><div id="d">There</div></div>`,
+ async function(browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, MOVE, function() {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "c");
+ });
+
+ testChildrenIds(containerAcc, ["a", "c"]);
+
+ await contentSpawnMutation(browser, MOVE, function() {
+ let span = content.document.createElement("span");
+ content.document.getElementById("container").appendChild(span);
+
+ let b = content.document.createElement("div");
+ b.id = "b";
+ content.document.getElementById("container").appendChild(b);
+ });
+
+ testChildrenIds(containerAcc, ["a", "b", "c"]);
+
+ await contentSpawnMutation(browser, MOVE, function() {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "c d");
+ });
+
+ testChildrenIds(containerAcc, ["a", "b", "c", "d"]);
+ }
+);
diff --git a/accessible/tests/browser/tree/browser_searchbar.js b/accessible/tests/browser/tree/browser_searchbar.js
new file mode 100644
index 0000000000..e41ba9819d
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_searchbar.js
@@ -0,0 +1,52 @@
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// eslint-disable-next-line camelcase
+add_task(async function test_searchbar_a11y_tree() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+
+ let searchbar = await TestUtils.waitForCondition(
+ () => document.getElementById("searchbar"),
+ "wait for search bar to appear"
+ );
+
+ // Make sure the popup has been rendered so it shows up in the a11y tree.
+ let popup = document.getElementById("PopupSearchAutoComplete");
+ let promise = BrowserTestUtils.waitForEvent(popup, "popupshown", false);
+ searchbar.textbox.openPopup();
+ await promise;
+
+ promise = BrowserTestUtils.waitForEvent(popup, "popuphidden", false);
+ searchbar.textbox.closePopup();
+ await promise;
+
+ const TREE = {
+ role: ROLE_EDITCOMBOBOX,
+
+ children: [
+ // input element
+ {
+ role: ROLE_ENTRY,
+ children: [],
+ },
+
+ // context menu
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [],
+ },
+
+ // result list
+ {
+ role: ROLE_GROUPING,
+ // not testing the structure inside the result list
+ },
+ ],
+ };
+
+ testAccessibleTree(searchbar, TREE);
+});
diff --git a/accessible/tests/browser/tree/browser_shadowdom.js b/accessible/tests/browser/tree/browser_shadowdom.js
new file mode 100644
index 0000000000..728284d3e2
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_shadowdom.js
@@ -0,0 +1,64 @@
+/* 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 REORDER = { expected: [[EVENT_REORDER, "container"]] };
+
+// Dynamically inserted slotted accessible elements should be in
+// the accessible tree.
+const snippet = `
+<script>
+customElements.define("x-el", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.innerHTML =
+ "<div role='presentation'><slot></slot></div>";
+ }
+});
+</script>
+<x-el id="container" role="group"><label id="l1">label1</label></x-el>
+`;
+
+addAccessibleTask(snippet, async function(browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(container, ["l1"]);
+
+ await contentSpawnMutation(browser, REORDER, function() {
+ let labelEl = content.document.createElement("label");
+ labelEl.id = "l2";
+
+ let containerEl = content.document.getElementById("container");
+ containerEl.appendChild(labelEl);
+ });
+
+ testChildrenIds(container, ["l1", "l2"]);
+});
+
+// Dynamically inserted not accessible custom element containing an accessible
+// in its shadow DOM.
+const snippet2 = `
+<script>
+customElements.define("x-el2", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.innerHTML = "<input id='input'>";
+ }
+});
+</script>
+<div role="group" id="container"></div>
+`;
+
+addAccessibleTask(snippet2, async function(browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ await contentSpawnMutation(browser, REORDER, function() {
+ content.document.getElementById("container").innerHTML = "<x-el2></x-el2>";
+ });
+
+ testChildrenIds(container, ["input"]);
+});
diff --git a/accessible/tests/browser/tree/head.js b/accessible/tests/browser/tree/head.js
new file mode 100644
index 0000000000..867a1b1417
--- /dev/null
+++ b/accessible/tests/browser/tree/head.js
@@ -0,0 +1,34 @@
+/* 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 testChildrenIds */
+
+// Load the shared-head file first.
+/* import-globals-from ../shared-head.js */
+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 }
+);
+
+/*
+ * A test function for comparing the IDs of an accessible's children
+ * with an expected array of IDs.
+ */
+function testChildrenIds(acc, expectedIds) {
+ let ids = arrayFromChildren(acc).map(child => getAccessibleDOMNodeID(child));
+ Assert.deepEqual(
+ ids,
+ expectedIds,
+ `Children for ${getAccessibleDOMNodeID(acc)} are wrong.`
+ );
+}