summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/tests/browser')
-rw-r--r--accessible/tests/browser/.eslintrc.js28
-rw-r--r--accessible/tests/browser/Common.sys.mjs451
-rw-r--r--accessible/tests/browser/Layout.sys.mjs178
-rw-r--r--accessible/tests/browser/atk/a11y_setup.py64
-rw-r--r--accessible/tests/browser/atk/browser.toml16
-rw-r--r--accessible/tests/browser/atk/browser_role.js33
-rw-r--r--accessible/tests/browser/atk/browser_table.js54
-rw-r--r--accessible/tests/browser/atk/head.js18
-rw-r--r--accessible/tests/browser/bounds/browser.toml34
-rw-r--r--accessible/tests/browser/bounds/browser_accessible_moved.js49
-rw-r--r--accessible/tests/browser/bounds/browser_caret_rect.js140
-rw-r--r--accessible/tests/browser/bounds/browser_position.js135
-rw-r--r--accessible/tests/browser/bounds/browser_test_display_contents.js44
-rw-r--r--accessible/tests/browser/bounds/browser_test_iframe_transform.js209
-rw-r--r--accessible/tests/browser/bounds/browser_test_resolution.js72
-rw-r--r--accessible/tests/browser/bounds/browser_test_simple_transform.js225
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom.js65
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom_text.js86
-rw-r--r--accessible/tests/browser/bounds/browser_zero_area.js118
-rw-r--r--accessible/tests/browser/bounds/head.js19
-rw-r--r--accessible/tests/browser/browser.toml52
-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.js84
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js84
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_reference.js58
-rw-r--r--accessible/tests/browser/browser_shutdown_parent_own_reference.js96
-rw-r--r--accessible/tests/browser/browser_shutdown_pref.js72
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_acc_reference.js70
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js72
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_no_reference.js135
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_only.js53
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_own_reference.js175
-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.toml125
-rw-r--r--accessible/tests/browser/e10s/browser_caching_actions.js295
-rw-r--r--accessible/tests/browser/e10s/browser_caching_attributes.js763
-rw-r--r--accessible/tests/browser/e10s/browser_caching_description.js280
-rw-r--r--accessible/tests/browser/e10s/browser_caching_document_props.js80
-rw-r--r--accessible/tests/browser/e10s/browser_caching_domnodeid.js32
-rw-r--r--accessible/tests/browser/e10s/browser_caching_hyperlink.js228
-rw-r--r--accessible/tests/browser/e10s/browser_caching_innerHTML.js48
-rw-r--r--accessible/tests/browser/e10s/browser_caching_interfaces.js59
-rw-r--r--accessible/tests/browser/e10s/browser_caching_large_update.js66
-rw-r--r--accessible/tests/browser/e10s/browser_caching_name.js542
-rw-r--r--accessible/tests/browser/e10s/browser_caching_position.js194
-rw-r--r--accessible/tests/browser/e10s/browser_caching_relations.js291
-rw-r--r--accessible/tests/browser/e10s/browser_caching_relations_002.js366
-rw-r--r--accessible/tests/browser/e10s/browser_caching_states.js552
-rw-r--r--accessible/tests/browser/e10s/browser_caching_table.js665
-rw-r--r--accessible/tests/browser/e10s/browser_caching_text_bounds.js737
-rw-r--r--accessible/tests/browser/e10s/browser_caching_uniqueid.js30
-rw-r--r--accessible/tests/browser/e10s/browser_caching_value.js457
-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.js119
-rw-r--r--accessible/tests/browser/e10s/browser_file_input.js77
-rw-r--r--accessible/tests/browser/e10s/browser_language.js29
-rw-r--r--accessible/tests/browser/e10s/browser_obj_group.js430
-rw-r--r--accessible/tests/browser/e10s/browser_obj_group_002.js390
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js45
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js457
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_canvas.js28
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js105
-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_image.js192
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_imagemap.js190
-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_move.js84
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_optgroup.js100
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_removal.js58
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js73
-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.js69
-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/fonts/Ahem.sjs241
-rw-r--r--accessible/tests/browser/e10s/head.js192
-rw-r--r--accessible/tests/browser/events/browser.toml33
-rw-r--r--accessible/tests/browser/events/browser_alert.js46
-rw-r--r--accessible/tests/browser/events/browser_test_A11yUtils_announce.js57
-rw-r--r--accessible/tests/browser/events/browser_test_caret_move_granularity.js102
-rw-r--r--accessible/tests/browser/events/browser_test_docload.js128
-rw-r--r--accessible/tests/browser/events/browser_test_focus_browserui.js57
-rw-r--r--accessible/tests/browser/events/browser_test_focus_dialog.js76
-rw-r--r--accessible/tests/browser/events/browser_test_focus_urlbar.js438
-rw-r--r--accessible/tests/browser/events/browser_test_panel.js54
-rw-r--r--accessible/tests/browser/events/browser_test_scrolling.js153
-rw-r--r--accessible/tests/browser/events/browser_test_selection_urlbar.js61
-rw-r--r--accessible/tests/browser/events/browser_test_textcaret.js58
-rw-r--r--accessible/tests/browser/events/head.js18
-rw-r--r--accessible/tests/browser/fission/browser.toml24
-rw-r--r--accessible/tests/browser/fission/browser_content_tree.js75
-rw-r--r--accessible/tests/browser/fission/browser_hidden_iframe.js70
-rw-r--r--accessible/tests/browser/fission/browser_nested_iframe.js164
-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.js62
-rw-r--r--accessible/tests/browser/fission/browser_take_focus.js73
-rw-r--r--accessible/tests/browser/fission/head.js18
-rw-r--r--accessible/tests/browser/general/browser.toml13
-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.js71
-rw-r--r--accessible/tests/browser/head.js120
-rw-r--r--accessible/tests/browser/hittest/browser.toml24
-rw-r--r--accessible/tests/browser/hittest/browser_test_browser.js68
-rw-r--r--accessible/tests/browser/hittest/browser_test_general.js425
-rw-r--r--accessible/tests/browser/hittest/browser_test_scroll_hittest.js105
-rw-r--r--accessible/tests/browser/hittest/browser_test_shadowroot.js60
-rw-r--r--accessible/tests/browser/hittest/browser_test_text.js84
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom.js38
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom_text.js64
-rw-r--r--accessible/tests/browser/hittest/head.js113
-rw-r--r--accessible/tests/browser/mac/browser.toml95
-rw-r--r--accessible/tests/browser/mac/browser_app.js355
-rw-r--r--accessible/tests/browser/mac/browser_aria_busy.js44
-rw-r--r--accessible/tests/browser/mac/browser_aria_controls_flowto.js92
-rw-r--r--accessible/tests/browser/mac/browser_aria_current.js58
-rw-r--r--accessible/tests/browser/mac/browser_aria_expanded.js45
-rw-r--r--accessible/tests/browser/mac/browser_aria_haspopup.js320
-rw-r--r--accessible/tests/browser/mac/browser_aria_setsize.js47
-rw-r--r--accessible/tests/browser/mac/browser_attributed_text.js165
-rw-r--r--accessible/tests/browser/mac/browser_bounds.js77
-rw-r--r--accessible/tests/browser/mac/browser_details_summary.js69
-rw-r--r--accessible/tests/browser/mac/browser_focus.js44
-rw-r--r--accessible/tests/browser/mac/browser_heading.js77
-rw-r--r--accessible/tests/browser/mac/browser_hierarchy.js75
-rw-r--r--accessible/tests/browser/mac/browser_input.js225
-rw-r--r--accessible/tests/browser/mac/browser_label_title.js111
-rw-r--r--accessible/tests/browser/mac/browser_link.js222
-rw-r--r--accessible/tests/browser/mac/browser_live_regions.js165
-rw-r--r--accessible/tests/browser/mac/browser_mathml.js146
-rw-r--r--accessible/tests/browser/mac/browser_menulist.js103
-rw-r--r--accessible/tests/browser/mac/browser_navigate.js394
-rw-r--r--accessible/tests/browser/mac/browser_outline.js566
-rw-r--r--accessible/tests/browser/mac/browser_outline_xul.js274
-rw-r--r--accessible/tests/browser/mac/browser_popupbutton.js166
-rw-r--r--accessible/tests/browser/mac/browser_radio_position.js321
-rw-r--r--accessible/tests/browser/mac/browser_range.js190
-rw-r--r--accessible/tests/browser/mac/browser_required.js175
-rw-r--r--accessible/tests/browser/mac/browser_rich_listbox.js73
-rw-r--r--accessible/tests/browser/mac/browser_roles_elements.js334
-rw-r--r--accessible/tests/browser/mac/browser_rootgroup.js246
-rw-r--r--accessible/tests/browser/mac/browser_rotor.js1752
-rw-r--r--accessible/tests/browser/mac/browser_selectables.js363
-rw-r--r--accessible/tests/browser/mac/browser_table.js629
-rw-r--r--accessible/tests/browser/mac/browser_text_basics.js426
-rw-r--r--accessible/tests/browser/mac/browser_text_input.js657
-rw-r--r--accessible/tests/browser/mac/browser_text_leaf.js83
-rw-r--r--accessible/tests/browser/mac/browser_text_selection.js187
-rw-r--r--accessible/tests/browser/mac/browser_toggle_radio_check.js304
-rw-r--r--accessible/tests/browser/mac/browser_webarea.js77
-rw-r--r--accessible/tests/browser/mac/doc_aria_tabs.html95
-rw-r--r--accessible/tests/browser/mac/doc_menulist.xhtml19
-rw-r--r--accessible/tests/browser/mac/doc_rich_listbox.xhtml22
-rw-r--r--accessible/tests/browser/mac/doc_textmarker_test.html2427
-rw-r--r--accessible/tests/browser/mac/doc_tree.xhtml59
-rw-r--r--accessible/tests/browser/mac/head.js133
-rw-r--r--accessible/tests/browser/pivot/browser.toml10
-rw-r--r--accessible/tests/browser/pivot/browser_pivot.js103
-rw-r--r--accessible/tests/browser/pivot/head.js122
-rw-r--r--accessible/tests/browser/python_runner_wsh.py88
-rw-r--r--accessible/tests/browser/role/browser.toml15
-rw-r--r--accessible/tests/browser/role/browser_computedARIARole.js88
-rw-r--r--accessible/tests/browser/role/browser_minimumRole.js59
-rw-r--r--accessible/tests/browser/role/head.js18
-rw-r--r--accessible/tests/browser/scroll/browser.toml19
-rw-r--r--accessible/tests/browser/scroll/browser_scrollToPoint.js31
-rw-r--r--accessible/tests/browser/scroll/browser_test_scrollTo.js53
-rw-r--r--accessible/tests/browser/scroll/browser_test_scroll_bounds.js662
-rw-r--r--accessible/tests/browser/scroll/browser_test_scroll_substring.js67
-rw-r--r--accessible/tests/browser/scroll/browser_test_zoom_text.js145
-rw-r--r--accessible/tests/browser/scroll/head.js18
-rw-r--r--accessible/tests/browser/selectable/browser.toml13
-rw-r--r--accessible/tests/browser/selectable/browser_test_aria_select.js164
-rw-r--r--accessible/tests/browser/selectable/browser_test_select.js329
-rw-r--r--accessible/tests/browser/selectable/head.js88
-rw-r--r--accessible/tests/browser/shared-head.js973
-rw-r--r--accessible/tests/browser/states/browser.toml22
-rw-r--r--accessible/tests/browser/states/browser_test_link.js44
-rw-r--r--accessible/tests/browser/states/browser_test_select_visibility.js76
-rw-r--r--accessible/tests/browser/states/browser_test_visibility.js181
-rw-r--r--accessible/tests/browser/states/browser_test_visibility_2.js131
-rw-r--r--accessible/tests/browser/states/head.js91
-rw-r--r--accessible/tests/browser/telemetry/browser.toml6
-rw-r--r--accessible/tests/browser/telemetry/browser_HCM_telemetry.js407
-rw-r--r--accessible/tests/browser/text/browser.toml24
-rw-r--r--accessible/tests/browser/text/browser_editabletext.js173
-rw-r--r--accessible/tests/browser/text/browser_text.js326
-rw-r--r--accessible/tests/browser/text/browser_text_caret.js452
-rw-r--r--accessible/tests/browser/text/browser_text_paragraph_boundary.js22
-rw-r--r--accessible/tests/browser/text/browser_text_selection.js344
-rw-r--r--accessible/tests/browser/text/browser_text_spelling.js151
-rw-r--r--accessible/tests/browser/text/browser_textleafpoint.js524
-rw-r--r--accessible/tests/browser/text/head.js276
-rw-r--r--accessible/tests/browser/tree/browser.toml31
-rw-r--r--accessible/tests/browser/tree/browser_aria_owns.js278
-rw-r--r--accessible/tests/browser/tree/browser_browser_element.js16
-rw-r--r--accessible/tests/browser/tree/browser_css_content_visibility.js121
-rw-r--r--accessible/tests/browser/tree/browser_general.js128
-rw-r--r--accessible/tests/browser/tree/browser_lazy_tabs.js43
-rw-r--r--accessible/tests/browser/tree/browser_link.js208
-rw-r--r--accessible/tests/browser/tree/browser_searchbar.js96
-rw-r--r--accessible/tests/browser/tree/browser_shadowdom.js98
-rw-r--r--accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js54
-rw-r--r--accessible/tests/browser/tree/head.js33
-rw-r--r--accessible/tests/browser/windows/a11y_setup.py145
-rw-r--r--accessible/tests/browser/windows/a11y_setup_requirements.txt1
-rw-r--r--accessible/tests/browser/windows/ia2/browser.toml9
-rw-r--r--accessible/tests/browser/windows/ia2/browser_role.js39
-rw-r--r--accessible/tests/browser/windows/ia2/head.js18
-rw-r--r--accessible/tests/browser/windows/uia/browser.toml11
-rw-r--r--accessible/tests/browser/windows/uia/browser_controlType.js30
-rw-r--r--accessible/tests/browser/windows/uia/browser_elementFromPoint.js34
-rw-r--r--accessible/tests/browser/windows/uia/head.js18
233 files changed, 36968 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.sys.mjs b/accessible/tests/browser/Common.sys.mjs
new file mode 100644
index 0000000000..466a0d2b99
--- /dev/null
+++ b/accessible/tests/browser/Common.sys.mjs
@@ -0,0 +1,451 @@
+/* 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 { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+const MAX_TRIM_LENGTH = 100;
+
+export 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.sys.mjs b/accessible/tests/browser/Layout.sys.mjs
new file mode 100644
index 0000000000..15b0060717
--- /dev/null
+++ b/accessible/tests/browser/Layout.sys.mjs
@@ -0,0 +1,178 @@
+/* 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 { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+import { CommonUtils } from "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs";
+
+export const Layout = {
+ /**
+ * Zoom the given document.
+ */
+ zoomDocument(doc, zoom) {
+ const bc = BrowsingContext.getFromWindow(doc.defaultView);
+ // To mirror the behaviour of the UI, we set the zoom
+ // value on the top level browsing context. This value automatically
+ // propagates down to iframes.
+ bc.top.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 ratio = win.devicePixelRatio;
+
+ // 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/atk/a11y_setup.py b/accessible/tests/browser/atk/a11y_setup.py
new file mode 100644
index 0000000000..c5fe3481ff
--- /dev/null
+++ b/accessible/tests/browser/atk/a11y_setup.py
@@ -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/.
+
+"""Python environment for ATK a11y browser tests.
+"""
+
+import os
+import subprocess
+import sys
+
+import psutil
+
+# pyatspi can't be installed using pip. Rely on the system installation.
+# Get the path to the system installation of pyatspi.
+pyatspiFile = subprocess.check_output(
+ (
+ os.path.join(sys.base_prefix, "bin", "python3"),
+ "-c",
+ "import pyatspi; print(pyatspi.__file__)",
+ ),
+ encoding="utf-8",
+).rstrip()
+sys.path.append(os.path.dirname(os.path.dirname(pyatspiFile)))
+import pyatspi
+
+sys.path.pop()
+del pyatspiFile
+
+
+def getDoc():
+ """Get the Accessible for the document being tested."""
+ # We can compare the parent process ids to find the Firefox started by the
+ # test harness.
+ commonPid = psutil.Process().ppid()
+ for app in pyatspi.Registry.getDesktop(0):
+ if (
+ app.name == "Firefox"
+ and psutil.Process(app.get_process_id()).ppid() == commonPid
+ ):
+ break
+ else:
+ raise LookupError("Couldn't find Firefox application Accessible")
+ root = app[0]
+ for embeds in root.getRelationSet():
+ if embeds.getRelationType() == pyatspi.RELATION_EMBEDS:
+ break
+ else:
+ raise LookupError("Firefox root doesn't have RELATION_EMBEDS")
+ doc = embeds.getTarget(0)
+ child = doc[0]
+ if child.get_attributes().get("id") == "default-iframe-id":
+ # This is an iframe or remoteIframe test.
+ doc = child[0]
+ return doc
+
+
+def findByDomId(root, id):
+ for child in root:
+ if child.get_attributes().get("id") == id:
+ return child
+ descendant = findByDomId(child, id)
+ if descendant:
+ return descendant
diff --git a/accessible/tests/browser/atk/browser.toml b/accessible/tests/browser/atk/browser.toml
new file mode 100644
index 0000000000..99cdba4ca2
--- /dev/null
+++ b/accessible/tests/browser/atk/browser.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'linux'",
+ "headless",
+]
+support-files = ["head.js"]
+prefs = [
+ # Enabling the a11y service from XPCOM doesn't seem to be enough to get ATK
+ # working correctly. Force enable it before the test starts.
+ "accessibility.force_disabled=-1",
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+]
+
+["browser_role.js"]
+["browser_table.js"]
diff --git a/accessible/tests/browser/atk/browser_role.js b/accessible/tests/browser/atk/browser_role.js
new file mode 100644
index 0000000000..7b870b3337
--- /dev/null
+++ b/accessible/tests/browser/atk/browser_role.js
@@ -0,0 +1,33 @@
+/* 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 ATSPI_ROLE_DOCUMENT_WEB = 95;
+const ATSPI_ROLE_PARAGRAPH = 73;
+
+addAccessibleTask(
+ `
+<p id="p">p</p>
+ `,
+ async function (browser, docAcc) {
+ let role = await runPython(`
+ global doc
+ doc = getDoc()
+ return doc.getRole()
+ `);
+ is(role, ATSPI_ROLE_DOCUMENT_WEB, "doc has correct ATSPI role");
+ ok(
+ await runPython(`
+ global p
+ p = findByDomId(doc, "p")
+ return p == doc[0]
+ `),
+ "doc's first child is p"
+ );
+ role = await runPython(`p.getRole()`);
+ is(role, ATSPI_ROLE_PARAGRAPH, "p has correct ATSPI role");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/atk/browser_table.js b/accessible/tests/browser/atk/browser_table.js
new file mode 100644
index 0000000000..98b3270465
--- /dev/null
+++ b/accessible/tests/browser/atk/browser_table.js
@@ -0,0 +1,54 @@
+/* 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 getRowColumnSpan.
+ */
+addAccessibleTask(
+ `
+<table>
+ <tr>
+ <th id="ab" colspan="2">ab</th>
+ <td id="cf" rowspan="2">cf</td>
+ </tr>
+ <tr>
+ <td id="d">d</td>
+ <td>e</td>
+ </tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ let result = await runPython(`
+ global doc
+ doc = getDoc()
+ ab = findByDomId(doc, "ab")
+ return str(ab.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=0, column=0, row_span=1, column_span=2)",
+ "ab getColumnRowSpan correct"
+ );
+ result = await runPython(`
+ cf = findByDomId(doc, "cf")
+ return str(cf.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=0, column=2, row_span=2, column_span=1)",
+ "cf getColumnRowSpan correct"
+ );
+ result = await runPython(`
+ d = findByDomId(doc, "d")
+ return str(d.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=1, column=0, row_span=1, column_span=1)",
+ "d getColumnRowSpan correct"
+ );
+ }
+);
diff --git a/accessible/tests/browser/atk/head.js b/accessible/tests/browser/atk/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/atk/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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/bounds/browser.toml b/accessible/tests/browser/bounds/browser.toml
new file mode 100644
index 0000000000..da1fe37a1e
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_accessible_moved.js"]
+
+["browser_caret_rect.js"]
+
+["browser_position.js"]
+
+["browser_test_display_contents.js"]
+
+["browser_test_iframe_transform.js"]
+
+["browser_test_resolution.js"]
+skip-if = ["os == 'win'"] # bug 1372296
+
+["browser_test_simple_transform.js"]
+
+["browser_test_zoom.js"]
+skip-if = ["true"] # Bug 1734271
+
+["browser_test_zoom_text.js"]
+https_first_disabled = true
+skip-if = ["os == 'win'"] # bug 1372296
+
+["browser_zero_area.js"]
diff --git a/accessible/tests/browser/bounds/browser_accessible_moved.js b/accessible/tests/browser/bounds/browser_accessible_moved.js
new file mode 100644
index 0000000000..307c680000
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_accessible_moved.js
@@ -0,0 +1,49 @@
+/* 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 assertBoundsNonZero(acc) {
+ // XXX We don't use getBounds because it uses BoundsInCSSPixels(), but that
+ // isn't implemented for the cache yet.
+ let x = {};
+ let y = {};
+ let width = {};
+ let height = {};
+ acc.getBounds(x, y, width, height);
+ ok(x.value > 0, "x is non-0");
+ ok(y.value > 0, "y is non-0");
+ ok(width.value > 0, "width is non-0");
+ ok(height.value > 0, "height is non-0");
+}
+
+/**
+ * Test that bounds aren't 0 after an Accessible is moved (but not re-created).
+ */
+addAccessibleTask(
+ `
+<div id="root" role="group"><div id="scrollable" role="presentation" style="height: 1px;"><button id="button">test</button></div></div>
+ `,
+ async function (browser, docAcc) {
+ let button = findAccessibleChildByID(docAcc, "button");
+ assertBoundsNonZero(button);
+
+ const root = findAccessibleChildByID(docAcc, "root");
+ let reordered = waitForEvent(EVENT_REORDER, root);
+ // scrollable wasn't in the a11y tree, but this will force it to be created.
+ // button will be moved inside it.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("scrollable").style.overflow = "scroll";
+ });
+ await reordered;
+
+ const scrollable = findAccessibleChildByID(docAcc, "scrollable");
+ assertBoundsNonZero(scrollable);
+ // XXX button's RemoteAccessible was recreated, so we have to fetch it
+ // again. This shouldn't be necessary once bug 1739050 is fixed.
+ button = findAccessibleChildByID(docAcc, "button");
+ assertBoundsNonZero(button);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_caret_rect.js b/accessible/tests/browser/bounds/browser_caret_rect.js
new file mode 100644
index 0000000000..ac0ee3aa50
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_caret_rect.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";
+
+async function getCaretRect(browser, id) {
+ // The caret rect can only be queried on LocalAccessible. On Windows, we do
+ // send it across processes with caret events, but this currently can't be
+ // queried outside of the event, nor with XPCOM.
+ const [x, y, w, h] = await invokeContentTask(browser, [id], contentId => {
+ const node = content.document.getElementById(contentId);
+ const contentAcc = content.CommonUtils.accService.getAccessibleFor(node);
+ contentAcc.QueryInterface(Ci.nsIAccessibleText);
+ const caretX = {};
+ const caretY = {};
+ const caretW = {};
+ const caretH = {};
+ contentAcc.getCaretRect(caretX, caretY, caretW, caretH);
+ return [caretX.value, caretY.value, caretW.value, caretH.value];
+ });
+ info(`Caret bounds: ${x}, ${y}, ${w}, ${h}`);
+ return [x, y, w, h];
+}
+
+async function testCaretRect(browser, docAcc, id, offset) {
+ const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
+ is(acc.caretOffset, offset, `Caret at offset ${offset}`);
+ const charX = {};
+ const charY = {};
+ const charW = {};
+ const charH = {};
+ const atEnd = offset == acc.characterCount;
+ const empty = offset == 0 && atEnd;
+ const queryOffset = atEnd && !empty ? offset - 1 : offset;
+ acc.getCharacterExtents(
+ queryOffset,
+ charX,
+ charY,
+ charW,
+ charH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ info(
+ `Character ${queryOffset} bounds: ${charX.value}, ${charY.value}, ${charW.value}, ${charH.value}`
+ );
+ const [caretX, caretY, caretW, caretH] = await getCaretRect(browser, id);
+ if (atEnd) {
+ ok(caretX > charX.value, "Caret x after last character x");
+ } else {
+ is(caretX, charX.value, "Caret x same as character x");
+ }
+ is(caretY, charY.value, "Caret y same as character y");
+ is(caretW, 1, "Caret width is 1");
+ if (!empty) {
+ is(caretH, charH.value, "Caret height same as character height");
+ }
+}
+
+function getAccBounds(acc) {
+ const x = {};
+ const y = {};
+ const w = {};
+ const h = {};
+ acc.getBounds(x, y, w, h);
+ return [x.value, y.value, w.value, h.value];
+}
+
+/**
+ * Test the caret rect in content documents.
+ */
+addAccessibleTask(
+ `
+<input id="input" value="ab">
+<input id="emptyInput">
+ `,
+ async function (browser, docAcc) {
+ async function runTests() {
+ const input = findAccessibleChildByID(docAcc, "input", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing input");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.takeFocus();
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 0);
+ info("Setting caretOffset to 1");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.caretOffset = 1;
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 1);
+ info("Setting caretOffset to 2");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.caretOffset = 2;
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 2);
+ info("Resetting caretOffset to 0");
+ input.caretOffset = 0;
+
+ const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing emptyInput");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
+ emptyInput.takeFocus();
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "emptyInput", 0);
+ }
+
+ await runTests();
+
+ // Check that the caret rect is correct when the title bar is shown.
+ if (LINUX || Services.env.get("MOZ_HEADLESS")) {
+ // Disabling tabs in title bar doesn't change the bounds on Linux or in
+ // headless mode.
+ info("Skipping title bar tests");
+ return;
+ }
+ const [, origDocY] = getAccBounds(docAcc);
+ info("Showing title bar");
+ let titleBarChanged = BrowserTestUtils.waitForMutationCondition(
+ document.documentElement,
+ { attributes: true, attributeFilter: ["tabsintitlebar"] },
+ () => !document.documentElement.hasAttribute("tabsintitlebar")
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.inTitlebar", false]],
+ });
+ await titleBarChanged;
+ const [, newDocY] = getAccBounds(docAcc);
+ Assert.greater(
+ newDocY,
+ origDocY,
+ "Doc has larger y after title bar change"
+ );
+ await runTests();
+ await SpecialPowers.popPrefEnv();
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_position.js b/accessible/tests/browser/bounds/browser_position.js
new file mode 100644
index 0000000000..bfba58be87
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_position.js
@@ -0,0 +1,135 @@
+/* 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 changing the left/top CSS properties.
+ */
+addAccessibleTask(
+ `
+<div id="div" style="position: relative; left: 0px; top: 0px; width: fit-content;">
+ test
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "div", browser);
+ info("Changing left");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").style.left = "200px";
+ });
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ info("Changing top");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").style.top = "200px";
+ });
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ },
+ { chrome: true, topLevel: true, iframe: true }
+);
+
+/**
+ * Test moving one element by inserting a second element before it such that the
+ * second element doesn't reflow.
+ */
+addAccessibleTask(
+ `
+<div id="reflowContainer">
+ <p>1</p>
+ <p id="reflow2" hidden>2</p>
+ <p id="reflow3">3</p>
+</div>
+<p id="noReflow">noReflow</p>
+ `,
+ async function (browser, docAcc) {
+ for (const id of ["reflowContainer", "reflow3", "noReflow"]) {
+ await testBoundsWithContent(docAcc, id, browser);
+ }
+ // Show p2, which will reflow everything inside "reflowContainer", but just
+ // move "noReflow" down without reflowing it.
+ info("Showing p2");
+ let shown = waitForEvent(EVENT_SHOW, "reflow2");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("reflow2").hidden = false;
+ });
+ await waitForContentPaint(browser);
+ await shown;
+ for (const id of ["reflowContainer", "reflow2", "reflow3", "noReflow"]) {
+ await testBoundsWithContent(docAcc, id, browser);
+ }
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test bounds when an Accessible is re-parented.
+ */
+addAccessibleTask(
+ `
+<div id="container">
+ <p style="height: 300px;">1</p>
+ <div class="pParent">
+ <p id="p2">2</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const paraTree = { PARAGRAPH: [{ TEXT_LEAF: [] }] };
+ const container = findAccessibleChildByID(docAcc, "container");
+ testAccessibleTree(container, { SECTION: [paraTree, paraTree] });
+ await testBoundsWithContent(docAcc, "p2", browser);
+ // Add a click listener to the div containing p2. This will cause an
+ // Accessible to be created for that div, which didn't have one before.
+ info("Adding click listener to pParent");
+ let reordered = waitForEvent(EVENT_REORDER, container);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .querySelector(".pParent")
+ .addEventListener("click", function () {});
+ });
+ await reordered;
+ testAccessibleTree(container, {
+ SECTION: [paraTree, { SECTION: [paraTree] }],
+ });
+ await testBoundsWithContent(docAcc, "p2", browser);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test the bounds of items in an inline list with content that offsets the
+ * origin of the list's bounding box (creating an IB split within the UL frame).
+ */
+addAccessibleTask(
+ `
+ <style>
+ ul,li {
+ display:inline;
+ list-style-type:none;
+ list-style-position:inside;
+ margin:0;
+ padding:0;
+ }
+ </style>
+ <div id="container" style="background:green; max-width: 400px;">List of information: <ul id="list"><li id="one">item one</li> | <li id="two">item two</li> | <li id="three">item three</li> | <li id="four">item four</li> | <li id="five">item five</li></ul></div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "list", browser);
+ await testBoundsWithContent(docAcc, "one", browser);
+ await testBoundsWithContent(docAcc, "two", browser);
+ await testBoundsWithContent(docAcc, "three", browser);
+ await testBoundsWithContent(docAcc, "four", browser);
+ await testBoundsWithContent(docAcc, "five", browser);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_display_contents.js b/accessible/tests/browser/bounds/browser_test_display_contents.js
new file mode 100644
index 0000000000..db1bfce178
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_display_contents.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/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 p = findAccessibleChildByID(accDoc, "div");
+ let p2 = findAccessibleChildByID(accDoc, "p");
+
+ await testContentBounds(browser, p);
+ await testContentBounds(browser, p2);
+}
+
+/**
+ * Test accessible bounds for accs with display:contents
+ */
+addAccessibleTask(
+ `
+ <div id="div">before
+ <ul id="ul" style="display: contents;">
+ <li id="li" style="display: contents;">
+ <p id="p">item</p>
+ </li>
+ </ul>
+ </div>`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_iframe_transform.js b/accessible/tests/browser/bounds/browser_test_iframe_transform.js
new file mode 100644
index 0000000000..a44ab75faf
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_iframe_transform.js
@@ -0,0 +1,209 @@
+/* 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 TRANSLATION_OFFSET = 50;
+const ELEM_ID = "test-elem-id";
+
+// Modify the style of an iframe within the content process. This is different
+// from, e.g., invokeSetStyle, because this function doesn't rely on
+// invokeContentTask, which runs in the context of the iframe itself.
+async function invokeSetStyleIframe(browser, id, style, value) {
+ if (value) {
+ Logger.log(`Setting ${style} style to ${value} for iframe with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${style} style from iframe with id: ${id}`);
+ }
+
+ // Translate the iframe itself (not content within it).
+ await SpecialPowers.spawn(
+ browser,
+ [id, style, value],
+ (iframeId, iframeStyle, iframeValue) => {
+ const elm = content.document.getElementById(iframeId);
+ if (iframeValue) {
+ elm.style[iframeStyle] = iframeValue;
+ } else {
+ delete elm.style[iframeStyle];
+ }
+ }
+ );
+}
+
+// Test the accessible's bounds, comparing them to the content bounds from DOM.
+// This function also accepts an offset, which is necessary in some cases where
+// DOM doesn't know about cross-process offsets.
+function testBoundsWithOffset(browser, iframeDocAcc, id, domElmBounds, offset) {
+ // Get the bounds as reported by the accessible.
+ const acc = findAccessibleChildByID(iframeDocAcc, id);
+ const accX = {};
+ const accY = {};
+ const accWidth = {};
+ const accHeight = {};
+ acc.getBounds(accX, accY, accWidth, accHeight);
+
+ // getContentBoundsForDOMElm's result doesn't include iframe translation
+ // for in-process iframes, but does for out-of-process iframes. To account
+ // for that here, manually add in the translation offset when examining an
+ // in-process iframe.
+ const addTranslationOffset = !gIsRemoteIframe;
+ const expectedX = addTranslationOffset
+ ? domElmBounds[0] + offset
+ : domElmBounds[0];
+ const expectedY = addTranslationOffset
+ ? domElmBounds[1] + offset
+ : domElmBounds[1];
+ const expectedWidth = domElmBounds[2];
+ const expectedHeight = domElmBounds[3];
+
+ let boundsAreEquivalent = true;
+ boundsAreEquivalent &&= accX.value == expectedX;
+ boundsAreEquivalent &&= accY.value == expectedY;
+ boundsAreEquivalent &&= accWidth.value == expectedWidth;
+ boundsAreEquivalent &&= accHeight.value == expectedHeight;
+ return boundsAreEquivalent;
+}
+
+addAccessibleTask(
+ `<div id='${ELEM_ID}'>hello world</div>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+
+ await testBoundsWithContent(iframeDocAcc, ELEM_ID, browser);
+
+ // Translate the iframe, which should modify cross-process offset.
+ await invokeSetStyleIframe(
+ browser,
+ DEFAULT_IFRAME_ID,
+ "transform",
+ `translate(${TRANSLATION_OFFSET}px, ${TRANSLATION_OFFSET}px)`
+ );
+
+ // Allow content to advance to update DOM, then capture the DOM bounds.
+ await waitForContentPaint(browser);
+ const domElmBoundsAfterTranslate = await getContentBoundsForDOMElm(
+ browser,
+ ELEM_ID
+ );
+
+ // Ensure that there's enough time for the cache to update.
+ await untilCacheOk(() => {
+ return testBoundsWithOffset(
+ browser,
+ iframeDocAcc,
+ ELEM_ID,
+ domElmBoundsAfterTranslate,
+ TRANSLATION_OFFSET
+ );
+ }, "Accessible bounds have changed in the cache and match DOM bounds.");
+
+ // Adjust padding of the iframe, then verify bounds adjust properly.
+ // iframes already have a border by default, so we check padding here.
+ const PADDING_OFFSET = 100;
+ await invokeSetStyleIframe(
+ browser,
+ DEFAULT_IFRAME_ID,
+ "padding",
+ `${PADDING_OFFSET}px`
+ );
+
+ // Allow content to advance to update DOM, then capture the DOM bounds.
+ await waitForContentPaint(browser);
+ const domElmBoundsAfterAddingPadding = await getContentBoundsForDOMElm(
+ browser,
+ ELEM_ID
+ );
+
+ await untilCacheOk(() => {
+ return testBoundsWithOffset(
+ browser,
+ iframeDocAcc,
+ ELEM_ID,
+ domElmBoundsAfterAddingPadding,
+ TRANSLATION_OFFSET
+ );
+ }, "Accessible bounds have changed in the cache and match DOM bounds.");
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: {
+ style: `height: 100px; width: 100px;`,
+ },
+ }
+);
+
+/**
+ * Test document bounds change notifications.
+ * Note: This uses iframes to change the doc container size in order
+ * to have the doc accessible's bounds change.
+ */
+addAccessibleTask(
+ `<div id="div" style="width: 30px; height: 30px"></div>`,
+ async function (browser, accDoc, foo) {
+ const docWidth = () => {
+ let width = {};
+ accDoc.getBounds({}, {}, width, {});
+ return width.value;
+ };
+
+ await untilCacheIs(docWidth, 0, "Doc width is 0");
+ await invokeSetStyleIframe(browser, DEFAULT_IFRAME_ID, "width", `300px`);
+ await untilCacheIs(docWidth, 300, "Doc width is 300");
+ },
+ {
+ chrome: false,
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "width: 0;" },
+ }
+);
+
+/**
+ * Test document bounds after re-creating an iframe.
+ */
+addAccessibleTask(
+ `
+<ol id="ol">
+ <iframe id="iframe" src="data:text/html,"></iframe>
+</ol>
+ `,
+ async function (browser, docAcc) {
+ let iframeDoc = findAccessibleChildByID(docAcc, "iframe").firstChild;
+ ok(iframeDoc, "Got the iframe document");
+ const origX = {};
+ const origY = {};
+ iframeDoc.getBounds(origX, origY, {}, {});
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ // This will cause a bounds cache update to be queued for the iframe doc.
+ content.document.getElementById("iframe").width = "600";
+ // This will recreate the ol a11y subtree, including the iframe. The
+ // iframe document will be unbound briefly while this happens. We want to
+ // be sure processing the bounds cache update queued above doesn't assert
+ // while the document is unbound. The setTimeout is necessary to get the
+ // cache update to happen at the right time.
+ content.setTimeout(
+ () => (content.document.getElementById("ol").type = "i"),
+ 0
+ );
+ });
+ await reordered;
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ // We don't currently fire an event when a DocAccessible is re-bound to a new OuterDoc.
+ await BrowserTestUtils.waitForCondition(() => iframe.firstChild);
+ iframeDoc = iframe.firstChild;
+ ok(iframeDoc, "Got the iframe document after re-creation");
+ const newX = {};
+ const newY = {};
+ iframeDoc.getBounds(newX, newY, {}, {});
+ ok(
+ origX.value == newX.value && origY.value == newY.value,
+ "Iframe document x and y are same after iframe re-creation"
+ );
+ }
+);
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..0b0b47418d
--- /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.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ 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.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ 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_simple_transform.js b/accessible/tests/browser/bounds/browser_test_simple_transform.js
new file mode 100644
index 0000000000..7197968b40
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_simple_transform.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";
+
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// test basic translation
+addAccessibleTask(
+ `<p id="translate">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "translate", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("translate");
+ p.style = "transform: translate(100px, 100px);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "translate", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test translation with two children.
+addAccessibleTask(
+ `
+<div role="main" style="translate: 0 300px;">
+ <p id="p1">hello</p>
+ <p id="p2">world</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "p1", browser);
+ await testBoundsWithContent(docAcc, "p2", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// test basic rotation
+addAccessibleTask(
+ `<p id="rotate">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "rotate", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("rotate");
+ p.style = "transform: rotate(-40deg);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "rotate", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// test basic scale
+addAccessibleTask(
+ `<p id="scale">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "scale", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("scale");
+ p.style = "transform: scale(2);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "scale", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test will-change: transform with no transform.
+addAccessibleTask(
+ `
+<div id="willChangeTop" style="will-change: transform;">
+ <p>hello</p>
+ <p id="willChangeTopP2">world</p>
+</div>
+<div role="group">
+ <div id="willChangeInner" style="will-change: transform;">
+ <p>hello</p>
+ <p id="willChangeInnerP2">world</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ // Even though willChangeTop has no transform, it has
+ // will-change: transform, which means nsIFrame::IsTransformed returns
+ // true. We don't cache identity matrices, but because there is an offset
+ // to the root frame, layout includes this in the returned transform
+ // matrix. That means we get a non-identity matrix and thus we cache it.
+ // This is why we only test the identity matrix cache optimization for
+ // willChangeInner.
+ let hasTransform;
+ try {
+ const willChangeInner = findAccessibleChildByID(
+ docAcc,
+ "willChangeInner"
+ );
+ willChangeInner.cache.getStringProperty("transform");
+ hasTransform = true;
+ } catch (e) {
+ hasTransform = false;
+ }
+ ok(!hasTransform, "willChangeInner has no cached transform");
+ await testBoundsWithContent(docAcc, "willChangeTopP2", browser);
+ await testBoundsWithContent(docAcc, "willChangeInnerP2", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that a transform forces creation of an accessible.
+addAccessibleTask(
+ `
+<div id="container">
+ <div style="transform:translate(100px,100px);">
+ <p>test</p>
+ </div>
+</div>
+
+<div id="div-presentational" role="presentation" style="transform:translate(100px,100px);">
+ <p>test</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const tree = { TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ const divWithTransform = findAccessibleChildByID(
+ docAcc,
+ "container"
+ ).firstChild;
+ testAccessibleTree(divWithTransform, tree);
+ // testBoundsWithContent takes an id, but divWithTransform doesn't have one,
+ // so we can't test the bounds for it.
+
+ // An accessible should still be created, even if the role is "presentation."
+ const divPresentational = findAccessibleChildByID(
+ docAcc,
+ "div-presentational"
+ );
+ testAccessibleTree(divPresentational, tree);
+ await testBoundsWithContent(docAcc, "div-presentational", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that adding a transform on the fly forces creation of an accessible.
+addAccessibleTask(
+ `
+<div id="div-to-transform" role="none" style="position: absolute; width: 300px; height: 300px;">
+ <p>test</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ let divToTransform = findAccessibleChildByID(docAcc, "div-to-transform");
+ ok(!divToTransform, "There should not be a div accessible.");
+
+ // Translate the div.
+ await invokeContentTask(browser, [], () => {
+ let div = content.document.getElementById("div-to-transform");
+ div.style.transform = "translate(100%, 100%)";
+ });
+ await waitForContentPaint(browser);
+
+ // Verify that the SECTION accessible appeared after we gave it a transform.
+ divToTransform = findAccessibleChildByID(docAcc, "div-to-transform");
+ const tree = {
+ TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }],
+ };
+ testAccessibleTree(divToTransform, tree);
+
+ // Verify that the bounds of the div are correctly modified.
+ await testBoundsWithContent(docAcc, "div-to-transform", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test translated, position: absolute Accessible in a container.
+addAccessibleTask(
+ `
+<div id="container">
+ <div id="transform" style="position: absolute; transform: translate(100px, 100px);">
+ <p id="p">test</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "transform", browser);
+ await testBoundsWithContent(docAcc, "p", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test bounds of a rotated element after scroll.
+addAccessibleTask(
+ `
+<div id="scrollable" style="transform: rotate(180deg); overflow: scroll; height: 500px;">
+ <p id="test">hello world</p><hr style="height: 100vh;">
+</div>
+ `,
+ async function (browser, docAcc) {
+ info(
+ "Testing that the unscrolled bounds of a transformed element are correct."
+ );
+ await testBoundsWithContent(docAcc, "test", browser);
+
+ await invokeContentTask(browser, [], () => {
+ // Scroll the scrollable region down (scrolls up due to the transform).
+ let elem = content.document.getElementById("scrollable");
+ elem.scrollTo(0, elem.scrollHeight);
+ });
+
+ info(
+ "Testing that the scrolled bounds of a transformed element are correct."
+ );
+ await testBoundsWithContent(docAcc, "test", browser);
+ },
+ { topLevel: true, 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..ac84e485a4
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom.js
@@ -0,0 +1,65 @@
+/* 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 SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ 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..3f40b698bf
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom_text.js
@@ -0,0 +1,86 @@
+/* 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
+ );
+ // A 0 range should return an empty rect.
+ testTextBounds(
+ hyperTextNode,
+ 0,
+ 0,
+ [0, 0, 0, 0],
+ 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
+ );
+ // A 0 range in an empty input should still return
+ // rect of input node.
+ testTextBounds(
+ inputNode,
+ 0,
+ 0,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ }
+
+ await testTextNode("p1");
+ await testTextNode("p2");
+ await testEmptyInputNode("i1");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.zoomDocument(content.document, 2.0);
+ });
+
+ await testTextNode("p1");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ 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/browser_zero_area.js b/accessible/tests/browser/bounds/browser_zero_area.js
new file mode 100644
index 0000000000..c0f9db2673
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_zero_area.js
@@ -0,0 +1,118 @@
+/* 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, expectedWidth, expectedHeight) {
+ let [expectedX, expectedY] = 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);
+ is(height, expectedHeight, "Wrong height of " + prettyAccName);
+}
+/**
+ * Test accessible bounds with different combinations of overflow and
+ * non-zero frame area.
+ */
+addAccessibleTask(
+ `
+ <div id="a1" style="height:100px; width:100px; background:green;"></div>
+ <div id="a2" style="height:100px; width:100px; background:green;"><div style="height:300px; max-width: 300px; background:blue;"></div></div>
+ <div id="a3" style="height:0; width:0;"><div style="height:200px; width:200px; background:green;"></div></div>
+ `,
+ async function (browser, accDoc) {
+ const a1 = findAccessibleChildByID(accDoc, "a1");
+ const a2 = findAccessibleChildByID(accDoc, "a2");
+ const a3 = findAccessibleChildByID(accDoc, "a3");
+ await testContentBounds(browser, a1, 100, 100);
+ await testContentBounds(browser, a2, 100, 100);
+ await testContentBounds(browser, a3, 200, 200);
+ }
+);
+
+/**
+ * Ensure frames with zero area have their x, y coordinates correctly reported
+ * in bounds()
+ */
+addAccessibleTask(
+ `
+<br>
+<div id="a" style="height:0; width:0;"></div>
+`,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ await testContentBounds(browser, a, 0, 0);
+ }
+);
+
+/**
+ * Ensure accessibles have accurately signed dimensions and position when
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+<input type="radio" id="radio" style="left: -671091em; position: absolute;">
+`,
+ async function (browser, accDoc) {
+ const radio = findAccessibleChildByID(accDoc, "radio");
+ const contentDPR = await getContentDPR(browser);
+ const [x, y, width, height] = getBounds(radio, contentDPR);
+ ok(x < 0, "X coordinate should be negative");
+ ok(y > 0, "Y coordinate should be positive");
+ ok(width > 0, "Width should be positive");
+ ok(height > 0, "Height should be positive");
+ // Note: the exact values of x, y, width, and height
+ // are inconsistent with the DOM element values of those
+ // fields, so we don't check our bounds against them with
+ // `testContentBounds` here. DOM reports a negative width,
+ // positive height, and a slightly different (+/- 20)
+ // x and y.
+ }
+);
+
+/**
+ * Test height: 0 with align-items: flex-end. This causes the content to
+ * overflow above the frame's main rect.
+ */
+addAccessibleTask(
+ `
+<aside style="height: 0; display: flex; align-items: flex-end;">
+ <div id="inner0">testing</div>
+</aside>
+<aside style="height: 1; display: flex; align-items: flex-end;">
+ <div id="inner1">testing</div>
+</aside>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "inner0", browser);
+ await testBoundsWithContent(docAcc, "inner1", browser);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test a div (block) inside a span (inline). This causes the span's primary
+ * frame to have an empty rect offset from its visible content.
+ */
+addAccessibleTask(
+ `
+<span id="span" tabindex="-1">
+ <div id="div">Testing</div>
+</span>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "span", browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ },
+ { chrome: true, topLevel: 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..c1882b9495
--- /dev/null
+++ b/accessible/tests/browser/bounds/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.
+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.toml b/accessible/tests/browser/browser.toml
new file mode 100644
index 0000000000..a6264f837b
--- /dev/null
+++ b/accessible/tests/browser/browser.toml
@@ -0,0 +1,52 @@
+[DEFAULT]
+skip-if = ["a11y_checks"] # 1534855
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "*.sys.mjs",
+ "atk/a11y_setup.py",
+ "head.js",
+ "python_runner_wsh.py",
+ "shared-head.js",
+ "windows/a11y_setup.py",
+ "windows/a11y_setup_requirements.txt",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_shutdown_acc_reference.js"]
+
+["browser_shutdown_doc_acc_reference.js"]
+
+["browser_shutdown_multi_acc_reference_doc.js"]
+
+["browser_shutdown_multi_acc_reference_obj.js"]
+
+["browser_shutdown_multi_proxy_acc_reference_doc.js"]
+skip-if = ["os == 'linux' && verify && debug"]
+
+["browser_shutdown_multi_proxy_acc_reference_obj.js"]
+skip-if = ["os == 'linux' && verify && debug"]
+
+["browser_shutdown_multi_reference.js"]
+
+["browser_shutdown_parent_own_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_pref.js"]
+
+["browser_shutdown_proxy_acc_reference.js"]
+
+["browser_shutdown_proxy_doc_acc_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_remote_no_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_remote_only.js"]
+
+["browser_shutdown_remote_own_reference.js"]
+
+["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..1768095f94
--- /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..8f7bf6d423
--- /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..273fc7175d
--- /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..af21b3dc4c
--- /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..e4091c5216
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.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";
+
+add_task(async function () {
+ 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;
+ }
+ );
+});
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..f6eca362b0
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.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";
+
+add_task(async function () {
+ 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;
+ }
+ );
+});
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..a92f6faf61
--- /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..472e977626
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_parent_own_reference.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";
+
+add_task(async function () {
+ 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, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // 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]);
+ }
+ );
+});
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..7144cff019
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js
@@ -0,0 +1,70 @@
+/* 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 () {
+ 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;
+ }
+ );
+});
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..6d4ad71f1e
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.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";
+
+add_task(async function () {
+ 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;
+ }
+ );
+});
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..6a38ff15b8
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_no_reference.js
@@ -0,0 +1,135 @@
+/* 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 () {
+ 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, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // 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."
+ )
+ );
+ }
+ );
+});
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..95c3604400
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_only.js
@@ -0,0 +1,53 @@
+/* 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 () {
+ 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, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // 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."
+ );
+ }
+ );
+});
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..a30d191b53
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_own_reference.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ 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, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // 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."
+ )
+ );
+ }
+ );
+});
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..fafa59bb50
--- /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..92d2823388
--- /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.toml b/accessible/tests/browser/e10s/browser.toml
new file mode 100644
index 0000000000..dfac6b5219
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser.toml
@@ -0,0 +1,125 @@
+[DEFAULT]
+subsuite = "a11y"
+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",
+ "fonts/Ahem.sjs",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/events/slow_image.sjs",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png",
+]
+prefs = [
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+ "dom.element.popover.enabled=true"
+]
+
+# Caching tests
+["browser_caching_actions.js"]
+
+["browser_caching_attributes.js"]
+
+["browser_caching_description.js"]
+
+["browser_caching_document_props.js"]
+
+["browser_caching_domnodeid.js"]
+
+["browser_caching_hyperlink.js"]
+
+["browser_caching_innerHTML.js"]
+skip-if = ["os != 'win'"]
+
+["browser_caching_interfaces.js"]
+
+["browser_caching_large_update.js"]
+
+["browser_caching_name.js"]
+
+["browser_caching_position.js"]
+
+["browser_caching_relations.js"]
+
+["browser_caching_relations_002.js"]
+
+["browser_caching_states.js"]
+
+["browser_caching_table.js"]
+
+["browser_caching_text_bounds.js"]
+
+["browser_caching_uniqueid.js"]
+
+["browser_caching_value.js"]
+
+# Events tests
+["browser_events_announcement.js"]
+skip-if = ["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_file_input.js"]
+
+["browser_language.js"]
+
+["browser_obj_group.js"]
+
+["browser_obj_group_002.js"]
+
+# Tree update tests
+["browser_treeupdate_ariadialog.js"]
+
+["browser_treeupdate_ariaowns.js"]
+
+["browser_treeupdate_canvas.js"]
+
+["browser_treeupdate_csscontentvisibility.js"]
+
+["browser_treeupdate_cssoverflow.js"]
+
+["browser_treeupdate_doc.js"]
+
+["browser_treeupdate_gencontent.js"]
+
+["browser_treeupdate_hidden.js"]
+
+["browser_treeupdate_image.js"]
+
+["browser_treeupdate_imagemap.js"]
+
+["browser_treeupdate_list.js"]
+
+["browser_treeupdate_list_editabledoc.js"]
+
+["browser_treeupdate_listener.js"]
+
+["browser_treeupdate_move.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"]
diff --git a/accessible/tests/browser/e10s/browser_caching_actions.js b/accessible/tests/browser/e10s/browser_caching_actions.js
new file mode 100644
index 0000000000..893c818b75
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_actions.js
@@ -0,0 +1,295 @@
+/* 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 gClickEvents = ["mousedown", "mouseup", "click"];
+
+const gActionDescrMap = {
+ jump: "Jump",
+ press: "Press",
+ check: "Check",
+ uncheck: "Uncheck",
+ select: "Select",
+ open: "Open",
+ close: "Close",
+ switch: "Switch",
+ click: "Click",
+ collapse: "Collapse",
+ expand: "Expand",
+ activate: "Activate",
+ cycle: "Cycle",
+ "click ancestor": "Click ancestor",
+};
+
+async function testActions(browser, docAcc, id, expectedActions, domEvents) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ is(acc.actionCount, expectedActions.length, "Correct action count");
+
+ let actionNames = [];
+ let actionDescriptions = [];
+ for (let i = 0; i < acc.actionCount; i++) {
+ actionNames.push(acc.getActionName(i));
+ actionDescriptions.push(acc.getActionDescription(i));
+ }
+
+ is(actionNames.join(","), expectedActions.join(","), "Correct action names");
+ is(
+ actionDescriptions.join(","),
+ expectedActions.map(a => gActionDescrMap[a]).join(","),
+ "Correct action descriptions"
+ );
+
+ if (!domEvents) {
+ return;
+ }
+
+ // We need to set up the listener, and wait for the promise in two separate
+ // content tasks.
+ await invokeContentTask(browser, [id, domEvents], (_id, _domEvents) => {
+ let promises = _domEvents.map(
+ evtName =>
+ new Promise(resolve => {
+ const listener = e => {
+ if (e.target.id == _id) {
+ content.removeEventListener(evtName, listener);
+ content.evtPromise = null;
+ resolve(42);
+ }
+ };
+ content.addEventListener(evtName, listener);
+ })
+ );
+ content.evtPromise = Promise.all(promises);
+ });
+
+ acc.doAction(0);
+
+ let eventFired = await invokeContentTask(browser, [], async () => {
+ await content.evtPromise;
+ return true;
+ });
+
+ ok(eventFired, `DOM events fired '${domEvents}'`);
+}
+
+addAccessibleTask(
+ `<ul>
+ <li id="li_clickable1" onclick="">Clickable list item</li>
+ <li id="li_clickable2" onmousedown="">Clickable list item</li>
+ <li id="li_clickable3" onmouseup="">Clickable list item</li>
+ </ul>
+
+ <img id="onclick_img" onclick=""
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+
+ <a id="link1" href="#">linkable textleaf accessible</a>
+ <div id="link2" onclick="">linkable textleaf accessible</div>
+
+ <a id="link3" href="#">
+ <img id="link3img" alt="image in link"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+
+ <a href="about:mozilla" id="link4" target="_blank" rel="opener">
+ <img src="../moz.png" id="link4img">
+ </a>
+ <a id="link5" onmousedown="">
+ <img src="../moz.png" id="link5img">
+ </a>
+ <a id="link6" onclick="">
+ <img src="../moz.png" id="link6img">
+ </a>
+ <a id="link7" onmouseup="">
+ <img src="../moz.png" id="link7img">
+ </a>
+
+ <div>
+ <label for="TextBox_t2" id="label1">
+ <span>Explicit</span>
+ </label>
+ <input name="in2" id="TextBox_t2" type="text" maxlength="17">
+ </div>
+
+ <div onclick=""><p id="p_in_clickable_div">p in clickable div</p></div>
+ `,
+ async function (browser, docAcc) {
+ is(docAcc.actionCount, 0, "Doc should not have any actions");
+
+ const _testActions = async (id, expectedActions, domEvents) => {
+ await testActions(browser, docAcc, id, expectedActions, domEvents);
+ };
+
+ await _testActions("li_clickable1", ["click"], gClickEvents);
+ await _testActions("li_clickable2", ["click"], gClickEvents);
+ await _testActions("li_clickable3", ["click"], gClickEvents);
+
+ await _testActions("onclick_img", ["click"], gClickEvents);
+ await _testActions("link1", ["jump"], gClickEvents);
+ await _testActions("link2", ["click"], gClickEvents);
+ await _testActions("link3", ["jump"], gClickEvents);
+ await _testActions("link3img", ["click ancestor"], gClickEvents);
+ await _testActions("link4", ["jump"], gClickEvents);
+ await _testActions("link4img", ["click ancestor"], gClickEvents);
+ await _testActions("link5", ["click"], gClickEvents);
+ await _testActions("link5img", ["click ancestor"], gClickEvents);
+ await _testActions("link6", ["click"], gClickEvents);
+ await _testActions("link6img", ["click ancestor"], gClickEvents);
+ await _testActions("link7", ["click"], gClickEvents);
+ await _testActions("link7img", ["click ancestor"], gClickEvents);
+ await _testActions("label1", ["click"], gClickEvents);
+ await _testActions("p_in_clickable_div", ["click ancestor"], gClickEvents);
+
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("li_clickable1")
+ .removeAttribute("onclick");
+ });
+
+ let acc = findAccessibleChildByID(docAcc, "li_clickable1");
+ await untilCacheIs(() => acc.actionCount, 0, "li has no actions");
+ let thrown = false;
+ try {
+ acc.doAction(0);
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "doAction should throw exception");
+
+ // Remove 'for' from label
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label1").removeAttribute("for");
+ });
+ acc = findAccessibleChildByID(docAcc, "label1");
+ await untilCacheIs(() => acc.actionCount, 0, "label has no actions");
+ thrown = false;
+ try {
+ acc.doAction(0);
+ ok(false, "doAction should throw exception");
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "doAction should throw exception");
+
+ // Add 'longdesc' to image
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("onclick_img")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("longdesc", "http://example.com");
+ });
+ acc = findAccessibleChildByID(docAcc, "onclick_img");
+ await untilCacheIs(() => acc.actionCount, 2, "img has 2 actions");
+ await _testActions("onclick_img", ["click", "showlongdesc"]);
+
+ // Remove 'onclick' from image with 'longdesc'
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("onclick_img").removeAttribute("onclick");
+ });
+ acc = findAccessibleChildByID(docAcc, "onclick_img");
+ await untilCacheIs(() => acc.actionCount, 1, "img has 1 actions");
+ await _testActions("onclick_img", ["showlongdesc"]);
+
+ // Remove 'href' from link and test linkable child
+ let link1Acc = findAccessibleChildByID(docAcc, "link1");
+ is(
+ link1Acc.firstChild.getActionName(0),
+ "click ancestor",
+ "linkable child has click ancestor action"
+ );
+ let onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link1Acc],
+ [EVENT_SHOW, "link1"],
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ let link1 = content.document.getElementById("link1");
+ link1.removeAttribute("href");
+ });
+ await onRecreation;
+ link1Acc = findAccessibleChildByID(docAcc, "link1");
+ await untilCacheIs(() => link1Acc.actionCount, 0, "link has no actions");
+ is(link1Acc.firstChild.actionCount, 0, "linkable child's actions removed");
+
+ // Add a click handler to the body. Ensure it propagates to descendants.
+ await invokeContentTask(browser, [], () => {
+ content.document.body.onclick = () => {};
+ });
+ await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action");
+ await _testActions("link1", ["click ancestor"]);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.body.onclick = null;
+ });
+ await untilCacheIs(() => docAcc.actionCount, 0, "Doc has no actions");
+ is(link1Acc.actionCount, 0, "link has no actions");
+
+ // Add a click handler to the root element. Ensure it propagates to
+ // descendants.
+ await invokeContentTask(browser, [], () => {
+ content.document.documentElement.onclick = () => {};
+ });
+ await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action");
+ await _testActions("link1", ["click ancestor"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test access key.
+ */
+addAccessibleTask(
+ `
+<button id="noKey">noKey</button>
+<button id="key" accesskey="a">key</button>
+ `,
+ async function (browser, docAcc) {
+ const noKey = findAccessibleChildByID(docAcc, "noKey");
+ is(noKey.accessKey, "", "noKey has no accesskey");
+ const key = findAccessibleChildByID(docAcc, "key");
+ is(key.accessKey, MAC ? "⌃⌥a" : "Alt+Shift+a", "key has correct accesskey");
+
+ info("Changing accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").accessKey = "b";
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ MAC ? "⌃⌥b" : "Alt+Shift+b",
+ "Correct accesskey after change"
+ );
+
+ info("Removing accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").removeAttribute("accesskey");
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ "",
+ "Empty accesskey after removal"
+ );
+
+ info("Adding accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").accessKey = "c";
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ MAC ? "⌃⌥c" : "Alt+Shift+c",
+ "Correct accesskey after addition"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: false, // Bug 1796846
+ remoteIframe: false, // Bug 1796846
+ }
+);
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..139015061f
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_attributes.js
@@ -0,0 +1,763 @@
+/* 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);
+ }
+ },
+ {
+ // These tests don't work yet with the parent process cache.
+ topLevel: false,
+ iframe: false,
+ remoteIframe: false,
+ }
+);
+
+/**
+ * Test caching of the tag attribute.
+ */
+addAccessibleTask(
+ `
+<p id="p">text</p>
+<textarea id="textarea"></textarea>
+ `,
+ async function (browser, docAcc) {
+ testAttrs(docAcc, { tag: "body" }, true);
+ const p = findAccessibleChildByID(docAcc, "p");
+ testAttrs(p, { tag: "p" }, true);
+ const textLeaf = p.firstChild;
+ testAbsentAttrs(textLeaf, { tag: "" });
+ const textarea = findAccessibleChildByID(docAcc, "textarea");
+ testAttrs(textarea, { tag: "textarea" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the text-input-type attribute.
+ */
+addAccessibleTask(
+ `
+ <input id="default">
+ <input id="email" type="email">
+ <input id="password" type="password">
+ <input id="text" type="text">
+ <input id="date" type="date">
+ <input id="time" type="time">
+ <input id="checkbox" type="checkbox">
+ <input id="radio" type="radio">
+ `,
+ async function (browser, docAcc) {
+ function testInputType(id, inputType) {
+ if (inputType == undefined) {
+ testAbsentAttrs(findAccessibleChildByID(docAcc, id), {
+ "text-input-type": "",
+ });
+ } else {
+ testAttrs(
+ findAccessibleChildByID(docAcc, id),
+ { "text-input-type": inputType },
+ true
+ );
+ }
+ }
+
+ testInputType("default");
+ testInputType("email", "email");
+ testInputType("password", "password");
+ testInputType("text", "text");
+ testInputType("date", "date");
+ testInputType("time", "time");
+ testInputType("checkbox");
+ testInputType("radio");
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test caching of the display attribute.
+ */
+addAccessibleTask(
+ `
+<div id="div">
+ <ins id="ins">a</ins>
+ <button id="button">b</button>
+</div>
+<p>
+ <span id="presentationalSpan" role="none"
+ style="display: block; position: absolute; top: 0; left: 0; translate: 1px;">
+ a
+ </span>
+</p>
+ `,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ testAttrs(div, { display: "block" }, true);
+ const ins = findAccessibleChildByID(docAcc, "ins");
+ testAttrs(ins, { display: "inline" }, true);
+ const textLeaf = ins.firstChild;
+ testAbsentAttrs(textLeaf, { display: "" });
+ const button = findAccessibleChildByID(docAcc, "button");
+ testAttrs(button, { display: "inline-block" }, true);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("ins").style.display = "block";
+ content.document.body.offsetTop; // Flush layout.
+ });
+ await untilCacheIs(
+ () => ins.attributes.getStringProperty("display"),
+ "block",
+ "ins display attribute changed to block"
+ );
+
+ // This span has role="none", but we force a generic Accessible because it
+ // has a transform. role="none" might have been used to avoid exposing
+ // display: block, so ensure we don't expose that.
+ const presentationalSpan = findAccessibleChildByID(
+ docAcc,
+ "presentationalSpan"
+ );
+ testAbsentAttrs(presentationalSpan, { display: "" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that there is no display attribute on image map areas.
+ */
+addAccessibleTask(
+ `
+<map name="normalMap">
+ <area id="normalArea" shape="default">
+</map>
+<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#normalMap">
+<audio>
+ <map name="unslottedMap">
+ <area id="unslottedArea" shape="default">
+ </map>
+</audio>
+<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#unslottedMap">
+ `,
+ async function (browser, docAcc) {
+ const normalArea = findAccessibleChildByID(docAcc, "normalArea");
+ testAbsentAttrs(normalArea, { display: "" });
+ const unslottedArea = findAccessibleChildByID(docAcc, "unslottedArea");
+ testAbsentAttrs(unslottedArea, { display: "" });
+ },
+ { topLevel: true }
+);
+
+/**
+ * Test caching of the explicit-name attribute.
+ */
+addAccessibleTask(
+ `
+<h1 id="h1">content</h1>
+<button id="buttonContent">content</button>
+<button id="buttonLabel" aria-label="label">content</button>
+<button id="buttonEmpty"></button>
+<button id="buttonSummary"><details><summary>test</summary></details></button>
+<div id="div"></div>
+ `,
+ async function (browser, docAcc) {
+ const h1 = findAccessibleChildByID(docAcc, "h1");
+ testAbsentAttrs(h1, { "explicit-name": "" });
+ const buttonContent = findAccessibleChildByID(docAcc, "buttonContent");
+ testAbsentAttrs(buttonContent, { "explicit-name": "" });
+ const buttonLabel = findAccessibleChildByID(docAcc, "buttonLabel");
+ testAttrs(buttonLabel, { "explicit-name": "true" }, true);
+ const buttonEmpty = findAccessibleChildByID(docAcc, "buttonEmpty");
+ testAbsentAttrs(buttonEmpty, { "explicit-name": "" });
+ const buttonSummary = findAccessibleChildByID(docAcc, "buttonSummary");
+ testAbsentAttrs(buttonSummary, { "explicit-name": "" });
+ const div = findAccessibleChildByID(docAcc, "div");
+ testAbsentAttrs(div, { "explicit-name": "" });
+
+ info("Setting aria-label on h1");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, h1);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("h1").setAttribute("aria-label", "label");
+ });
+ await nameChanged;
+ testAttrs(h1, { "explicit-name": "true" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of ARIA attributes that are exposed via object attributes.
+ */
+addAccessibleTask(
+ `
+<div id="currentTrue" aria-current="true">currentTrue</div>
+<div id="currentFalse" aria-current="false">currentFalse</div>
+<div id="currentPage" aria-current="page">currentPage</div>
+<div id="currentBlah" aria-current="blah">currentBlah</div>
+<div id="haspopupMenu" aria-haspopup="menu">haspopup</div>
+<div id="rowColCountPositive" role="table" aria-rowcount="1000" aria-colcount="1000">
+ <div role="row">
+ <div id="rowColIndexPositive" role="cell" aria-rowindex="100" aria-colindex="100">positive</div>
+ </div>
+</div>
+<div id="rowColCountNegative" role="table" aria-rowcount="-1" aria-colcount="-1">
+ <div role="row">
+ <div id="rowColIndexNegative" role="cell" aria-rowindex="-1" aria-colindex="-1">negative</div>
+ </div>
+</div>
+<div id="rowColCountInvalid" role="table" aria-rowcount="z" aria-colcount="z">
+ <div role="row">
+ <div id="rowColIndexInvalid" role="cell" aria-rowindex="z" aria-colindex="z">invalid</div>
+ </div>
+</div>
+<div id="foo" aria-foo="bar">foo</div>
+<div id="mutate" aria-current="true">mutate</div>
+ `,
+ async function (browser, docAcc) {
+ const currentTrue = findAccessibleChildByID(docAcc, "currentTrue");
+ testAttrs(currentTrue, { current: "true" }, true);
+ const currentFalse = findAccessibleChildByID(docAcc, "currentFalse");
+ testAbsentAttrs(currentFalse, { current: "" });
+ const currentPage = findAccessibleChildByID(docAcc, "currentPage");
+ testAttrs(currentPage, { current: "page" }, true);
+ // Test that token normalization works.
+ const currentBlah = findAccessibleChildByID(docAcc, "currentBlah");
+ testAttrs(currentBlah, { current: "true" }, true);
+ const haspopupMenu = findAccessibleChildByID(docAcc, "haspopupMenu");
+ testAttrs(haspopupMenu, { haspopup: "menu" }, true);
+
+ // Test normalization of integer values.
+ const rowColCountPositive = findAccessibleChildByID(
+ docAcc,
+ "rowColCountPositive"
+ );
+ testAttrs(
+ rowColCountPositive,
+ { rowcount: "1000", colcount: "1000" },
+ true
+ );
+ const rowColIndexPositive = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexPositive"
+ );
+ testAttrs(rowColIndexPositive, { rowindex: "100", colindex: "100" }, true);
+ const rowColCountNegative = findAccessibleChildByID(
+ docAcc,
+ "rowColCountNegative"
+ );
+ testAttrs(rowColCountNegative, { rowcount: "-1", colcount: "-1" }, true);
+ const rowColIndexNegative = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexNegative"
+ );
+ testAbsentAttrs(rowColIndexNegative, { rowindex: "", colindex: "" });
+ const rowColCountInvalid = findAccessibleChildByID(
+ docAcc,
+ "rowColCountInvalid"
+ );
+ testAbsentAttrs(rowColCountInvalid, { rowcount: "", colcount: "" });
+ const rowColIndexInvalid = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexInvalid"
+ );
+ testAbsentAttrs(rowColIndexInvalid, { rowindex: "", colindex: "" });
+
+ // Test that unknown aria- attributes get exposed.
+ const foo = findAccessibleChildByID(docAcc, "foo");
+ testAttrs(foo, { foo: "bar" }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAttrs(mutate, { current: "true" }, true);
+ info("mutate: Removing aria-current");
+ let changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("aria-current");
+ });
+ await changed;
+ testAbsentAttrs(mutate, { current: "" });
+ info("mutate: Adding aria-current");
+ changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-current", "page");
+ });
+ await changed;
+ testAttrs(mutate, { current: "page" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test support for the xml-roles attribute.
+ */
+addAccessibleTask(
+ `
+<div id="knownRole" role="main">knownRole</div>
+<div id="emptyRole" role="">emptyRole</div>
+<div id="unknownRole" role="foo">unknownRole</div>
+<div id="multiRole" role="foo main">multiRole</div>
+<main id="landmarkMarkup">landmarkMarkup</main>
+<main id="landmarkMarkupWithRole" role="banner">landmarkMarkupWithRole</main>
+<main id="landmarkMarkupWithEmptyRole" role="">landmarkMarkupWithEmptyRole</main>
+<article id="markup">markup</article>
+<article id="markupWithRole" role="banner">markupWithRole</article>
+<article id="markupWithEmptyRole" role="">markupWithEmptyRole</article>
+ `,
+ async function (browser, docAcc) {
+ const knownRole = findAccessibleChildByID(docAcc, "knownRole");
+ testAttrs(knownRole, { "xml-roles": "main" }, true);
+ const emptyRole = findAccessibleChildByID(docAcc, "emptyRole");
+ testAbsentAttrs(emptyRole, { "xml-roles": "" });
+ const unknownRole = findAccessibleChildByID(docAcc, "unknownRole");
+ testAttrs(unknownRole, { "xml-roles": "foo" }, true);
+ const multiRole = findAccessibleChildByID(docAcc, "multiRole");
+ testAttrs(multiRole, { "xml-roles": "foo main" }, true);
+ const landmarkMarkup = findAccessibleChildByID(docAcc, "landmarkMarkup");
+ testAttrs(landmarkMarkup, { "xml-roles": "main" }, true);
+ const landmarkMarkupWithRole = findAccessibleChildByID(
+ docAcc,
+ "landmarkMarkupWithRole"
+ );
+ testAttrs(landmarkMarkupWithRole, { "xml-roles": "banner" }, true);
+ const landmarkMarkupWithEmptyRole = findAccessibleChildByID(
+ docAcc,
+ "landmarkMarkupWithEmptyRole"
+ );
+ testAttrs(landmarkMarkupWithEmptyRole, { "xml-roles": "main" }, true);
+ const markup = findAccessibleChildByID(docAcc, "markup");
+ testAttrs(markup, { "xml-roles": "article" }, true);
+ const markupWithRole = findAccessibleChildByID(docAcc, "markupWithRole");
+ testAttrs(markupWithRole, { "xml-roles": "banner" }, true);
+ const markupWithEmptyRole = findAccessibleChildByID(
+ docAcc,
+ "markupWithEmptyRole"
+ );
+ testAttrs(markupWithEmptyRole, { "xml-roles": "article" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test lie region attributes.
+ */
+addAccessibleTask(
+ `
+<div id="noLive"><p>noLive</p></div>
+<output id="liveMarkup"><p>liveMarkup</p></output>
+<div id="ariaLive" aria-live="polite"><p>ariaLive</p></div>
+<div id="liveRole" role="log"><p>liveRole</p></div>
+<div id="nonLiveRole" role="group"><p>nonLiveRole</p></div>
+<div id="other" aria-atomic="true" aria-busy="true" aria-relevant="additions"><p>other</p></div>
+ `,
+ async function (browser, docAcc) {
+ const noLive = findAccessibleChildByID(docAcc, "noLive");
+ for (const acc of [noLive, noLive.firstChild]) {
+ testAbsentAttrs(acc, {
+ live: "",
+ "container-live": "",
+ "container-live-role": "",
+ atomic: "",
+ "container-atomic": "",
+ busy: "",
+ "container-busy": "",
+ relevant: "",
+ "container-relevant": "",
+ });
+ }
+ const liveMarkup = findAccessibleChildByID(docAcc, "liveMarkup");
+ testAttrs(liveMarkup, { live: "polite" }, true);
+ testAttrs(liveMarkup.firstChild, { "container-live": "polite" }, true);
+ const ariaLive = findAccessibleChildByID(docAcc, "ariaLive");
+ testAttrs(ariaLive, { live: "polite" }, true);
+ testAttrs(ariaLive.firstChild, { "container-live": "polite" }, true);
+ const liveRole = findAccessibleChildByID(docAcc, "liveRole");
+ testAttrs(liveRole, { live: "polite" }, true);
+ testAttrs(
+ liveRole.firstChild,
+ { "container-live": "polite", "container-live-role": "log" },
+ true
+ );
+ const nonLiveRole = findAccessibleChildByID(docAcc, "nonLiveRole");
+ testAbsentAttrs(nonLiveRole, { live: "" });
+ testAbsentAttrs(nonLiveRole.firstChild, {
+ "container-live": "",
+ "container-live-role": "",
+ });
+ const other = findAccessibleChildByID(docAcc, "other");
+ testAttrs(
+ other,
+ { atomic: "true", busy: "true", relevant: "additions" },
+ true
+ );
+ testAttrs(
+ other.firstChild,
+ {
+ "container-atomic": "true",
+ "container-busy": "true",
+ "container-relevant": "additions",
+ },
+ true
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test the id attribute.
+ */
+addAccessibleTask(
+ `
+<p id="withId">withId</p>
+<div id="noIdParent"><p>noId</p></div>
+ `,
+ async function (browser, docAcc) {
+ const withId = findAccessibleChildByID(docAcc, "withId");
+ testAttrs(withId, { id: "withId" }, true);
+ const noId = findAccessibleChildByID(docAcc, "noIdParent").firstChild;
+ testAbsentAttrs(noId, { id: "" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test the valuetext attribute.
+ */
+addAccessibleTask(
+ `
+<div id="valuenow" role="slider" aria-valuenow="1"></div>
+<div id="valuetext" role="slider" aria-valuetext="text"></div>
+<div id="noValue" role="button"></div>
+ `,
+ async function (browser, docAcc) {
+ const valuenow = findAccessibleChildByID(docAcc, "valuenow");
+ testAttrs(valuenow, { valuetext: "1" }, true);
+ const valuetext = findAccessibleChildByID(docAcc, "valuetext");
+ testAttrs(valuetext, { valuetext: "text" }, true);
+ const noValue = findAccessibleChildByID(docAcc, "noValue");
+ testAbsentAttrs(noValue, { valuetext: "valuetext" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+function untilCacheAttrIs(acc, attr, val, msg) {
+ return untilCacheOk(() => {
+ try {
+ return acc.attributes.getStringProperty(attr) == val;
+ } catch (e) {
+ return false;
+ }
+ }, msg);
+}
+
+function untilCacheAttrAbsent(acc, attr, msg) {
+ return untilCacheOk(() => {
+ try {
+ acc.attributes.getStringProperty(attr);
+ } catch (e) {
+ return true;
+ }
+ return false;
+ }, msg);
+}
+
+/**
+ * Test the class attribute.
+ */
+addAccessibleTask(
+ `
+<div id="oneClass" class="c1">oneClass</div>
+<div id="multiClass" class="c1 c2">multiClass</div>
+<div id="noClass">noClass</div>
+<div id="mutate">mutate</div>
+ `,
+ async function (browser, docAcc) {
+ const oneClass = findAccessibleChildByID(docAcc, "oneClass");
+ testAttrs(oneClass, { class: "c1" }, true);
+ const multiClass = findAccessibleChildByID(docAcc, "multiClass");
+ testAttrs(multiClass, { class: "c1 c2" }, true);
+ const noClass = findAccessibleChildByID(docAcc, "noClass");
+ testAbsentAttrs(noClass, { class: "" });
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { class: "" });
+ info("Adding class to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").className = "c1 c2";
+ });
+ await untilCacheAttrIs(mutate, "class", "c1 c2", "mutate class correct");
+ info("Removing class from mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("class");
+ });
+ await untilCacheAttrAbsent(mutate, "class", "mutate class not present");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the src attribute.
+ */
+const kImgUrl = "https://example.com/a11y/accessible/tests/mochitest/moz.png";
+addAccessibleTask(
+ `
+<img id="noAlt" src="${kImgUrl}">
+<img id="alt" alt="alt" src="${kImgUrl}">
+<img id="mutate">
+ `,
+ async function (browser, docAcc) {
+ const noAlt = findAccessibleChildByID(docAcc, "noAlt");
+ testAttrs(noAlt, { src: kImgUrl }, true);
+ const alt = findAccessibleChildByID(docAcc, "alt");
+ testAttrs(alt, { src: kImgUrl }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { src: "" });
+ info("Adding src to mutate");
+ await invokeContentTask(browser, [kImgUrl], url => {
+ content.document.getElementById("mutate").src = url;
+ });
+ await untilCacheAttrIs(mutate, "src", kImgUrl, "mutate src correct");
+ info("Removing src from mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("src");
+ });
+ await untilCacheAttrAbsent(mutate, "src", "mutate src not present");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the placeholder attribute.
+ */
+addAccessibleTask(
+ `
+<input id="htmlWithLabel" aria-label="label" placeholder="HTML">
+<input id="htmlNoLabel" placeholder="HTML">
+<input id="ariaWithLabel" aria-label="label" aria-placeholder="ARIA">
+<input id="ariaNoLabel" aria-placeholder="ARIA">
+<input id="both" aria-label="label" placeholder="HTML" aria-placeholder="ARIA">
+<input id="mutate" placeholder="HTML">
+ `,
+ async function (browser, docAcc) {
+ const htmlWithLabel = findAccessibleChildByID(docAcc, "htmlWithLabel");
+ testAttrs(htmlWithLabel, { placeholder: "HTML" }, true);
+ const htmlNoLabel = findAccessibleChildByID(docAcc, "htmlNoLabel");
+ // placeholder is used as name, so not exposed as attribute.
+ testAbsentAttrs(htmlNoLabel, { placeholder: "" });
+ const ariaWithLabel = findAccessibleChildByID(docAcc, "ariaWithLabel");
+ testAttrs(ariaWithLabel, { placeholder: "ARIA" }, true);
+ const ariaNoLabel = findAccessibleChildByID(docAcc, "ariaNoLabel");
+ // No label doesn't impact aria-placeholder.
+ testAttrs(ariaNoLabel, { placeholder: "ARIA" }, true);
+ const both = findAccessibleChildByID(docAcc, "both");
+ testAttrs(both, { placeholder: "HTML" }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { placeholder: "" });
+ info("Adding label to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-label", "label");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "HTML",
+ "mutate placeholder correct"
+ );
+ info("Removing mutate placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("placeholder");
+ });
+ await untilCacheAttrAbsent(
+ mutate,
+ "placeholder",
+ "mutate placeholder not present"
+ );
+ info("Setting mutate aria-placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-placeholder", "ARIA");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "ARIA",
+ "mutate placeholder correct"
+ );
+ info("Setting mutate placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("placeholder", "HTML");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "HTML",
+ "mutate placeholder correct"
+ );
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the ispopup attribute.
+ */
+addAccessibleTask(
+ `<div id="popover" popover>popover</div>`,
+ async function testIspopup(browser, docAcc) {
+ info("Showing popover");
+ let shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("popover").showPopover();
+ });
+ let popover = (await shown).accessible;
+ testAttrs(popover, { ispopup: "auto" }, true);
+ info("Setting popover to null");
+ // Setting popover causes the Accessible to be recreated.
+ shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("popover").popover = null;
+ });
+ popover = (await shown).accessible;
+ testAbsentAttrs(popover, { ispopup: "" });
+ info("Setting popover to manual and showing");
+ shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ const popoverDom = content.document.getElementById("popover");
+ popoverDom.popover = "manual";
+ popoverDom.showPopover();
+ });
+ popover = (await shown).accessible;
+ testAttrs(popover, { ispopup: "manual" }, true);
+ },
+ { chrome: true, topLevel: 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..d489620e16
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_description.js
@@ -0,0 +1,280 @@
+/* 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_NAME_CHANGE, "image"]],
+ 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: "No description change when @alt is dropped but @aria-describedby remains",
+ attrs: [
+ {
+ attr: "alt",
+ },
+ ],
+ waitFor: [[EVENT_NAME_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: "title",
+ value: "title",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ 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_NAME_CHANGE, "image"]],
+ 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" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" />`,
+ 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 }
+);
+
+/**
+ * Test that the description is updated when the content of a hidden aria-describedby
+ * subtree changes.
+ */
+addAccessibleTask(
+ `
+<button id="button" aria-describedby="desc">
+<div id="desc" hidden>a</div>
+ `,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testDescr(button, "a");
+ info("Changing desc textContent");
+ let descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("desc").textContent = "c";
+ });
+ await descChanged;
+ testDescr(button, "c");
+ info("Prepending text node to desc");
+ descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("desc")
+ .prepend(content.document.createTextNode("b"));
+ });
+ await descChanged;
+ testDescr(button, "bc");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test aria-description, including mutations.
+ */
+addAccessibleTask(
+ `<button id="button" aria-description="a">button</button>`,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testDescr(button, "a");
+ info("Changing aria-description");
+ let changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description", "b");
+ await changed;
+ testDescr(button, "b");
+ info("Removing aria-description");
+ changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description");
+ await changed;
+ testDescr(button, "");
+ info("Setting aria-description");
+ changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description", "c");
+ await changed;
+ testDescr(button, "c");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_document_props.js b/accessible/tests/browser/e10s/browser_caching_document_props.js
new file mode 100644
index 0000000000..cf3a4e5721
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_document_props.js
@@ -0,0 +1,80 @@
+/* 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, docAcc) {
+ info("Testing top level doc");
+ queryInterfaces(docAcc, [nsIAccessibleDocument]);
+ const topUrl =
+ (browser.isRemoteBrowser ? CURRENT_CONTENT_DIR : CURRENT_DIR) +
+ "e10s/doc_treeupdate_whitespace.html";
+ is(docAcc.URL, topUrl, "Initial URL correct");
+ is(docAcc.mimeType, "text/html", "Mime type is correct");
+ info("Changing URL");
+ await invokeContentTask(browser, [], () => {
+ content.history.pushState(
+ null,
+ "",
+ content.document.location.href + "/after"
+ );
+ });
+ is(docAcc.URL, topUrl + "/after", "URL correct after change");
+
+ // We can't use the harness to manage iframes for us because it uses data
+ // URIs for in-process iframes, but data URIs don't support
+ // history.pushState.
+
+ async function testIframe() {
+ queryInterfaces(iframeDocAcc, [nsIAccessibleDocument]);
+ is(iframeDocAcc.URL, src, "Initial URL correct");
+ is(iframeDocAcc.mimeType, "text/html", "Mime type is correct");
+ info("Changing URL");
+ await invokeContentTask(browser, [], async () => {
+ await SpecialPowers.spawn(content.iframe, [], () => {
+ content.history.pushState(
+ null,
+ "",
+ content.document.location.href + "/after"
+ );
+ });
+ });
+ is(iframeDocAcc.URL, src + "/after", "URL correct after change");
+ }
+
+ info("Testing same origin (in-process) iframe");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let src = "https://example.com/initial.html";
+ let loaded = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ evt => evt.accessible.parent.parent == docAcc
+ );
+ await invokeContentTask(browser, [src], cSrc => {
+ content.iframe = content.document.createElement("iframe");
+ content.iframe.src = cSrc;
+ content.document.body.append(content.iframe);
+ });
+ let iframeDocAcc = (await loaded).accessible;
+ await testIframe();
+
+ info("Testing different origin (out-of-process) iframe");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ src = "https://example.net/initial.html";
+ loaded = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ evt => evt.accessible.parent.parent == docAcc
+ );
+ await invokeContentTask(browser, [src], cSrc => {
+ content.iframe.src = cSrc;
+ });
+ iframeDocAcc = (await await loaded).accessible;
+ await testIframe();
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_domnodeid.js b/accessible/tests/browser/e10s/browser_caching_domnodeid.js
new file mode 100644
index 0000000000..722cc9a970
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_domnodeid.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";
+
+/**
+ * Test DOM ID caching on remotes.
+ */
+addAccessibleTask(
+ '<div id="div"></div>',
+ async function (browser, accDoc) {
+ const div = findAccessibleChildByID(accDoc, "div");
+ ok(div, "Got accessible with 'div' ID.");
+
+ let contentPromise = invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").id = "foo";
+ });
+ // We don't await for content task to return because we want to exercise the
+ // untilCacheIs function and demonstrate that it can await for a passing
+ // `is` test.
+ await untilCacheIs(
+ () => div.id,
+ "foo",
+ "ID is correct and updated in cache"
+ );
+
+ // Don't leave test without the content task promise resolved.
+ await contentPromise;
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_hyperlink.js b/accessible/tests/browser/e10s/browser_caching_hyperlink.js
new file mode 100644
index 0000000000..4b3f8a1bda
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_hyperlink.js
@@ -0,0 +1,228 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testLinkIndexAtOffset(id, offset, index) {
+ let htAcc = getAccessible(id, [nsIAccessibleHyperText]);
+ is(
+ htAcc.getLinkIndexAtOffset(offset),
+ index,
+ "Wrong link index at offset " + offset + " for ID " + id + "!"
+ );
+}
+
+function testThis(
+ paragraph,
+ docURI,
+ id,
+ charIndex,
+ expectedLinkIndex,
+ expectedAnchors,
+ expectedURIs,
+ valid = true
+) {
+ testLinkIndexAtOffset(paragraph, charIndex, expectedLinkIndex);
+
+ let linkAcc = paragraph.getLinkAt(expectedLinkIndex);
+ ok(linkAcc, "No accessible for link " + id + "!");
+
+ is(linkAcc.valid, valid, `${id} is valid.`);
+
+ let linkIndex = paragraph.getLinkIndex(linkAcc);
+ is(linkIndex, expectedLinkIndex, "Wrong link index for " + id + "!");
+
+ is(linkAcc.anchorCount, expectedAnchors.length, "Correct number of anchors");
+ for (let i = 0; i < expectedAnchors.length; i++) {
+ let uri = linkAcc.getURI(i);
+ is(
+ (uri ? uri.spec : "").replace(docURI, ""),
+ expectedURIs[i],
+ `Wrong anchor URI at ${i} for "${id}"`
+ );
+ is(
+ getAccessibleDOMNodeID(linkAcc.getAnchor(i)),
+ expectedAnchors[i],
+ `Wrong anchor at ${i} for "${id}"`
+ );
+ }
+}
+
+/**
+ * Test hyperlinks
+ */
+addAccessibleTask(
+ `
+ <p id="testParagraph"><br
+ >Simple link:<br
+ ><a id="NormalHyperlink" href="https://www.mozilla.org">Mozilla Foundation</a><br
+ >ARIA link:<br
+ ><span id="AriaHyperlink" role="link"
+ onclick="window.open('https://www.mozilla.org/');"
+ tabindex="0">Mozilla Foundation Home</span><br
+ >Invalid, non-focusable hyperlink:<br
+ ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true"
+ onclick="window.open('https:/www.mozilla.org/');">Invalid link</span><br
+ >Image map:<br
+ ><map name="atoz_map"><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14"
+ id="b"
+ shape="rect"></area
+ ><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14"
+ id="a"
+ shape="rect"></area></map
+ ><img width="447" id="imgmap"
+ height="15"
+ usemap="#atoz_map"
+ src="../letters.gif"></img><br
+ >Empty link:<br
+ ><a id="emptyLink" href=""><img src=""></img></a><br
+ >Link with embedded span<br
+ ><a id="LinkWithSpan" href="https://www.heise.de/"><span lang="de">Heise Online</span></a><br
+ >Named anchor, must not have "linked" state for it to be exposed correctly:<br
+ ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a>
+ </p>
+ `,
+ function (browser, accDoc) {
+ const paragraph = findAccessibleChildByID(accDoc, "testParagraph", [
+ nsIAccessibleHyperText,
+ ]);
+ is(paragraph.linkCount, 7, "Wrong link count for paragraph!");
+
+ const docURI = accDoc.URL;
+ // normal hyperlink
+ testThis(
+ paragraph,
+ docURI,
+ "NormalHyperlink",
+ 14,
+ 0,
+ ["NormalHyperlink"],
+ ["https://www.mozilla.org/"]
+ );
+
+ // ARIA hyperlink
+ testThis(
+ paragraph,
+ docURI,
+ "AriaHyperlink",
+ 27,
+ 1,
+ ["AriaHyperlink"],
+ [""]
+ );
+
+ // ARIA hyperlink with status invalid
+ testThis(
+ paragraph,
+ docURI,
+ "InvalidAriaHyperlink",
+ 63,
+ 2,
+ ["InvalidAriaHyperlink"],
+ [""],
+ false
+ );
+
+ // image map, but not its link children. They are not part of hypertext.
+ testThis(
+ paragraph,
+ docURI,
+ "imgmap",
+ 76,
+ 3,
+ ["b", "a"],
+ [
+ "https://www.bbc.co.uk/radio4/atoz/index.shtml#b",
+ "https://www.bbc.co.uk/radio4/atoz/index.shtml#a",
+ ]
+ );
+
+ // empty hyperlink
+ testThis(paragraph, docURI, "emptyLink", 90, 4, ["emptyLink"], [""]);
+
+ // normal hyperlink with embedded span
+ testThis(
+ paragraph,
+ docURI,
+ "LinkWithSpan",
+ 116,
+ 5,
+ ["LinkWithSpan"],
+ ["https://www.heise.de/"]
+ );
+
+ // Named anchor
+ testThis(paragraph, docURI, "namedAnchor", 193, 6, ["namedAnchor"], [""]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test paragraph with link
+ */
+addAccessibleTask(
+ `
+ <p id="p"><a href="http://mozilla.org">mozilla.org</a></p>
+ `,
+ function (browser, accDoc) {
+ // Paragraph with link
+ const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]);
+ const link = p.getLinkAt(0);
+ is(link, p.getChildAt(0), "Wrong link for p2");
+ is(p.linkCount, 1, "Wrong link count for p2");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test paragraph with link
+ */
+addAccessibleTask(
+ `
+ <p id="p"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p>
+ `,
+ function (browser, accDoc) {
+ // Paragraph with link
+ const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]);
+
+ // getLinkIndexAtOffset, causes the offsets to be cached;
+ testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset(p, 9, 2); // the end, latest link
+
+ // the second pass to make sure link indexes are calculated propertly from
+ // cached offsets.
+ testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset(p, 9, 2); // the end, latest link
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_innerHTML.js b/accessible/tests/browser/e10s/browser_caching_innerHTML.js
new file mode 100644
index 0000000000..7baee32e26
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_innerHTML.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";
+
+/**
+ * Test caching of innerHTML on math elements for Windows clients.
+ */
+addAccessibleTask(
+ `
+<p id="p">test</p>
+<math id="math"><mfrac><mi>x</mi><mi>y</mi></mfrac></math>
+ `,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ let hasHtml;
+ try {
+ p.cache.getStringProperty("html");
+ hasHtml = true;
+ } catch (e) {
+ hasHtml = false;
+ }
+ ok(!hasHtml, "p doesn't have cached html");
+
+ const math = findAccessibleChildByID(docAcc, "math");
+ is(
+ math.cache.getStringProperty("html"),
+ "<mfrac><mi>x</mi><mi>y</mi></mfrac>",
+ "math cached html is correct"
+ );
+
+ info("Mutating math");
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelectorAll("mi")[1].textContent = "z";
+ });
+ await untilCacheIs(
+ () => math.cache.getStringProperty("html"),
+ "<mfrac><mi>x</mi><mi>z</mi></mfrac>",
+ "math cached html is correct after mutation"
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_interfaces.js b/accessible/tests/browser/e10s/browser_caching_interfaces.js
new file mode 100644
index 0000000000..c83d486bc6
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_interfaces.js
@@ -0,0 +1,59 @@
+/* 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 caching of accessible interfaces
+ */
+addAccessibleTask(
+ `
+ <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ <select id="select" multiple></select>
+ <input id="number-input" type="number">
+ <table id="table">
+ <tr><td id="cell"><a id="link" href="#">hello</a></td></tr>
+ </table>
+ `,
+ async function (browser, accDoc) {
+ ok(
+ accDoc instanceof nsIAccessibleDocument,
+ "Document has Document interface"
+ );
+ ok(
+ accDoc instanceof nsIAccessibleHyperText,
+ "Document has HyperText interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "img") instanceof nsIAccessibleImage,
+ "img has Image interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "select") instanceof
+ nsIAccessibleSelectable,
+ "select has Selectable interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "number-input") instanceof
+ nsIAccessibleValue,
+ "number-input has Value interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "table") instanceof nsIAccessibleTable,
+ "table has Table interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "cell") instanceof nsIAccessibleTableCell,
+ "cell has TableCell interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperLink,
+ "link has HyperLink interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperText,
+ "link has HyperText interface"
+ );
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_large_update.js b/accessible/tests/browser/e10s/browser_caching_large_update.js
new file mode 100644
index 0000000000..ccf8a86921
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_large_update.js
@@ -0,0 +1,66 @@
+/* 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 a large update which adds many thousands of Accessibles with a
+ * lot of content in each.
+ */
+addAccessibleTask(
+ `<main id="main" hidden></main>`,
+ async function (browser, docAcc) {
+ let shown = waitForEvent(EVENT_SHOW, "main");
+ await invokeContentTask(browser, [], () => {
+ // Make a long string.
+ let text = "";
+ for (let i = 0; i < 100; ++i) {
+ text += "a";
+ }
+ // Create lots of nodes which include the long string.
+ const contMain = content.document.getElementById("main");
+ // 15000 children of main.
+ for (let w = 0; w < 15000; ++w) {
+ // Each of those goes 9 deep.
+ let parent = contMain;
+ for (let d = 0; d < 10; ++d) {
+ const div = content.document.createElement("div");
+ div.setAttribute("aria-label", `${w} ${d} ${text}`);
+ parent.append(div);
+ parent = div;
+ }
+ }
+ contMain.hidden = false;
+ });
+ const main = (await shown).accessible;
+ is(main.childCount, 15000, "main has correct number of children");
+
+ // We don't want to output passes for every check, since that would output
+ // hundreds of thousands of lines, which slows the test to a crawl. Instead,
+ // output any failures and keep track of overall success/failure.
+ let treeOk = true;
+ function check(val, msg) {
+ if (!val) {
+ ok(false, msg);
+ treeOk = false;
+ }
+ }
+
+ info("Checking tree");
+ for (let w = 0; w < 15000; ++w) {
+ let acc = main.getChildAt(w);
+ let parent = main;
+ for (let d = 0; d < 10; ++d) {
+ check(acc, `Got child ${w} depth ${d}`);
+ const name = `${w} ${d}`;
+ check(acc.name.startsWith(name + " "), `${name}: correct name`);
+ check(acc.parent == parent, `${name}: correct parent`);
+ parent = acc;
+ acc = acc.firstChild;
+ }
+ }
+ // check() sets treeOk to false for any failure.
+ ok(treeOk, "Tree is correct");
+ }
+);
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..55f506b85a
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_name.js
@@ -0,0 +1,542 @@
+/* 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";
+requestLongerTimeout(2);
+
+/* 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
+ *
+ */
+const ARIARule = [{ attr: "aria-labelledby" }, { attr: "aria-label" }];
+const HTMLControlHeadRule = [...ARIARule, { elm: "label" }];
+const rules = {
+ CSSContent: [{ elm: "style" }, { fromsubtree: true }],
+ HTMLARIAGridCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLControl: [
+ ...HTMLControlHeadRule,
+ { fromsubtree: true },
+ { attr: "title" },
+ ],
+ HTMLElm: [...ARIARule, { attr: "title" }],
+ HTMLImg: [...ARIARule, { attr: "alt" }, { attr: "title" }],
+ HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }],
+ HTMLInputButton: [
+ ...HTMLControlHeadRule,
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImage: [
+ ...HTMLControlHeadRule,
+ { attr: "alt" },
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImageNoValidSrc: [
+ ...HTMLControlHeadRule,
+ { attr: "alt" },
+ { attr: "value" },
+ ],
+ HTMLInputReset: [...HTMLControlHeadRule, { attr: "value" }],
+ HTMLInputSubmit: [...HTMLControlHeadRule, { attr: "value" }],
+ HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLLinkImage: [...ARIARule, { fromsubtree: true }, { 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: "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"
+ href=""
+ 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"
+ href=""
+ 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, id } structure that contains an
+ * accessible 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, acc } = target;
+ let { attr } = rule;
+
+ testName(acc, expected);
+
+ let nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+ await invokeContentTask(browser, [id, attr], (contentId, contentAttr) => {
+ content.document.getElementById(contentId).removeAttribute(contentAttr);
+ });
+ let event = await nameChange;
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * 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, id } structure that contains an
+ * accessible 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, acc } = target;
+ let { elm } = rule;
+
+ testName(acc, expected);
+ let nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+
+ await invokeContentTask(browser, [elm], contentElm => {
+ content.document.querySelector(`${contentElm}`).remove();
+ });
+ let event = await nameChange;
+
+ // 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, id } structure that contains an
+ * accessible 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 nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+
+ await invokeContentTask(browser, [id], contentId => {
+ let elm = content.document.getElementById(contentId);
+ while (elm.firstChild) {
+ elm.firstChild.remove();
+ }
+ });
+ let event = await nameChange;
+
+ // 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, id } structure that contains an
+ * accessible 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);
+ let target = { id, acc };
+ await testNameRule(browser, target, rules[ruleset], expected);
+ Services.obs.removeObserver(observer, "accessible-event");
+ },
+ { iframe: true, remoteIframe: true }
+ )
+);
+
+/**
+ * Test caching of the document title.
+ */
+addAccessibleTask(
+ ``,
+ async function (browser, docAcc) {
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, docAcc);
+ await invokeContentTask(browser, [], () => {
+ content.document.title = "new title";
+ });
+ await nameChanged;
+ testName(docAcc, "new title");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the name is updated when the content of a hidden aria-labelledby
+ * subtree changes.
+ */
+addAccessibleTask(
+ `
+<button id="button" aria-labelledby="label">
+<div id="label" hidden>a</div>
+ `,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testName(button, "a");
+ info("Changing label textContent");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label").textContent = "c";
+ });
+ await nameChanged;
+ testName(button, "c");
+ info("Prepending text node to label");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("label")
+ .prepend(content.document.createTextNode("b"));
+ });
+ await nameChanged;
+ testName(button, "bc");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_position.js b/accessible/tests/browser/e10s/browser_caching_position.js
new file mode 100644
index 0000000000..5de246bb91
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_position.js
@@ -0,0 +1,194 @@
+/* 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 });
+
+function getCachedBounds(acc) {
+ let cachedBounds = "";
+ try {
+ cachedBounds = acc.cache.getStringProperty("relative-bounds");
+ } catch (e) {
+ ok(false, "Unable to fetch cached bounds from cache!");
+ }
+ return cachedBounds;
+}
+
+async function testCoordinates(accDoc, id, expectedWidthPx, expectedHeightPx) {
+ let acc = findAccessibleChildByID(accDoc, id, [Ci.nsIAccessibleImage]);
+ if (!acc) {
+ return;
+ }
+
+ let screenX = {};
+ let screenY = {};
+ let windowX = {};
+ let windowY = {};
+ let parentX = {};
+ let parentY = {};
+
+ // get screen coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE,
+ screenX,
+ screenY
+ );
+ // get window coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE,
+ windowX,
+ windowY
+ );
+ // get parent related coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE,
+ parentX,
+ parentY
+ );
+ // XXX For linked images, a negative parentY value is returned, and the
+ // screenY coordinate is the link's screenY coordinate minus 1.
+ // Until this is fixed, set parentY to -1 if it's negative.
+ if (parentY.value < 0) {
+ parentY.value = -1;
+ }
+
+ // See if asking image for child at image's screen coordinates gives
+ // correct accessible. getChildAtPoint operates on screen coordinates.
+ let tempAcc = null;
+ try {
+ tempAcc = acc.getChildAtPoint(screenX.value, screenY.value);
+ } catch (e) {}
+ is(tempAcc, acc, "Wrong accessible returned for position of " + id + "!");
+
+ // get image's parent.
+ let imageParentAcc = null;
+ try {
+ imageParentAcc = acc.parent;
+ } catch (e) {}
+ ok(imageParentAcc, "no parent accessible for " + id + "!");
+
+ if (imageParentAcc) {
+ // See if parent's screen coordinates plus image's parent relative
+ // coordinates equal to image's screen coordinates.
+ let parentAccX = {};
+ let parentAccY = {};
+ let parentAccWidth = {};
+ let parentAccHeight = {};
+ imageParentAcc.getBounds(
+ parentAccX,
+ parentAccY,
+ parentAccWidth,
+ parentAccHeight
+ );
+ is(
+ parentAccX.value + parentX.value,
+ screenX.value,
+ "Wrong screen x coordinate for " + id + "!"
+ );
+ // XXX see bug 456344
+ // is(
+ // parentAccY.value + parentY.value,
+ // screenY.value,
+ // "Wrong screen y coordinate for " + id + "!"
+ // );
+ }
+
+ let [expectedW, expectedH] = CSSToDevicePixels(
+ window,
+ expectedWidthPx,
+ expectedHeightPx
+ );
+ let width = {};
+ let height = {};
+ acc.getImageSize(width, height);
+ is(width.value, expectedW, "Wrong width for " + id + "!");
+ is(height.value, expectedH, "wrong height for " + id + "!");
+}
+
+addAccessibleTask(
+ `
+ <br>Simple image:<br>
+ <img id="nonLinkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>
+ <br>Linked image:<br>
+ <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a>
+ <br>Image with longdesc:<br>
+ <img id="longdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with invalid url in longdesc:<br>
+ <img id="invalidLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with click and longdesc:<br>
+ <img id="clickAndLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html"
+ alt="Another image of Mozilla logo" onclick="alert('Clicked!');"/>
+
+ <br>image described by a link to be treated as longdesc<br>
+ <img id="longdesc2" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link" href="longdesc_src.html">link to description of image</a>
+
+ <br>Image described by a link to be treated as longdesc with whitespaces<br>
+ <img id="longdesc3" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link2"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link2" href="longdesc src.html">link to description of image</a>
+
+ <br>Image with click:<br>
+ <img id="click" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ alt="A third image of Mozilla logo" onclick="alert('Clicked, too!');"/>
+ `,
+ async function (browser, docAcc) {
+ // Test non-linked image
+ await testCoordinates(docAcc, "nonLinkedImage", 89, 38);
+
+ // Test linked image
+ await testCoordinates(docAcc, "linkedImage", 89, 38);
+
+ // Image with long desc
+ await testCoordinates(docAcc, "longdesc", 89, 38);
+
+ // Image with invalid url in long desc
+ await testCoordinates(docAcc, "invalidLongdesc", 89, 38);
+
+ // Image with click and long desc
+ await testCoordinates(docAcc, "clickAndLongdesc", 89, 38);
+
+ // Image with click
+ await testCoordinates(docAcc, "click", 89, 38);
+
+ // Image with long desc
+ await testCoordinates(docAcc, "longdesc2", 89, 38);
+
+ // Image described by HTML:a@href with whitespaces
+ await testCoordinates(docAcc, "longdesc3", 89, 38);
+ }
+);
+
+addAccessibleTask(
+ `
+ <br>Linked image:<br>
+ <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a>
+ `,
+ async function (browser, docAcc) {
+ const imgAcc = findAccessibleChildByID(docAcc, "linkedImage", [
+ Ci.nsIAccessibleImage,
+ ]);
+ const origCachedBounds = getCachedBounds(imgAcc);
+
+ await invokeContentTask(browser, [], () => {
+ const imgNode = content.document.getElementById("linkedImage");
+ imgNode.style = "margin-left: 1000px; margin-top: 500px;";
+ });
+
+ await untilCacheOk(() => {
+ return origCachedBounds != getCachedBounds(imgAcc);
+ }, "Cached bounds update after mutation");
+ },
+ {
+ // We can only access the `cache` attribute of an accessible when
+ // the cache is enabled and we're in a remote browser.
+ topLevel: true,
+ iframe: 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..cc83930830
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_relations.js
@@ -0,0 +1,291 @@
+/* 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";
+requestLongerTimeout(2);
+
+/**
+ * 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],
+ ["aria-details", RELATION_DETAILS, RELATION_DETAILS_FOR],
+ ["aria-errormessage", RELATION_ERRORMSG, RELATION_ERRORMSG_FOR],
+];
+
+/**
+ * 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 }
+);
+
+/**
+ * Test caching of relations with respect to label objects and their "for" attr.
+ */
+addAccessibleTask(
+ `
+ <input type="checkbox" id="dependant1">
+ <input type="checkbox" id="dependant2">
+ <label id="host">label</label>`,
+ async function (browser, accDoc) {
+ await testRelated(
+ browser,
+ accDoc,
+ "for",
+ RELATION_LABEL_FOR,
+ RELATION_LABELLED_BY
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rel caching for element with existing relation attribute.
+ */
+addAccessibleTask(
+ `<div id="label">label</div><button id="button" aria-labelledby="label">`,
+ async function (browser, accDoc) {
+ const button = findAccessibleChildByID(accDoc, "button");
+ const label = findAccessibleChildByID(accDoc, "label");
+
+ await testCachedRelation(button, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, button);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of relations with respect to output objects and their "for" attr.
+ */
+addAccessibleTask(
+ `
+ <form oninput="host.value=parseInt(dependant1.value)+parseInt(dependant2.value)">
+ <input type="number" id="dependant1" value="50"> +
+ <input type="number" id="dependant2" value="25"> =
+ <output name="host" id="host"></output>
+ </form>`,
+ async function (browser, accDoc) {
+ await testRelated(
+ browser,
+ accDoc,
+ "for",
+ RELATION_CONTROLLED_BY,
+ RELATION_CONTROLLER_FOR
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rel caching for <label> element with existing "for" attribute.
+ */
+addAccessibleTask(
+ `data:text/html,<label id="label" for="input">label</label><input id="input">`,
+ async function (browser, accDoc) {
+ const input = findAccessibleChildByID(accDoc, "input");
+ const label = findAccessibleChildByID(accDoc, "label");
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test caching of relations with respect to label objects that are ancestors of
+ * their target.
+ */
+addAccessibleTask(
+ `
+ <label id="host">
+ <input type="checkbox" id="dependant1">
+ </label>`,
+ async function (browser, accDoc) {
+ const input = findAccessibleChildByID(accDoc, "dependant1");
+ const label = findAccessibleChildByID(accDoc, "host");
+
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test EMBEDS on root accessible.
+ */
+addAccessibleTask(
+ `hello world`,
+ async function (browser, primaryDocAcc, secondaryDocAcc) {
+ // The root accessible should EMBED the top level
+ // content document. If this test runs in an iframe,
+ // the test harness will pass in doc accs for both the
+ // iframe (primaryDocAcc) and the top level remote
+ // browser (secondaryDocAcc). We should use the second
+ // one.
+ // If this is not in an iframe, we'll only get
+ // a single docAcc (primaryDocAcc) which refers to
+ // the top level content doc.
+ const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc;
+ await testRelation(
+ getRootAccessible(document),
+ RELATION_EMBEDS,
+ topLevelDoc
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test CONTAINING_TAB_PANE
+ */
+addAccessibleTask(
+ `<p id="p">hello world</p>`,
+ async function (browser, primaryDocAcc, secondaryDocAcc) {
+ // The CONTAINING_TAB_PANE of any acc should be the top level
+ // content document. If this test runs in an iframe,
+ // the test harness will pass in doc accs for both the
+ // iframe (primaryDocAcc) and the top level remote
+ // browser (secondaryDocAcc). We should use the second
+ // one.
+ // If this is not in an iframe, we'll only get
+ // a single docAcc (primaryDocAcc) which refers to
+ // the top level content doc.
+ const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc;
+ await testCachedRelation(
+ findAccessibleChildByID(primaryDocAcc, "p"),
+ RELATION_CONTAINING_TAB_PANE,
+ topLevelDoc
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test relation caching on link
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="#item">a</a>
+ <div id="item">hello</div>
+ <div id="item2">world</div>
+ <a id="link2" href="#anchor">b</a>
+ <a id="namedLink" name="anchor">c</a>`,
+ async function (browser, accDoc) {
+ const link = findAccessibleChildByID(accDoc, "link");
+ const link2 = findAccessibleChildByID(accDoc, "link2");
+ const namedLink = findAccessibleChildByID(accDoc, "namedLink");
+ const item = findAccessibleChildByID(accDoc, "item");
+ const item2 = findAccessibleChildByID(accDoc, "item2");
+
+ await testCachedRelation(link, RELATION_LINKS_TO, item);
+ await testCachedRelation(link2, RELATION_LINKS_TO, namedLink);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").href = "";
+ content.document.getElementById("namedLink").name = "newName";
+ });
+
+ await testCachedRelation(link, RELATION_LINKS_TO, null);
+ await testCachedRelation(link2, RELATION_LINKS_TO, null);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").href = "#item2";
+ });
+
+ await testCachedRelation(link, RELATION_LINKS_TO, item2);
+ },
+ {
+ chrome: true,
+ // IA2 doesn't have a LINKS_TO relation and Windows non-cached
+ // RemoteAccessible uses IA2, so we can't run these tests in this case.
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria trees.
+ */
+addAccessibleTask(
+ `
+ <div role="tree" id="tree">
+ <div role="treeitem" id="treeitem">test</div>
+ <div role="treeitem" id="treeitem2">test</div>
+ </div>`,
+ async function (browser, accDoc) {
+ const tree = findAccessibleChildByID(accDoc, "tree");
+ const treeItem = findAccessibleChildByID(accDoc, "treeitem");
+ const treeItem2 = findAccessibleChildByID(accDoc, "treeitem2");
+
+ await testCachedRelation(tree, RELATION_NODE_PARENT_OF, [
+ treeItem,
+ treeItem2,
+ ]);
+ await testCachedRelation(treeItem, RELATION_NODE_CHILD_OF, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria lists.
+ */
+addAccessibleTask(
+ `
+ <div id="l1" role="list">
+ <div id="l1i1" role="listitem" aria-level="1">a</div>
+ <div id="l1i2" role="listitem" aria-level="2">b</div>
+ <div id="l1i3" role="listitem" aria-level="1">c</div>
+ </div>`,
+ async function (browser, accDoc) {
+ const list = findAccessibleChildByID(accDoc, "l1");
+ const listItem1 = findAccessibleChildByID(accDoc, "l1i1");
+ const listItem2 = findAccessibleChildByID(accDoc, "l1i2");
+ const listItem3 = findAccessibleChildByID(accDoc, "l1i3");
+
+ await testCachedRelation(list, RELATION_NODE_PARENT_OF, [
+ listItem1,
+ listItem3,
+ ]);
+ await testCachedRelation(listItem1, RELATION_NODE_CHILD_OF, list);
+ await testCachedRelation(listItem3, RELATION_NODE_CHILD_OF, list);
+
+ await testCachedRelation(listItem1, RELATION_NODE_PARENT_OF, listItem2);
+ await testCachedRelation(listItem2, RELATION_NODE_CHILD_OF, listItem1);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test NODE_CHILD_OF relation caching for JAWS window emulation special case.
+ */
+addAccessibleTask(
+ ``,
+ async function (browser, accDoc) {
+ await testCachedRelation(accDoc, RELATION_NODE_CHILD_OF, accDoc.parent);
+ },
+ { topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_relations_002.js b/accessible/tests/browser/e10s/browser_caching_relations_002.js
new file mode 100644
index 0000000000..072656eb5e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_relations_002.js
@@ -0,0 +1,366 @@
+/* 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";
+requestLongerTimeout(2);
+
+/**
+ * Test MEMBER_OF relation caching on HTML radio buttons
+ */
+addAccessibleTask(
+ `
+ <input type="radio" id="r1">I have no name<br>
+ <input type="radio" id="r2">I also have no name<br>
+ <input type="radio" id="r3" name="n">I have a name<br>
+ <input type="radio" id="r4" name="a">I have a different name<br>
+ <fieldset role="radiogroup">
+ <input type="radio" id="r5" name="n">I have an already used name
+ and am in a different part of the tree
+ <input type="radio" id="r6" name="r">I have a different name but am
+ in the same group
+ </fieldset>`,
+ async function (browser, accDoc) {
+ const r1 = findAccessibleChildByID(accDoc, "r1");
+ const r2 = findAccessibleChildByID(accDoc, "r2");
+ const r3 = findAccessibleChildByID(accDoc, "r3");
+ const r4 = findAccessibleChildByID(accDoc, "r4");
+ const r5 = findAccessibleChildByID(accDoc, "r5");
+ const r6 = findAccessibleChildByID(accDoc, "r6");
+
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, [r3, r5]);
+ await testCachedRelation(r4, RELATION_MEMBER_OF, r4);
+ await testCachedRelation(r5, RELATION_MEMBER_OF, [r3, r5]);
+ await testCachedRelation(r6, RELATION_MEMBER_OF, r6);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("r5").name = "a";
+ });
+
+ await testCachedRelation(r3, RELATION_MEMBER_OF, r3);
+ await testCachedRelation(r4, RELATION_MEMBER_OF, [r5, r4]);
+ await testCachedRelation(r5, RELATION_MEMBER_OF, [r5, r4]);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test MEMBER_OF relation caching on aria radio buttons
+ */
+addAccessibleTask(
+ `
+ <div role="radio" id="r1">I have no radio group</div><br>
+ <fieldset role="radiogroup" id="fs">
+ <div role="radio" id="r2">hello</div><br>
+ <div role="radio" id="r3">world</div><br>
+ </fieldset>`,
+ async function (browser, accDoc) {
+ const r1 = findAccessibleChildByID(accDoc, "r1");
+ const r2 = findAccessibleChildByID(accDoc, "r2");
+ let r3 = findAccessibleChildByID(accDoc, "r3");
+
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, [r2, r3]);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, [r2, r3]);
+ const r = waitForEvent(EVENT_INNER_REORDER, "fs");
+ await invokeContentTask(browser, [], () => {
+ let innerRadio = content.document.getElementById("r3");
+ content.document.body.appendChild(innerRadio);
+ });
+ await r;
+
+ r3 = findAccessibleChildByID(accDoc, "r3");
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, r2);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, null);
+ },
+ {
+ chrome: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via accessible shutdown.
+ */
+addAccessibleTask(
+ `
+ <div id="d"></div>
+ <label id="l">
+ <select id="s">
+ `,
+ async function (browser, accDoc) {
+ const label = findAccessibleChildByID(accDoc, "l");
+ const select = findAccessibleChildByID(accDoc, "s");
+ const div = findAccessibleChildByID(accDoc, "d");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, select);
+ await testCachedRelation(select, RELATION_LABELLED_BY, label);
+ await testCachedRelation(div, RELATION_LABELLED_BY, null);
+
+ const r = waitForEvent(EVENT_REORDER, "l");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("s").remove();
+ });
+ await r;
+ await invokeContentTask(browser, [], () => {
+ const l = content.document.getElementById("l");
+ l.htmlFor = "d";
+ });
+ await testCachedRelation(label, RELATION_LABEL_FOR, div);
+ await testCachedRelation(div, RELATION_LABELLED_BY, label);
+ },
+ {
+ chrome: false,
+ iframe: true,
+ remoteIframe: true,
+ topLevel: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via DOM ID reuse.
+ */
+addAccessibleTask(
+ `
+ <div id="label">before</div><input id="input" aria-labelledby="label">
+ `,
+ async function (browser, accDoc) {
+ let label = findAccessibleChildByID(accDoc, "label");
+ const input = findAccessibleChildByID(accDoc, "input");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+
+ const r = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label").remove();
+ let l = content.document.createElement("div");
+ l.id = "label";
+ l.textContent = "after";
+ content.document.body.insertBefore(
+ l,
+ content.document.getElementById("input")
+ );
+ });
+ await r;
+ label = findAccessibleChildByID(accDoc, "label");
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ },
+ {
+ chrome: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test LINKS_TO relation caching an anchor with multiple hashes
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="#foo#bar">Origin</a><br>
+ <a id="anchor" name="foo#bar">Destination`,
+ async function (browser, accDoc) {
+ const link = findAccessibleChildByID(accDoc, "link");
+ const anchor = findAccessibleChildByID(accDoc, "anchor");
+
+ await testCachedRelation(link, RELATION_LINKS_TO, anchor);
+ },
+ {
+ chrome: true,
+ // IA2 doesn't have a LINKS_TO relation and Windows non-cached
+ // RemoteAccessible uses IA2, so we can't run these tests in this case.
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via accessible shutdown.
+ */
+addAccessibleTask(
+ `
+ <div id="d"></div>
+ <label id="l">
+ <select id="s">
+ `,
+ async function (browser, accDoc) {
+ const label = findAccessibleChildByID(accDoc, "l");
+ const select = findAccessibleChildByID(accDoc, "s");
+ const div = findAccessibleChildByID(accDoc, "d");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, select);
+ await testCachedRelation(select, RELATION_LABELLED_BY, label);
+ await testCachedRelation(div, RELATION_LABELLED_BY, null);
+ await untilCacheOk(() => {
+ try {
+ // We should get an acc ID back from this, but we don't have a way of
+ // verifying its correctness -- it should be the ID of the select.
+ return label.cache.getStringProperty("for");
+ } catch (e) {
+ ok(false, "Exception thrown while trying to read from the cache");
+ return false;
+ }
+ }, "Label for relation exists");
+
+ const r = waitForEvent(EVENT_REORDER, "l");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("s").remove();
+ });
+ await r;
+ await untilCacheOk(() => {
+ try {
+ label.cache.getStringProperty("for");
+ } catch (e) {
+ // This property should no longer exist in the cache, so we should
+ // get an exception if we try to fetch it.
+ return true;
+ }
+ return false;
+ }, "Label for relation exists");
+
+ await invokeContentTask(browser, [], () => {
+ const l = content.document.getElementById("l");
+ l.htmlFor = "d";
+ });
+ await testCachedRelation(label, RELATION_LABEL_FOR, div);
+ await testCachedRelation(div, RELATION_LABELLED_BY, label);
+ },
+ {
+ /**
+ * This functionality is broken in our LocalAcccessible implementation,
+ * so we avoid running this test in chrome or when the cache is off.
+ */
+ chrome: false,
+ iframe: true,
+ remoteIframe: true,
+ topLevel: true,
+ }
+);
+
+/**
+ * Test label relations on HTML figure/figcaption.
+ */
+addAccessibleTask(
+ `
+<figure id="figure1">
+ before
+ <figcaption id="caption1">caption1</figcaption>
+ after
+</figure>
+<figure id="figure2" aria-labelledby="label">
+ <figcaption id="caption2">caption2</figure>
+</figure>
+<div id="label">label</div>
+ `,
+ async function (browser, docAcc) {
+ const figure1 = findAccessibleChildByID(docAcc, "figure1");
+ let caption1 = findAccessibleChildByID(docAcc, "caption1");
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
+ await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);
+
+ info("Hiding caption1");
+ let mutated = waitForEvent(EVENT_HIDE, caption1);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("caption1").hidden = true;
+ });
+ await mutated;
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, null);
+
+ info("Showing caption1");
+ mutated = waitForEvent(EVENT_SHOW, "caption1");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("caption1").hidden = false;
+ });
+ caption1 = (await mutated).accessible;
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
+ await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);
+
+ const figure2 = findAccessibleChildByID(docAcc, "figure2");
+ const caption2 = findAccessibleChildByID(docAcc, "caption2");
+ const label = findAccessibleChildByID(docAcc, "label");
+ await testCachedRelation(figure2, RELATION_LABELLED_BY, [label, caption2]);
+ await testCachedRelation(caption2, RELATION_LABEL_FOR, figure2);
+ await testCachedRelation(label, RELATION_LABEL_FOR, figure2);
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test details relations on popovers and their invokers.
+ */
+addAccessibleTask(
+ `
+<button id="hide" popovertarget="popover" popovertargetaction="hide">hide</button>
+<button id="toggle1" popovertarget="popover">toggle1</button>
+<button id="toggle2">toggle2</button>
+<button id="toggleSibling">toggleSibling</button>
+<div id="popover" popover>popover</div>
+<div id="details">details</div>
+ `,
+ async function testPopover(browser, docAcc) {
+ // The popover is hidden, so nothing should be referring to it.
+ const hide = findAccessibleChildByID(docAcc, "hide");
+ await testCachedRelation(hide, RELATION_DETAILS, []);
+ const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
+ await testCachedRelation(toggle1, RELATION_DETAILS, []);
+ const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ const toggleSibling = findAccessibleChildByID(docAcc, "toggleSibling");
+ await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
+
+ info("Showing popover");
+ let shown = waitForEvent(EVENT_SHOW, "popover");
+ toggle1.doAction(0);
+ const popover = (await shown).accessible;
+ await testCachedRelation(toggle1, RELATION_DETAILS, popover);
+ // toggle2 shouldn't have a details relation because it doesn't have a
+ // popovertarget.
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ // hide shouldn't have a details relation because its action is hide.
+ await testCachedRelation(hide, RELATION_DETAILS, []);
+ // toggleSibling shouldn't have a details relation because it is a sibling
+ // of the popover.
+ await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Setting toggle2 popovertargetaction");
+ await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover");
+ await testCachedRelation(toggle2, RELATION_DETAILS, popover);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]);
+
+ info("Removing toggle2 popovertarget");
+ await invokeSetAttribute(browser, "toggle2", "popovertarget", null);
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Setting aria-details on toggle1");
+ await invokeSetAttribute(browser, "toggle1", "aria-details", "details");
+ const details = findAccessibleChildByID(docAcc, "details");
+ // aria-details overrides popover.
+ await testCachedRelation(toggle1, RELATION_DETAILS, details);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, []);
+
+ info("Removing aria-details from toggle1");
+ await invokeSetAttribute(browser, "toggle1", "aria-details", null);
+ await testCachedRelation(toggle1, RELATION_DETAILS, popover);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Hiding popover");
+ let hidden = waitForEvent(EVENT_HIDE, popover);
+ toggle1.doAction(0);
+ // The relations between toggle1 and popover are removed when popover shuts
+ // down. However, this doesn't cause a cache update notification. Therefore,
+ // to avoid timing out in testCachedRelation, we must wait for a hide event
+ // first.
+ await hidden;
+ await testCachedRelation(toggle1, RELATION_DETAILS, []);
+ },
+ { chrome: false, topLevel: 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..37f8c46966
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_states.js
@@ -0,0 +1,552 @@
+/* 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) {
+ const [expState, expExtState, absState, absExtState] = expected;
+ info(desc);
+ let onUpdate = waitForEvent(EVENT_STATE_CHANGE, evt => {
+ if (getAccessibleDOMNodeID(evt.accessible) != id) {
+ return false;
+ }
+ // Events can be fired for states other than the ones we're interested
+ // in. If this happens, the states we're expecting might not be exposed
+ // yet.
+ const scEvt = evt.QueryInterface(nsIAccessibleStateChangeEvent);
+ if (scEvt.isExtraState) {
+ if (scEvt.state & expExtState || scEvt.state & absExtState) {
+ return true;
+ }
+ return false;
+ }
+ return scEvt.state & expState || scEvt.state & absState;
+ });
+ 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 }
+);
+
+/**
+ * Test caching of the focused state.
+ */
+addAccessibleTask(
+ `
+ <button id="b1">b1</button>
+ <button id="b2">b2</button>
+ `,
+ async function (browser, docAcc) {
+ const b1 = findAccessibleChildByID(docAcc, "b1");
+ const b2 = findAccessibleChildByID(docAcc, "b2");
+
+ let focused = waitForEvent(EVENT_FOCUS, b1);
+ await invokeFocus(browser, "b1");
+ await focused;
+ testStates(docAcc, 0, 0, STATE_FOCUSED);
+ testStates(b1, STATE_FOCUSED);
+ testStates(b2, 0, 0, STATE_FOCUSED);
+
+ focused = waitForEvent(EVENT_FOCUS, b2);
+ await invokeFocus(browser, "b2");
+ await focused;
+ testStates(b2, STATE_FOCUSED);
+ testStates(b1, 0, 0, STATE_FOCUSED);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the document initially gets the focused state.
+ * We can't do this in the test above because that test runs in iframes as well
+ * as a top level document.
+ */
+addAccessibleTask(
+ `
+ <button id="b1">b1</button>
+ <button id="b2">b2</button>
+ `,
+ async function (browser, docAcc) {
+ testStates(docAcc, STATE_FOCUSED);
+ }
+);
+
+/**
+ * Test caching of the focused state in iframes.
+ */
+addAccessibleTask(
+ `
+ <button id="button">button</button>
+ `,
+ async function (browser, iframeDocAcc, topDocAcc) {
+ testStates(topDocAcc, STATE_FOCUSED);
+ const button = findAccessibleChildByID(iframeDocAcc, "button");
+ testStates(button, 0, 0, STATE_FOCUSED);
+ let focused = waitForEvent(EVENT_FOCUS, button);
+ info("Focusing button in iframe");
+ button.takeFocus();
+ await focused;
+ testStates(topDocAcc, 0, 0, STATE_FOCUSED);
+ testStates(button, STATE_FOCUSED);
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the focusable state in iframes which are initially visibility: hidden.
+ */
+addAccessibleTask(
+ `
+<button id="button"></button>
+<span id="span" tabindex="-1">span</span>`,
+ async function (browser, topDocAcc) {
+ info("Changing visibility on iframe");
+ let reordered = waitForEvent(EVENT_REORDER, topDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], iframeId => {
+ content.document.getElementById(iframeId).style.visibility = "";
+ });
+ await reordered;
+ // The iframe doc a11y tree might not be built yet.
+ const iframeDoc = await TestUtils.waitForCondition(() =>
+ findAccessibleChildByID(topDocAcc, DEFAULT_IFRAME_DOC_BODY_ID)
+ );
+ // Log/verify whether this is an in-process or OOP iframe.
+ await comparePIDs(browser, gIsRemoteIframe);
+ const button = findAccessibleChildByID(iframeDoc, "button");
+ testStates(button, STATE_FOCUSABLE);
+ const span = findAccessibleChildByID(iframeDoc, "span");
+ ok(span, "span Accessible exists");
+ testStates(span, STATE_FOCUSABLE);
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "visibility: hidden;" },
+ skipFissionDocLoad: true,
+ }
+);
+
+function checkOpacity(acc, present) {
+ let [, extraState] = getStates(acc);
+ let currOpacity = extraState & EXT_STATE_OPAQUE;
+ return present ? currOpacity : !currOpacity;
+}
+
+/**
+ * Test caching of the OPAQUE1 state.
+ */
+addAccessibleTask(
+ `
+ <div id="div">hello world</div>
+ `,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ await untilCacheOk(() => checkOpacity(div, true), "Found opaque state");
+
+ await invokeContentTask(browser, [], () => {
+ let elm = content.document.getElementById("div");
+ elm.style = "opacity: 0.4;";
+ elm.offsetTop; // Flush layout.
+ });
+
+ await untilCacheOk(
+ () => checkOpacity(div, false),
+ "Did not find opaque state"
+ );
+
+ await invokeContentTask(browser, [], () => {
+ let elm = content.document.getElementById("div");
+ elm.style = "opacity: 1;";
+ elm.offsetTop; // Flush layout.
+ });
+
+ await untilCacheOk(() => checkOpacity(div, true), "Found opaque state");
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test caching of the editable state.
+ */
+addAccessibleTask(
+ `<div id="div" contenteditable></div>`,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ testStates(div, 0, EXT_STATE_EDITABLE, 0, 0);
+ // Ensure that a contentEditable descendant doesn't cause editable to be
+ // exposed on the document.
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Setting contentEditable on the body");
+ let stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true),
+ waitForStateChange(docAcc, STATE_READONLY, false, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.body.contentEditable = true;
+ });
+ await stateChanged;
+ testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0);
+
+ info("Clearing contentEditable on the body");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true),
+ waitForStateChange(docAcc, STATE_READONLY, true, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.body.contentEditable = false;
+ });
+ await stateChanged;
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Clearing contentEditable on div");
+ stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, false, true);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").contentEditable = false;
+ });
+ await stateChanged;
+ testStates(div, 0, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Setting contentEditable on div");
+ stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, true, true);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").contentEditable = true;
+ });
+ await stateChanged;
+ testStates(div, 0, EXT_STATE_EDITABLE, 0, 0);
+
+ info("Setting designMode on document");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true),
+ waitForStateChange(docAcc, STATE_READONLY, false, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.designMode = "on";
+ });
+ await stateChanged;
+ testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0);
+
+ info("Clearing designMode on document");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true),
+ waitForStateChange(docAcc, STATE_READONLY, true, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.designMode = "off";
+ });
+ await stateChanged;
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test caching of the stale and busy states.
+ */
+addAccessibleTask(
+ `<iframe id="iframe"></iframe>`,
+ async function (browser, docAcc) {
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ info("Setting iframe src");
+ // This iframe won't finish loading. Thus, it will get the stale state and
+ // won't fire a document load complete event. We use the reorder event on
+ // the iframe to know when the document has been created.
+ let reordered = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("iframe").src =
+ 'data:text/html,<img src="http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs">';
+ });
+ const iframeDoc = (await reordered).accessible.firstChild;
+ testStates(iframeDoc, STATE_BUSY, EXT_STATE_STALE, 0, 0);
+
+ info("Finishing load of iframe doc");
+ let loadCompleted = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, iframeDoc);
+ await fetch(
+ "https://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete"
+ );
+ await loadCompleted;
+ testStates(iframeDoc, 0, 0, STATE_BUSY, EXT_STATE_STALE);
+ },
+ { topLevel: true, chrome: true }
+);
+
+/**
+ * Test implicit selected state.
+ */
+addAccessibleTask(
+ `
+<div role="tablist">
+ <div id="noSel" role="tab" tabindex="0">noSel</div>
+ <div id="selFalse" role="tab" aria-selected="false" tabindex="0">selFalse</div>
+</div>
+<div role="listbox" aria-multiselectable="true">
+ <div id="multiNoSel" role="option" tabindex="0">multiNoSel</div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const noSel = findAccessibleChildByID(docAcc, "noSel");
+ testStates(noSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing noSel");
+ let focused = waitForEvent(EVENT_FOCUS, noSel);
+ noSel.takeFocus();
+ await focused;
+ testStates(noSel, STATE_FOCUSED | STATE_SELECTED, 0, 0, 0);
+
+ const selFalse = findAccessibleChildByID(docAcc, "selFalse");
+ testStates(selFalse, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing selFalse");
+ focused = waitForEvent(EVENT_FOCUS, selFalse);
+ selFalse.takeFocus();
+ await focused;
+ testStates(selFalse, STATE_FOCUSED, 0, STATE_SELECTED, 0);
+
+ const multiNoSel = findAccessibleChildByID(docAcc, "multiNoSel");
+ testStates(multiNoSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing multiNoSel");
+ focused = waitForEvent(EVENT_FOCUS, multiNoSel);
+ multiNoSel.takeFocus();
+ await focused;
+ testStates(multiNoSel, STATE_FOCUSED, 0, STATE_SELECTED, 0);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test invalid state determined via DOM.
+ */
+addAccessibleTask(
+ `<input type="email" id="email">`,
+ async function (browser, docAcc) {
+ const email = findAccessibleChildByID(docAcc, "email");
+ info("Focusing email");
+ let focused = waitForEvent(EVENT_FOCUS, email);
+ email.takeFocus();
+ await focused;
+ info("Typing a");
+ let invalidChanged = waitForStateChange(email, STATE_INVALID, true);
+ EventUtils.sendString("a");
+ await invalidChanged;
+ testStates(email, STATE_INVALID);
+ info("Typing @b");
+ invalidChanged = waitForStateChange(email, STATE_INVALID, false);
+ EventUtils.sendString("@b");
+ await invalidChanged;
+ testStates(email, 0, 0, STATE_INVALID);
+ info("Typing backspace");
+ invalidChanged = waitForStateChange(email, STATE_INVALID, true);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await invalidChanged;
+ testStates(email, STATE_INVALID);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the expanded state for popover target element.
+ */
+addAccessibleTask(
+ `
+ <button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button>
+ <button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button>
+ <button id="toggle">toggle</button>
+ <div id="mypopover" popover>
+ Popover content
+ <button id="hide-inside" popovertarget="mypopover" popovertargetaction="hide">Hide inside popover</button>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const show = findAccessibleChildByID(docAcc, "show-popover-btn");
+ const hide = findAccessibleChildByID(docAcc, "hide-popover-btn");
+ testStates(show, STATE_COLLAPSED, 0);
+ testStates(hide, STATE_COLLAPSED, 0);
+ const toggle = findAccessibleChildByID(docAcc, "toggle");
+ testStates(
+ toggle,
+ 0,
+ 0,
+ STATE_EXPANDED | STATE_COLLAPSED,
+ EXT_STATE_EXPANDABLE
+ );
+
+ info("Setting toggle's popovertarget");
+ let stateChanged = waitForStateChange(
+ toggle,
+ EXT_STATE_EXPANDABLE,
+ true,
+ true
+ );
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("toggle")
+ .setAttribute("popovertarget", "mypopover");
+ });
+ await stateChanged;
+
+ // Changes to the popover should fire events on all invokers.
+ const changeEvents = [
+ [EVENT_STATE_CHANGE, show],
+ [EVENT_STATE_CHANGE, hide],
+ [EVENT_STATE_CHANGE, toggle],
+ ];
+ info("Expanding popover");
+ let onShowing = waitForEvents(changeEvents);
+ await show.doAction(0);
+ await onShowing;
+ testStates(show, STATE_EXPANDED, 0);
+ testStates(hide, STATE_EXPANDED, 0);
+ testStates(toggle, STATE_EXPANDED, 0);
+ const hideInside = findAccessibleChildByID(show, "hide-inside");
+ testStates(hideInside, 0, 0, STATE_EXPANDED | STATE_COLLAPSED, 0);
+
+ info("Collapsing popover");
+ let onHiding = waitForEvents(changeEvents);
+ await hide.doAction(0);
+ await onHiding;
+ testStates(hide, STATE_COLLAPSED, 0);
+ testStates(show, STATE_COLLAPSED, 0);
+ testStates(toggle, STATE_COLLAPSED, 0);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_table.js b/accessible/tests/browser/e10s/browser_caching_table.js
new file mode 100644
index 0000000000..9c8bcb9616
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_table.js
@@ -0,0 +1,665 @@
+/* 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/. */
+
+/**
+ * Test tables for both local and remote Accessibles. There is more extensive
+ * coverage in ../../mochitest/table. These tests are primarily to ensure that
+ * the cache works as expected and that there is consistency between local and
+ * remote.
+ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/table.js */
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts(
+ { name: "table.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test table counts, indexes, extents and implicit headers.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <thead>
+ <tr><th id="a">a</th><th id="bc" colspan="2">bc</th><th id="d">d</th></tr>
+ </thead>
+ <tbody>
+ <tr><th id="ei" rowspan="2">ei</th><td id="fj" rowspan="0">fj</td><td id="g">g</td><td id="h">h</td></tr>
+ <tr><td id="k">k</td></tr>
+ </tbody>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 3, "table rowCount correct");
+ is(table.columnCount, 4, "table columnCount correct");
+ testTableIndexes(table, [
+ [0, 1, 1, 2],
+ [3, 4, 5, 6],
+ [3, 4, 7, -1],
+ ]);
+ const cells = {};
+ for (const id of ["a", "bc", "d", "ei", "fj", "g", "h", "k"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ is(cells.a.rowExtent, 1, "a rowExtent correct");
+ is(cells.a.columnExtent, 1, "a columnExtent correct");
+ is(cells.bc.rowExtent, 1, "bc rowExtent correct");
+ is(cells.bc.columnExtent, 2, "bc columnExtent correct");
+ is(cells.ei.rowExtent, 2, "ei rowExtent correct");
+ is(cells.fj.rowExtent, 2, "fj rowExtent correct");
+ testHeaderCells([
+ {
+ cell: cells.ei,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.g,
+ rowHeaderCells: [cells.ei],
+ columnHeaderCells: [cells.bc],
+ },
+ {
+ cell: cells.k,
+ rowHeaderCells: [cells.ei],
+ columnHeaderCells: [cells.bc],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table explicit headers.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th id="a">a</th><th id="b">b</th></tr>
+ <tr><td id="c" headers="b d">c</td><th scope="row" id="d">d</th></tr>
+ <tr><td id="e" headers="c f">e</td><td id="f">f</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const cells = {};
+ for (const id of ["a", "b", "c", "d", "e", "f"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [cells.d],
+ columnHeaderCells: [cells.b],
+ },
+ {
+ cell: cells.e,
+ rowHeaderCells: [cells.f],
+ columnHeaderCells: [cells.c],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test that an inner table doesn't impact an outer table.
+ */
+addAccessibleTask(
+ `
+<table id="outerTable">
+ <tr><th id="outerCell">outerCell<table id="innerTable">
+ <tr><th id="innerCell">a</th></tr></table>
+ </table></th></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const outerTable = findAccessibleChildByID(docAcc, "outerTable", [
+ nsIAccessibleTable,
+ ]);
+ is(outerTable.rowCount, 1, "outerTable rowCount correct");
+ is(outerTable.columnCount, 1, "outerTable columnCount correct");
+ const outerCell = findAccessibleChildByID(docAcc, "outerCell");
+ is(
+ outerTable.getCellAt(0, 0),
+ outerCell,
+ "outerTable returns correct cell"
+ );
+ const innerTable = findAccessibleChildByID(docAcc, "innerTable", [
+ nsIAccessibleTable,
+ ]);
+ is(innerTable.rowCount, 1, "innerTable rowCount correct");
+ is(innerTable.columnCount, 1, "innerTable columnCount correct");
+ const innerCell = findAccessibleChildByID(docAcc, "innerCell");
+ is(
+ innerTable.getCellAt(0, 0),
+ innerCell,
+ "innerTable returns correct cell"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table caption and summary.
+ */
+addAccessibleTask(
+ `
+<table id="t1">
+ <caption id="c1">c1</caption>
+ <tr><th>a</th></tr>
+</table>
+<table id="t2" summary="s2">
+ <tr><th>a</th></tr>
+</table>
+<table id="t3" summary="s3">
+ <caption id="c3">c3</caption>
+ <tr><th>a</th></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const t1 = findAccessibleChildByID(docAcc, "t1", [nsIAccessibleTable]);
+ const c1 = findAccessibleChildByID(docAcc, "c1");
+ is(t1.caption, c1, "t1 caption correct");
+ ok(!t1.summary, "t1 no summary");
+ const t2 = findAccessibleChildByID(docAcc, "t2", [nsIAccessibleTable]);
+ ok(!t2.caption, "t2 caption is null");
+ is(t2.summary, "s2", "t2 summary correct");
+ const t3 = findAccessibleChildByID(docAcc, "t3", [nsIAccessibleTable]);
+ const c3 = findAccessibleChildByID(docAcc, "c3");
+ is(t3.caption, c3, "t3 caption correct");
+ is(t3.summary, "s3", "t3 summary correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table layout guess.
+ */
+addAccessibleTask(
+ `
+<table id="layout"><tr><td>a</td></tr></table>
+<table id="data"><tr><th>a</th></tr></table>
+<table id="mutate"><tr><td>a</td><td>b</td></tr></table>
+<div id="newTableContainer"></div>
+ `,
+ async function (browser, docAcc) {
+ const layout = findAccessibleChildByID(docAcc, "layout");
+ testAttrs(layout, { "layout-guess": "true" }, true);
+ const data = findAccessibleChildByID(docAcc, "data");
+ testAbsentAttrs(data, { "layout-guess": "true" });
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAttrs(mutate, { "layout-guess": "true" }, true);
+
+ info("mutate: Adding 5 rows");
+ let reordered = waitForEvent(EVENT_REORDER, mutate);
+ await invokeContentTask(browser, [], () => {
+ const frag = content.document.createDocumentFragment();
+ for (let r = 0; r < 6; ++r) {
+ const tr = content.document.createElement("tr");
+ tr.innerHTML = "<td>a</td><td>b</td>";
+ frag.append(tr);
+ }
+ content.document.getElementById("mutate").tBodies[0].append(frag);
+ });
+ await reordered;
+ testAbsentAttrs(mutate, { "layout-guess": "true" });
+
+ info("mutate: Removing 5 rows");
+ reordered = waitForEvent(EVENT_REORDER, mutate);
+ await invokeContentTask(browser, [], () => {
+ // Pause refresh driver so all the children removals below will
+ // be collated into the same tick and only one 'reorder' event will
+ // be dispatched.
+ content.windowUtils.advanceTimeAndRefresh(100);
+
+ let tBody = content.document.getElementById("mutate").tBodies[0];
+ for (let r = 0; r < 6; ++r) {
+ tBody.lastChild.remove();
+ }
+
+ // Resume refresh driver
+ content.windowUtils.restoreNormalRefresh();
+ });
+ await reordered;
+ testAttrs(mutate, { "layout-guess": "true" }, true);
+
+ info("mutate: Adding new table");
+ let shown = waitForEvent(EVENT_SHOW, "newTable");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById(
+ "newTableContainer"
+ ).innerHTML = `<table id="newTable"><tr><th>a</th></tr></table>`;
+ });
+ let newTable = (await shown).accessible;
+ testAbsentAttrs(newTable, { "layout-guess": "true" });
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table layout guess with border styling changes.
+ */
+addAccessibleTask(
+ `
+ <table id="layout"><tr><td id="cell">a</td><td>b</td></tr>
+ <tr><td>c</td><td>d</td></tr><tr><td>c</td><td>d</td></tr></table>
+ `,
+ async function (browser, docAcc) {
+ const layout = findAccessibleChildByID(docAcc, "layout");
+ testAttrs(layout, { "layout-guess": "true" }, true);
+ info("changing border style on table cell");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("cell").style.border = "1px solid black";
+ content.document.body.offsetTop; // Flush layout.
+ });
+ await untilCacheOk(() => {
+ // manually verify the attribute doesn't exist, since `testAbsentAttrs`
+ // has internal calls to ok() which fail if the cache hasn't yet updated
+ for (let prop of layout.attributes.enumerate()) {
+ if (prop.key == "layout-guess") {
+ return false;
+ }
+ }
+ return true;
+ }, "Table is a data table");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test ARIA grid.
+ */
+addAccessibleTask(
+ `
+<div id="grid" role="grid">
+ <div role="rowgroup">
+ <div role="row"><div id="a" role="columnheader">a</div><div id="b" role="columnheader">b</div></div>
+ </div>
+ <div tabindex="-1">
+ <div role="row"><div id="c" role="rowheader">c</div><div id="d" role="gridcell">d</div></div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const grid = findAccessibleChildByID(docAcc, "grid", [nsIAccessibleTable]);
+ is(grid.rowCount, 2, "grid rowCount correct");
+ is(grid.columnCount, 2, "grid columnCount correct");
+ testTableIndexes(grid, [
+ [0, 1],
+ [2, 3],
+ ]);
+ const cells = {};
+ for (const id of ["a", "b", "c", "d"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ is(cells.a.rowExtent, 1, "a rowExtent correct");
+ is(cells.a.columnExtent, 1, "a columnExtent correct");
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.d,
+ rowHeaderCells: [cells.c],
+ columnHeaderCells: [cells.b],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+function setNodeHidden(browser, id, hidden) {
+ return invokeContentTask(browser, [id, hidden], (cId, cHidden) => {
+ content.document.getElementById(cId).hidden = cHidden;
+ });
+}
+
+/**
+ * Test that the table is updated correctly when it is mutated.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr id="r1"><td>a</td><td id="b">b</td></tr>
+ <tr id="r2" hidden><td>c</td><td>d</td></tr>
+</table>
+<div id="owner"></div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 2, "table columnCount correct");
+ testTableIndexes(table, [[0, 1]]);
+ info("Showing r2");
+ let reordered = waitForEvent(EVENT_REORDER, table);
+ await setNodeHidden(browser, "r2", false);
+ await reordered;
+ is(table.rowCount, 2, "table rowCount correct");
+ testTableIndexes(table, [
+ [0, 1],
+ [2, 3],
+ ]);
+ info("Hiding r2");
+ reordered = waitForEvent(EVENT_REORDER, table);
+ await setNodeHidden(browser, "r2", true);
+ await reordered;
+ is(table.rowCount, 1, "table rowCount correct");
+ testTableIndexes(table, [[0, 1]]);
+ info("Hiding b");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await setNodeHidden(browser, "b", true);
+ await reordered;
+ is(table.columnCount, 1, "table columnCount correct");
+ testTableIndexes(table, [[0]]);
+ info("Showing b");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await setNodeHidden(browser, "b", false);
+ await reordered;
+ is(table.columnCount, 2, "table columnCount correct");
+ info("Moving b out of table using aria-owns");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("owner").setAttribute("aria-owns", "b");
+ });
+ await reordered;
+ is(table.columnCount, 1, "table columnCount correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test the handling of ARIA tables with display: contents.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="table" style="display: contents;">
+ <div role="row"><div role="cell">a</div></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test a broken ARIA table with an invalid cell.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="table">
+ <div role="main">
+ <div role="row">
+ <div id="cell" role="cell">a</div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 0, "table rowCount correct");
+ is(table.columnCount, 0, "table columnCount correct");
+ const cell = findAccessibleChildByID(docAcc, "cell");
+ let queryOk = false;
+ try {
+ cell.QueryInterface(nsIAccessibleTableCell);
+ queryOk = true;
+ } catch (e) {}
+ ok(!queryOk, "Got nsIAccessibleTableCell on an invalid cell");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test that building the cache for a malformed table with an iframe inside a
+ * row doesn't crash (bug 1800780).
+ */
+addAccessibleTask(
+ `<table><tr id="tr"></tr></table>`,
+ async function (browser, docAcc) {
+ let reordered = waitForEvent(EVENT_REORDER, "tr");
+ await invokeContentTask(browser, [], () => {
+ const iframe = content.document.createElement("iframe");
+ content.document.getElementById("tr").append(iframe);
+ });
+ await reordered;
+ },
+ { topLevel: true }
+);
+
+/**
+ * Verify that table row and column information is correct when there are
+ * intervening generics between the table and a rowgroup.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="columnheader">a</div>
+ </div>
+ </div>
+ <div tabindex="-1" style="height: 1px; overflow: auto;">
+ <div role="rowgroup">
+ <div role="row">
+ <div id="cell" role="gridcell">b</div>
+ </div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+
+ info("Verifying that the table row and column counts are correct.");
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+
+ info("Verifying that the cell row and column extents are correct.");
+ const cell = findAccessibleChildByID(docAcc, "cell", [
+ nsIAccessibleTableCell,
+ ]);
+ is(cell.rowExtent, 1, "cell rowExtent correct");
+ is(cell.columnExtent, 1, "cell colExtent correct");
+ is(cell.rowIndex, 1, "cell rowIndex correct");
+ is(cell.columnIndex, 0, "cell columnIndex correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that table row and column information is correct when there are
+ * intervening generics between rows and cells.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="columnheader">a</div>
+ </div>
+ </div>
+ <div role="rowgroup">
+ <div role="row">
+ <div tabindex="-1" style="height: 1px; overflow: auto;">
+ <div id="cell" role="gridcell">b</div>
+ </div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+
+ info("Verifying that the table row and column counts are correct.");
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+
+ info("Verifying that the cell row and column extents are correct.");
+ const cell = findAccessibleChildByID(docAcc, "cell", [
+ nsIAccessibleTableCell,
+ ]);
+ is(cell.rowExtent, 1, "cell rowExtent correct");
+ is(cell.columnExtent, 1, "cell colExtent correct");
+ is(cell.rowIndex, 1, "cell rowIndex correct");
+ is(cell.columnIndex, 0, "cell columnIndex correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that we don't crash for authoring error like <table role="gridcell">.
+ */
+addAccessibleTask(
+ `<table id="table" role="gridcell">`,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table");
+ ok(table, "Retrieved table Accessible");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test ARIA tables in SVG.
+ */
+addAccessibleTask(
+ `
+<svg id="table" role="table">
+ <text id="caption" role="caption">caption</text>
+ <g role="row">
+ <text id="a" role="columnheader">a</text>
+ <text id="b" role="columnheader">b</text>
+ </g>
+ <g role="row">
+ <text id="c" role="cell">c</text>
+ <text id="d" role="cell">d</text>
+ </g>
+</svg>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 2, "table columnCount correct");
+ const caption = findAccessibleChildByID(docAcc, "caption");
+ is(table.caption, caption, "table caption correct");
+ testTableIndexes(table, [
+ [0, 1],
+ [2, 3],
+ ]);
+ const cells = {};
+ for (const id of ["a", "b", "c", "d"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.d,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.b],
+ },
+ ]);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Verify that we don't crash for authoring error like <tr role="grid">.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th>a</th></tr>
+ <tr role="grid"><td id="b">b</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+ const b = findAccessibleChildByID(docAcc, "b");
+ let queryOk = false;
+ try {
+ b.QueryInterface(nsIAccessibleTableCell);
+ queryOk = true;
+ } catch (e) {}
+ ok(!queryOk, "No nsIAccessibleTableCell on invalid cell b");
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_text_bounds.js b/accessible/tests/browser/e10s/browser_caching_text_bounds.js
new file mode 100644
index 0000000000..3e37bf7490
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_text_bounds.js
@@ -0,0 +1,737 @@
+/* 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";
+requestLongerTimeout(3);
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+// Note that testTextNode, testChar and testTextRange currently don't handle
+// white space in the code that doesn't get rendered on screen. To work around
+// this, ensure that containers you want to test are all on a single line in the
+// test snippet.
+
+async function testTextNode(accDoc, browser, id) {
+ await testTextRange(accDoc, browser, id, 0, -1);
+}
+
+async function testChar(accDoc, browser, id, idx) {
+ await testTextRange(accDoc, browser, id, idx, idx + 1);
+}
+
+async function testTextRange(accDoc, browser, id, start, end) {
+ const r = await invokeContentTask(
+ browser,
+ [id, start, end],
+ (_id, _start, _end) => {
+ const htNode = content.document.getElementById(_id);
+ let [eX, eY, eW, eH] = [
+ Number.MAX_SAFE_INTEGER,
+ Number.MAX_SAFE_INTEGER,
+ 0,
+ 0,
+ ];
+ let traversed = 0;
+ let localStart = _start;
+ let endTraversal = false;
+ for (let element of htNode.childNodes) {
+ // ignore whitespace, but not embedded elements
+ let isEmbeddedElement = false;
+ if (element.length == undefined) {
+ let potentialTextContainer = element;
+ while (
+ potentialTextContainer &&
+ potentialTextContainer.length == undefined
+ ) {
+ potentialTextContainer = element.firstChild;
+ }
+ if (potentialTextContainer && potentialTextContainer.length) {
+ // If we can reach some text from this container, use that as part
+ // of our range. This is important when testing with intervening inline
+ // elements. ie. <pre><code>ab%0acd
+ element = potentialTextContainer;
+ } else if (element.firstChild) {
+ isEmbeddedElement = true;
+ } else {
+ continue;
+ }
+ }
+ if (element.length + traversed < _start) {
+ // If our start index is not within this
+ // node, keep looking.
+ traversed += element.length;
+ localStart -= element.length;
+ continue;
+ }
+
+ let rect;
+ if (isEmbeddedElement) {
+ rect = element.getBoundingClientRect();
+ } else {
+ const range = content.document.createRange();
+ range.setStart(element, localStart);
+
+ if (_end != -1 && _end - traversed <= element.length) {
+ // If the current node contains
+ // our end index, stop here.
+ endTraversal = true;
+ range.setEnd(element, _end - traversed);
+ } else {
+ range.setEnd(element, element.length);
+ }
+
+ rect = range.getBoundingClientRect();
+ }
+
+ const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX;
+ const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY;
+ eX = Math.min(eX, rect.x);
+ eY = Math.min(eY, rect.y);
+ eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX);
+ eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY);
+
+ if (endTraversal) {
+ break;
+ }
+ localStart = 0;
+ traversed += element.length;
+ }
+ return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)];
+ }
+ );
+ let hyperTextNode = findAccessibleChildByID(accDoc, id);
+
+ // Add in the doc's screen coords because getBoundingClientRect
+ // is relative to the document, not the screen. This assumes the doc's
+ // screen coords are correct. We use getBoundsInCSSPixels to avoid factoring
+ // in the DPR ourselves.
+ let x = {};
+ let y = {};
+ let w = {};
+ let h = {};
+ accDoc.getBoundsInCSSPixels(x, y, w, h);
+ r[0] += x.value;
+ r[1] += y.value;
+ if (end != -1 && end - start == 1) {
+ // If we're only testing a character, use this function because it calls
+ // CharBounds() directly instead of TextBounds().
+ testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE);
+ } else {
+ testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE);
+ }
+}
+
+/**
+ * Since testTextChar can't handle non-rendered white space, this function first
+ * uses testTextChar to verify the first character and then ensures all
+ * characters thereafter have an incrementing x and a non-0 width.
+ */
+async function testLineWithNonRenderedSpace(docAcc, browser, id, length) {
+ await testChar(docAcc, browser, id, 0);
+ const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
+ let prevX = -1;
+ for (let offset = 0; offset < length; ++offset) {
+ const x = {};
+ const y = {};
+ const w = {};
+ const h = {};
+ acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
+ ok(x.value > prevX, `${id}: offset ${offset} x is larger (${x.value})`);
+ prevX = x.value;
+ ok(w.value > 0, `${id}: offset ${offset} width > 0`);
+ }
+}
+
+/**
+ * Test the text range boundary for simple LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' style='font-family: monospace;'>ل</p>
+ <p id='p3' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
+ <pre id='p4' style='font-family: monospace;'>a%0abcdef</pre>
+ `,
+ async function (browser, accDoc) {
+ info("Testing simple LtR text");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ await testTextNode(accDoc, browser, "p4");
+ },
+ {
+ iframe: true,
+ }
+);
+
+/**
+ * Test the partial text range boundary for LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing partial ranges in LtR text");
+ await testTextRange(accDoc, browser, "p1", 0, 4);
+ await testTextRange(accDoc, browser, "p1", 2, 8);
+ await testTextRange(accDoc, browser, "p1", 12, 17);
+ await testTextRange(accDoc, browser, "p2", 0, 4);
+ await testTextRange(accDoc, browser, "p2", 2, 8);
+ await testTextRange(accDoc, browser, "p2", 6, 11);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for multiline LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p4' dir='ltr' style='font-family: monospace;'>Привіт Світ<br>Привіт Світ</p>
+ <p id='p5' dir='ltr' style='font-family: monospace;'>Привіт Світ<br> Я ще трохи тексту в другому рядку</p>
+ <p id='p6' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
+ <p id='p7' style='font-family: monospace;'>hello world<br>hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing multiline LtR text");
+ await testTextNode(accDoc, browser, "p4");
+ await testTextNode(accDoc, browser, "p5");
+ // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping)
+ await testTextNode(accDoc, browser, "p7");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for simple RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='rtl' style='font-family: monospace;'>ل</p>
+ <p id='p3' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
+ <pre id='p4' dir='rtl' style='font-family: monospace;'>a%0abcdef</pre>
+ `,
+ async function (browser, accDoc) {
+ info("Testing simple RtL text");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ await testTextNode(accDoc, browser, "p4");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for multiline RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p4' dir='rtl' style='font-family: monospace;'>لل لللل لل<br>لل لللل لل</p>
+ <p id='p5' dir='rtl' style='font-family: monospace;'>لل لللل لل<br> لل لل لل لل ل لل لل لل</p>
+ <p id='p6' dir='rtl' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
+ <p id='p7' dir='rtl' style='font-family: monospace;'>hello world<br>hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing multiline RtL text");
+ await testTextNode(accDoc, browser, "p4");
+ //await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char
+ // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping)
+ await testTextNode(accDoc, browser, "p7");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the partial text range boundary for RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing partial ranges in RtL text");
+ await testTextRange(accDoc, browser, "p1", 0, 4);
+ await testTextRange(accDoc, browser, "p1", 2, 8);
+ await testTextRange(accDoc, browser, "p1", 12, 17);
+ await testTextRange(accDoc, browser, "p2", 0, 4);
+ await testTextRange(accDoc, browser, "p2", 2, 8);
+ await testTextRange(accDoc, browser, "p2", 6, 10);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test simple vertical text in rl and lr layouts
+ */
+addAccessibleTask(
+ `
+ <div style="writing-mode: vertical-rl;">
+ <p id='p1'>你好世界</p>
+ <p id='p2'>hello world</p>
+ <br>
+ <p id='p3'>こんにちは世界</p>
+ </div>
+ <div style="writing-mode: vertical-lr;">
+ <p id='p4'>你好世界</p>
+ <p id='p5'>hello world</p>
+ <br>
+ <p id='p6'>こんにちは世界</p>
+ </div>
+ `,
+ async function (browser, accDoc) {
+ info("Testing vertical-rl");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ info("Testing vertical-lr");
+ await testTextNode(accDoc, browser, "p4");
+ await testTextNode(accDoc, browser, "p5");
+ await testTextNode(accDoc, browser, "p6");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test multiline vertical-rl text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='writing-mode: vertical-rl;'>你好世界<br>你好世界</p>
+ <p id='p2' style='writing-mode: vertical-rl;'>hello world<br>hello world</p>
+ <br>
+ <p id='p3' style='writing-mode: vertical-rl;'>你好世界<br> 你好世界 你好世界</p>
+ <p id='p4' style='writing-mode: vertical-rl;'>hello world<br> hello world hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing vertical-rl multiline");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ // await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text with embedded chars
+ */
+addAccessibleTask(
+ `<p id='p1' style='font-family: monospace;'>hello <a href="google.com">world</a></p>
+ <p id='p2' style='font-family: monospace;'>hello<br><a href="google.com">world</a></p>
+ <div id='d3'><p></p>hello world</div>
+ <div id='d4'>hello world<p></p></div>
+ <div id='d5'>oh<p></p>hello world</div>`,
+ async function (browser, accDoc) {
+ info("Testing embedded chars");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "d3");
+ await testTextNode(accDoc, browser, "d4");
+ await testTextNode(accDoc, browser, "d5");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test bounds after text mutations.
+ */
+addAccessibleTask(
+ `<p id="p">a</p>`,
+ async function (browser, docAcc) {
+ await testTextNode(docAcc, browser, "p");
+ const p = findAccessibleChildByID(docAcc, "p");
+ info("Appending a character to text leaf");
+ let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("p").firstChild.data = "ab";
+ });
+ await textInserted;
+ await testTextNode(docAcc, browser, "p");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds on the insertion point at the end of a text box.
+ */
+addAccessibleTask(
+ `<input id="input" value="a">`,
+ async function (browser, docAcc) {
+ const input = findAccessibleChildByID(docAcc, "input");
+ testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds after non-br line break.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre id="t">XX
+XXX</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 3);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds in a pre with padding.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ padding: 20px;
+ }
+ </style>
+ <pre id="t">XX
+XXX</pre>`,
+ async function (browser, docAcc) {
+ await testTextNode(docAcc, browser, "t");
+ await testChar(docAcc, browser, "t", 3);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text bounds with an invalid end offset.
+ */
+addAccessibleTask(
+ `<p id="p">a</p>`,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE);
+ },
+ { chrome: true, topLevel: !true }
+);
+
+/**
+ * Test character bounds in an intervening inline element with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre id="t"><code role="none">XX
+XXX
+XX
+X</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds in an intervening inline element with margins
+ * and with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ </style>
+ <div>hello<pre id="t" style="margin-left:100px;margin-top:30px;background-color:blue;">XX
+XXX
+XX
+X</pre></div>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text bounds in a textarea after scrolling.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea" rows="1">a
+b
+c</textarea>
+ `,
+ async function (browser, docAcc) {
+ // We can't use testChar because Range.getBoundingClientRect isn't supported
+ // inside textareas.
+ const textarea = findAccessibleChildByID(docAcc, "textarea");
+ textarea.QueryInterface(nsIAccessibleText);
+ const oldY = {};
+ textarea.getCharacterExtents(
+ 4,
+ {},
+ oldY,
+ {},
+ {},
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ info("Moving textarea caret to c");
+ await invokeContentTask(browser, [], () => {
+ const textareaDom = content.document.getElementById("textarea");
+ textareaDom.focus();
+ textareaDom.selectionStart = 4;
+ });
+ await waitForContentPaint(browser);
+ const newY = {};
+ textarea.getCharacterExtents(
+ 4,
+ {},
+ newY,
+ {},
+ {},
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ ok(newY.value < oldY.value, "y coordinate smaller after scrolling down");
+ },
+ { chrome: true, topLevel: true, iframe: !true }
+);
+
+/**
+ * Test magic offsets with GetCharacter/RangeExtents.
+ */
+addAccessibleTask(
+ `<input id="input" value="abc">`,
+ async function (browser, docAcc) {
+ const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
+ info("Setting caret and focusing input");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ await invokeContentTask(browser, [], () => {
+ const inputDom = content.document.getElementById("input");
+ inputDom.selectionStart = inputDom.selectionEnd = 1;
+ inputDom.focus();
+ });
+ await caretMoved;
+ is(input.caretOffset, 1, "input caretOffset is 1");
+ let expectedX = {};
+ let expectedY = {};
+ let expectedW = {};
+ let expectedH = {};
+ let magicX = {};
+ let magicY = {};
+ let magicW = {};
+ let magicH = {};
+ input.getCharacterExtents(
+ 1,
+ expectedX,
+ expectedY,
+ expectedW,
+ expectedH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ input.getCharacterExtents(
+ nsIAccessibleText.TEXT_OFFSET_CARET,
+ magicX,
+ magicY,
+ magicW,
+ magicH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ Assert.deepEqual(
+ [magicX.value, magicY.value, magicW.value, magicH.value],
+ [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
+ "GetCharacterExtents correct with TEXT_OFFSET_CARET"
+ );
+ input.getRangeExtents(
+ 1,
+ 3,
+ expectedX,
+ expectedY,
+ expectedW,
+ expectedH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ input.getRangeExtents(
+ nsIAccessibleText.TEXT_OFFSET_CARET,
+ nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT,
+ magicX,
+ magicY,
+ magicW,
+ magicH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ Assert.deepEqual(
+ [magicX.value, magicY.value, magicW.value, magicH.value],
+ [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
+ "GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT"
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: !true }
+);
+
+/**
+ * Test wrapped text and pre-formatted text beginning with an empty line.
+ */
+addAccessibleTask(
+ `
+<style>
+ #wrappedText {
+ width: 3ch;
+ font-family: monospace;
+ }
+</style>
+<p id="wrappedText"><a href="https://example.com/">a</a>b cd</p>
+<p id="emptyFirstLine" style="white-space: pre-line;">
+foo</p>
+ `,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "wrappedText", 0);
+ await testChar(docAcc, browser, "wrappedText", 1);
+ await testChar(docAcc, browser, "wrappedText", 2);
+ await testChar(docAcc, browser, "wrappedText", 3);
+ await testChar(docAcc, browser, "wrappedText", 4);
+
+ // We can't use testChar for emptyFirstLine because it doesn't handle white
+ // space properly. Instead, verify that the first character is at the top
+ // left of the text leaf.
+ const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [
+ nsIAccessibleText,
+ ]);
+ const emptyFirstLineLeaf = emptyFirstLine.firstChild;
+ const leafX = {};
+ const leafY = {};
+ emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {});
+ testTextPos(
+ emptyFirstLine,
+ 0,
+ [leafX.value, leafY.value],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: !true }
+);
+
+/**
+ * Test character bounds in an intervening inline element with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre><code id="t" role="group">XX
+XXX
+XX
+X</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds where content white space isn't rendered.
+ */
+addAccessibleTask(
+ `
+<p id="single">a b</p>
+<p id="multi"><ins>a </ins>
+b</p>
+<pre id="pre">a b</pre>
+ `,
+ async function (browser, docAcc) {
+ await testLineWithNonRenderedSpace(docAcc, browser, "single", 3);
+ await testLineWithNonRenderedSpace(docAcc, browser, "multi", 2);
+ for (let offset = 0; offset < 4; ++offset) {
+ await testChar(docAcc, browser, "pre", offset);
+ }
+ },
+ { chrome: true, topLevel: 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..92eb2fe998
--- /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..2b968b5948
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_value.js
@@ -0,0 +1,457 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test 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.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ 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 currentValue is called",
+ id: "slider",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ acc.currentValue = 4;
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: ["4", 4, 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: "Setting currentValue on a progress accessible should fail",
+ id: "progress",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ try {
+ acc.currentValue = 25;
+ ok(false, "Setting currValue on progress element should fail");
+ } catch (e) {}
+ },
+ 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.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_LEFT", {}, content);
+ });
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "5",
+ },
+ {
+ desc: "Value should change when currentValue is called",
+ id: "range",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ acc.currentValue = 4;
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "4",
+ },
+ {
+ desc: "Initially textbox value is text subtree",
+ id: "textbox",
+ expected: "Some rich text",
+ },
+ {
+ desc: "Textbox value changes when subtree changes",
+ id: "textbox",
+ async action(browser) {
+ await invokeContentTask(browser, [], () => {
+ let boldText = content.document.createElement("strong");
+ boldText.textContent = " bold";
+ content.document.getElementById("textbox").appendChild(boldText);
+ });
+ },
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "Some rich text bold",
+ },
+];
+
+/**
+ * Like testValue in accessible/tests/mochitest/value.js, but waits for cache
+ * updates.
+ */
+async function testValue(acc, value, currValue, minValue, maxValue, minIncr) {
+ const pretty = prettyName(acc);
+ await untilCacheIs(() => acc.value, value, `Wrong value of ${pretty}`);
+
+ await untilCacheIs(
+ () => acc.currentValue,
+ currValue,
+ `Wrong current value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.minimumValue,
+ minValue,
+ `Wrong minimum value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.maximumValue,
+ maxValue,
+ `Wrong maximum value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.minimumIncrement,
+ minIncr,
+ `Wrong minimum increment value of ${pretty}`
+ );
+}
+
+/**
+ * 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">
+ <div contenteditable="yes" role="textbox" id="textbox">Some <a href="#">rich</a> text</div>`,
+ 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, acc);
+ } else if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, id, attr, value);
+ }
+ }
+
+ await onUpdate;
+ if (Array.isArray(expected)) {
+ acc.QueryInterface(nsIAccessibleValue);
+ await testValue(acc, ...expected);
+ } else {
+ is(acc.value, expected, `Correct value for ${prettyName(acc)}`);
+ }
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of link URL values.
+ */
+addAccessibleTask(
+ `<a id="link" href="https://example.com/">Test</a>`,
+ async function (browser, docAcc) {
+ let link = findAccessibleChildByID(docAcc, "link");
+ is(link.value, "https://example.com/", "link initial value correct");
+ const textLeaf = link.firstChild;
+ is(textLeaf.value, "https://example.com/", "link initial value correct");
+
+ info("Changing link href");
+ await invokeSetAttribute(browser, "link", "href", "https://example.net/");
+ await untilCacheIs(
+ () => link.value,
+ "https://example.net/",
+ "link value correct after change"
+ );
+
+ info("Removing link href");
+ let onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ await invokeSetAttribute(browser, "link", "href");
+ await onRecreation;
+ link = findAccessibleChildByID(docAcc, "link");
+ await untilCacheIs(() => link.value, "", "link value empty after removal");
+
+ info("Setting link href");
+ onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ await invokeSetAttribute(browser, "link", "href", "https://example.com/");
+ await onRecreation;
+ link = findAccessibleChildByID(docAcc, "link");
+ await untilCacheIs(
+ () => link.value,
+ "https://example.com/",
+ "link value correct after change"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of active state for select options - see bug 1788143.
+ */
+addAccessibleTask(
+ `
+ <select id="select">
+ <option id="first_option">First</option>
+ <option id="second_option">Second</option>
+ </select>`,
+ async function (browser, docAcc) {
+ const select = findAccessibleChildByID(docAcc, "select");
+ is(select.value, "First", "Select initial value correct");
+
+ // Focus the combo box.
+ await invokeFocus(browser, "select");
+
+ // Select the second option (drop-down collapsed).
+ let p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "second_option"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("select").selectedIndex = 1;
+ });
+ await p;
+
+ is(select.value, "Second", "Select value correct after changing option");
+
+ // Expand the combobox dropdown.
+ p = waitForEvent(EVENT_STATE_CHANGE, "ContentSelectDropdown");
+ EventUtils.synthesizeKey("VK_SPACE");
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "first_option"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ [EVENT_HIDE, "ContentSelectDropdown"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+
+ // Press the up arrow to select the first option (drop-down expanded).
+ // Then, press Enter to confirm the selection and close the dropdown.
+ // We do both of these together to unify testing across platforms, since
+ // events are not entirely consistent on Windows vs. Linux + macOS.
+ EventUtils.synthesizeKey("VK_UP");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await p;
+
+ is(
+ select.value,
+ "First",
+ "Select value correct after changing option back"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test combobox values for non-editable comboboxes.
+ */
+addAccessibleTask(
+ `
+ <div id="combo-div-1" role="combobox">value</div>
+ <div id="combo-div-2" role="combobox">
+ <div role="listbox">
+ <div role="option">value</div>
+ </div>
+ </div>
+ <div id="combo-div-3" role="combobox">
+ <div role="group">value</div>
+ </div>
+ <div id="combo-div-4" role="combobox">foo
+ <div role="listbox">
+ <div role="option">bar</div>
+ </div>
+ </div>
+
+ <input id="combo-input-1" role="combobox" value="value" disabled></input>
+ <input id="combo-input-2" role="combobox" value="value" disabled>testing</input>
+
+ <div id="combo-div-selected" role="combobox">
+ <div role="listbox">
+ <div aria-selected="true" role="option">value</div>
+ </div>
+ </div>
+`,
+ async function (browser, docAcc) {
+ const comboDiv1 = findAccessibleChildByID(docAcc, "combo-div-1");
+ const comboDiv2 = findAccessibleChildByID(docAcc, "combo-div-2");
+ const comboDiv3 = findAccessibleChildByID(docAcc, "combo-div-3");
+ const comboDiv4 = findAccessibleChildByID(docAcc, "combo-div-4");
+ const comboInput1 = findAccessibleChildByID(docAcc, "combo-input-1");
+ const comboInput2 = findAccessibleChildByID(docAcc, "combo-input-2");
+ const comboDivSelected = findAccessibleChildByID(
+ docAcc,
+ "combo-div-selected"
+ );
+
+ // Text as a descendant of the combobox: included in the value.
+ is(comboDiv1.value, "value", "Combobox value correct");
+
+ // Text as the descendant of a listbox: excluded from the value.
+ is(comboDiv2.value, "", "Combobox value correct");
+
+ // Text as the descendant of some other role that includes text in name computation.
+ // Here, the group role contains the text node with "value" in it.
+ is(comboDiv3.value, "value", "Combobox value correct");
+
+ // Some descendant text included, but text descendant of a listbox excluded.
+ is(comboDiv4.value, "foo", "Combobox value correct");
+
+ // Combobox inputs with explicit value report that value.
+ is(comboInput1.value, "value", "Combobox value correct");
+ is(comboInput2.value, "value", "Combobox value correct");
+
+ // Combobox role with aria-selected reports correct value.
+ is(comboDivSelected.value, "value", "Combobox value correct");
+ },
+ { chrome: true, 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..046a7706e3
--- /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..dff6586bf3
--- /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..77bd70c0f6
--- /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..fb03ce2329
--- /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..a510c5b9b5
--- /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..f39ecea8c4
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_textchange.js
@@ -0,0 +1,119 @@
+/* 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.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ 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);
+
+ // Adding text should not send events with diffs for non-editable text.
+ // We do this to avoid screen readers reading out confusing diffs for
+ // live regions.
+ events = [
+ { isInserted: false, str: "def", offset: 0 },
+ { isInserted: true, str: "deDEFf", offset: 0 },
+ ];
+ 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_file_input.js b/accessible/tests/browser/e10s/browser_file_input.js
new file mode 100644
index 0000000000..238e48740e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_file_input.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/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+<input type="file" id="noName">
+<input type="file" id="ariaLabel" aria-label="ariaLabel">
+<label>wrappingLabel <input type="file" id="wrappingLabel"></label>
+<label for="labelFor">labelFor</label> <input type="file" id="labelFor">
+<input type="file" id="title" title="title">
+ `,
+ async function (browser, docAcc) {
+ const browseButton = "Browse…";
+ const noFileSuffix = `${browseButton} No file selected.`;
+ const noName = findAccessibleChildByID(docAcc, "noName");
+ testName(noName, noFileSuffix);
+ const ariaLabel = findAccessibleChildByID(docAcc, "ariaLabel");
+ testName(ariaLabel, `ariaLabel ${noFileSuffix}`);
+ const wrappingLabel = findAccessibleChildByID(docAcc, "wrappingLabel");
+ testName(wrappingLabel, `wrappingLabel ${noFileSuffix}`);
+ const labelFor = findAccessibleChildByID(docAcc, "labelFor");
+ testName(labelFor, `labelFor ${noFileSuffix}`);
+ const title = findAccessibleChildByID(docAcc, "title");
+ testName(title, noFileSuffix);
+ testDescr(title, "title");
+
+ // Test that the name of the button changes correctly when a file is chosen.
+ function chooseFile(id) {
+ return invokeContentTask(browser, [id], contentId => {
+ const MockFilePicker = content.SpecialPowers.MockFilePicker;
+ MockFilePicker.init(content);
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ const input = content.document.getElementById(contentId);
+ const inputReceived = new Promise(resolve =>
+ input.addEventListener(
+ "input",
+ event => {
+ MockFilePicker.cleanup();
+ resolve(event.target.files[0].name);
+ },
+ { once: true }
+ )
+ );
+ input.click();
+ return inputReceived;
+ });
+ }
+
+ info("noName: Choosing file");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, "noName");
+ const fn = await chooseFile("noName");
+ // e.g. "Browse…helloworld.txt"
+ const withFileSuffix = `${browseButton} ${fn}`;
+ await nameChanged;
+ testName(noName, withFileSuffix);
+
+ info("ariaLabel: Choosing file");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, "ariaLabel");
+ await chooseFile("ariaLabel");
+ await nameChanged;
+ testName(ariaLabel, `ariaLabel ${withFileSuffix}`);
+
+ info("wrappingLabel: Choosing file");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, "wrappingLabel");
+ await chooseFile("wrappingLabel");
+ await nameChanged;
+ testName(wrappingLabel, `wrappingLabel ${withFileSuffix}`);
+ },
+ { topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_language.js b/accessible/tests/browser/e10s/browser_language.js
new file mode 100644
index 0000000000..684d915693
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_language.js
@@ -0,0 +1,29 @@
+/* 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(
+ `
+<script>
+ // We can't include the html element in snippets, so set lang on it here.
+ document.documentElement.lang = "en";
+</script>
+<div id="inheritEn"></div>
+<div id="de" lang="de">
+ <div id="inheritDe"></div>
+ <div id="fr" lang="fr"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ is(docAcc.language, "en", "Document language correct");
+ const inheritEn = findAccessibleChildByID(docAcc, "inheritEn");
+ is(inheritEn.language, "en", "inheritEn language correct");
+ const de = findAccessibleChildByID(docAcc, "de");
+ is(de.language, "de", "de language correct");
+ const fr = findAccessibleChildByID(docAcc, "fr");
+ is(fr.language, "fr", "fr language correct");
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_obj_group.js b/accessible/tests/browser/e10s/browser_obj_group.js
new file mode 100644
index 0000000000..7e22b8b491
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_obj_group.js
@@ -0,0 +1,430 @@
+/* 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 });
+
+/**
+ * select elements
+ */
+addAccessibleTask(
+ `<select>
+ <option id="opt1-nosize">option1</option>
+ <option id="opt2-nosize">option2</option>
+ <option id="opt3-nosize">option3</option>
+ <option id="opt4-nosize">option4</option>
+ </select>
+
+ <select size="4">
+ <option id="opt1">option1</option>
+ <option id="opt2">option2</option>
+ </select>
+
+ <select size="4">
+ <optgroup id="select2_optgroup" label="group">
+ <option id="select2_opt1">option1</option>
+ <option id="select2_opt2">option2</option>
+ </optgroup>
+ <option id="select2_opt3">option3</option>
+ <option id="select2_opt4">option4</option>
+ </select>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with no size attribute.
+ testGroupAttrs(getAcc("opt1-nosize"), 1, 4);
+ testGroupAttrs(getAcc("opt2-nosize"), 2, 4);
+ testGroupAttrs(getAcc("opt3-nosize"), 3, 4);
+ testGroupAttrs(getAcc("opt4-nosize"), 4, 4);
+
+ // Container should have item count and not hierarchical
+ testGroupParentAttrs(getAcc("opt1-nosize").parent, 4, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select
+ testGroupAttrs(getAcc("opt1"), 1, 2);
+ testGroupAttrs(getAcc("opt2"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with optgroup
+ testGroupAttrs(getAcc("select2_opt3"), 1, 2, 1);
+ testGroupAttrs(getAcc("select2_opt4"), 2, 2, 1);
+ testGroupAttrs(getAcc("select2_opt1"), 1, 2, 2);
+ testGroupAttrs(getAcc("select2_opt2"), 2, 2, 2);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+/**
+ * HTML radios
+ */
+addAccessibleTask(
+ `<form>
+ <input type="radio" id="radio1" name="group1"/>
+ <input type="radio" id="radio2" name="group1"/>
+ </form>
+
+ <input type="radio" id="radio3" name="group2"/>
+ <label><input type="radio" id="radio4" name="group2"/></label>
+
+ <form>
+ <input type="radio" style="display: none;" name="group3">
+ <input type="radio" id="radio5" name="group3">
+ <input type="radio" id="radio6" name="group4">
+ </form>
+
+ <input type="radio" id="radio7">`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within form
+ testGroupAttrs(getAcc("radio1"), 1, 2);
+ testGroupAttrs(getAcc("radio2"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within document
+ testGroupAttrs(getAcc("radio3"), 1, 2);
+ // radio4 is wrapped in a label
+ testGroupAttrs(getAcc("radio4"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Hidden HTML input@type="radio"
+ testGroupAttrs(getAcc("radio5"), 1, 1);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" with different name but same parent
+ testGroupAttrs(getAcc("radio6"), 1, 1);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" with no name
+ testGroupAttrs(getAcc("radio7"), 0, 0);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+/**
+ * lists
+ */
+addAccessibleTask(
+ `<ul id="ul">
+ <li id="li1">Oranges</li>
+ <li id="li2">Apples</li>
+ <li id="li3">Bananas</li>
+ </ul>
+
+ <ol id="ol">
+ <li id="li4">Oranges</li>
+ <li id="li5">Apples</li>
+ <li id="li6">Bananas
+ <ul id="ol_nested">
+ <li id="n_li4">Oranges</li>
+ <li id="n_li5">Apples</li>
+ <li id="n_li6">Bananas</li>
+ </ul>
+ </li>
+ </ol>
+
+ <span role="list" id="aria-list_1">
+ <span role="listitem" id="li7">Oranges</span>
+ <span role="listitem" id="li8">Apples</span>
+ <span role="listitem" id="li9">Bananas</span>
+ </span>
+
+ <span role="list" id="aria-list_2">
+ <span role="listitem" id="li10">Oranges</span>
+ <span role="listitem" id="li11">Apples</span>
+ <span role="listitem" id="li12">Bananas
+ <span role="list" id="aria-list_2_1">
+ <span role="listitem" id="n_li10">Oranges</span>
+ <span role="listitem" id="n_li11">Apples</span>
+ <span role="listitem" id="n_li12">Bananas</span>
+ </span>
+ </span>
+ </span>
+
+ <div role="list" id="aria-list_3">
+ <div role="listitem" id="lgt_li1">Item 1
+ <div role="group">
+ <div role="listitem" id="lgt_li1_nli1">Item 1A</div>
+ <div role="listitem" id="lgt_li1_nli2">Item 1B</div>
+ </div>
+ </div>
+ <div role="listitem" id="lgt_li2">Item 2
+ <div role="group">
+ <div role="listitem" id="lgt_li2_nli1">Item 2A</div>
+ <div role="listitem" id="lgt_li2_nli2">Item 2B</div>
+ </div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol
+ testGroupAttrs(getAcc("li1"), 1, 3);
+ testGroupAttrs(getAcc("li2"), 2, 3);
+ testGroupAttrs(getAcc("li3"), 3, 3);
+
+ // ul should have item count and not hierarchical
+ testGroupParentAttrs(getAcc("ul"), 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol (nested lists)
+
+ testGroupAttrs(getAcc("li4"), 1, 3, 1);
+ testGroupAttrs(getAcc("li5"), 2, 3, 1);
+ testGroupAttrs(getAcc("li6"), 3, 3, 1);
+ // ol with nested list should have 1st level item count and be hierarchical
+ testGroupParentAttrs(getAcc("ol"), 3, true);
+
+ testGroupAttrs(getAcc("n_li4"), 1, 3, 2);
+ testGroupAttrs(getAcc("n_li5"), 2, 3, 2);
+ testGroupAttrs(getAcc("n_li6"), 3, 3, 2);
+ // nested ol should have item count and be hierarchical
+ testGroupParentAttrs(getAcc("ol_nested"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list
+ testGroupAttrs(getAcc("li7"), 1, 3);
+ testGroupAttrs(getAcc("li8"), 2, 3);
+ testGroupAttrs(getAcc("li9"), 3, 3);
+ // simple flat aria list
+ testGroupParentAttrs(getAcc("aria-list_1"), 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> list -> listitem)
+ testGroupAttrs(getAcc("li10"), 1, 3, 1);
+ testGroupAttrs(getAcc("li11"), 2, 3, 1);
+ testGroupAttrs(getAcc("li12"), 3, 3, 1);
+ // aria list with nested list
+ testGroupParentAttrs(getAcc("aria-list_2"), 3, true);
+
+ testGroupAttrs(getAcc("n_li10"), 1, 3, 2);
+ testGroupAttrs(getAcc("n_li11"), 2, 3, 2);
+ testGroupAttrs(getAcc("n_li12"), 3, 3, 2);
+ // nested aria list.
+ testGroupParentAttrs(getAcc("aria-list_2_1"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> group -> listitem)
+ testGroupAttrs(getAcc("lgt_li1"), 1, 2, 1);
+ testGroupAttrs(getAcc("lgt_li1_nli1"), 1, 2, 2);
+ testGroupAttrs(getAcc("lgt_li1_nli2"), 2, 2, 2);
+ testGroupAttrs(getAcc("lgt_li2"), 2, 2, 1);
+ testGroupAttrs(getAcc("lgt_li2_nli1"), 1, 2, 2);
+ testGroupAttrs(getAcc("lgt_li2_nli2"), 2, 2, 2);
+ // aria list with nested list
+ testGroupParentAttrs(getAcc("aria-list_3"), 2, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul role="menubar" id="menubar">
+ <li role="menuitem" aria-haspopup="true" id="menu_item1">File
+ <ul role="menu" id="menu">
+ <li role="menuitem" id="menu_item1.1">New</li>
+ <li role="menuitem" id="menu_item1.2">Open…</li>
+ <li role="separator">-----</li>
+ <li role="menuitem" id="menu_item1.3">Item</li>
+ <li role="menuitemradio" id="menu_item1.4">Radio</li>
+ <li role="menuitemcheckbox" id="menu_item1.5">Checkbox</li>
+ </ul>
+ </li>
+ <li role="menuitem" aria-haspopup="false" id="menu_item2">Help</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox)
+ testGroupAttrs(getAcc("menu_item1"), 1, 2);
+ testGroupAttrs(getAcc("menu_item2"), 2, 2);
+ testGroupAttrs(getAcc("menu_item1.1"), 1, 2);
+ testGroupAttrs(getAcc("menu_item1.2"), 2, 2);
+ testGroupAttrs(getAcc("menu_item1.3"), 1, 3);
+ testGroupAttrs(getAcc("menu_item1.4"), 2, 3);
+ testGroupAttrs(getAcc("menu_item1.5"), 3, 3);
+ // menu bar item count
+ testGroupParentAttrs(getAcc("menubar"), 2, false);
+ // Bug 1492529. Menu should have total number of items 5 from both sets,
+ // but only has the first 2 item set.
+ todoAttr(getAcc("menu"), "child-item-count", "5");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="tablist_1" role="tablist">
+ <li id="tab_1" role="tab">Crust</li>
+ <li id="tab_2" role="tab">Veges</li>
+ <li id="tab_3" role="tab">Carnivore</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tab
+ testGroupAttrs(getAcc("tab_1"), 1, 3);
+ testGroupAttrs(getAcc("tab_2"), 2, 3);
+ testGroupAttrs(getAcc("tab_3"), 3, 3);
+ // tab list tab count
+ testGroupParentAttrs(getAcc("tablist_1"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="rg1" role="radiogroup">
+ <li id="r1" role="radio" aria-checked="false">Thai</li>
+ <li id="r2" role="radio" aria-checked="false">Subway</li>
+ <li id="r3" role="radio" aria-checked="false">Jimmy Johns</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA radio
+ testGroupAttrs(getAcc("r1"), 1, 3);
+ testGroupAttrs(getAcc("r2"), 2, 3);
+ testGroupAttrs(getAcc("r3"), 3, 3);
+ // explicit aria radio group
+ testGroupParentAttrs(getAcc("rg1"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<table role="tree" id="tree_1">
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="true" aria-level="1"
+ id="ti1">vegetables</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti2">cucumber</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti3">carrot</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="false" aria-level="1"
+ id="ti4">cars</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti5">mercedes</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti6">BMW</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti7">Audi</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="1" id="ti8">people</td>
+ </tr>
+ </table>
+
+ <ul role="tree" id="tree_2">
+ <li role="treeitem" id="tree2_ti1">Item 1
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+ </ul>
+ </li>
+ <li role="treeitem" id="tree2_ti2">Item 2
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti2a">Item 2A</li>
+ <li role="treeitem" id="tree2_ti2b">Item 2B</li>
+ </ul>
+ </li>
+ </div>
+
+ <div role="tree" id="tree_3">
+ <div role="treeitem" id="tree3_ti1">Item 1</div>
+ <div role="group">
+ <li role="treeitem" id="tree3_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree3_ti1b">Item 1B</li>
+ </div>
+ <div role="treeitem" id="tree3_ti2">Item 2</div>
+ <div role="group">
+ <div role="treeitem" id="tree3_ti2a">Item 2A</div>
+ <div role="treeitem" id="tree3_ti2b">Item 2B</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree
+ testGroupAttrs(getAcc("ti1"), 1, 3, 1);
+ testGroupAttrs(getAcc("ti2"), 1, 2, 2);
+ testGroupAttrs(getAcc("ti3"), 2, 2, 2);
+ testGroupAttrs(getAcc("ti4"), 2, 3, 1);
+ testGroupAttrs(getAcc("ti5"), 1, 3, 2);
+ testGroupAttrs(getAcc("ti6"), 2, 3, 2);
+ testGroupAttrs(getAcc("ti7"), 3, 3, 2);
+ testGroupAttrs(getAcc("ti8"), 3, 3, 1);
+ testGroupParentAttrs(getAcc("tree_1"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem -> group -> treeitem)
+ testGroupAttrs(getAcc("tree2_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree2_ti1a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti1b"), 2, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti2"), 2, 2, 1);
+ testGroupAttrs(getAcc("tree2_ti2a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti2b"), 2, 2, 2);
+ testGroupParentAttrs(getAcc("tree_2"), 2, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem, group -> treeitem)
+ testGroupAttrs(getAcc("tree3_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree3_ti1a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti1b"), 2, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti2"), 2, 2, 1);
+ testGroupAttrs(getAcc("tree3_ti2a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti2b"), 2, 2, 2);
+ testGroupParentAttrs(getAcc("tree_3"), 2, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_obj_group_002.js b/accessible/tests/browser/e10s/browser_obj_group_002.js
new file mode 100644
index 0000000000..54cad4a019
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_obj_group_002.js
@@ -0,0 +1,390 @@
+/* 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(
+ `<table role="grid" id="grid">
+ <tr role="row" id="grid_row1">
+ <td role="gridcell" id="grid_cell1">cell1</td>
+ <td role="gridcell" id="grid_cell2">cell2</td>
+ </tr>
+ <tr role="row" id="grid_row2">
+ <td role="gridcell" id="grid_cell3">cell3</td>
+ <td role="gridcell" id="grid_cell4">cell4</td>
+ </tr>
+ </table>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid
+ testGroupAttrs(getAcc("grid_row1"), 1, 2);
+ testAbsentAttrs(getAcc("grid_cell1"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("grid_cell2"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("grid_row2"), 2, 2);
+ testAbsentAttrs(getAcc("grid_cell3"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("grid_cell4"), { posinset: "", setsize: "" });
+ testGroupParentAttrs(getAcc("grid"), 2, false, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="treegrid" id="treegrid" aria-colcount="4">
+ <div role="row" aria-level="1" id="treegrid_row1">
+ <div role="gridcell" id="treegrid_cell1">cell1</div>
+ <div role="gridcell" id="treegrid_cell2">cell2</div>
+ </div>
+ <div role="row" aria-level="2" id="treegrid_row2">
+ <div role="gridcell" id="treegrid_cell3">cell1</div>
+ <div role="gridcell" id="treegrid_cell4">cell2</div>
+ </div>
+ <div role="row" id="treegrid_row3">
+ <div role="gridcell" id="treegrid_cell5">cell1</div>
+ <div role="gridcell" id="treegrid_cell6">cell2</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA treegrid
+ testGroupAttrs(getAcc("treegrid_row1"), 1, 2, 1);
+ testAbsentAttrs(getAcc("treegrid_cell1"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell2"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("treegrid_row2"), 1, 1, 2);
+ testAbsentAttrs(getAcc("treegrid_cell3"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell4"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("treegrid_row3"), 2, 2, 1);
+ testAbsentAttrs(getAcc("treegrid_cell5"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell6"), { posinset: "", setsize: "" });
+
+ testGroupParentAttrs(getAcc("treegrid"), 2, true);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs(getAcc("treegrid_row1"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div id="headings">
+ <h1 id="h1">heading1</h1>
+ <h2 id="h2">heading2</h2>
+ <h3 id="h3">heading3</h3>
+ <h4 id="h4">heading4</h4>
+ <h5 id="h5">heading5</h5>
+ <h6 id="h6">heading6</h6>
+ <div id="ariaHeadingNoLevel" role="heading">ariaHeadingNoLevel</div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML headings
+ testGroupAttrs(getAcc("h1"), 0, 0, 1);
+ testGroupAttrs(getAcc("h2"), 0, 0, 2);
+ testGroupAttrs(getAcc("h3"), 0, 0, 3);
+ testGroupAttrs(getAcc("h4"), 0, 0, 4);
+ testGroupAttrs(getAcc("h5"), 0, 0, 5);
+ testGroupAttrs(getAcc("h6"), 0, 0, 6);
+ testGroupAttrs(getAcc("ariaHeadingNoLevel"), 0, 0, 2);
+ // No child item counts or "tree" flag for parent of headings
+ testAbsentAttrs(getAcc("headings"), { "child-item-count": "", tree: "" });
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="combo1" role="combobox">Password
+ <li id="combo1_opt1" role="option">Xyzzy</li>
+ <li id="combo1_opt2" role="option">Plughs</li>
+ <li id="combo1_opt3" role="option">Shazaam</li>
+ <li id="combo1_opt4" role="option">JoeSentMe</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA combobox
+ testGroupAttrs(getAcc("combo1_opt1"), 1, 4);
+ testGroupAttrs(getAcc("combo1_opt2"), 2, 4);
+ testGroupAttrs(getAcc("combo1_opt3"), 3, 4);
+ testGroupAttrs(getAcc("combo1_opt4"), 4, 4);
+ testGroupParentAttrs(getAcc("combo1"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="table" aria-colcount="4" aria-rowcount="2" id="table">
+ <div role="row" id="table_row" aria-rowindex="2">
+ <div role="cell" id="table_cell" aria-colindex="3">cell</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA table
+ testGroupAttrs(getAcc("table_cell"), 3, 4);
+ testGroupAttrs(getAcc("table_row"), 2, 2);
+
+ // grid child item count provided by aria-rowcount
+ testGroupParentAttrs(getAcc("table"), 2, false);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs(getAcc("table_row"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="grid" aria-readonly="true">
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_1">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_2">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Attributes calculated even when row is wrapped in a div.
+ testGroupAttrs(getAcc("wrapped_row_1"), 1, 2, null);
+ testGroupAttrs(getAcc("wrapped_row_2"), 2, 2, null);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="list" aria-owns="t1_li1 t1_li2 t1_li3" id="aria-list_4">
+ <div role="listitem" id="t1_li2">Apples</div>
+ <div role="listitem" id="t1_li1">Oranges</div>
+ </div>
+ <div role="listitem" id="t1_li3">Bananas</div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list constructed by ARIA owns
+ testGroupAttrs(getAcc("t1_li1"), 1, 3);
+ testGroupAttrs(getAcc("t1_li2"), 2, 3);
+ testGroupAttrs(getAcc("t1_li3"), 3, 3);
+ testGroupParentAttrs(getAcc("aria-list_4"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<!-- ARIA comments, 1 level, group pos and size calculation -->
+ <article>
+ <p id="comm_single_1" role="comment">Comment 1</p>
+ <p id="comm_single_2" role="comment">Comment 2</p>
+ </article>
+
+ <!-- Nested comments -->
+ <article>
+ <div id="comm_nested_1" role="comment"><p>Comment 1 level 1</p>
+ <div id="comm_nested_1_1" role="comment"><p>Comment 1 level 2</p></div>
+ <div id="comm_nested_1_2" role="comment"><p>Comment 2 level 2</p></div>
+ </div>
+ <div id="comm_nested_2" role="comment"><p>Comment 2 level 1</p>
+ <div id="comm_nested_2_1" role="comment"><p>Comment 3 level 2</p>
+ <div id="comm_nested_2_1_1" role="comment"><p>Comment 1 level 3</p></div>
+ </div>
+ </div>
+ <div id="comm_nested_3" role="comment"><p>Comment 3 level 1</p></div>
+ </article>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Test group attributes of ARIA comments
+ testGroupAttrs(getAcc("comm_single_1"), 1, 2, 1);
+ testGroupAttrs(getAcc("comm_single_2"), 2, 2, 1);
+ testGroupAttrs(getAcc("comm_nested_1"), 1, 3, 1);
+ testGroupAttrs(getAcc("comm_nested_1_1"), 1, 2, 2);
+ testGroupAttrs(getAcc("comm_nested_1_2"), 2, 2, 2);
+ testGroupAttrs(getAcc("comm_nested_2"), 2, 3, 1);
+ testGroupAttrs(getAcc("comm_nested_2_1"), 1, 1, 2);
+ testGroupAttrs(getAcc("comm_nested_2_1_1"), 1, 1, 3);
+ testGroupAttrs(getAcc("comm_nested_3"), 3, 3, 1);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="tree" id="tree4"><div role="treeitem"
+ id="tree4_ti1">Item 1</div><div role="treeitem"
+ id="tree4_ti2">Item 2</div></div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Test that group position information updates after deleting node.
+ testGroupAttrs(getAcc("tree4_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree4_ti2"), 2, 2, 1);
+ testGroupParentAttrs(getAcc("tree4"), 2, true);
+
+ let p = waitForEvent(EVENT_REORDER, "tree4");
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("tree4_ti1").remove();
+ });
+
+ await p;
+ testGroupAttrs(getAcc("tree4_ti2"), 1, 1, 1);
+ testGroupParentAttrs(getAcc("tree4"), 1, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+// Verify that intervening SECTION accs in ARIA compound widgets do not split
+// up the group info for descendant owned elements. Test various types of
+// widgets that should all be treated the same.
+addAccessibleTask(
+ `<div role="tree" id="tree">
+ <div tabindex="0">
+ <div role="treeitem" id="ti1">treeitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="treeitem" id="ti2">treeitem 2</div>
+ </div>
+ </div>
+ <div role="listbox" id="listbox">
+ <div tabindex="0">
+ <div role="option" id="opt1">option 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="option" id="opt2">option 2</div>
+ </div>
+ </div>
+ <div role="list" id="list">
+ <div tabindex="0">
+ <div role="listitem" id="li1">listitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="listitem" id="li2">listitem 2</div>
+ </div>
+ </div>
+ <div role="menu" id="menu">
+ <div tabindex="0">
+ <div role="menuitem" id="mi1">menuitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="menuitem" id="mi2">menuitem 2</div>
+ </div>
+ </div>
+ <div role="radiogroup" id="radiogroup">
+ <div tabindex="0">
+ <div role="radio" id="r1">radio 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="radio" id="r2">radio 2</div>
+ </div>
+ </div>
+`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ testGroupAttrs(getAcc("ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("ti2"), 2, 2, 1);
+
+ testGroupAttrs(getAcc("opt1"), 1, 2, 0);
+ testGroupAttrs(getAcc("opt2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("li1"), 1, 2, 0);
+ testGroupAttrs(getAcc("li2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("mi1"), 1, 2, 0);
+ testGroupAttrs(getAcc("mi2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("r1"), 1, 2, 0);
+ testGroupAttrs(getAcc("r2"), 2, 2, 0);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+// Verify that non-generic accessibles (like buttons) correctly split the group
+// info of descendant owned elements.
+addAccessibleTask(
+ `<div role="tree" id="tree">
+ <div role="button">
+ <div role="treeitem" id="ti1">first</div>
+ </div>
+ <div tabindex="0">
+ <div role="treeitem" id="ti2">second</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ testGroupAttrs(getAcc("ti1"), 1, 1, 1);
+ testGroupAttrs(getAcc("ti2"), 1, 1, 1);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
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..6d5995531e
--- /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..78f52d3162
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js
@@ -0,0 +1,457 @@
+/* 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 }
+);
+
+// Test owning an ancestor which isn't created yet with an iframe in the
+// subtree.
+addAccessibleTask(
+ `
+ <span id="a">
+ <div id="b" aria-owns="c"></div>
+ </span>
+ <div id="c">
+ <iframe></iframe>
+ </div>
+ <script>
+ document.getElementById("c").setAttribute("aria-owns", "a");
+ </script>
+ `,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ // b
+ SECTION: [
+ {
+ // c
+ SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }],
+ },
+ ],
+ },
+ ],
+ });
+ }
+);
+
+// Verify that removing the parent of a DOM-sibling aria-owned child keeps the
+// formerly-owned child in the tree.
+addAccessibleTask(
+ `<input id='x'></input><div aria-owns='x'></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ SECTION: [{ ENTRY: [] }] }],
+ });
+
+ info("Removing the div that aria-owns a DOM sibling");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("div").remove();
+ });
+ await onReorder;
+
+ info("Verifying that the formerly-owned child is still present");
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ ENTRY: [] }],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that removing the parent of multiple DOM-sibling aria-owned children
+// keeps all formerly-owned children in the tree.
+addAccessibleTask(
+ `<input id='x'></input><input id='y'><div aria-owns='x y'></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ SECTION: [{ ENTRY: [] }, { ENTRY: [] }],
+ },
+ ],
+ });
+
+ info("Removing the div that aria-owns DOM siblings");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("div").remove();
+ });
+ await onReorder;
+
+ info("Verifying that the formerly-owned children are still present");
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ ENTRY: [] }, { ENTRY: [] }],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that reordering owned elements by changing the aria-owns attribute
+// properly reorders owned elements.
+addAccessibleTask(
+ `
+<div id="container" aria-owns="b d c a">
+ <div id="a" role="button"></div>
+ <div id="b" role="checkbox"></div>
+</div>
+<div id="c" role="radio"></div>
+<div id="d"></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ SECTION: [
+ { CHECKBUTTON: [] }, // b
+ { SECTION: [] }, // d
+ { RADIOBUTTON: [] }, // c
+ { PUSHBUTTON: [] }, // a
+ ],
+ },
+ ],
+ });
+
+ info("Removing the div that aria-owns other elements");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("#container").remove();
+ });
+ await onReorder;
+
+ info(
+ "Verify DOM children are removed, order of remaining elements is correct"
+ );
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ { RADIOBUTTON: [] }, // c
+ { SECTION: [] }, // d
+ ],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that we avoid sending unwanted hide events when doing multiple
+// aria-owns relocations in a single tick. Note that we're avoiding testing
+// chrome here since parent process locals don't track moves in the same way,
+// meaning our mechanism for avoiding duplicate hide events doesn't work.
+addAccessibleTask(
+ `
+<div id='b' aria-owns='a'></div>
+<div id='d'></div>
+<dd id='f'>
+ <div id='a' aria-owns='d'></div>
+</dd>
+ `,
+ async function (browser, accDoc) {
+ const b = findAccessibleChildByID(accDoc, "b");
+ const waitFor = {
+ expected: [
+ [EVENT_HIDE, b],
+ [EVENT_SHOW, "d"],
+ [EVENT_REORDER, accDoc],
+ ],
+ unexpected: [
+ [EVENT_HIDE, "d"],
+ [EVENT_REORDER, "a"],
+ ],
+ };
+ info(
+ "Verifying that events are fired properly after doing two aria-owns relocations"
+ );
+ await contentSpawnMutation(browser, waitFor, function () {
+ content.document.querySelector("#b").remove();
+ content.document.querySelector("#f").remove();
+ });
+ },
+ { chrome: false, 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..ad7338f725
--- /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_csscontentvisibility.js b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js
new file mode 100644
index 0000000000..8a1f93d0be
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js
@@ -0,0 +1,105 @@
+/* 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 = `
+ <style>
+ #target {
+ width: 150px;
+ height: 150px;
+ background-color: lightblue;
+ }
+ #child {
+ width: 100px;
+ height: 100px;
+ background-color: lightgreen;
+ }
+ #content-child {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ display: contents;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .auto {
+ content-visibility: auto;
+ }
+ #hidden-subtree-2 {
+ visibility: hidden;
+ }
+ </style>
+ <div class="hidden" id="target">
+ <div id="child">A</div>
+ <div id="content-child">B</div>
+ <div id="hidden-subtree-1" class="hidden">C</div>
+ <div id="hidden-subtree-2">D</div>
+ <div id="shadow-host"></div>
+ </div>
+ <script>
+ const host = document.querySelector("#shadow-host");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = "<div id='shadowDiv'>E</div>";
+ </script>
+ `;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.content-visibility.enabled", true]],
+ });
+});
+
+async function setContentVisibility(browser, value) {
+ let mutationPromise = (() => {
+ switch (value) {
+ case "hidden":
+ return waitForEvent(EVENT_REORDER, "target");
+ case "auto":
+ return waitForEvents({
+ expected: [
+ [EVENT_REORDER, "child"],
+ [EVENT_REORDER, "content-child"],
+ [EVENT_REORDER, "shadowDiv"],
+ [EVENT_REORDER, "target"],
+ ],
+ });
+ default:
+ throw new Error(`unexpected content-visibility: ${value}`);
+ }
+ })();
+
+ // Change the value of `content-visibility` property for the target
+ info(`Setting content-visibility: ${value}`);
+ await invokeSetAttribute(browser, "target", "class", value);
+ await mutationPromise;
+}
+
+addAccessibleTask(
+ snippet,
+ async function (browser, accDoc) {
+ const target = findAccessibleChildByID(accDoc, "target");
+
+ info("Initial Accessibility Structure Test");
+ testAccessibleTree(target, { SECTION: [] });
+
+ await setContentVisibility(browser, "auto");
+ testAccessibleTree(target, {
+ SECTION: [
+ { SECTION: [{ TEXT_LEAF: [] }] } /* child */,
+ { SECTION: [{ TEXT_LEAF: [] }] } /* content-child */,
+ { SECTION: [] } /* hidden-subtree-1 */,
+ { SECTION: [{ SECTION: [{ TEXT_LEAF: [] }] }] } /* shadow-host */,
+ ],
+ });
+
+ await setContentVisibility(browser, "hidden");
+ testAccessibleTree(target, { SECTION: [] });
+ },
+ { iframe: true, remoteIframe: true, chrome: 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..4d18f1c08d
--- /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..982b039762
--- /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..95406d96cf
--- /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..d3817a003b
--- /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_image.js b/accessible/tests/browser/e10s/browser_treeupdate_image.js
new file mode 100644
index 0000000000..cf45de65e0
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_image.js
@@ -0,0 +1,192 @@
+/* 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 IMG_ID = "img";
+const ALT_TEXT = "some-text";
+const ARIA_LABEL = "some-label";
+
+// Verify that granting alt text adds the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has empty alt text so it should not be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add the alt text. The graphic should have been inserted into the tree.
+ info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ await shown;
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the graphic accessible exists even with a missing alt attribute.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has no alt attribute so the name is empty.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: null,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Add the alt text. The graphic should still be present in the tree.
+ info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`);
+ const shown = waitForEvent(EVENT_NAME_CHANGE, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ await shown;
+ tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that removing alt text removes the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt="${ALT_TEXT}"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has alt text so it should be in the tree.
+ let acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Set the alt text empty. The graphic should have been removed from the tree.
+ info(`Setting empty alt text for img id ${IMG_ID}`);
+ const hidden = waitForEvent(EVENT_HIDE, acc);
+ await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => {
+ let elm = content.document.getElementById(id);
+ elm.setAttribute(attr, value);
+ });
+ await hidden;
+ acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presence of an aria-label creates an accessible, even if
+// there is no alt text.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" aria-label="${ARIA_LABEL}" alt=""/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has empty alt text, but it does have an
+ // aria-label, so it should be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ARIA_LABEL,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Add the alt text. The graphic should still be in the tree.
+ info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ tree = {
+ role: ROLE_GRAPHIC,
+ name: ARIA_LABEL,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presence of a click listener results in the graphic
+// accessible's presence in the tree.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`,
+ async function (browser, accDoc) {
+ // Add a click listener to the img element.
+ info(`Adding click listener to img id '${IMG_ID}'`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeContentTask(browser, [IMG_ID], id => {
+ content.document.getElementById(id).addEventListener("click", () => {});
+ });
+ await shown;
+
+ // Test initial state; the img has empty alt text, but it does have a click
+ // listener, so it should be in the tree.
+ let acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: null,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presentation role prevents creation of the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" role="presentation"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img is presentational and should not be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add some alt text. There should still be no accessible for the img in the tree.
+ info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ ok(!acc, "Image has no Accessible");
+
+ // Remove the presentation role. The accessible should be created.
+ info(`Removing presentation role from img id ${IMG_ID}`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "role", "");
+ await shown;
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that setting empty alt text on a hidden image does not crash.
+// See Bug 1799208 for more info.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" hidden/>`,
+ async function (browser, accDoc) {
+ // Test initial state; should be no accessible since img is hidden.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add empty alt text. We shouldn't crash.
+ info(`Adding empty alt text "" to img id '${IMG_ID}'`);
+ await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => {
+ let elm = content.document.getElementById(id);
+ elm.setAttribute(attr, value);
+ });
+ ok(true, "Setting empty alt text on a hidden image did not crash");
+ },
+ { chrome: true, 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..82fbd3427e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+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",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ 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");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ 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..2ca14d5572
--- /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..dd678d93fa
--- /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..735f7871af
--- /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_move.js b/accessible/tests/browser/e10s/browser_treeupdate_move.js
new file mode 100644
index 0000000000..8ed6188ef3
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_move.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";
+
+/* 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 moving Accessibles:
+ * 1. A moved Accessible keeps the same Accessible.
+ * 2. If the moved Accessible is focused, it remains focused.
+ * 3. A child of the moved Accessible also keeps the same Accessible.
+ * 4. A child removed at the same time as the move gets shut down.
+ */
+addAccessibleTask(
+ `
+<div id="scrollable" role="presentation" style="height: 1px;">
+ <div contenteditable id="textbox" role="textbox">
+ <h1 id="heading">Heading</h1>
+ <p id="para">Para</p>
+ </div>
+ <iframe id="iframe" src="https://example.com/"></iframe>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const textbox = findAccessibleChildByID(docAcc, "textbox");
+ const heading = findAccessibleChildByID(docAcc, "heading");
+ const para = findAccessibleChildByID(docAcc, "para");
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ const iframeDoc = iframe.firstChild;
+ ok(iframeDoc, "iframe contains a document");
+
+ let focused = waitForEvent(EVENT_FOCUS, textbox);
+ textbox.takeFocus();
+ await focused;
+ testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT);
+
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ // scrollable wasn't in the a11y tree, but this will force it to be created.
+ // textbox will be moved inside it.
+ content.document.getElementById("scrollable").style.overflow = "scroll";
+ content.document.getElementById("heading").remove();
+ });
+ await reordered;
+ // Despite the move, ensure textbox is still alive and is focused.
+ testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT);
+ // Ensure para (a child of textbox) is also still alive.
+ ok(!isDefunct(para), "para is alive");
+ // heading was a child of textbox, but was removed when textbox
+ // was moved. Ensure it is dead.
+ ok(isDefunct(heading), "heading is dead");
+ // Ensure the iframe and its embedded document are alive.
+ ok(!isDefunct(iframe), "iframe is alive");
+ ok(!isDefunct(iframeDoc), "iframeDoc is alive");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that moving a subtree containing an iframe doesn't cause assertions or
+ * crashes. Note that aria-owns moves Accessibles even if it is set before load.
+ */
+addAccessibleTask(
+ `
+<div id="container">
+ <iframe id="iframe"></iframe>
+ <div aria-owns="iframe"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ testAccessibleTree(container, {
+ SECTION: [{ SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] }],
+ });
+ },
+ { topLevel: 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..ec7eed0919
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js
@@ -0,0 +1,100 @@
+/* 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: [] }, { COMBOBOX_OPTION: [] }],
+ },
+ {
+ 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..6b5246f0bf
--- /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..a82fc4c04d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.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/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.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content);
+ });
+ info("Waiting for parent focus");
+ 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"
+ );
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+ info("Waiting for child focus");
+ 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..c188a06044
--- /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..0c617e7026
--- /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..636a00e210
--- /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..78ab47cd51
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+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",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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..089391523d
--- /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: absolute; 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..18a65d8192
--- /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" href=""></a> <a href=""><img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/fonts/Ahem.sjs b/accessible/tests/browser/e10s/fonts/Ahem.sjs
new file mode 100644
index 0000000000..e801a801ab
--- /dev/null
+++ b/accessible/tests/browser/e10s/fonts/Ahem.sjs
@@ -0,0 +1,241 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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";
+
+/*
+ * A CORS-enabled font resource.
+ */
+
+const FONT_BYTES = atob(
+ "AAEAAAALAIAAAwAwT1MvMnhQSo0AAAE4AAAAYGNtYXAP1hZGAAAFbAAABnJnYXNwABcACQAAMLAA" +
+ "AAAQZ2x5ZkmzdNoAAAvgAAAaZGhlYWTWok4cAAAAvAAAADZoaGVhBwoEFgAAAPQAAAAkaG10eLkg" +
+ "AH0AAAGYAAAD1GxvY2EgdSciAAAmRAAAAextYXhwAPgACQAAARgAAAAgbmFtZX4UjLgAACgwAAAG" +
+ "aHBvc3SN0B2KAAAumAAAAhgAAQAAAAEAQhIXUWdfDzz1AAkD6AAAAACzb19ZAAAAAMAtq0kAAP84" +
+ "A+gDIAAAAAMAAgAAAAAAAAABAAADIP84AAAD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAA9QABAAAA" +
+ "9QAIAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAID6AGQAAUAAAK8AooAAACPArwCigAAAcUAMgED" +
+ "AAACAAQJAAAAAAAAgAAArxAAIEgAAAAAAAAAAFczQwAAQAAg8AIDIP84AAADIADIIAABEUAAAAAD" +
+ "IAMgAAAAIAAAA+gAfQAAAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAAAAADAAAAAwAABEwAAQAAAAAAHAADAAEAAAImAAYCCgAAAAAB" +
+ "AAABAAAAAAAAAAAAAAAAAAAAAQACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAQAAAAAAAwAEAAUABgAHAAgACQAAAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkA" +
+ "GgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2" +
+ "ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIA" +
+ "UwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAAAAYQBiAGMAZABlAGYAZwBoAGkAagBrAGwAbQBu" +
+ "AG8AcABxAHIAcwB0AHUAdgB3AHgAeQB6AHsAfAB9AH4AfwCAANsAgQCCAIMAhADdAIUAhgCHAIgA" +
+ "4wCJAIoA6gCLAIwA6ACNAOsA7ACOAI8A5ADmAOUA1ADpAJAAkQDTAJIAkwCUAJUAlgDnANEA7QDS" +
+ "AJcAmADeAAMAmgCbAJwAzgDPANUA1gDYANkAnQCeAJ8A7gCgANAA4gChAOAA4QAAAAAA3ACiANcA" +
+ "2gDfAKMApAClAKYApwCoAKkAqgCrAKwArQAAAK4ArwCwALEAsgCzALQAtQC2ALcAuAC5ALoAuwC8" +
+ "AAQCJgAAAE4AQAAFAA4AJgB+AP8BMQFTAXgBkgLHAskC3QOUA6kDvAPAIBAgFCAaIB4gIiAmIDAg" +
+ "OiBEISIhJiICIgYiDyISIhoiHiIrIkgiYCJlIvIlyvAC//8AAAAgACgAoAExAVIBeAGSAsYCyQLY" +
+ "A5QDqQO8A8AgECATIBggHCAgICYgMCA5IEQhIiEmIgIiBiIPIhEiGSIeIisiSCJgImQi8iXK8AD/" +
+ "///j/+IAAP+B/3z/WP8/AAD97AAA/T79KvzT/RTf/+DCAADgvOC74Ljgr+Cn4J7fwd+t3uLezN7W" +
+ "AAAAAN7K3r7epd6K3ofd+9skEO8AAQAAAAAASgAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAP4A" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOwA7gAAAAAAAAAAAAAAAAAAAAAAAACZAJUAggCDAKEAjgC9" +
+ "AIQAigCIAJAAlwCWAMQAhwC1AIEAjQDHAMgAiQCPAIUAogC5AMYAkQCYAMoAyQDLAJQAmgClAKMA" +
+ "mwBhAGIAiwBjAKcAZACkAKYAqwCoAKkAqgC+AGUArgCsAK0AnABmAMUAjACxAK8AsABnAMAAwgCG" +
+ "AGkAaABqAGwAawBtAJIAbgBwAG8AcQByAHQAcwB1AHYAvwB3AHkAeAB6AHwAewCfAJMAfgB9AH8A" +
+ "gADBAMMAoACzALwAtgC3ALgAuwC0ALoAnQCeANcA5gDEAKIA5wAEAiYAAABOAEAABQAOACYAfgD/" +
+ "ATEBUwF4AZICxwLJAt0DlAOpA7wDwCAQIBQgGiAeICIgJiAwIDogRCEiISYiAiIGIg8iEiIaIh4i" +
+ "KyJIImAiZSLyJcrwAv//AAAAIAAoAKABMQFSAXgBkgLGAskC2AOUA6kDvAPAIBAgEyAYIBwgICAm" +
+ "IDAgOSBEISIhJiICIgYiDyIRIhkiHiIrIkgiYCJkIvIlyvAA////4//iAAD/gf98/1j/PwAA/ewA" +
+ "AP0+/Sr80/0U3//gwgAA4Lzgu+C44K/gp+Ce38Hfrd7i3sze1gAAAADeyt6+3qXeit6H3fvbJBDv" +
+ "AAEAAAAAAEoAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AADsAO4AAAAAAAAAAAAAAAAAAAAAAAAAmQCVAIIAgwChAI4AvQCEAIoAiACQAJcAlgDEAIcAtQCB" +
+ "AI0AxwDIAIkAjwCFAKIAuQDGAJEAmADKAMkAywCUAJoApQCjAJsAYQBiAIsAYwCnAGQApACmAKsA" +
+ "qACpAKoAvgBlAK4ArACtAJwAZgDFAIwAsQCvALAAZwDAAMIAhgBpAGgAagBsAGsAbQCSAG4AcABv" +
+ "AHEAcgB0AHMAdQB2AL8AdwB5AHgAegB8AHsAnwCTAH4AfQB/AIAAwQDDAKAAswC8ALYAtwC4ALsA" +
+ "tAC6AJ0AngDXAOYAxACiAOcAAAACAH0AAANrAyAAAwAHAAAzESERJSERIX0C7v2PAfT+DAMg/OB9" +
+ "AiYAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AAAAAMAADEhFSED6PwYyAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAAAAAPoAyAAAwAAESERIQPo/BgDIPzgAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAAABQAFAAU" +
+ "ABQAIgAwAD4ATABaAGgAdgCEAJIAoACuALwAygDYAOYA9AECARABHgEsAToBSAFWAWQBcgGAAY4B" +
+ "nAGqAbgBxgHUAeIB8AH+AgwCGgIoAjYCRAJSAmACbgJ8AooCmAKmArQCwgLQAt4C7AL6AwgDFgMk" +
+ "AzIDQANOA1wDagN4A4YDlAOiA7ADvgPMA9oD6AP2BAQEEgQgBC4EPARKBFgEZARyBIAEjgScBKoE" +
+ "uATGBNQE4gTwBP4FDAUaBSgFNgVEBVIFYAVuBXwFigWYBaYFtAXCBdAF3gXsBfoGCAYWBiQGMgZA" +
+ "Bk4GXAZqBngGhgaUBqIGsAa+BswG2gboBvYHBAcSByAHLgc8B0oHWAdmB3QHggeQB54HrAe6B8gH" +
+ "1gfkB/IIAAgOCBwIKgg4CDgIRghUCGIIcAh+CIwImgioCLYIxAjSCOAI7gj8CQoJGAkmCTQJQglQ" +
+ "CV4JbAl6CYgJlgmkCbIJwAnOCdwJ6gn4CgYKFAoiCjAKPgpMCloKaAp2CoQKkgqgCq4KvArKCtgK" +
+ "5gr0CwILEAseCywLOgtIC1YLZAtyC4ALjgucC6oLuAvGC9QL4gvwC/4MDAwaDCgMNgxEDFIMYAxu" +
+ "DHwMigyYDKYMtAzCDNAM3gzsDPoNCA0WDSQNMgAAABsBSgAAAAAAAAAAAZ4AAAAAAAAAAAABAAgB" +
+ "ngAAAAAAAAACAA4BpgAAAAAAAAADACABtAAAAAAAAAAEAAgB1AAAAAAAAAAFABYB3AAAAAAAAAAG" +
+ "AAgB8gABAAAAAAAAAM8B+gABAAAAAAABAAQCyQABAAAAAAACAAcCzQABAAAAAAADABAC1AABAAAA" +
+ "AAAEAAQC5AABAAAAAAAFAAsC6AABAAAAAAAGAAQC8wABAAAAAAAQAAQC9wABAAAAAAARAAcC+wAB" +
+ "AAAAAAASAAQDAgADAAEECQAAAZ4DBgADAAEECQABAAgEpAADAAEECQACAA4ErAADAAEECQADACAE" +
+ "ugADAAEECQAEAAgE2gADAAEECQAFABYE4gADAAEECQAGAAgE+AADAAEECQAQAAgFAAADAAEECQAR" +
+ "AA4FCAADAAEECQASAAgFFgBNAG8AcwB0ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAA" +
+ "dABoAGUAIABlAG0AIABzAHEAdQBhAHIAZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABl" +
+ "ACAAYQBuAGQAIAAiAHAAIgAsACAAdwBoAGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8A" +
+ "ZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0AIAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBz" +
+ "AGUAZgB1AGwAIABmAG8AcgAgAHQAZQBzAHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4A" +
+ "IABzAHkAcwB0AGUAbQBzAC4AIABQAHIAbwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBh" +
+ "AGgAcgBuAGUAcgAgAGYAbwByACAAdABoAGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAA" +
+ "YgByAG8AdwBzAGUAcgAgAHQAZQBzAHQAaQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBl" +
+ "AHIAcwBpAG8AbgAgADEALgAxACAAQQBoAGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4A" +
+ "MQBBAGgAZQBtTW9zdCBjaGFyYWN0ZXJzIGFyZSB0aGUgZW0gc3F1YXJlLCBleGNlcHQgJkVBY3V0" +
+ "ZSBhbmQgInAiLCB3aGljaCBzaG93IGFzY2VudC9kZXNjZW50IGZyb20gdGhlIGJhc2VsaW5lLiBV" +
+ "c2VmdWwgZm9yIHRlc3RpbmcgY29tcG9zaXRpb24gc3lzdGVtcy4gUHJvZHVjZWQgYnkgVG9kZCBG" +
+ "YWhybmVyIGZvciB0aGUgQ1NTIFNhbXVyYWkncyBicm93c2VyIHRlc3RpbmcuQWhlbVJlZ3VsYXJW" +
+ "ZXJzaW9uIDEuMSBBaGVtQWhlbVZlcnNpb24gMS4xQWhlbUFoZW1SZWd1bGFyQWhlbQBNAG8AcwB0" +
+ "ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAAdABoAGUAIABlAG0AIABzAHEAdQBhAHIA" +
+ "ZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABlACAAYQBuAGQAIAAiAHAAIgAsACAAdwBo" +
+ "AGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8AZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0A" +
+ "IAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBzAGUAZgB1AGwAIABmAG8AcgAgAHQAZQBz" +
+ "AHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4AIABzAHkAcwB0AGUAbQBzAC4AIABQAHIA" +
+ "bwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBhAGgAcgBuAGUAcgAgAGYAbwByACAAdABo" +
+ "AGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAAYgByAG8AdwBzAGUAcgAgAHQAZQBzAHQA" +
+ "aQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgAxACAAQQBo" +
+ "AGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4AMQBBAGgAZQBtAEEAaABlAG0AUgBlAGcA" +
+ "dQBsAGEAcgBBAGgAZQBtAAIAAAAAAAD/ewAUAAAAAQAAAAAAAAAAAAAAAAAAAAAA9QAAAQIAAgAD" +
+ "AAQABQAGAAcACAAJAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAA" +
+ "IQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9" +
+ "AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkA" +
+ "WgBbAFwAXQBeAF8AYABhAGIAYwBkAGUAZgBnAGgAaQBqAGsAbABtAG4AbwBwAHEAcgBzAHQAdQB2" +
+ "AHcAeAB5AHoAewB8AH0AfgB/AIAAgQCDAIQAhQCGAIgAiQCKAIsAjQCOAJAAkQCTAJYAlwCdAJ4A" +
+ "oAChAKIAowCkAKkAqgCsAK0ArgCvALYAtwC4ALoAvQDDAMcAyADJAMoAywDMAM0AzgDPANAA0QDT" +
+ "ANQA1QDWANcA2ADZANoA2wDcAN0A3gDfAOAA4QDoAOkA6gDrAOwA7QDuAO8A8ADxAPIA8wD0APUA" +
+ "9gAAAAAAsACxALsApgCoAJ8AmwCyALMAxAC0ALUAxQCCAMIAhwCrAMYAvgC/ALwAjACYAJoAmQCl" +
+ "AJIAnACPAJQAlQCnALkA0gDAAMEBAwACAQQETlVMTAJIVANERUwAAAADAAgAAgAQAAH//wAD"
+);
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.write(FONT_BYTES);
+}
diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js
new file mode 100644
index 0000000000..bdbcb7445f
--- /dev/null
+++ b/accessible/tests/browser/e10s/head.js
@@ -0,0 +1,192 @@
+/* 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 testCachedRelation, testRelated */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js and relations.js.
+/* import-globals-from ../../mochitest/relations.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "relations.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test the accessible relation.
+ *
+ * @param identifier [in] identifier to get an accessible, may be ID
+ * attribute or DOM element or accessible object
+ * @param relType [in] relation type (see constants above)
+ * @param relatedIdentifiers [in] identifier or array of identifiers of
+ * expected related accessibles
+ */
+async function testCachedRelation(identifier, relType, relatedIdentifiers) {
+ const relDescr = getRelationErrorMsg(identifier, relType);
+ const relDescrStart = getRelationErrorMsg(identifier, relType, true);
+ info(`Testing ${relDescr}`);
+
+ if (!relatedIdentifiers) {
+ await untilCacheOk(function () {
+ let r = getRelationByType(identifier, relType);
+ if (r) {
+ info(`Fetched ${r.targetsCount} relations from cache`);
+ } else {
+ info("Could not fetch relations");
+ }
+ return r && !r.targetsCount;
+ }, relDescrStart + " has no targets, as expected");
+ return;
+ }
+
+ const relatedIds =
+ relatedIdentifiers instanceof Array
+ ? relatedIdentifiers
+ : [relatedIdentifiers];
+ await untilCacheOk(function () {
+ let r = getRelationByType(identifier, relType);
+ if (r) {
+ info(
+ `Fetched ${r.targetsCount} relations from cache, looking for ${relatedIds.length}`
+ );
+ } else {
+ info("Could not fetch relations");
+ }
+
+ return r && r.targetsCount == relatedIds.length;
+ }, "Found correct number of expected relations");
+
+ let targets = [];
+ for (let idx = 0; idx < relatedIds.length; idx++) {
+ targets.push(getAccessible(relatedIds[idx]));
+ }
+
+ if (targets.length != relatedIds.length) {
+ return;
+ }
+
+ await untilCacheOk(function () {
+ const relation = getRelationByType(identifier, relType);
+ const actualTargets = relation ? relation.getTargets() : null;
+ if (!actualTargets) {
+ info("Could not fetch relations");
+ return false;
+ }
+
+ // Check if all given related accessibles are targets of obtained relation.
+ for (let idx = 0; idx < targets.length; idx++) {
+ let isFound = false;
+ for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) {
+ if (targets[idx] == relatedAcc) {
+ isFound = true;
+ break;
+ }
+ }
+
+ if (!isFound) {
+ info(
+ prettyName(relatedIds[idx]) +
+ " could not be found in relation: " +
+ relDescr
+ );
+ return false;
+ }
+ }
+
+ return true;
+ }, "All given related accessibles are targets of fetched relation.");
+
+ await untilCacheOk(function () {
+ const relation = getRelationByType(identifier, relType);
+ const actualTargets = relation ? relation.getTargets() : null;
+ if (!actualTargets) {
+ info("Could not fetch relations");
+ return false;
+ }
+
+ // Check if all obtained targets are given related accessibles.
+ for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) {
+ let wasFound = false;
+ for (let idx = 0; idx < targets.length; idx++) {
+ if (relatedAcc == targets[idx]) {
+ wasFound = true;
+ }
+ }
+ if (!wasFound) {
+ info(
+ prettyName(relatedAcc) +
+ " was found, but shouldn't be in relation: " +
+ relDescr
+ );
+ return false;
+ }
+ }
+ return true;
+ }, "No unexpected targets found.");
+}
+
+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);
+ }
+ }
+
+ await testCachedRelation(dependant1, dependantRelation, expected[0]);
+ await testCachedRelation(dependant2, dependantRelation, expected[1]);
+ await testCachedRelation(host, hostRelation, expected[2]);
+ }
+}
diff --git a/accessible/tests/browser/events/browser.toml b/accessible/tests/browser/events/browser.toml
new file mode 100644
index 0000000000..7ec3c3621a
--- /dev/null
+++ b/accessible/tests/browser/events/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_alert.js"]
+
+["browser_test_A11yUtils_announce.js"]
+
+["browser_test_caret_move_granularity.js"]
+
+["browser_test_docload.js"]
+skip-if = ["true"]
+
+["browser_test_focus_browserui.js"]
+
+["browser_test_focus_dialog.js"]
+
+["browser_test_focus_urlbar.js"]
+skip-if = ["os == 'win'"] # Bug 1818994
+
+["browser_test_panel.js"]
+
+["browser_test_scrolling.js"]
+
+["browser_test_selection_urlbar.js"]
+
+["browser_test_textcaret.js"]
diff --git a/accessible/tests/browser/events/browser_alert.js b/accessible/tests/browser/events/browser_alert.js
new file mode 100644
index 0000000000..f35a602fa3
--- /dev/null
+++ b/accessible/tests/browser/events/browser_alert.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that alert events aren't fired when reflow happens but no actual
+ * insertion occurs.
+ */
+addAccessibleTask(
+ `
+<div id="alert" role="alert">
+ <div id="content" hidden>content</div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const alert = findAccessibleChildByID(docAcc, "alert");
+ info("Showing content");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_ALERT, alert]] },
+ () => {
+ content.document.getElementById("content").hidden = false;
+ }
+ );
+ info("Changing content display style and removing text");
+ const content = findAccessibleChildByID(docAcc, "content");
+ await contentSpawnMutation(
+ browser,
+ {
+ expected: [[EVENT_REORDER, content]],
+ unexpected: [[EVENT_ALERT, alert]],
+ },
+ () => {
+ const node = content.document.getElementById("content");
+ node.textContent = "";
+ // This causes the node's layout frame to be reconstructed. This in
+ // turn causes a11y to queue it as an insertion in case there were
+ // changes. Because it already has an Accessible, This node is skipped
+ // when processing insertions, so we should not fire an alert event.
+ node.style.display = "flex";
+ }
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
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_caret_move_granularity.js b/accessible/tests/browser/events/browser_test_caret_move_granularity.js
new file mode 100644
index 0000000000..c72ae42d85
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_caret_move_granularity.js
@@ -0,0 +1,102 @@
+/* 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 CLUSTER_AMOUNT = Ci.nsISelectionListener.CLUSTER_AMOUNT;
+const WORD_AMOUNT = Ci.nsISelectionListener.WORD_AMOUNT;
+const LINE_AMOUNT = Ci.nsISelectionListener.LINE_AMOUNT;
+const BEGINLINE_AMOUNT = Ci.nsISelectionListener.BEGINLINE_AMOUNT;
+const ENDLINE_AMOUNT = Ci.nsISelectionListener.ENDLINE_AMOUNT;
+
+const isMac = AppConstants.platform == "macosx";
+
+function matchCaretMoveEvent(id, caretOffset) {
+ return evt => {
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ return (
+ getAccessibleDOMNodeID(evt.accessible) == id &&
+ evt.caretOffset == caretOffset
+ );
+ };
+}
+
+addAccessibleTask(
+ `<textarea id="textarea" style="scrollbar-width: none;" cols="15">` +
+ `one two three four five six seven eight` +
+ `</textarea>`,
+ async function (browser, accDoc) {
+ const textarea = findAccessibleChildByID(accDoc, "textarea");
+ let caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 0)
+ );
+ textarea.takeFocus();
+ let evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 1)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, CLUSTER_AMOUNT, "Caret moved by cluster");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 15)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ todo(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, LINE_AMOUNT, "Caret moved by line");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 14)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, BEGINLINE_AMOUNT, "Caret moved to line start");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 28)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_End");
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ is(evt.granularity, ENDLINE_AMOUNT, "Caret moved to line end");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 24)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { altKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, WORD_AMOUNT, "Caret moved by word");
+ }
+);
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..78ac77fd8c
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_docload.js
@@ -0,0 +1,128 @@
+/* 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.startLoadingURIString(
+ 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.startLoadingURIString(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.startLoadingURIString(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([
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("http://www.wronguri.wronguri/")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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.startLoadingURIString(
+ 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..969d336c74
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_browserui.js
@@ -0,0 +1,57 @@
+/* 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) {
+ await SpecialPowers.pushPrefEnv({
+ // If Fission is disabled, the pref is no-op.
+ set: [["fission.bfcacheInParent", true]],
+ });
+
+ 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(Services.io.newURI(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..68b2b07f3c
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_urlbar.js
@@ -0,0 +1,438 @@
+/* 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 }
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+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 isEventForResultButton(event) {
+ let parent = event.accessible.parent;
+ return (
+ event.accessible.role == ROLE_PUSHBUTTON &&
+ parent?.role == ROLE_COMBOBOX_LIST
+ );
+}
+
+/**
+ * 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([
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example1.com/blah",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example2.com/blah",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example1.com/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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);
+ }
+
+ if (
+ AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
+ ) {
+ // With native context menus, we do not observe accessibility events and we
+ // cannot send synthetic key events to the menu.
+ info("Opening and closing context native context menu");
+ let contextMenu = gURLBar.querySelector("menupopup");
+ let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
+ type: "contextmenu",
+ });
+ await popupshown;
+ let popuphidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ await popuphidden;
+ } else {
+ info(
+ "Ensuring context menu gets menu event on launch, and item focus on down"
+ );
+ let menuEvent = waitForEvent(
+ nsIAccessibleEvent.EVENT_MENUPOPUP_START,
+ isEventForMenuPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
+ type: "contextmenu",
+ });
+ 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;
+ }
+ info("Ensuring address bar is focused after context menu is dismissed.");
+ 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,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ helpUrl: "http://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { 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, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the help button is focused on tab");
+ info("Also ensuring that the help button is a part of a labelled group");
+ focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_Tab");
+ 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 shift+tab");
+ focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ 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_panel.js b/accessible/tests/browser/events/browser_test_panel.js
new file mode 100644
index 0000000000..8a4ab89705
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_panel.js
@@ -0,0 +1,54 @@
+/* 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 });
+
+// Verify we receive hide and show notifications when the chrome
+// XUL alert is closed or opened. Mac expects both notifications to
+// properly communicate live region changes.
+async function runTests(browser) {
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ // When available, the popup panel makes itself a child of the chrome window.
+ // To verify it isn't accessible without reproducing the entirety of the chrome
+ // window tree, we check instead that the panel is not accessible.
+ ok(!isAccessible(PopupNotifications.panel), "Popup panel is not accessible");
+
+ const panelShown = waitForEvent(EVENT_SHOW, PopupNotifications.panel);
+ const notification = PopupNotifications.show(
+ browser,
+ "test-notification",
+ "hello world",
+ PopupNotifications.panel.id
+ );
+
+ await panelShown;
+
+ ok(isAccessible(PopupNotifications.panel), "Popup panel is accessible");
+ testAccessibleTree(PopupNotifications.panel, {
+ ALERT: [
+ { LABEL: [{ TEXT_LEAF: [] }] },
+ { PUSHBUTTON: [] },
+ { PUSHBUTTON: [] },
+ ],
+ });
+ // Verify the popup panel is associated with the chrome window.
+ is(
+ PopupNotifications.panel.ownerGlobal,
+ getMainChromeWindow(window),
+ "Popup panel is associated with the chrome window"
+ );
+
+ const panelHidden = waitForEvent(EVENT_HIDE, PopupNotifications.panel);
+ PopupNotifications.remove(notification);
+
+ await panelHidden;
+
+ ok(!isAccessible(PopupNotifications.panel), "Popup panel is not accessible");
+}
+
+addAccessibleTask(``, runTests);
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..9678ee767b
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_scrolling.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";
+
+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>
+ <textarea id="textarea" rows="1">a
+b
+c</textarea>
+ `,
+ 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"
+ );
+
+ // textarea scrolling
+ info("Moving textarea caret to c");
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, "textarea"],
+ [EVENT_SCROLLING_END, "textarea"],
+ ]);
+ await invokeContentTask(browser, [], () => {
+ const textareaDom = content.document.getElementById("textarea");
+ textareaDom.focus();
+ textareaDom.selectionStart = 4;
+ });
+ await onScrolling;
+ }
+);
+
+// Verify that the scrolling start event is fired for an anchor change.
+addAccessibleTask(
+ `
+ <p>a</p>
+ <p>b</p>
+ <p id="c">c</p>
+ `,
+ async function (browser, accDoc) {
+ let onScrollingStart = waitForEvent(EVENT_SCROLLING_START, "c");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#c";
+ });
+ await onScrollingStart;
+ },
+ { chrome: true, topLevel: true }
+);
+
+// Ensure that a scrollable, focused non-interactive element receives a
+// scrolling start event when an anchor jump to that element is triggered.
+addAccessibleTask(
+ `
+<div style="height: 100vh; width: 100vw; overflow: auto;" id="scrollable">
+ <h1 style="height: 300%;" id="inside-scrollable">test</h1>
+</div>
+ `,
+ async function (browser, accDoc) {
+ let onScrollingStart = waitForEvent(
+ EVENT_SCROLLING_START,
+ "inside-scrollable"
+ );
+ await invokeContentTask(browser, [], () => {
+ const scrollable = content.document.getElementById("scrollable");
+ scrollable.focus();
+ content.location.hash = "#inside-scrollable";
+ });
+ await onScrollingStart;
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
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..8f8fdb92f7
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_selection_urlbar.js
@@ -0,0 +1,61 @@
+/* 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 });
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+// 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();
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "addons",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: Services.io.newURI("http://www.addons.mozilla.org/"),
+ });
+
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(newWin);
+ await PlacesUtils.bookmarks.remove(bookmark);
+ });
+ 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..e0f6f1a176
--- /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.startLoadingURIString(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..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/events/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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.toml b/accessible/tests/browser/fission/browser.toml
new file mode 100644
index 0000000000..0332573db9
--- /dev/null
+++ b/accessible/tests/browser/fission/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_content_tree.js"]
+
+["browser_hidden_iframe.js"]
+https_first_disabled = true
+
+["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..1592ae6a1a
--- /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..61414b611d
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_hidden_iframe.js
@@ -0,0 +1,70 @@
+/* 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 not be accessible");
+
+ info(
+ "Show the IFRAME and check that it's now available in the accessibility tree."
+ );
+
+ const events = [[EVENT_REORDER, contentDocAcc]];
+
+ 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);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+
+ // Wait for the child iframe to layout itself. This can happen during or
+ // after the reorder event, depending on timing.
+ iframeDocAcc = await TestUtils.waitForCondition(() => {
+ return findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID);
+ });
+
+ 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..d6600a2d5e
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_nested_iframe.js
@@ -0,0 +1,164 @@
+/* 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";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+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) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ let nestedDocAcc = findAccessibleChildByID(
+ iframeDocAcc,
+ NESTED_IFRAME_DOC_BODY_ID
+ );
+ let waitForNestedDocLoad = false;
+ if (nestedDocAcc) {
+ const state = {};
+ nestedDocAcc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ info("Nested IFRAME document accessible is present but busy");
+ waitForNestedDocLoad = true;
+ } else {
+ ok(true, "Nested IFRAME document accessible is present and ready");
+ }
+ } else {
+ info("Nested IFRAME document accessible is not present yet");
+ waitForNestedDocLoad = true;
+ }
+ if (waitForNestedDocLoad) {
+ info("Waiting for doc load complete on nested iframe document");
+ 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..d7123109f4
--- /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..8aee02037c
--- /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..e97cda3cc3
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_src_change.js
@@ -0,0 +1,62 @@
+/* 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,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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..7ec037566d
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_take_focus.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/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ `<div role="group"><input id="textbox" value="hello"/></div>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ const textbox = findAccessibleChildByID(iframeDocAcc, "textbox");
+ const iframe = findAccessibleChildByID(contentDocAcc, "default-iframe-id");
+ const iframeDoc = findAccessibleChildByID(
+ contentDocAcc,
+ "default-iframe-body-id"
+ );
+ const root = getRootAccessible(document);
+
+ testStates(textbox, STATE_FOCUSABLE, 0, STATE_FOCUSED);
+
+ let onFocus = waitForEvent(EVENT_FOCUS, textbox);
+ textbox.takeFocus();
+ await onFocus;
+
+ testStates(textbox, STATE_FOCUSABLE | STATE_FOCUSED, 0);
+
+ is(
+ getAccessibleDOMNodeID(contentDocAcc.focusedChild),
+ "textbox",
+ "correct focusedChild from top doc"
+ );
+
+ is(
+ getAccessibleDOMNodeID(iframeDocAcc.focusedChild),
+ "textbox",
+ "correct focusedChild from iframe"
+ );
+
+ is(
+ getAccessibleDOMNodeID(root.focusedChild),
+ "textbox",
+ "correct focusedChild from root"
+ );
+
+ ok(!iframe.focusedChild, "correct focusedChild from iframe (null)");
+
+ onFocus = waitForEvent(EVENT_FOCUS, iframeDoc);
+ iframeDoc.takeFocus();
+ await onFocus;
+
+ is(
+ getAccessibleDOMNodeID(contentDocAcc.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from top doc"
+ );
+ is(
+ getAccessibleDOMNodeID(iframe.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from iframe"
+ );
+ is(
+ getAccessibleDOMNodeID(root.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from root"
+ );
+ },
+ { 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..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/fission/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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.toml b/accessible/tests/browser/general/browser.toml
new file mode 100644
index 0000000000..95c66e0904
--- /dev/null
+++ b/accessible/tests/browser/general/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/browser/shared-head.js",
+ "head.js",
+ "!/accessible/tests/mochitest/*.js",
+]
+skip-if = ["a11y_checks"]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["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..5cfd66ee3a
--- /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.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+// 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..841436043c
--- /dev/null
+++ b/accessible/tests/browser/general/head.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";
+
+/* exported initAccessibilityService, openNewTab, shutdownAccessibilityService */
+
+// Load the shared-head file first.
+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..2e1ab7f897
--- /dev/null
+++ b/accessible/tests/browser/head.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";
+
+/* exported initAccService, shutdownAccService, waitForEvent, accConsumersChanged */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+);
+
+/**
+ * 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.toml b/accessible/tests/browser/hittest/browser.toml
new file mode 100644
index 0000000000..7f82a6ff49
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_browser.js"]
+
+["browser_test_general.js"]
+
+["browser_test_scroll_hittest.js"]
+
+["browser_test_shadowroot.js"]
+
+["browser_test_text.js"]
+
+["browser_test_zoom.js"]
+
+["browser_test_zoom_text.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_general.js b/accessible/tests/browser/hittest/browser_test_general.js
new file mode 100644
index 0000000000..ca3a879c18
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_general.js
@@ -0,0 +1,425 @@
+/* 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);
+
+ await 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."
+ );
+
+ const txt = findAccessibleChildByID(accDoc, "txt");
+ await testChildAtPoint(dpr, 1, 1, txt, txt, txt);
+
+ info(
+ "::MustPrune case, point is outside of textbox accessible but is in document."
+ );
+ await testChildAtPoint(dpr, -1, -1, txt, null, null);
+
+ info("::MustPrune case, point is outside of root accessible.");
+ await testChildAtPoint(dpr, -10000, -10000, txt, null, null);
+
+ info("Not specific case, point is inside of btn accessible.");
+ const btn = findAccessibleChildByID(accDoc, "btn");
+ await testChildAtPoint(dpr, 1, 1, btn, btn, btn);
+
+ info("Not specific case, point is outside of btn accessible.");
+ await 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.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+ );
+ 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");
+ await testChildAtPoint(dpr, 1, 1, area, area, area);
+
+ info("Test image maps. Their children are not in the layout tree.");
+ await waitForImageMap(browser, accDoc);
+ const imgmap = findAccessibleChildByID(accDoc, "imgmap");
+ ok(imgmap, "Image map exists");
+ 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
+ );
+
+ info("hittesting table, row, cells -- rows are not in the layout tree");
+ const table = findAccessibleChildByID(accDoc, "table");
+ const row = findAccessibleChildByID(accDoc, "row");
+ const cell1 = findAccessibleChildByID(accDoc, "cell1");
+
+ await hitTest(browser, table, row, cell1);
+
+ info("Testing that an inaccessible child doesn't break hit testing");
+ const containerWithInaccessibleChild = findAccessibleChildByID(
+ accDoc,
+ "containerWithInaccessibleChild"
+ );
+ const containerWithInaccessibleChildP2 = findAccessibleChildByID(
+ accDoc,
+ "containerWithInaccessibleChild_p2"
+ );
+ await hitTest(
+ browser,
+ containerWithInaccessibleChild,
+ containerWithInaccessibleChildP2,
+ containerWithInaccessibleChildP2.firstChild
+ );
+
+ info("Testing wrapped text");
+ const wrappedTextLinkFirstP = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLinkFirstP"
+ );
+ const wrappedTextLinkFirstA = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLinkFirstA"
+ );
+ await hitTest(
+ browser,
+ wrappedTextLinkFirstP,
+ wrappedTextLinkFirstA,
+ wrappedTextLinkFirstA.firstChild
+ );
+ const wrappedTextLeafFirstP = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLeafFirstP"
+ );
+ const wrappedTextLeafFirstMark = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLeafFirstMark"
+ );
+ await hitTest(
+ browser,
+ wrappedTextLeafFirstP,
+ wrappedTextLeafFirstMark,
+ wrappedTextLeafFirstMark.firstChild
+ );
+
+ info("Testing image");
+ const imageP = findAccessibleChildByID(accDoc, "imageP");
+ const image = findAccessibleChildByID(accDoc, "image");
+ await hitTest(browser, imageP, image, image);
+
+ info("Testing image map with 0-sized area");
+ const mapWith0AreaP = findAccessibleChildByID(accDoc, "mapWith0AreaP");
+ const mapWith0Area = findAccessibleChildByID(accDoc, "mapWith0Area");
+ await hitTest(browser, mapWith0AreaP, mapWith0Area, mapWith0Area);
+}
+
+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>
+
+ <table id="table" border>
+ <tr id="row">
+ <td id="cell1">hello</td>
+ <td id="cell2">world</td>
+ </tr>
+ </table>
+
+ <div id="containerWithInaccessibleChild">
+ <p>hi</p>
+ <p aria-hidden="true">hi</p>
+ <p id="containerWithInaccessibleChild_p2">bye</p>
+ </div>
+
+ <p id="wrappedTextLinkFirstP" style="width: 3ch; font-family: monospace;">
+ <a id="wrappedTextLinkFirstA" href="https://example.com/">a</a>b cd
+ </p>
+
+ <p id="wrappedTextLeafFirstP" style="width: 3ch; font-family: monospace;">
+ <mark id="wrappedTextLeafFirstMark">a</mark><a href="https://example.com/">b cd</a>
+ </p>
+
+ <p id="imageP">
+ <img id="image" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+ </p>
+
+ <map id="0Area">
+ <area shape="rect">
+ </map>
+ <p id="mapWith0AreaP">
+ <img id="mapWith0Area" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif" usemap="#0Area">
+ </p>
+ `,
+ runTests,
+ {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+addAccessibleTask(
+ `
+ <div id="container">
+ <h1 id="a">A</h1><h1 id="b">B</h1>
+ </div>
+ `,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ const b = findAccessibleChildByID(accDoc, "b");
+ const dpr = await getContentDPR(browser);
+ // eslint-disable-next-line no-unused-vars
+ const [x, y, w, h] = Layout.getBounds(a, dpr);
+ // The point passed below will be made relative to `b`, but
+ // we'd like to test a point within `a`. Pass `a`s negative
+ // width for an x offset. Pass zero as a y offset,
+ // assuming the headings are on the same line.
+ await testChildAtPoint(dpr, -w, 0, b, null, null);
+ },
+ {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+addAccessibleTask(
+ `
+ <style>
+ div {
+ width: 50px;
+ height: 50px;
+ position: relative;
+ }
+
+ div > div {
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ opacity: 0.9;
+ }
+ </style>
+ <div id="a" style="background-color: orange;">
+ <div id="aa" style="background-color: purple;"></div>
+ </div>
+ <div id="b" style="background-color: yellowgreen;">
+ <div id="bb" style="top: -30px; background-color: turquoise"></div>
+ </div>`,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ const aa = findAccessibleChildByID(accDoc, "aa");
+ const dpr = await getContentDPR(browser);
+ const [, , w, h] = Layout.getBounds(a, dpr);
+ // test upper left of `a`
+ await testChildAtPoint(dpr, 1, 1, a, aa, aa);
+ // test upper right of `a`
+ await testChildAtPoint(dpr, w - 1, 1, a, a, a);
+ // test just outside upper left of `a`
+ await testChildAtPoint(dpr, 1, -1, a, null, null);
+ // test halfway down/left of `a`
+ await testChildAtPoint(dpr, 1, Math.round(h / 2), a, a, a);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: false,
+ remoteIframe: false,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+/**
+ * Verify that hit testing returns the proper accessible when one acc content
+ * is partially hidden due to overflow:hidden;
+ */
+addAccessibleTask(
+ `
+ <style>
+ div div {
+ overflow: hidden;
+ font-family: monospace;
+ width: 2ch;
+ }
+ </style>
+ <div id="container" style="display: flex; flex-direction: row-reverse;">
+ <div id="aNode">abcde</div><div id="fNode">fghij</div>
+ </div>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const aNode = findAccessibleChildByID(docAcc, "aNode");
+ const fNode = findAccessibleChildByID(docAcc, "fNode");
+ const dpr = await getContentDPR(browser);
+ const [, , containerWidth] = Layout.getBounds(container, dpr);
+ const [, , aNodeWidth] = Layout.getBounds(aNode, dpr);
+
+ await testChildAtPoint(
+ dpr,
+ containerWidth - 1,
+ 1,
+ container,
+ aNode,
+ aNode.firstChild
+ );
+ await testChildAtPoint(
+ dpr,
+ containerWidth - aNodeWidth - 1,
+ 1,
+ container,
+ fNode,
+ fNode.firstChild
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing is appropriately fuzzy when working with generics.
+ * If we match on a generic which contains additional generics and a single text
+ * leaf, we should return the text leaf as the deepest match instead of the
+ * generic itself.
+ */
+addAccessibleTask(
+ `
+ <a href="example.com" id="link">
+ <span style="overflow:hidden;" id="generic"><span aria-hidden="true" id="visible">I am some visible text</span><span id="invisible" style="overflow:hidden; height: 1px; width: 1px; position:absolute; clip: rect(0 0 0 0); display:block;">I am some invisible text</span></span>
+ </a>`,
+ async function (browser, docAcc) {
+ const link = findAccessibleChildByID(docAcc, "link");
+ const generic = findAccessibleChildByID(docAcc, "generic");
+ const invisible = findAccessibleChildByID(docAcc, "invisible");
+ const dpr = await getContentDPR(browser);
+
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ link,
+ generic, // Direct Child
+ invisible.firstChild // Deepest Child
+ );
+
+ await testOffsetAtPoint(
+ findAccessibleChildByID(docAcc, "invisible", [Ci.nsIAccessibleText]),
+ 1,
+ 1,
+ COORDTYPE_PARENT_RELATIVE,
+ 0
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing is appropriately fuzzy when working with generics with siblings.
+ * We should return the deepest text leaf as the deepest match instead of the generic itself.
+ */
+addAccessibleTask(
+ `
+<div id="generic"><span aria-hidden="true" id="visible">Mozilla</span><span id="invisible" style="display: block !important;border: 0 !important;clip: rect(0 0 0 0) !important;height: 1px !important;margin: -1px !important;overflow: hidden !important;padding: 0 !important;position: absolute !important;white-space: nowrap !important;width: 1px !important;">hello world<br><div id="extraContainer">Mozilla</div></span><br>I am some other text</div>`,
+ async function (browser, docAcc) {
+ const generic = findAccessibleChildByID(docAcc, "generic");
+ const invisible = findAccessibleChildByID(docAcc, "invisible");
+ const dpr = await getContentDPR(browser);
+
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ generic,
+ invisible, // Direct Child
+ invisible.firstChild // Deepest Child
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing correctly ignores
+ * elements with pointer-events: none;
+ */
+addAccessibleTask(
+ `<div id="container" style="position:relative;"><button id="obscured">click me</button><div id="overlay" style="pointer-events:none; top:0; bottom:0; left:0; right:0; position: absolute;"></div></div><button id="clickable">I am clickable</button>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const obscured = findAccessibleChildByID(docAcc, "obscured");
+ const clickable = findAccessibleChildByID(docAcc, "clickable");
+ const dpr = await getContentDPR(browser);
+ let [targetX, targetY, targetW, targetH] = Layout.getBounds(obscured, dpr);
+ const [x, y] = Layout.getBounds(docAcc, dpr);
+ await testChildAtPoint(
+ dpr,
+ targetX - x + targetW / 2,
+ targetY - y + targetH / 2,
+ docAcc,
+ container, // Direct Child
+ obscured // Deepest Child
+ );
+
+ [targetX, targetY, targetW, targetH] = Layout.getBounds(clickable, dpr);
+ await testChildAtPoint(
+ dpr,
+ targetX - x + targetW / 2,
+ targetY - y + targetH / 2,
+ docAcc,
+ clickable, // Direct Child
+ clickable // Deepest Child
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_scroll_hittest.js b/accessible/tests/browser/hittest/browser_test_scroll_hittest.js
new file mode 100644
index 0000000000..246dcd3d09
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_scroll_hittest.js
@@ -0,0 +1,105 @@
+/* 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";
+
+/**
+ * Verify that hit testing returns the proper accessible when one accessible
+ * covers another accessible due to scroll clipping. See Bug 1819741.
+ */
+addAccessibleTask(
+ `
+<div id="container" style="height: 100%; position: absolute; flex-direction: column; display: flex;">
+ <div id="title-bar" style="height: 500px; background-color: red;"></div>
+ <div id="message-container" style="overflow-y: hidden; display: flex;">
+ <div style="overflow-y: auto;" id="message-scrollable">
+ <p style="white-space: pre-line;">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dictum luctus molestie. Nam in libero mi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
+
+ Praesent aliquet semper libero, eu ullamcorper tortor vestibulum ac. Sed non pharetra sem. Quisque sodales ipsum a ipsum condimentum porttitor. Integer luctus pellentesque ipsum, eu dignissim nunc fermentum in.
+
+ Etiam blandit nisl vitae dolor molestie faucibus. In euismod, massa vitae commodo bibendum, urna augue pharetra nibh, et sagittis libero est in ligula. Mauris tincidunt risus ornare, rutrum augue in, blandit ligula. Aenean ultrices vel risus sit amet varius.
+
+ Vivamus pretium ultricies nisi a cursus. Integer cursus quam a metus ultricies, vel pulvinar nunc varius. Quisque facilisis lorem eget ipsum vehicula, laoreet congue lorem viverra.
+
+ Praesent dignissim, diam sed semper ultricies, diam ex laoreet justo, ac euismod massa metus pharetra nunc. Vestibulum sapien erat, consequat at eleifend id, suscipit sit amet mi.
+
+ Curabitur sed mauris vitae justo rutrum convallis ac sed justo. Ut nec est sed nisi feugiat egestas. Mauris accumsan mi eget nibh fermentum, in dignissim odio feugiat.
+
+ Maecenas augue dolor, gravida ut ultrices ultricies, condimentum et dui. In sed augue fermentum, posuere velit et, pulvinar tellus. Morbi id fermentum quam, at varius arcu.
+
+ Duis elementum vitae sapien id tincidunt. Aliquam velit ligula, sollicitudin eget placerat non, aliquam at erat. Pellentesque non porta metus. Mauris vel finibus sem, nec ullamcorper leo.
+
+ Nulla sit amet lorem vitae diam consectetur porttitor a cursus massa. Sed id ornare lorem. Sed placerat facilisis ipsum et ultricies. Sed eu semper enim, ut aliquet odio.
+
+ Sed nulla ex, pharetra vel porttitor congue, dictum et purus. Suspendisse vel risus sit amet nulla volutpat ullamcorper. Morbi et ullamcorper est. Pellentesque eget porta risus. Nullam non felis elementum, auctor massa et, consectetur neque.
+
+ Fusce sit amet arcu finibus, ornare sem sed, tempus nibh. Donec rutrum odio eget bibendum pulvinar. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
+
+ Phasellus sed risus diam. Vivamus mollis, risus ac feugiat pellentesque, ligula tortor finibus libero, et venenatis turpis lectus et justo. Suspendisse euismod mi at lectus sagittis dignissim. Mauris a ornare enim.
+ </p>
+ </div>
+ </div>
+ <div id="footer-bar" style="height: 500px; background-color: blue;"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const scrollable = findAccessibleChildByID(docAcc, "message-scrollable");
+ const titleBar = findAccessibleChildByID(docAcc, "title-bar");
+ const footerBar = findAccessibleChildByID(docAcc, "footer-bar");
+ const dpr = await getContentDPR(browser);
+ const [, , , titleBarHeight] = Layout.getBounds(titleBar, dpr);
+ const [, , , scrollableHeight] = Layout.getBounds(scrollable, dpr);
+
+ // Verify that the child at this point is not the underlying paragraph.
+ info(
+ "Testing that the deepest child at this point is the overlaid section, not the paragraph beneath it."
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight - 1,
+ container,
+ titleBar,
+ titleBar
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight + scrollableHeight + 1,
+ container,
+ footerBar,
+ footerBar
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // Scroll the text down.
+ let elem = content.document.getElementById("message-scrollable");
+ elem.scrollTo(0, elem.scrollHeight);
+ });
+ await waitForContentPaint(browser);
+
+ info(
+ "Testing that the deepest child at this point is still the overlaid section, after scrolling the paragraph."
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight - 1,
+ container,
+ titleBar,
+ titleBar
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight + scrollableHeight + 1,
+ container,
+ footerBar,
+ footerBar
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
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..ed67b02349
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_shadowroot.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";
+
+async function runTests(browser, accDoc) {
+ const dpr = await getContentDPR(browser);
+ let componentAcc = findAccessibleChildByID(accDoc, "component1");
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ componentAcc,
+ componentAcc.firstChild,
+ componentAcc.firstChild
+ );
+
+ componentAcc = findAccessibleChildByID(accDoc, "component2");
+ await 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)
+ shadow.innerHTML = child.data;
+ }
+ }
+ </script>
+ `,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_text.js b/accessible/tests/browser/hittest/browser_test_text.js
new file mode 100644
index 0000000000..130f077f29
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_text.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";
+
+addAccessibleTask(
+ `
+a
+<div id="noChars" style="width: 5px; height: 5px;"><p></p></div>
+<p id="twoText"><span>a</span><span>b</span></p>
+<div id="iframeAtEnd" style="width: 20px; height: 20px;">
+ a
+ <iframe width="1" height="1"></iframe>
+</div>
+<button id="pointBeforeText">
+ <div style="display: flex;">
+ <div style="width: 100px; background-color: red;" role="none"></div>
+ test
+ <div style="width: 100px; background-color: blue;" role="none"></div>
+ </div>
+</button>
+ `,
+ async function (browser, docAcc) {
+ const dpr = await getContentDPR(browser);
+ // Test getOffsetAtPoint on a container containing no characters. The inner
+ // container does not include the requested point, but the outer one does.
+ const noChars = findAccessibleChildByID(docAcc, "noChars", [
+ Ci.nsIAccessibleText,
+ ]);
+ let [x, y] = Layout.getBounds(noChars, dpr);
+ await testOffsetAtPoint(noChars, x, y, COORDTYPE_SCREEN_RELATIVE, -1);
+
+ // Test that the correct offset is returned for a point in a second text
+ // leaf.
+ const twoText = findAccessibleChildByID(docAcc, "twoText", [
+ Ci.nsIAccessibleText,
+ ]);
+ const text2 = twoText.getChildAt(1);
+ [x, y] = Layout.getBounds(text2, dpr);
+ await testOffsetAtPoint(twoText, x, y, COORDTYPE_SCREEN_RELATIVE, 1);
+
+ // Test offsetAtPoint when there is an iframe at the end of the container.
+ const iframeAtEnd = findAccessibleChildByID(docAcc, "iframeAtEnd", [
+ Ci.nsIAccessibleText,
+ ]);
+ let width;
+ let height;
+ [x, y, width, height] = Layout.getBounds(iframeAtEnd, dpr);
+ x += width - 1;
+ y += height - 1;
+ await testOffsetAtPoint(iframeAtEnd, x, y, COORDTYPE_SCREEN_RELATIVE, -1);
+
+ // Test that 0 is returned if the point is within the container but before
+ // the rectangle at offset 0. This is buggy behavior that some users have
+ // unfortunately come to rely on (bug 1816601).
+ const pointBeforeText = findAccessibleChildByID(docAcc, "pointBeforeText", [
+ Ci.nsIAccessibleText,
+ ]);
+ [x, y, width, height] = Layout.getBounds(pointBeforeText, dpr);
+ await testOffsetAtPoint(
+ pointBeforeText,
+ x + 1,
+ y + 1,
+ COORDTYPE_SCREEN_RELATIVE,
+ 0
+ );
+ // But this buggy behavior only applies for a point before offset 0, not
+ // a point after the last offset. So it's asymmetrically buggy. :(
+ await testOffsetAtPoint(
+ pointBeforeText,
+ x + width - 1,
+ y + height - 1,
+ COORDTYPE_SCREEN_RELATIVE,
+ -1
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: 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..84383df483
--- /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.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ 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..9e429c16b3
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_zoom_text.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";
+
+async function runTests(browser, accDoc) {
+ const expectedLength = await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+ );
+ const hyperText = CommonUtils.getNode("paragraph", content.document);
+ return Math.floor(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)
+ );
+
+ await testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ 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)
+ );
+
+ await testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+}
+
+addAccessibleTask(
+ `<p id="paragraph" style="font-family: monospace;">hello world hello world</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..c2904b1578
--- /dev/null
+++ b/accessible/tests/browser/hittest/head.js
@@ -0,0 +1,113 @@
+/* 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 CommonUtils, testChildAtPoint, Layout, hitTest, testOffsetAtPoint */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+);
+
+const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+);
+
+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.
+ }
+ info("could not get child at point");
+ return null;
+}
+
+async function testChildAtPoint(dpr, x, y, container, child, grandChild) {
+ const [containerX, containerY] = Layout.getBounds(container, dpr);
+ x += containerX;
+ y += containerY;
+ let actual = null;
+ await untilCacheIs(
+ () => {
+ actual = getChildAtPoint(container, x, y, false);
+ info(`Got direct child match of ${CommonUtils.prettyName(actual)}`);
+ return actual;
+ },
+ child,
+ `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(
+ child
+ )} and got ${CommonUtils.prettyName(actual)}`
+ );
+ actual = null;
+ await untilCacheIs(
+ () => {
+ actual = getChildAtPoint(container, x, y, true);
+ info(`Got deepest child match of ${CommonUtils.prettyName(actual)}`);
+ return actual;
+ },
+ grandChild,
+ `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(
+ grandChild
+ )} and got ${CommonUtils.prettyName(actual)}`
+ );
+}
+
+/**
+ * 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;
+
+ await untilCacheIs(
+ () => getChildAtPoint(container, x, y, false),
+ child,
+ `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(child)}`
+ );
+ await untilCacheIs(
+ () => getChildAtPoint(container, x, y, true),
+ grandChild,
+ `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(grandChild)}`
+ );
+}
+
+/**
+ * Test if getOffsetAtPoint returns the given text offset at given coordinates.
+ */
+async function testOffsetAtPoint(hyperText, x, y, coordType, expectedOffset) {
+ await untilCacheIs(
+ () => hyperText.getOffsetAtPoint(x, y, coordType),
+ expectedOffset,
+ `Wrong offset at given point (${x}, ${y}) for ${prettyName(hyperText)}`
+ );
+}
diff --git a/accessible/tests/browser/mac/browser.toml b/accessible/tests/browser/mac/browser.toml
new file mode 100644
index 0000000000..b180a42ab7
--- /dev/null
+++ b/accessible/tests/browser/mac/browser.toml
@@ -0,0 +1,95 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = ["os != 'mac'"]
+support-files = [
+ "head.js",
+ "doc_aria_tabs.html",
+ "doc_textmarker_test.html",
+ "doc_rich_listbox.xhtml",
+ "doc_menulist.xhtml",
+ "doc_tree.xhtml",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_app.js"]
+https_first_disabled = true
+
+["browser_aria_busy.js"]
+
+["browser_aria_controls_flowto.js"]
+
+["browser_aria_current.js"]
+
+["browser_aria_expanded.js"]
+
+["browser_aria_haspopup.js"]
+
+["browser_aria_setsize.js"]
+
+["browser_attributed_text.js"]
+
+["browser_bounds.js"]
+
+["browser_details_summary.js"]
+
+["browser_focus.js"]
+
+["browser_heading.js"]
+
+["browser_hierarchy.js"]
+
+["browser_input.js"]
+
+["browser_label_title.js"]
+
+["browser_link.js"]
+
+["browser_live_regions.js"]
+
+["browser_mathml.js"]
+
+["browser_menulist.js"]
+
+["browser_navigate.js"]
+
+["browser_outline.js"]
+
+["browser_outline_xul.js"]
+
+["browser_popupbutton.js"]
+
+["browser_radio_position.js"]
+
+["browser_range.js"]
+
+["browser_required.js"]
+
+["browser_rich_listbox.js"]
+
+["browser_roles_elements.js"]
+
+["browser_rootgroup.js"]
+
+["browser_rotor.js"]
+
+["browser_selectables.js"]
+
+["browser_table.js"]
+
+["browser_text_basics.js"]
+
+["browser_text_input.js"]
+skip-if = ["os == 'mac'"] # Bug 1778821
+
+["browser_text_leaf.js"]
+
+["browser_text_selection.js"]
+
+["browser_toggle_radio_check.js"]
+
+["browser_webarea.js"]
diff --git a/accessible/tests/browser/mac/browser_app.js b/accessible/tests/browser/mac/browser_app.js
new file mode 100644
index 0000000000..bedefae440
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_app.js
@@ -0,0 +1,355 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getMacAccessible(accOrElmOrID) {
+ return new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let acc = getAccessible(accOrElmOrID);
+ if (acc) {
+ clearInterval(intervalId);
+ resolve(
+ acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface)
+ );
+ }
+ }, 10);
+ });
+}
+
+/**
+ * Test a11yUtils announcements are exposed to VO
+ */
+add_task(async () => {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,"
+ );
+ const alert = document.getElementById("a11y-announcement");
+ ok(alert, "Found alert to send announcements");
+
+ const alerted = waitForMacEvent("AXAnnouncementRequested", (iface, data) => {
+ return data.AXAnnouncementKey == "hello world";
+ });
+
+ A11yUtils.announce({
+ raw: "hello world",
+ });
+ await alerted;
+ await BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test browser tabs
+ */
+add_task(async () => {
+ let newTabs = await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Two</title>"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Three</title>"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Four</title>"
+ ),
+ ]);
+
+ // Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs
+ is(gBrowser.tabs.length, 4, "We now have 4 open tabs");
+
+ let tablist = await getMacAccessible("tabbrowser-tabs");
+ is(
+ tablist.getAttributeValue("AXRole"),
+ "AXTabGroup",
+ "Correct role for tablist"
+ );
+
+ let tabMacAccs = tablist.getAttributeValue("AXTabs");
+ is(tabMacAccs.length, 4, "4 items in AXTabs");
+
+ let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+
+ let tab = selectedTabs[0];
+ is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
+ is(
+ tab.getAttributeValue("AXSubrole"),
+ "AXTabButton",
+ "Correct subrole for tab"
+ );
+ is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab");
+
+ let tabToSelect = tabMacAccs[2];
+ is(
+ tabToSelect.getAttributeValue("AXTitle"),
+ "Three",
+ "Correct title for tab"
+ );
+
+ let actions = tabToSelect.actionNames;
+ ok(true, actions);
+ ok(actions.includes("AXPress"), "Has switch action");
+
+ // When tab is clicked selection of tab group changes,
+ // and focus goes to the web area. Wait for both.
+ let evt = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXRole") == "AXWebArea"
+ ),
+ ]);
+ tabToSelect.performAction("AXPress");
+ await evt;
+
+ selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+ is(
+ selectedTabs[0].getAttributeValue("AXTitle"),
+ "Three",
+ "Correct title for tab"
+ );
+
+ // Close all open tabs
+ await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t)));
+});
+
+/**
+ * Test ignored invisible items in root
+ */
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:license",
+ },
+ async browser => {
+ let root = await getMacAccessible(document);
+ let rootChildCount = () => root.getAttributeValue("AXChildren").length;
+
+ // With no popups, the root accessible has 5 visible children:
+ // 1. Tab bar (#TabsToolbar)
+ // 2. Navigation bar (#nav-bar)
+ // 3. Content area (#tabbrowser-tabpanels)
+ // 4. Some fullscreen pointer grabber (#fullscreen-and-pointerlock-wrapper)
+ // 5. Accessibility announcements dialog (#a11y-announcement)
+ let baseRootChildCount = 5;
+ is(
+ rootChildCount(),
+ baseRootChildCount,
+ "Root with no popups has 5 children"
+ );
+
+ // Open a context menu
+ const menu = document.getElementById("contentAreaContextMenu");
+ if (
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
+ ) {
+ // Native context menu - do not expect accessibility notifications.
+ let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await popupshown;
+
+ is(
+ rootChildCount(),
+ baseRootChildCount,
+ "Native context menus do not show up in the root children"
+ );
+
+ // Close context menu
+ let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ menu.hidePopup();
+ await popuphidden;
+ } else {
+ // Non-native menu
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await waitForMacEvent("AXMenuOpened");
+
+ // Now root has 1 more child
+ is(rootChildCount(), baseRootChildCount + 1, "Root has 1 more child");
+
+ // Close context menu
+ let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ await closed;
+ }
+
+ // We're back to base child count
+ is(rootChildCount(), baseRootChildCount, "Root has original child count");
+
+ // Open site identity popup
+ document.getElementById("identity-icon-box").click();
+ const identityPopup = document.getElementById("identity-popup");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
+
+ // Now root has another child
+ is(rootChildCount(), baseRootChildCount + 1, "Root has another child");
+
+ // Close popup
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden");
+
+ // We're back to the base child count
+ is(rootChildCount(), baseRootChildCount, "Root has the base child count");
+ }
+ );
+});
+
+/**
+ * Tests for location bar
+ */
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com",
+ },
+ async browser => {
+ let input = await getMacAccessible("urlbar-input");
+ is(
+ input.getAttributeValue("AXValue"),
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ UrlbarTestUtils.trimURL("http://example.com"),
+ "Location bar has correct value"
+ );
+ }
+ );
+});
+
+/**
+ * Test context menu
+ */
+add_task(async () => {
+ if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
+ ok(true, "We cannot inspect native context menu contents; skip this test.");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: 'data:text/html,<a id="exampleLink" href="https://example.com">link</a>',
+ },
+ async browser => {
+ if (!Services.search.isInitialized) {
+ let aStatus = await Services.search.init();
+ Assert.ok(Components.isSuccessCode(aStatus));
+ Assert.ok(Services.search.isInitialized);
+ }
+
+ const hasContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ !!ContextualIdentityService.getPublicIdentities().length;
+ info(`${hasContainers ? "Do" : "Don't"} expect containers item.`);
+ const hasInspectA11y =
+ Services.prefs.getBoolPref("devtools.everOpened", false) ||
+ Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0;
+ info(`${hasInspectA11y ? "Do" : "Don't"} expect inspect a11y item.`);
+
+ // synthesize a right click on the link to open the link context menu
+ let menu = document.getElementById("contentAreaContextMenu");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#exampleLink",
+ { type: "contextmenu" },
+ browser
+ );
+ await waitForMacEvent("AXMenuOpened");
+
+ menu = await getMacAccessible(menu);
+ let menuChildren = menu.getAttributeValue("AXChildren");
+ const expectedChildCount = 12 + +hasContainers + +hasInspectA11y;
+ is(
+ menuChildren.length,
+ expectedChildCount,
+ `Context menu on link contains ${expectedChildCount} items.`
+ );
+ // items at indicies 3, 9, and 11 are the splitters when containers exist
+ // everything else should be a menu item, otherwise indicies of splitters are
+ // 3, 8, and 10
+ const splitterIndicies = hasContainers ? [4, 9, 11] : [3, 8, 10];
+ for (let i = 0; i < menuChildren.length; i++) {
+ if (splitterIndicies.includes(i)) {
+ is(
+ menuChildren[i].getAttributeValue("AXRole"),
+ "AXSplitter",
+ "found splitter in menu"
+ );
+ } else {
+ is(
+ menuChildren[i].getAttributeValue("AXRole"),
+ "AXMenuItem",
+ "found menu item in menu"
+ );
+ }
+ }
+
+ // check the containers sub menu in depth if it exists
+ if (hasContainers) {
+ is(
+ menuChildren[1].getAttributeValue("AXVisibleChildren"),
+ null,
+ "Submenu 1 has no visible chldren when hidden"
+ );
+
+ // focus the first submenu
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await waitForMacEvent("AXMenuOpened");
+
+ // after the submenu is opened, refetch it
+ menu = document.getElementById("contentAreaContextMenu");
+ menu = await getMacAccessible(menu);
+ menuChildren = menu.getAttributeValue("AXChildren");
+
+ // verify submenu-menuitem's attributes
+ is(
+ menuChildren[1].getAttributeValue("AXChildren").length,
+ 1,
+ "Submenu 1 has one child when open"
+ );
+ const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0];
+ is(
+ subMenu.getAttributeValue("AXRole"),
+ "AXMenu",
+ "submenu has role of menu"
+ );
+ const subMenuChildren = subMenu.getAttributeValue("AXChildren");
+ is(subMenuChildren.length, 4, "sub menu has 4 children");
+ is(
+ subMenu.getAttributeValue("AXVisibleChildren").length,
+ 4,
+ "submenu has 4 visible children"
+ );
+
+ // close context menu
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForMacEvent("AXMenuClosed");
+ }
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForMacEvent("AXMenuClosed");
+ }
+ );
+});
diff --git a/accessible/tests/browser/mac/browser_aria_busy.js b/accessible/tests/browser/mac/browser_aria_busy.js
new file mode 100644
index 0000000000..e75d334e29
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_busy.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-busy
+ */
+addAccessibleTask(
+ `<div id="section" role="group">Hello</div>`,
+ async (browser, accDoc) => {
+ let section = getNativeInterface(accDoc, "section");
+
+ ok(!section.getAttributeValue("AXElementBusy"), "section is not busy");
+
+ let busyChanged = waitForMacEvent("AXElementBusyChanged", "section");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("section")
+ .setAttribute("aria-busy", "true");
+ });
+ await busyChanged;
+
+ ok(section.getAttributeValue("AXElementBusy"), "section is busy");
+
+ busyChanged = waitForMacEvent("AXElementBusyChanged", "section");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("section")
+ .setAttribute("aria-busy", "false");
+ });
+ await busyChanged;
+
+ ok(!section.getAttributeValue("AXElementBusy"), "section is not busy");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_controls_flowto.js b/accessible/tests/browser/mac/browser_aria_controls_flowto.js
new file mode 100644
index 0000000000..5950a60399
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_controls_flowto.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test aria-controls
+ */
+addAccessibleTask(
+ `<button aria-controls="info" id="info-button">Show info</button>
+ <div id="info">Information.</div>
+ <div id="more-info">More information.</div>`,
+ async (browser, accDoc) => {
+ const getAriaControls = id =>
+ JSON.stringify(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXARIAControls")
+ .map(e => e.getAttributeValue("AXDOMIdentifier"))
+ );
+
+ await untilCacheIs(
+ () => getAriaControls("info-button"),
+ JSON.stringify(["info"]),
+ "Info-button has correct initial controls"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-controls", "info more-info");
+ });
+
+ await untilCacheIs(
+ () => getAriaControls("info-button"),
+ JSON.stringify(["info", "more-info"]),
+ "Info-button has correct controls after mutation"
+ );
+ }
+);
+
+function getLinkedUIElements(accDoc, id) {
+ return JSON.stringify(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXLinkedUIElements")
+ .map(e => e.getAttributeValue("AXDOMIdentifier"))
+ );
+}
+
+/**
+ * Test aria-flowto
+ */
+addAccessibleTask(
+ `<button aria-flowto="info" id="info-button">Show info</button>
+ <div id="info">Information.</div>
+ <div id="more-info">More information.</div>`,
+ async (browser, accDoc) => {
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "info-button"),
+ JSON.stringify(["info"]),
+ "Info-button has correct initial linked elements"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-flowto", "info more-info");
+ });
+
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "info-button"),
+ JSON.stringify(["info", "more-info"]),
+ "Info-button has correct linked elements after mutation"
+ );
+ }
+);
+
+/**
+ * Test aria-controls
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat-radio" name="animal"><label for="cat">Cat</label>
+ <input type="radio" id="dog-radio" name="animal" aria-flowto="info"><label for="dog">Dog</label>
+ <div id="info">Information.</div>`,
+ async (browser, accDoc) => {
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "dog-radio"),
+ JSON.stringify(["cat-radio", "dog-radio", "info"]),
+ "dog-radio has correct linked elements"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_current.js b/accessible/tests/browser/mac/browser_aria_current.js
new file mode 100644
index 0000000000..02c7a71b67
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_current.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-current
+ */
+addAccessibleTask(
+ `<a id="one" href="%23" aria-current="page">One</a><a id="two" href="%23">Two</a>`,
+ async (browser, accDoc) => {
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ "page",
+ "Correct aria-current for #one"
+ );
+ is(
+ two.getAttributeValue("AXARIACurrent"),
+ null,
+ "Correct aria-current for #two"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("one")
+ .setAttribute("aria-current", "step");
+ });
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ "step",
+ "Correct aria-current for #one"
+ );
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("one").removeAttribute("aria-current");
+ });
+ await stateChanged;
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ null,
+ "Correct aria-current for #one"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_expanded.js b/accessible/tests/browser/mac/browser_aria_expanded.js
new file mode 100644
index 0000000000..48fb615266
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_expanded.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+// Test aria-expanded on a button
+addAccessibleTask(
+ `hello world<br>
+ <button aria-expanded="false" id="b">I am a button</button><br>
+ goodbye`,
+ async (browser, accDoc) => {
+ let button = getNativeInterface(accDoc, "b");
+ is(button.getAttributeValue("AXExpanded"), 0, "button is not expanded");
+
+ let stateChanged = Promise.all([
+ waitForStateChange("b", STATE_EXPANDED, true),
+ waitForStateChange("b", STATE_COLLAPSED, false),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("b")
+ .setAttribute("aria-expanded", "true");
+ });
+ await stateChanged;
+ is(button.getAttributeValue("AXExpanded"), 1, "button is expanded");
+
+ stateChanged = Promise.all([
+ waitForStateChange("b", STATE_EXPANDED, false),
+ waitForStateChange("b", EXT_STATE_EXPANDABLE, false, true),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("b").removeAttribute("aria-expanded");
+ });
+ await stateChanged;
+
+ ok(
+ !button.attributeNames.includes("AXExpanded"),
+ "button has no expanded attr"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_haspopup.js b/accessible/tests/browser/mac/browser_aria_haspopup.js
new file mode 100644
index 0000000000..57f1e50f65
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_haspopup.js
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-haspopup
+ */
+addAccessibleTask(
+ `
+ <button aria-haspopup="false" id="false">action</button>
+
+ <button aria-haspopup="menu" id="menu">action</button>
+
+ <button aria-haspopup="listbox" id="listbox">action</button>
+
+ <button aria-haspopup="tree" id="tree">action</button>
+
+ <button aria-haspopup="grid" id="grid">action</button>
+
+ <button aria-haspopup="dialog" id="dialog">action</button>
+
+ `,
+ async (browser, accDoc) => {
+ // FALSE
+ let falseID = getNativeInterface(accDoc, "false");
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button with false"
+ );
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue val for button with false"
+ );
+ let attrChanged = waitForEvent(EVENT_STATE_CHANGE, "false");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("false")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for false"
+ );
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with true"
+ );
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "false");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("false").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for false"
+ );
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button after remove"
+ );
+
+ // MENU
+ let menuID = getNativeInterface(accDoc, "menu");
+ is(
+ menuID.getAttributeValue("AXPopupValue"),
+ "menu",
+ "Correct AXPopupValue val for button with menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with menu"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("menu")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => menuID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with menu"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "menu");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("menu").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ await untilCacheIs(
+ () => menuID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button after remove"
+ );
+
+ // LISTBOX
+ let listboxID = getNativeInterface(accDoc, "listbox");
+ is(
+ listboxID.getAttributeValue("AXPopupValue"),
+ "listbox",
+ "Correct AXPopupValue for button with listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("listbox")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => listboxID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("listbox")
+ .removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ listboxID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ // TREE
+ let treeID = getNativeInterface(accDoc, "tree");
+ is(
+ treeID.getAttributeValue("AXPopupValue"),
+ "tree",
+ "Correct AXPopupValue for button with tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with tree"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("tree")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => treeID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with tree"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "tree");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("tree").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ treeID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with tree after remove"
+ );
+
+ // GRID
+ let gridID = getNativeInterface(accDoc, "grid");
+ is(
+ gridID.getAttributeValue("AXPopupValue"),
+ "grid",
+ "Correct AXPopupValue for button with grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with grid"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("grid")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => gridID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with grid"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "grid");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("grid").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ gridID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with grid after remove"
+ );
+
+ // DIALOG
+ let dialogID = getNativeInterface(accDoc, "dialog");
+ is(
+ dialogID.getAttributeValue("AXPopupValue"),
+ "dialog",
+ "Correct AXPopupValue for button with dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with dialog"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("dialog")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => dialogID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with dialog"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("dialog")
+ .removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ dialogID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with dialog after remove"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_setsize.js b/accessible/tests/browser/mac/browser_aria_setsize.js
new file mode 100644
index 0000000000..e37afb4e63
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_setsize.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test aria-setsize and aria-posinset
+ */
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number">
+ <div role="option" id="two" aria-setsize="5" aria-posinset="2">Two</div>
+ <div role="option" id="three" aria-setsize="5" aria-posinset="3">Three</div>
+ <div role="option" id="four" aria-setsize="5" aria-posinset="4">Four</div>
+ </div>
+ <p id="p">Hello</p>`,
+ async (browser, accDoc) => {
+ function testPosInSet(id, setsize, posinset) {
+ let elem = getNativeInterface(accDoc, id);
+ is(
+ elem.getAttributeValue("AXARIASetSize"),
+ setsize,
+ `${id}: Correct set size`
+ );
+
+ is(
+ elem.getAttributeValue("AXARIAPosInSet"),
+ posinset,
+ `${id}: Correct position in set`
+ );
+ }
+
+ testPosInSet("two", 5, 2);
+ testPosInSet("three", 5, 3);
+ testPosInSet("four", 5, 4);
+
+ let p = getNativeInterface(accDoc, "p");
+ ok(
+ !p.attributeNames.includes("AXARIASetSize"),
+ "Paragraph should not have AXARIASetSize"
+ );
+ ok(
+ !p.attributeNames.includes("AXARIAPosInSet"),
+ "Paragraph should not have AXARIAPosInSet"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_attributed_text.js b/accessible/tests/browser/mac/browser_attributed_text.js
new file mode 100644
index 0000000000..afd4d2c4dc
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_attributed_text.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test read-only attributed strings
+addAccessibleTask(
+ `<h1>hello <a href="#" id="a1">world</a></h1>
+ <p>this <b style="color: red; background-color: yellow;" aria-invalid="spelling">is</b> <span style="text-decoration: underline dotted green;">a</span> <a href="#" id="a2">test</a></p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [
+ macDoc.getAttributeValue("AXStartTextMarker"),
+ macDoc.getAttributeValue("AXEndTextMarker"),
+ ]
+ );
+
+ let attributedText = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ let attributesList = attributedText.map(
+ ({
+ string,
+ AXForegroundColor,
+ AXBackgroundColor,
+ AXUnderline,
+ AXUnderlineColor,
+ AXHeadingLevel,
+ AXFont,
+ AXLink,
+ AXMarkedMisspelled,
+ }) => [
+ string,
+ AXForegroundColor,
+ AXBackgroundColor,
+ AXUnderline,
+ AXUnderlineColor,
+ AXHeadingLevel,
+ AXFont.AXFontSize,
+ AXLink ? AXLink.getAttributeValue("AXDOMIdentifier") : null,
+ AXMarkedMisspelled,
+ ]
+ );
+
+ Assert.deepEqual(attributesList, [
+ // string, fg color, bg color, underline, underline color, heading level, font size, link id, misspelled
+ ["hello ", "#000000", "#ffffff", null, null, 1, 32, null, null],
+ ["world", "#0000ee", "#ffffff", 1, "#0000ee", 1, 32, "a1", null],
+ ["this ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["is", "#ff0000", "#ffff00", null, null, null, 16, null, 1],
+ [" ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["a", "#000000", "#ffffff", 1, "#008000", null, 16, null, null],
+ [" ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["test", "#0000ee", "#ffffff", 1, "#0000ee", null, 16, "a2", null],
+ ]);
+
+ // Test different NSRange parameters for AXAttributedStringForRange
+ let worldLeaf = findAccessibleChildByID(accDoc, "a1").firstChild;
+ let wordStaticText = worldLeaf.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ attributedText = wordStaticText.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(4, 1)
+ );
+ is(attributedText.length, 1, "Last character is in single attribute run");
+ is(attributedText[0].string, "d", "Last character matches");
+
+ attributedText = wordStaticText.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(5, 1)
+ );
+ is(attributedText.length, 0, "Range is past accessible bounds");
+ }
+);
+
+// Test misspelling in text area
+addAccessibleTask(
+ `<textarea id="t">hello worlf, i love you</textarea>`,
+ async (browser, accDoc) => {
+ let textArea = getNativeInterface(accDoc, "t");
+ let spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t");
+ textArea.setAttributeValue("AXFocused", true);
+
+ let attributedText = [];
+
+ // For some internal reason we get several text attribute change events
+ // before the attributed text returned provides the misspelling attributes.
+ while (true) {
+ await spellDone;
+
+ let range = textArea.getAttributeValue("AXVisibleCharacterRange");
+ attributedText = textArea.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(...range)
+ );
+
+ if (attributedText.length != 3) {
+ spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t");
+ } else {
+ break;
+ }
+ }
+
+ ok(attributedText[1].AXMarkedMisspelled);
+ }
+);
+
+// Test getting a span of attributed text that includes an empty input element.
+addAccessibleTask(
+ `hello <input id="input"> world<button></button>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [
+ macDoc.getAttributeValue("AXStartTextMarker"),
+ macDoc.getAttributeValue("AXEndTextMarker"),
+ ]
+ );
+
+ let attributedText = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ let text = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range
+ );
+
+ is(
+ attributedText.length,
+ 4,
+ "Should be 4 attribute runs for 2 texts, input and button"
+ );
+ is(attributedText[0].string, `hello `, "Attributed string is correct");
+ ok(
+ !attributedText[0].AXAttachment,
+ "Regular string attributes run doesn't have attachment"
+ );
+ is(
+ attributedText[1].AXAttachment.getAttributeValue("AXRole"),
+ "AXTextField",
+ "Entry text attribute run has correct attachment"
+ );
+ is(
+ attributedText[3].AXAttachment.getAttributeValue("AXRole"),
+ "AXButton",
+ "Button text attribute run has correct attachment"
+ );
+ is(text, `hello world${kEmbedChar}`, "Unattributed string is correct");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_bounds.js b/accessible/tests/browser/mac/browser_bounds.js
new file mode 100644
index 0000000000..09343d7c9d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_bounds.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test position, size for onscreen content
+ */
+addAccessibleTask(
+ `I am some extra content<br>
+ <div id="hello" style="display:inline;">hello</div><br>
+ <div id="world" style="display:inline;">hello world<br>I am some text</div>`,
+ async (browser, accDoc) => {
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null");
+ ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null");
+
+ // AXSize and AXPosition are composed of AXFrame components, so we
+ // test them here instead of calling AXFrame directly.
+ const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize");
+ const [worldWidth, worldHeight] = world.getAttributeValue("AXSize");
+ ok(helloWidth > 0, "Hello has a positive width");
+ ok(helloHeight > 0, "Hello has a positive height");
+ ok(worldWidth > 0, "World has a positive width");
+ ok(worldHeight > 0, "World has a positive height");
+ ok(helloHeight < worldHeight, "Hello has a smaller height than world");
+ ok(helloWidth < worldWidth, "Hello has a smaller width than world");
+
+ // Note: these are mac screen coords, so our origin is bottom left
+ const [helloX, helloY] = hello.getAttributeValue("AXPosition");
+ const [worldX, worldY] = world.getAttributeValue("AXPosition");
+ ok(helloX > 0, "Hello has a positive X");
+ ok(helloY > 0, "Hello has a positive Y");
+ ok(worldX > 0, "World has a positive X");
+ ok(worldY > 0, "World has a positive Y");
+ ok(helloY > worldY, "Hello has a larger Y than world");
+ ok(helloX == worldX, "Hello and world have the same X");
+ }
+);
+
+/**
+ * Test position, size for offscreen content
+ */
+addAccessibleTask(
+ `I am some extra content<br>
+ <div id="hello" style="display:inline; position:absolute; left:-2000px;">hello</div><br>
+ <div id="world" style="display:inline; position:absolute; left:-2000px;">hello world<br>I am some text</div>`,
+ async (browser, accDoc) => {
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null");
+ ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null");
+
+ // AXSize and AXPosition are composed of AXFrame components, so we
+ // test them here instead of calling AXFrame directly.
+ const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize");
+ const [worldWidth, worldHeight] = world.getAttributeValue("AXSize");
+ ok(helloWidth > 0, "Hello has a positive width");
+ ok(helloHeight > 0, "Hello has a positive height");
+ ok(worldWidth > 0, "World has a positive width");
+ ok(worldHeight > 0, "World has a positive height");
+ ok(helloHeight < worldHeight, "Hello has a smaller height than world");
+ ok(helloWidth < worldWidth, "Hello has a smaller width than world");
+
+ // Note: these are mac screen coords, so our origin is bottom left
+ const [helloX, helloY] = hello.getAttributeValue("AXPosition");
+ const [worldX, worldY] = world.getAttributeValue("AXPosition");
+ ok(helloX < 0, "Hello has a negative X");
+ ok(helloY > 0, "Hello has a positive Y");
+ ok(worldX < 0, "World has a negative X");
+ ok(worldY > 0, "World has a positive Y");
+ ok(helloY > worldY, "Hello has a larger Y than world");
+ ok(helloX == worldX, "Hello and world have the same X");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js
new file mode 100644
index 0000000000..6157707f79
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_details_summary.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test details/summary
+ */
+addAccessibleTask(
+ `<details id="details"><summary id="summary">Foo</summary><p>Bar</p></details>`,
+ async (browser, accDoc) => {
+ let details = getNativeInterface(accDoc, "details");
+ is(
+ details.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct role for details"
+ );
+ is(
+ details.getAttributeValue("AXSubrole"),
+ "AXDetails",
+ "Correct subrole for details"
+ );
+
+ let detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 1, "collapsed details has only one child");
+
+ let summary = detailsChildren[0];
+ is(
+ summary.getAttributeValue("AXRole"),
+ "AXButton",
+ "Correct role for summary"
+ );
+ is(
+ summary.getAttributeValue("AXSubrole"),
+ "AXSummary",
+ "Correct subrole for summary"
+ );
+ is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed");
+
+ let actions = summary.actionNames;
+ ok(actions.includes("AXPress"), "Summary Has press action");
+
+ let stateChanged = waitForStateChange("summary", STATE_EXPANDED, true);
+ summary.performAction("AXPress");
+ // The reorder gecko event notifies us of a tree change.
+ await stateChanged;
+ is(summary.getAttributeValue("AXExpanded"), 1, "Summary is expanded");
+
+ detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 2, "collapsed details has only one child");
+
+ stateChanged = waitForStateChange("summary", STATE_EXPANDED, false);
+ summary.performAction("AXPress");
+ // The reorder gecko event notifies us of a tree change.
+ await stateChanged;
+ is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed 2");
+
+ detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 1, "collapsed details has only one child");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_focus.js b/accessible/tests/browser/mac/browser_focus.js
new file mode 100644
index 0000000000..6bceb06c6c
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_focus.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test focusability
+ */
+addAccessibleTask(
+ `
+ <div role="button" id="ariabutton">hello</div> <button id="button">world</button>
+ `,
+ async (browser, accDoc) => {
+ let ariabutton = getNativeInterface(accDoc, "ariabutton");
+ let button = getNativeInterface(accDoc, "button");
+
+ is(
+ ariabutton.getAttributeValue("AXFocused"),
+ 0,
+ "aria button is not focused"
+ );
+
+ is(button.getAttributeValue("AXFocused"), 0, "button is not focused");
+
+ ok(
+ !ariabutton.isAttributeSettable("AXFocused"),
+ "aria button should not be focusable"
+ );
+
+ ok(button.isAttributeSettable("AXFocused"), "button is focusable");
+
+ let evt = waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXDOMIdentifier") == "button"
+ );
+
+ button.setAttributeValue("AXFocused", true);
+
+ await evt;
+
+ is(button.getAttributeValue("AXFocused"), 1, "button is focused");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_heading.js b/accessible/tests/browser/mac/browser_heading.js
new file mode 100644
index 0000000000..fd8c12883d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_heading.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test whether line break code in text content will be removed
+ * and extra whitespaces will be trimmed.
+ */
+addAccessibleTask(
+ `
+ <h1 id="single-line-content">We’re building a richer search experience</h1>
+ <h1 id="multi-lines-content">
+We’re building a
+richest
+search experience
+ </h1>
+ `,
+ async (browser, accDoc) => {
+ const singleLineContentHeading = getNativeInterface(
+ accDoc,
+ "single-line-content"
+ );
+ is(
+ singleLineContentHeading.getAttributeValue("AXTitle"),
+ "We’re building a richer search experience"
+ );
+
+ const multiLinesContentHeading = getNativeInterface(
+ accDoc,
+ "multi-lines-content"
+ );
+ is(
+ multiLinesContentHeading.getAttributeValue("AXTitle"),
+ "We’re building a richest search experience"
+ );
+ }
+);
+
+/**
+ * Test AXTitle/AXDescription attributes of heading elements
+ */
+addAccessibleTask(
+ `
+ <h1 id="a">Hello <a href="#">world</a></h1>
+ <h1 id="b">Hello</h1>
+ <h1 id="c" aria-label="Goodbye">Hello</h1>
+ `,
+ async (browser, accDoc) => {
+ const a = getNativeInterface(accDoc, "a");
+ is(
+ a.getAttributeValue("AXTitle"),
+ "Hello world",
+ "Correct AXTitle for 'a'"
+ );
+ ok(
+ !a.getAttributeValue("AXDescription"),
+ "'a' Should not have AXDescription"
+ );
+
+ const b = getNativeInterface(accDoc, "b");
+ is(b.getAttributeValue("AXTitle"), "Hello", "Correct AXTitle for 'b'");
+ ok(
+ !b.getAttributeValue("AXDescription"),
+ "'b' Should not have AXDescription"
+ );
+
+ const c = getNativeInterface(accDoc, "c");
+ is(
+ c.getAttributeValue("AXDescription"),
+ "Goodbye",
+ "Correct AXDescription for 'c'"
+ );
+ ok(!c.getAttributeValue("AXTitle"), "'c' Should not have AXTitle");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_hierarchy.js b/accessible/tests/browser/mac/browser_hierarchy.js
new file mode 100644
index 0000000000..8a97e55c07
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_hierarchy.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test AXIndexForChildUIElement
+ */
+addAccessibleTask(
+ `<p id="p">Hello <a href="#" id="link">strange</a> world`,
+ (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+
+ let children = p.getAttributeValue("AXChildren");
+ is(children.length, 3, "p has 3 children");
+ is(
+ children[1].getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "second child is link"
+ );
+
+ let index = p.getParameterizedAttributeValue(
+ "AXIndexForChildUIElement",
+ children[1]
+ );
+ is(index, 1, "link is second child");
+ }
+);
+
+/**
+ * Test textbox with more than one child
+ */
+addAccessibleTask(
+ `<div id="textbox" role="textbox">Hello <a href="#">strange</a> world</div>`,
+ (browser, accDoc) => {
+ let textbox = getNativeInterface(accDoc, "textbox");
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 3,
+ "textbox has 3 children"
+ );
+ }
+);
+
+/**
+ * Test textbox with one child
+ */
+addAccessibleTask(
+ `<div id="textbox" role="textbox">Hello </div>`,
+ async (browser, accDoc) => {
+ let textbox = getNativeInterface(accDoc, "textbox");
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 0,
+ "textbox with one child is pruned"
+ );
+
+ let reorder = waitForEvent(EVENT_REORDER, "textbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ let link = content.document.createElement("a");
+ link.textContent = "World";
+ content.document.getElementById("textbox").appendChild(link);
+ });
+ await reorder;
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 2,
+ "textbox with two child is not pruned"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_input.js b/accessible/tests/browser/mac/browser_input.js
new file mode 100644
index 0000000000..7fa20a9d4b
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_input.js
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function selectedTextEventPromises(stateChangeType) {
+ return [
+ waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateChangeType == stateChangeType &&
+ elem.getAttributeValue("AXDOMIdentifier") == "body"
+ );
+ }),
+ waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateChangeType == stateChangeType &&
+ elem.getAttributeValue("AXDOMIdentifier") == "input"
+ );
+ }),
+ ];
+}
+
+async function testInput(browser, accDoc) {
+ let input = getNativeInterface(accDoc, "input");
+
+ is(input.getAttributeValue("AXDescription"), "Name", "Correct input label");
+ is(input.getAttributeValue("AXTitle"), "", "Correct input title");
+ is(input.getAttributeValue("AXValue"), "Elmer Fudd", "Correct input value");
+ is(
+ input.getAttributeValue("AXNumberOfCharacters"),
+ 10,
+ "Correct length of value"
+ );
+
+ ok(input.attributeNames.includes("AXSelectedText"), "Has AXSelectedText");
+ ok(
+ input.attributeNames.includes("AXSelectedTextRange"),
+ "Has AXSelectedTextRange"
+ );
+
+ let evt = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged", "input"),
+ ...selectedTextEventPromises(AXTextStateChangeTypeSelectionMove),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ await evt;
+
+ evt = Promise.all(
+ selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend)
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ let elm = content.document.getElementById("input");
+ if (elm.setSelectionRange) {
+ elm.setSelectionRange(6, 9);
+ } else {
+ let r = new content.Range();
+ let textNode = elm.firstElementChild.firstChild;
+ r.setStart(textNode, 6);
+ r.setEnd(textNode, 9);
+
+ let s = content.getSelection();
+ s.removeAllRanges();
+ s.addRange(r);
+ }
+ });
+ await evt;
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "Fud",
+ "Correct text is selected"
+ );
+
+ Assert.deepEqual(
+ input.getAttributeValue("AXSelectedTextRange"),
+ [6, 3],
+ "correct range selected"
+ );
+
+ ok(
+ input.isAttributeSettable("AXSelectedTextRange"),
+ "AXSelectedTextRange is settable"
+ );
+
+ evt = Promise.all(
+ selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend)
+ );
+ input.setAttributeValue("AXSelectedTextRange", NSRange(1, 7));
+ await evt;
+
+ Assert.deepEqual(
+ input.getAttributeValue("AXSelectedTextRange"),
+ [1, 7],
+ "correct range selected"
+ );
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "lmer Fu",
+ "Correct text is selected"
+ );
+
+ let domSelection = await SpecialPowers.spawn(browser, [], () => {
+ let elm = content.document.querySelector("input#input");
+ if (elm) {
+ return elm.value.substring(elm.selectionStart, elm.selectionEnd);
+ }
+
+ return content.getSelection().toString();
+ });
+
+ is(domSelection, "lmer Fu", "correct DOM selection");
+
+ is(
+ input.getParameterizedAttributeValue("AXStringForRange", NSRange(3, 5)),
+ "er Fu",
+ "AXStringForRange works"
+ );
+}
+
+/**
+ * Input selection test
+ */
+addAccessibleTask(
+ `<input aria-label="Name" id="input" value="Elmer Fudd">`,
+ testInput
+);
+
+/**
+ * contenteditable selection test
+ */
+addAccessibleTask(
+ `<div aria-label="Name" tabindex="0" role="textbox" aria-multiline="true" id="input" contenteditable>
+ <p>Elmer Fudd</p>
+ </div>`,
+ testInput
+);
+
+/**
+ * test contenteditable with selection that extends past editable part
+ */
+addAccessibleTask(
+ `<span aria-label="Name"
+ tabindex="0"
+ role="textbox"
+ id="input"
+ contenteditable>Elmer Fudd</span> <span id="notinput">is the name</span>`,
+ async (browser, accDoc) => {
+ let evt = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged", "input"),
+ waitForMacEvent("AXSelectedTextChanged", "body"),
+ waitForMacEvent("AXSelectedTextChanged", "input"),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ await evt;
+
+ evt = waitForEvent(EVENT_TEXT_CARET_MOVED);
+ await SpecialPowers.spawn(browser, [], () => {
+ let input = content.document.getElementById("input");
+ let notinput = content.document.getElementById("notinput");
+
+ let r = new content.Range();
+ r.setStart(input.firstChild, 4);
+ r.setEnd(notinput.firstChild, 6);
+
+ let s = content.getSelection();
+ s.removeAllRanges();
+ s.addRange(r);
+ });
+ await evt;
+
+ let input = getNativeInterface(accDoc, "input");
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "r Fudd",
+ "Correct text is selected in #input"
+ );
+
+ is(
+ stringForRange(
+ input,
+ input.getAttributeValue("AXSelectedTextMarkerRange")
+ ),
+ "r Fudd is the",
+ "Correct text is selected in document"
+ );
+ }
+);
+
+/**
+ * test nested content editables and their ancestor getters.
+ */
+addAccessibleTask(
+ `<div id="outer" role="textbox" contenteditable="true">
+ <p id="p">Bob <a href="#" id="link">Loblaw's</a></p>
+ <div id="inner" role="textbox" contenteditable="true">
+ Law <a href="#" id="inner_link">Blog</a>
+ </div>
+ </div>`,
+ (browser, accDoc) => {
+ let link = getNativeInterface(accDoc, "link");
+ let innerLink = getNativeInterface(accDoc, "inner_link");
+
+ let idmatches = (elem, id) => {
+ is(elem.getAttributeValue("AXDOMIdentifier"), id, "Matches ID");
+ };
+
+ idmatches(link.getAttributeValue("AXEditableAncestor"), "outer");
+ idmatches(link.getAttributeValue("AXFocusableAncestor"), "outer");
+ idmatches(link.getAttributeValue("AXHighestEditableAncestor"), "outer");
+
+ idmatches(innerLink.getAttributeValue("AXEditableAncestor"), "inner");
+ idmatches(innerLink.getAttributeValue("AXFocusableAncestor"), "inner");
+ idmatches(
+ innerLink.getAttributeValue("AXHighestEditableAncestor"),
+ "outer"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_label_title.js b/accessible/tests/browser/mac/browser_label_title.js
new file mode 100644
index 0000000000..2532247e0f
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_label_title.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test different labeling/titling schemes for text fields
+ */
+addAccessibleTask(
+ `<label for="n1">Label</label> <input id="n1">
+ <label for="n2">Two</label> <label for="n2">Labels</label> <input id="n2">
+ <input aria-label="ARIA Label" id="n3">`,
+ (browser, accDoc) => {
+ let n1 = getNativeInterface(accDoc, "n1");
+ let n1Label = n1.getAttributeValue("AXTitleUIElement");
+ // XXX: In Safari the label is an AXText with an AXValue,
+ // here it is an AXGroup witth an AXTitle
+ is(n1Label.getAttributeValue("AXTitle"), "Label");
+
+ let n2 = getNativeInterface(accDoc, "n2");
+ is(n2.getAttributeValue("AXDescription"), "TwoLabels");
+
+ let n3 = getNativeInterface(accDoc, "n3");
+ is(n3.getAttributeValue("AXDescription"), "ARIA Label");
+ }
+);
+
+/**
+ * Test to see that named groups get labels
+ */
+addAccessibleTask(
+ `<fieldset id="fieldset"><legend>Fields</legend><input aria-label="hello"></fieldset>`,
+ (browser, accDoc) => {
+ let fieldset = getNativeInterface(accDoc, "fieldset");
+ is(fieldset.getAttributeValue("AXDescription"), "Fields");
+ }
+);
+
+/**
+ * Test to see that list items don't get titled groups
+ */
+addAccessibleTask(
+ `<ul style="list-style: none;"><li id="unstyled-item">Hello</li></ul>
+ <ul><li id="styled-item">World</li></ul>`,
+ (browser, accDoc) => {
+ let unstyledItem = getNativeInterface(accDoc, "unstyled-item");
+ is(unstyledItem.getAttributeValue("AXTitle"), "");
+
+ let styledItem = getNativeInterface(accDoc, "unstyled-item");
+ is(styledItem.getAttributeValue("AXTitle"), "");
+ }
+);
+
+/**
+ * Test that we fire a title changed notification
+ */
+addAccessibleTask(
+ `<div id="elem" aria-label="Hello world"></div>`,
+ async (browser, accDoc) => {
+ let elem = getNativeInterface(accDoc, "elem");
+ is(elem.getAttributeValue("AXTitle"), "Hello world");
+ let evt = waitForMacEvent("AXTitleChanged", "elem");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("elem")
+ .setAttribute("aria-label", "Hello universe");
+ });
+ await evt;
+ is(elem.getAttributeValue("AXTitle"), "Hello universe");
+ }
+);
+
+/**
+ * Test articles supply only labels not titles
+ */
+addAccessibleTask(
+ `<article id="article" aria-label="Hello world"></article>`,
+ async (browser, accDoc) => {
+ let article = getNativeInterface(accDoc, "article");
+ is(article.getAttributeValue("AXDescription"), "Hello world");
+ ok(!article.getAttributeValue("AXTitle"));
+ }
+);
+
+/**
+ * Test text and number inputs supply only labels not titles
+ */
+addAccessibleTask(
+ `<label for="input">Your favorite number?</label><input type="text" name="input" value="11" id="input" aria-label="The best number you know of">`,
+ async (browser, accDoc) => {
+ let input = getNativeInterface(accDoc, "input");
+ is(input.getAttributeValue("AXDescription"), "The best number you know of");
+ ok(!input.getAttributeValue("AXTitle"));
+ let evt = waitForEvent(EVENT_SHOW, "input");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").setAttribute("type", "number");
+ });
+ await evt;
+ input = getNativeInterface(accDoc, "input");
+ is(input.getAttributeValue("AXDescription"), "The best number you know of");
+ ok(!input.getAttributeValue("AXTitle"));
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_link.js b/accessible/tests/browser/mac/browser_link.js
new file mode 100644
index 0000000000..b4597e10f4
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_link.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+/**
+ * Test visited link properties.
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="http://www.example.com/">I am a non-visited link</a><br>
+ `,
+ async (browser, accDoc) => {
+ let link = getNativeInterface(accDoc, "link");
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link");
+
+ is(link.getAttributeValue("AXVisited"), 0, "Link has not been visited");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await PlacesTestUtils.addVisits(["http://www.example.com/"]);
+
+ await stateChanged;
+ is(link.getAttributeValue("AXVisited"), 1, "Link has been visited");
+
+ // Ensure history is cleared before running
+ await PlacesUtils.history.clear();
+ }
+);
+
+/**
+ * Test linked vs unlinked anchor tags
+ */
+addAccessibleTask(
+ `
+ <a id="link1" href="#">I am a link link</a>
+ <a id="link2" onclick="console.log('hi')">I am a link-ish link</a>
+ <a id="link3">I am a non-link link</a>
+ `,
+ async (browser, accDoc) => {
+ let link1 = getNativeInterface(accDoc, "link1");
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXLink",
+ "a[href] gets correct link role"
+ );
+ ok(
+ link1.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link1.attributeNames.includes("AXURL"), "Link has URL attribute");
+
+ let link2 = getNativeInterface(accDoc, "link2");
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXLink",
+ "a[onclick] gets correct link role"
+ );
+ ok(
+ link2.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link2.attributeNames.includes("AXURL"), "Link has URL attribute");
+
+ let link3 = getNativeInterface(accDoc, "link3");
+ is(
+ link3.getAttributeValue("AXRole"),
+ "AXGroup",
+ "bare <a> gets correct group role"
+ );
+
+ let onRecreation = waitForEvent(EVENT_SHOW, "link1");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link1").removeAttribute("href");
+ });
+ await onRecreation;
+ link1 = getNativeInterface(accDoc, "link1");
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from href gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link2").removeAttribute("onclick");
+ });
+ await onRecreation;
+ link2 = getNativeInterface(accDoc, "link2");
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from onclick gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link3");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("link3")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("href", "http://example.com");
+ });
+ await onRecreation;
+ link3 = getNativeInterface(accDoc, "link3");
+ is(
+ link3.getAttributeValue("AXRole"),
+ "AXLink",
+ "href added to bare a gets link role"
+ );
+
+ ok(
+ link3.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute");
+ }
+);
+
+/**
+ * Test anchors and linked ui elements attr
+ */
+addAccessibleTask(
+ `
+ <a id="link0" href="http://example.com">I am a link</a>
+ <a id="link1" href="#">I am a link with an empty anchor</a>
+ <a id="link2" href="#hello">I am a link with no corresponding element</a>
+ <a id="link3" href="#world">I am a link with a corresponding element</a>
+ <a id="link4" href="#empty">I jump to an empty element</a>
+ <a id="link5" href="#namedElem">I jump to a named element</a>
+ <a id="link6" href="#emptyNamed">I jump to an empty named element</a>
+ <h1 id="world">I am that element</h1>
+ <h2 id="empty"></h2>
+ <a name="namedElem">I have a name</a>
+ <a name="emptyNamed"></a>
+ <h3>I have no name and no ID</h3>
+ <h4></h4>
+ `,
+ async (browser, accDoc) => {
+ let link0 = getNativeInterface(accDoc, "link0");
+ let link1 = getNativeInterface(accDoc, "link1");
+ let link2 = getNativeInterface(accDoc, "link2");
+ let link3 = getNativeInterface(accDoc, "link3");
+ let link4 = getNativeInterface(accDoc, "link4");
+ let link5 = getNativeInterface(accDoc, "link5");
+ let link6 = getNativeInterface(accDoc, "link6");
+
+ is(
+ link0.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 0 has no linked UI elements"
+ );
+ is(
+ link1.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 1 has no linked UI elements"
+ );
+ is(
+ link2.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 2 has no linked UI elements"
+ );
+ is(
+ link3.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 3 has one linked UI element"
+ );
+ is(
+ link3
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "I am that element",
+ "Link 3 is linked to the heading"
+ );
+ is(
+ link4.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 4 has one linked UI element"
+ );
+ is(
+ link4
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ null,
+ "Link 4 is linked to the heading"
+ );
+ is(
+ link5.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 5 has one linked UI element"
+ );
+ is(
+ link5
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "",
+ "Link 5 is linked to a named element"
+ );
+ is(
+ link6.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 6 has one linked UI element"
+ );
+ is(
+ link6
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "",
+ "Link 6 is linked to an empty named element"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js
new file mode 100644
index 0000000000..10a03120f8
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_live_regions.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test live region creation and removal.
+ */
+addAccessibleTask(
+ `
+ <div id="polite" aria-relevant="removals">Polite region</div>
+ <div id="assertive" aria-live="assertive">Assertive region</div>
+ `,
+ async (browser, accDoc) => {
+ let politeRegion = getNativeInterface(accDoc, "polite");
+ ok(
+ !politeRegion.attributeNames.includes("AXARIALive"),
+ "region is not live"
+ );
+
+ let liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "polite");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-atomic", "true");
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-live", "polite");
+ });
+ await liveRegionAdded;
+ is(
+ politeRegion.getAttributeValue("AXARIALive"),
+ "polite",
+ "region is now live"
+ );
+ ok(politeRegion.getAttributeValue("AXARIAAtomic"), "region is atomic");
+ is(
+ politeRegion.getAttributeValue("AXARIARelevant"),
+ "removals",
+ "region has defined aria-relevant"
+ );
+
+ let assertiveRegion = getNativeInterface(accDoc, "assertive");
+ is(
+ assertiveRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+ ok(
+ !assertiveRegion.getAttributeValue("AXARIAAtomic"),
+ "region is not atomic"
+ );
+ is(
+ assertiveRegion.getAttributeValue("AXARIARelevant"),
+ "additions text",
+ "region has default aria-relevant"
+ );
+
+ let liveRegionRemoved = waitForEvent(
+ EVENT_LIVE_REGION_REMOVED,
+ "assertive"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("assertive").removeAttribute("aria-live");
+ });
+ await liveRegionRemoved;
+ ok(!assertiveRegion.getAttributeValue("AXARIALive"), "region is not live");
+
+ liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "new-region");
+ await SpecialPowers.spawn(browser, [], () => {
+ let newRegionElm = content.document.createElement("div");
+ newRegionElm.id = "new-region";
+ newRegionElm.setAttribute("aria-live", "assertive");
+ content.document.body.appendChild(newRegionElm);
+ });
+ await liveRegionAdded;
+
+ let newRegion = getNativeInterface(accDoc, "new-region");
+ is(
+ newRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+
+ let loadComplete = Promise.all([
+ waitForMacEvent("AXLoadComplete"),
+ waitForMacEvent("AXLiveRegionCreated", "region-1"),
+ waitForMacEvent("AXLiveRegionCreated", "region-2"),
+ waitForMacEvent("AXLiveRegionCreated", "status"),
+ waitForMacEvent("AXLiveRegionCreated", "output"),
+ ]);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location = `data:text/html;charset=utf-8,
+ <div id="region-1" aria-live="polite"></div>
+ <div id="region-2" aria-live="assertive"></div>
+ <div id="region-3" aria-live="off"></div>
+ <div id="alert" role="alert"></div>
+ <div id="status" role="status"></div>
+ <output id="output"></output>`;
+ });
+ let webArea = (await loadComplete)[0];
+
+ is(webArea.getAttributeValue("AXRole"), "AXWebArea", "web area yeah");
+ const searchPred = {
+ AXSearchKey: "AXLiveRegionSearchKey",
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+ const liveRegions = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ Assert.deepEqual(
+ liveRegions.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["region-1", "region-2", "alert", "status", "output"],
+ "SearchPredicate returned all live regions"
+ );
+ }
+);
+
+/**
+ * Test live region changes
+ */
+addAccessibleTask(
+ `
+ <div id="live" aria-live="polite">
+ The time is <span id="time">4:55pm</span>
+ <p id="p" style="display: none">Georgia on my mind</p>
+ <button id="button" aria-label="Start"></button>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ let liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("time").textContent = "4:56pm";
+ });
+ await liveRegionChanged;
+ ok(true, "changed textContent");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "block";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to block");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "none";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to none");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Stop");
+ });
+ await liveRegionChanged;
+ ok(true, "changed aria-label");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_mathml.js b/accessible/tests/browser/mac/browser_mathml.js
new file mode 100644
index 0000000000..d4cf499810
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_mathml.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testMathAttr(iface, attr, subrole, textLeafValue) {
+ ok(iface.attributeNames.includes(attr), `Object has ${attr} attribute`);
+ let value = iface.getAttributeValue(attr);
+ is(
+ value.getAttributeValue("AXSubrole"),
+ subrole,
+ `${attr} value has correct subrole`
+ );
+
+ if (textLeafValue) {
+ let children = value.getAttributeValue("AXChildren");
+ is(children.length, 1, `${attr} value has one child`);
+
+ is(
+ children[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ `${attr} value's child is static text`
+ );
+ is(
+ children[0].getAttributeValue("AXValue"),
+ textLeafValue,
+ `${attr} value has correct text`
+ );
+ }
+}
+
+addAccessibleTask(
+ `<math id="math">
+ <msqrt id="sqrt">
+ <mn>2</mn>
+ </msqrt>
+ </math>`,
+ async (browser, accDoc) => {
+ let math = getNativeInterface(accDoc, "math");
+ is(
+ math.getAttributeValue("AXSubrole"),
+ "AXDocumentMath",
+ "Math element has correct subrole"
+ );
+
+ let sqrt = getNativeInterface(accDoc, "sqrt");
+ is(
+ sqrt.getAttributeValue("AXSubrole"),
+ "AXMathSquareRoot",
+ "msqrt has correct subrole"
+ );
+
+ testMathAttr(sqrt, "AXMathRootRadicand", "AXMathNumber", "2");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mroot id="root">
+ <mi>sin</mi>
+ <mn>3</mn>
+ </mroot>
+ </math>`,
+ async (browser, accDoc) => {
+ let root = getNativeInterface(accDoc, "root");
+ is(
+ root.getAttributeValue("AXSubrole"),
+ "AXMathRoot",
+ "mroot has correct subrole"
+ );
+
+ testMathAttr(root, "AXMathRootRadicand", "AXMathIdentifier", "sin");
+ testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mfrac id="fraction">
+ <mn>2</mn>
+ <mn>3</mn>
+ </mfrac>
+ </math>`,
+ async (browser, accDoc) => {
+ let fraction = getNativeInterface(accDoc, "fraction");
+ is(
+ fraction.getAttributeValue("AXSubrole"),
+ "AXMathFraction",
+ "mfrac has correct subrole"
+ );
+ ok(fraction.attributeNames.includes("AXMathFractionNumerator"));
+ ok(fraction.attributeNames.includes("AXMathFractionDenominator"));
+ ok(fraction.attributeNames.includes("AXMathLineThickness"));
+
+ // Bug 1639745
+ todo_is(fraction.getAttributeValue("AXMathLineThickness"), 1);
+
+ testMathAttr(fraction, "AXMathFractionNumerator", "AXMathNumber", "2");
+ testMathAttr(fraction, "AXMathFractionDenominator", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <msubsup id="subsup">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mn>1</mn>
+ </msubsup>
+ </math>`,
+ async (browser, accDoc) => {
+ let subsup = getNativeInterface(accDoc, "subsup");
+ is(
+ subsup.getAttributeValue("AXSubrole"),
+ "AXMathSubscriptSuperscript",
+ "msubsup has correct subrole"
+ );
+
+ testMathAttr(subsup, "AXMathSubscript", "AXMathNumber", "0");
+ testMathAttr(subsup, "AXMathSuperscript", "AXMathNumber", "1");
+ testMathAttr(subsup, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <munderover id="underover">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mi>∞</mi>
+ </munderover>
+ </math>`,
+ async (browser, accDoc) => {
+ let underover = getNativeInterface(accDoc, "underover");
+ is(
+ underover.getAttributeValue("AXSubrole"),
+ "AXMathUnderOver",
+ "munderover has correct subrole"
+ );
+
+ testMathAttr(underover, "AXMathUnder", "AXMathNumber", "0");
+ testMathAttr(underover, "AXMathOver", "AXMathIdentifier", "∞");
+ testMathAttr(underover, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_menulist.js b/accessible/tests/browser/mac/browser_menulist.js
new file mode 100644
index 0000000000..b26a0be782
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_menulist.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ "mac/doc_menulist.xhtml",
+ async (browser, accDoc) => {
+ const menulist = getNativeInterface(accDoc, "defaultZoom");
+
+ let actions = menulist.actionNames;
+ ok(actions.includes("AXPress"), "menu has press action");
+
+ let event = waitForMacEvent("AXMenuOpened");
+ menulist.performAction("AXPress");
+ const menupopup = await event;
+
+ const menuItems = menupopup.getAttributeValue("AXChildren");
+ is(menuItems.length, 4, "Found four children in menulist");
+ is(
+ menuItems[0].getAttributeValue("AXTitle"),
+ "50%",
+ "First item has correct title"
+ );
+ is(
+ menuItems[1].getAttributeValue("AXTitle"),
+ "100%",
+ "Second item has correct title"
+ );
+ is(
+ menuItems[2].getAttributeValue("AXTitle"),
+ "150%",
+ "Third item has correct title"
+ );
+ is(
+ menuItems[3].getAttributeValue("AXTitle"),
+ "200%",
+ "Fourth item has correct title"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
+
+addAccessibleTask(
+ "mac/doc_menulist.xhtml",
+ async (browser, accDoc) => {
+ const menulist = getNativeInterface(accDoc, "defaultZoom");
+
+ const actions = menulist.actionNames;
+ ok(actions.includes("AXPress"), "menu has press action");
+ let event = waitForMacEvent("AXMenuOpened");
+ menulist.performAction("AXPress");
+ await event;
+
+ const menu = menulist.getAttributeValue("AXChildren")[0];
+ ok(menu, "Menulist contains menu");
+ const children = menu.getAttributeValue("AXChildren");
+ is(children.length, 4, "Menu has 4 items");
+
+ // Menu is open, initial focus should land on the first item
+ is(
+ children[0].getAttributeValue("AXSelected"),
+ 1,
+ "First menu item is selected"
+ );
+ // focus the second item, and verify it is selected
+ event = waitForMacEvent("AXFocusedUIElementChanged", (iface, data) => {
+ try {
+ return iface.getAttributeValue("AXTitle") == "100%";
+ } catch (e) {
+ return false;
+ }
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await event;
+
+ is(
+ children[0].getAttributeValue("AXSelected"),
+ 0,
+ "First menu item is no longer selected"
+ );
+ is(
+ children[1].getAttributeValue("AXSelected"),
+ 1,
+ "Second menu item is selected"
+ );
+ // press the second item, check for selected event
+ event = waitForMacEvent("AXMenuItemSelected");
+ children[1].performAction("AXPress");
+ await event;
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_navigate.js b/accessible/tests/browser/mac/browser_navigate.js
new file mode 100644
index 0000000000..69486676e4
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_navigate.js
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test navigation of same/different type content
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1>
+ world<br>
+ <a href="example.com" id="link">I am a link</a>
+ <h1 id="goodbye">goodbye</h1>`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXSameTypeSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const hello = getNativeInterface(accDoc, "hello");
+ const goodbye = getNativeInterface(accDoc, "goodbye");
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ searchPred.AXStartElement = hello;
+
+ let sameItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(sameItem.length, 1, "Found one item");
+ is(
+ "goodbye",
+ sameItem[0].getAttributeValue("AXTitle"),
+ "Found correct item of same type"
+ );
+
+ searchPred.AXDirection = "AXDirectionPrevious";
+ searchPred.AXStartElement = goodbye;
+ sameItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(sameItem.length, 1, "Found one item");
+ is(
+ "hello",
+ sameItem[0].getAttributeValue("AXTitle"),
+ "Found correct item of same type"
+ );
+
+ searchPred.AXSearchKey = "AXDifferentTypeSearchKey";
+ let diffItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(diffItem.length, 1, "Found one item");
+ is(
+ "I am a link",
+ diffItem[0].getAttributeValue("AXValue"),
+ "Found correct item of different type"
+ );
+ }
+);
+
+/**
+ * Test navigation of heading levels
+ */
+addAccessibleTask(
+ `
+ <h1 id="a">a</h1>
+ <h2 id="b">b</h2>
+ <h3 id="c">c</h3>
+ <h4 id="d">d</h4>
+ <h5 id="e">e</h5>
+ <h6 id="f">f</h5>
+ <h1 id="g">g</h1>
+ <h2 id="h">h</h2>
+ <h3 id="i">i</h3>
+ <h4 id="j">j</h4>
+ <h5 id="k">k</h5>
+ <h6 id="l">l</h5>
+ this is some regular text that should be ignored
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingLevel1SearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let h1Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h1Count, "Found two h1 items");
+
+ let h1s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const a = getNativeInterface(accDoc, "a");
+ const g = getNativeInterface(accDoc, "g");
+
+ is(
+ a.getAttributeValue("AXValue"),
+ h1s[0].getAttributeValue("AXValue"),
+ "Found correct h1 heading"
+ );
+
+ is(
+ g.getAttributeValue("AXValue"),
+ h1s[1].getAttributeValue("AXValue"),
+ "Found correct h1 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel2SearchKey";
+
+ let h2Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h2Count, "Found two h2 items");
+
+ let h2s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const b = getNativeInterface(accDoc, "b");
+ const h = getNativeInterface(accDoc, "h");
+
+ is(
+ b.getAttributeValue("AXValue"),
+ h2s[0].getAttributeValue("AXValue"),
+ "Found correct h2 heading"
+ );
+
+ is(
+ h.getAttributeValue("AXValue"),
+ h2s[1].getAttributeValue("AXValue"),
+ "Found correct h2 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel3SearchKey";
+
+ let h3Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h3Count, "Found two h3 items");
+
+ let h3s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const c = getNativeInterface(accDoc, "c");
+ const i = getNativeInterface(accDoc, "i");
+
+ is(
+ c.getAttributeValue("AXValue"),
+ h3s[0].getAttributeValue("AXValue"),
+ "Found correct h3 heading"
+ );
+
+ is(
+ i.getAttributeValue("AXValue"),
+ h3s[1].getAttributeValue("AXValue"),
+ "Found correct h3 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel4SearchKey";
+
+ let h4Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h4Count, "Found two h4 items");
+
+ let h4s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const d = getNativeInterface(accDoc, "d");
+ const j = getNativeInterface(accDoc, "j");
+
+ is(
+ d.getAttributeValue("AXValue"),
+ h4s[0].getAttributeValue("AXValue"),
+ "Found correct h4 heading"
+ );
+
+ is(
+ j.getAttributeValue("AXValue"),
+ h4s[1].getAttributeValue("AXValue"),
+ "Found correct h4 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel5SearchKey";
+
+ let h5Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h5Count, "Found two h5 items");
+
+ let h5s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const e = getNativeInterface(accDoc, "e");
+ const k = getNativeInterface(accDoc, "k");
+
+ is(
+ e.getAttributeValue("AXValue"),
+ h5s[0].getAttributeValue("AXValue"),
+ "Found correct h5 heading"
+ );
+
+ is(
+ k.getAttributeValue("AXValue"),
+ h5s[1].getAttributeValue("AXValue"),
+ "Found correct h5 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel6SearchKey";
+
+ let h6Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h6Count, "Found two h6 items");
+
+ let h6s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const f = getNativeInterface(accDoc, "f");
+ const l = getNativeInterface(accDoc, "l");
+
+ is(
+ f.getAttributeValue("AXValue"),
+ h6s[0].getAttributeValue("AXValue"),
+ "Found correct h6 heading"
+ );
+
+ is(
+ l.getAttributeValue("AXValue"),
+ h6s[1].getAttributeValue("AXValue"),
+ "Found correct h6 heading"
+ );
+ }
+);
+
+/*
+ * Test rotor with blockquotes
+ */
+addAccessibleTask(
+ `
+ <blockquote id="first">hello I am a blockquote</blockquote>
+ <blockquote id="second">
+ I am also a blockquote of the same level
+ <br>
+ <blockquote id="third">but I have a different level</blockquote>
+ </blockquote>
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXBlockquoteSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let bquotes = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(bquotes.length, 3, "Found three blockquotes");
+
+ const first = getNativeInterface(accDoc, "first");
+ const second = getNativeInterface(accDoc, "second");
+ const third = getNativeInterface(accDoc, "third");
+ console.log("values :");
+ console.log(first.getAttributeValue("AXValue"));
+ is(
+ first.getAttributeValue("AXValue"),
+ bquotes[0].getAttributeValue("AXValue"),
+ "Found correct first blockquote"
+ );
+
+ is(
+ second.getAttributeValue("AXValue"),
+ bquotes[1].getAttributeValue("AXValue"),
+ "Found correct second blockquote"
+ );
+
+ is(
+ third.getAttributeValue("AXValue"),
+ bquotes[2].getAttributeValue("AXValue"),
+ "Found correct third blockquote"
+ );
+ }
+);
+
+/*
+ * Test rotor with graphics
+ */
+addAccessibleTask(
+ `
+ <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br>
+ <a href="http://example.com">
+ <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+ <img src="" id="img3">
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXGraphicSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let images = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(images.length, 3, "Found three images");
+
+ const img1 = getNativeInterface(accDoc, "img1");
+ const img2 = getNativeInterface(accDoc, "img2");
+ const img3 = getNativeInterface(accDoc, "img3");
+
+ is(
+ img1.getAttributeValue("AXDescription"),
+ images[0].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img2.getAttributeValue("AXDescription"),
+ images[1].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img3.getAttributeValue("AXDescription"),
+ images[2].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_outline.js b/accessible/tests/browser/mac/browser_outline.js
new file mode 100644
index 0000000000..ba211fdf4b
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_outline.js
@@ -0,0 +1,566 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test outline, outline rows with computed properties
+ */
+addAccessibleTask(
+ `
+ <h3 id="tree1">
+ Foods
+ </h3>
+ <ul role="tree" aria-labelledby="tree1" id="outline">
+ <li role="treeitem" aria-expanded="false">
+ <span>
+ Fruits
+ </span>
+ <ul>
+ <li role="none">Oranges</li>
+ <li role="treeitem" aria-expanded="true">
+ <span>
+ Apples
+ </span>
+ <ul role="group">
+ <li role="none">Honeycrisp</li>
+ <li role="none">Granny Smith</li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li id="vegetables" role="treeitem" aria-expanded="false">
+ <span>
+ Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem" aria-expanded="true">
+ <span>
+ Podded Vegetables
+ </span>
+ <ul role="group">
+ <li role="none">Lentil</li>
+ <li role="none">Pea</li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ `,
+ async (browser, accDoc) => {
+ const outline = getNativeInterface(accDoc, "outline");
+ is(
+ outline.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Correct role for outline"
+ );
+
+ const outChildren = outline.getAttributeValue("AXChildren");
+ is(outChildren.length, 2, "Outline has two direct children");
+ is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = outline.getAttributeValue("AXRows");
+ is(outRows.length, 4, "Outline has four rows");
+ is(
+ outRows[0].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children, only group"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[1].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[1].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of group"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedRows").length,
+ 1,
+ "Row has one row child"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[3].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[3]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[2].getAttributeValue("AXDescription"),
+ "Row is direct child of row[2]"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ let evt = waitForMacEvent("AXRowExpanded", "vegetables");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("vegetables")
+ .setAttribute("aria-expanded", "true");
+ });
+ await evt;
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 1,
+ "Row is disclosing after being expanded"
+ );
+
+ evt = waitForMacEvent("AXRowCollapsed", "vegetables");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("vegetables")
+ .setAttribute("aria-expanded", "false");
+ });
+ await evt;
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing after being collapsed again"
+ );
+ }
+);
+
+/**
+ * Test outline, outline rows with declared properties
+ */
+addAccessibleTask(
+ `
+ <h3 id="tree1">
+ Foods
+ </h3>
+ <ul role="tree" aria-labelledby="tree1" id="outline">
+ <li role="treeitem"
+ aria-level="1"
+ aria-setsize="2"
+ aria-posinset="1"
+ aria-expanded="false">
+ <span>
+ Fruits
+ </span>
+ <ul>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Oranges
+ </li>
+ <li role="treeitem"
+ aria-level="2"
+ aria-setsize="2"
+ aria-posinset="2"
+ aria-expanded="true">
+ <span>
+ Apples
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Honeycrisp
+ </li>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="2">
+ Granny Smith
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li role="treeitem"
+ aria-level="1"
+ aria-setsize="2"
+ aria-posinset="2"
+ aria-expanded="false">
+ <span>
+ Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="2"
+ aria-setsize="1"
+ aria-posinset="1"
+ aria-expanded="true">
+ <span>
+ Podded Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Lentil
+ </li>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="2">
+ Pea
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ `,
+ async (browser, accDoc) => {
+ const outline = getNativeInterface(accDoc, "outline");
+ is(
+ outline.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Correct role for outline"
+ );
+
+ const outChildren = outline.getAttributeValue("AXChildren");
+ is(outChildren.length, 2, "Outline has two direct children");
+ is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = outline.getAttributeValue("AXRows");
+ is(outRows.length, 9, "Outline has nine rows");
+ is(
+ outRows[0].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no direct row children, has list"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[2].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[2].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of group"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedRows").length,
+ 2,
+ "Row has two row children"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ is(
+ outRows[3].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[3]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[2].getAttributeValue("AXDescription"),
+ "Row is direct child of row 2"
+ );
+
+ is(
+ outRows[3].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Row is level two"
+ );
+
+ is(
+ outRows[5].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosedRows").length,
+ 1,
+ "Row has no one row child"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[6].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[6]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[5].getAttributeValue("AXDescription"),
+ "Row is direct child of row 5"
+ );
+ is(
+ outRows[6].getAttributeValue("AXDisclosedRows").length,
+ 2,
+ "Row has two row children"
+ );
+ is(
+ outRows[6].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ is(
+ outRows[7].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[7]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[6].getAttributeValue("AXDescription"),
+ "Row is direct child of row 6"
+ );
+ is(
+ outRows[7].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[7].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Row is level two"
+ );
+ }
+);
+
+// Test outline that isn't built with li/uls gets correct desc
+addAccessibleTask(
+ `
+ <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles">
+ <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div>
+ <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="false">Shared items</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ is(tree.getAttributeValue("AXRole"), "AXOutline", "Correct role for tree");
+
+ const treeItems = tree.getAttributeValue("AXChildren");
+ is(treeItems.length, 2, "Outline has two direct children");
+ is(treeItems[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(treeItems[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = tree.getAttributeValue("AXRows");
+ is(outRows.length, 2, "Outline has two rows");
+
+ is(
+ outRows[0].getAttributeValue("AXDescription"),
+ "My files",
+ "files labelled correctly"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDescription"),
+ "Shared items",
+ "shared items labelled correctly"
+ );
+ }
+);
+
+// Test outline registers AXDisclosed attr as settable
+addAccessibleTask(
+ `
+ <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles">
+ <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div>
+ <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="true">Shared items</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ const treeItems = tree.getAttributeValue("AXChildren");
+
+ is(treeItems.length, 2, "Outline has two direct children");
+ is(treeItems[0].getAttributeValue("AXDisclosing"), 0);
+ is(treeItems[1].getAttributeValue("AXDisclosing"), 1);
+
+ is(treeItems[0].isAttributeSettable("AXDisclosing"), true);
+ is(treeItems[1].isAttributeSettable("AXDisclosing"), true);
+
+ // attempt to change attribute values
+ treeItems[0].setAttributeValue("AXDisclosing", 1);
+ treeItems[0].setAttributeValue("AXDisclosing", 0);
+
+ // verify they're unchanged
+ is(treeItems[0].getAttributeValue("AXDisclosing"), 0);
+ is(treeItems[1].getAttributeValue("AXDisclosing"), 1);
+ }
+);
+
+// Test outline rows correctly expose checkable, checked/unchecked/mixed status
+addAccessibleTask(
+ `
+ <div role="tree" id="tree">
+ <div role="treeitem" aria-checked="false" id="l1">
+ Leaf 1
+ </div>
+ <div role="treeitem" aria-checked="true" id="l2">
+ Leaf 2
+ </div>
+ <div role="treeitem" id="l3">
+ Leaf 3
+ </div>
+ <div role="treeitem" aria-checked="mixed" id="l4">
+ Leaf 4
+ </div>
+ </div>
+
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ const treeItems = tree.getAttributeValue("AXChildren");
+
+ is(treeItems.length, 4, "Outline has four direct children");
+ is(
+ treeItems[0].getAttributeValue("AXValue"),
+ 0,
+ "Child one is not checked"
+ );
+ is(treeItems[1].getAttributeValue("AXValue"), 1, "Child two is checked");
+ is(
+ treeItems[2].getAttributeValue("AXValue"),
+ null,
+ "Child three is not checkable and has no val"
+ );
+ is(treeItems[3].getAttributeValue("AXValue"), 2, "Child four is mixed");
+
+ let stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l1"),
+ waitForStateChange("l1", STATE_CHECKED, true),
+ ]);
+ // We should get a state change event for checked.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("l1")
+ .setAttribute("aria-checked", "true");
+ });
+ await stateChanged;
+ is(treeItems[0].getAttributeValue("AXValue"), 1, "Child one is checked");
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l2"),
+ waitForMacEvent("AXValueChanged", "l2"),
+ waitForStateChange("l2", STATE_CHECKED, false),
+ waitForStateChange("l2", STATE_CHECKABLE, false),
+ ]);
+ // We should get a state change event for both checked and checkable,
+ // and value changes for both.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("l2").removeAttribute("aria-checked");
+ });
+ await stateChanged;
+ is(
+ treeItems[1].getAttributeValue("AXValue"),
+ null,
+ "Child two is not checkable and has no val"
+ );
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l3"),
+ waitForMacEvent("AXValueChanged", "l3"),
+ waitForStateChange("l3", STATE_CHECKED, true),
+ waitForStateChange("l3", STATE_CHECKABLE, true),
+ ]);
+ // We should get a state change event for both checked and checkable,
+ // and value changes for each.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("l3")
+ .setAttribute("aria-checked", "true");
+ });
+ await stateChanged;
+ is(treeItems[2].getAttributeValue("AXValue"), 1, "Child three is checked");
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l4"),
+ waitForMacEvent("AXValueChanged", "l4"),
+ waitForStateChange("l4", STATE_MIXED, false),
+ waitForStateChange("l4", STATE_CHECKABLE, false),
+ ]);
+ // We should get a state change event for both mixed and checkable,
+ // and value changes for each.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("l4").removeAttribute("aria-checked");
+ });
+ await stateChanged;
+ is(
+ treeItems[3].getAttributeValue("AXValue"),
+ null,
+ "Child four is not checkable and has no value"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_outline_xul.js b/accessible/tests/browser/mac/browser_outline_xul.js
new file mode 100644
index 0000000000..66eebebf50
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_outline_xul.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "mac/doc_tree.xhtml",
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ is(
+ tree.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Found tree with role outline"
+ );
+ // XUL trees store all rows as direct children of the outline,
+ // so we should see nine here instead of just three:
+ // (Groceries, Fruits, Veggies)
+ const treeChildren = tree.getAttributeValue("AXChildren");
+ is(treeChildren.length, 9, "Found nine direct children");
+
+ const treeCols = tree.getAttributeValue("AXColumns");
+ is(treeCols.length, 1, "Found one column in tree");
+
+ // Here, we should get only outline rows, not the title
+ const treeRows = tree.getAttributeValue("AXRows");
+ is(treeRows.length, 8, "Found 8 total rows");
+
+ is(
+ treeRows[0].getAttributeValue("AXDescription"),
+ "Fruits",
+ "Located correct first row, row has correct desc"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosing"),
+ 1,
+ "Fruits is disclosing"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Fruits is disclosed by outline"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Fruits is level zero"
+ );
+ let disclosedRows = treeRows[0].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Fruits discloses two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Apple",
+ "fruits discloses apple"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Orange",
+ "fruits discloses orange"
+ );
+
+ is(
+ treeRows[1].getAttributeValue("AXDescription"),
+ "Apple",
+ "Located correct second row, row has correct desc"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosing"),
+ 0,
+ "Apple is not disclosing"
+ );
+ is(
+ treeRows[1]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Fruits",
+ "Apple is disclosed by fruits"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Apple is level one"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Apple does not disclose rows"
+ );
+
+ is(
+ treeRows[2].getAttributeValue("AXDescription"),
+ "Orange",
+ "Located correct third row, row has correct desc"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Orange is not disclosing"
+ );
+ is(
+ treeRows[2]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Fruits",
+ "Orange is disclosed by fruits"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Orange is level one"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Orange does not disclose rows"
+ );
+
+ is(
+ treeRows[3].getAttributeValue("AXDescription"),
+ "Veggies",
+ "Located correct fourth row, row has correct desc"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosing"),
+ 1,
+ "Veggies is disclosing"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Veggies is disclosed by outline"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Veggies is level zero"
+ );
+ disclosedRows = treeRows[3].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Veggies discloses two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Veggies discloses green veggies"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Squash",
+ "Veggies discloses squash"
+ );
+
+ is(
+ treeRows[4].getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Located correct fifth row, row has correct desc"
+ );
+ is(
+ treeRows[4].getAttributeValue("AXDisclosing"),
+ 1,
+ "Green veggies is disclosing"
+ );
+ is(
+ treeRows[4]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Veggies",
+ "Green Veggies is disclosed by veggies"
+ );
+ is(
+ treeRows[4].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Green veggies is level one"
+ );
+ disclosedRows = treeRows[4].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Green veggies has two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Spinach",
+ "Green veggies discloses spinach"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Peas",
+ "Green veggies discloses peas"
+ );
+
+ is(
+ treeRows[5].getAttributeValue("AXDescription"),
+ "Spinach",
+ "Located correct sixth row, row has correct desc"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosing"),
+ 0,
+ "Spinach is not disclosing"
+ );
+ is(
+ treeRows[5]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Spinach is disclosed by green veggies"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Spinach is level two"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Spinach does not disclose rows"
+ );
+
+ is(
+ treeRows[6].getAttributeValue("AXDescription"),
+ "Peas",
+ "Located correct seventh row, row has correct desc"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosing"),
+ 0,
+ "Peas is not disclosing"
+ );
+ is(
+ treeRows[6]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Peas is disclosed by green veggies"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Peas is level two"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Peas does not disclose rows"
+ );
+
+ is(
+ treeRows[7].getAttributeValue("AXDescription"),
+ "Squash",
+ "Located correct eighth row, row has correct desc"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosing"),
+ 0,
+ "Squash is not disclosing"
+ );
+ is(
+ treeRows[7]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Veggies",
+ "Squash is disclosed by veggies"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Squash is level one"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Squash does not disclose rows"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_popupbutton.js b/accessible/tests/browser/mac/browser_popupbutton.js
new file mode 100644
index 0000000000..2d5ff1ac35
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_popupbutton.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+// Test dropdown select element
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a number">
+ <option id="one" selected>One</option>
+ <option id="two">Two</option>
+ <option id="three">Three</option>
+ <option id="four" disabled>Four</option>
+ </select>`,
+ async (browser, accDoc) => {
+ // Test combobox
+ let select = getNativeInterface(accDoc, "select");
+ is(
+ select.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "select has AXPopupButton role"
+ );
+ ok(select.attributeNames.includes("AXValue"), "select advertises AXValue");
+ is(
+ select.getAttributeValue("AXValue"),
+ "One",
+ "select has correctt initial value"
+ );
+ ok(
+ !select.attributeNames.includes("AXHasPopup"),
+ "select does not advertise AXHasPopup"
+ );
+ is(
+ select.getAttributeValue("AXHasPopup"),
+ null,
+ "select does not provide value for AXHasPopup"
+ );
+
+ ok(select.actionNames.includes("AXPress"), "Selectt has press action");
+ // These four events happen in quick succession when select is pressed
+ let events = Promise.all([
+ waitForMacEvent("AXMenuOpened"),
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ e => e.getAttributeValue("AXRole") == "AXPopUpButton"
+ ),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ e => e.getAttributeValue("AXRole") == "AXMenuItem"
+ ),
+ ]);
+ select.performAction("AXPress");
+ // Only capture the target of AXMenuOpened (first element)
+ let [menu] = await events;
+
+ is(menu.getAttributeValue("AXRole"), "AXMenu", "dropdown has AXMenu role");
+ is(
+ menu.getAttributeValue("AXSelectedChildren").length,
+ 1,
+ "dropdown has single selected child"
+ );
+
+ let selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(selectedChildren[0].getAttributeValue("AXRole"), "AXMenuItem");
+ is(selectedChildren[0].getAttributeValue("AXTitle"), "One");
+
+ let menuParent = menu.getAttributeValue("AXParent");
+ is(
+ menuParent.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "dropdown parent is a popup button"
+ );
+
+ let menuItems = menu.getAttributeValue("AXChildren").map(c => {
+ return [
+ c.getAttributeValue("AXMenuItemMarkChar"),
+ c.getAttributeValue("AXRole"),
+ c.getAttributeValue("AXTitle"),
+ c.getAttributeValue("AXEnabled"),
+ ];
+ });
+
+ Assert.deepEqual(
+ menuItems,
+ [
+ ["✓", "AXMenuItem", "One", true],
+ [null, "AXMenuItem", "Two", true],
+ [null, "AXMenuItem", "Three", true],
+ [null, "AXMenuItem", "Four", false],
+ ],
+ "Menu items have correct checkmark on current value, correctt roles, correct titles, and correct AXEnabled value"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let [, menuItem] = await events;
+ is(
+ menuItem.getAttributeValue("AXTitle"),
+ "Two",
+ "Focused menu item has correct title"
+ );
+
+ selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(
+ selectedChildren[0].getAttributeValue("AXTitle"),
+ "Two",
+ "Selected child matches focused item"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ [, menuItem] = await events;
+ is(
+ menuItem.getAttributeValue("AXTitle"),
+ "Three",
+ "Focused menu item has correct title"
+ );
+
+ selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(
+ selectedChildren[0].getAttributeValue("AXTitle"),
+ "Three",
+ "Selected child matches focused item"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXMenuClosed"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ ]);
+ menuItem.performAction("AXPress");
+ let [, newFocus] = await events;
+ is(
+ newFocus.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "Newly focused element is AXPopupButton"
+ );
+ is(
+ newFocus.getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Should return focus to select"
+ );
+ is(
+ newFocus.getAttributeValue("AXValue"),
+ "Three",
+ "select has correct new value"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_radio_position.js b/accessible/tests/browser/mac/browser_radio_position.js
new file mode 100644
index 0000000000..76f518a91e
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_radio_position.js
@@ -0,0 +1,321 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getChildRoles(parent) {
+ return parent
+ .getAttributeValue("AXChildren")
+ .map(c => c.getAttributeValue("AXRole"));
+}
+
+function getLinkedTitles(element) {
+ return element
+ .getAttributeValue("AXLinkedUIElements")
+ .map(c => c.getAttributeValue("AXTitle"));
+}
+
+/**
+ * Test radio group
+ */
+addAccessibleTask(
+ `<div role="radiogroup" id="radioGroup">
+ <div role="radio"
+ id="radioGroupItem1">
+ Regular crust
+ </div>
+ <div role="radio"
+ id="radioGroupItem2">
+ Deep dish
+ </div>
+ <div role="radio"
+ id="radioGroupItem3">
+ Thin crust
+ </div>
+ </div>`,
+ async (browser, accDoc) => {
+ let item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let item2 = getNativeInterface(accDoc, "radioGroupItem2");
+ let item3 = getNativeInterface(accDoc, "radioGroupItem3");
+ let titleList = ["Regular crust", "Deep dish", "Thin crust"];
+
+ Assert.deepEqual(
+ titleList,
+ [item1, item2, item3].map(c => c.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 1 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item1),
+ titleList,
+ "Item one has correctly ordered linked elements"
+ );
+
+ linkedElems = item2.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 2 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item2),
+ titleList,
+ "Item two has correctly ordered linked elements"
+ );
+
+ linkedElems = item3.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 3 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item3),
+ titleList,
+ "Item three has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test dynamic add to a radio group
+ */
+addAccessibleTask(
+ `<div role="radiogroup" id="radioGroup">
+ <div role="radio"
+ id="radioGroupItem1">
+ Option One
+ </div>
+ </div>`,
+ async (browser, accDoc) => {
+ let item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+
+ is(linkedElems.length, 1, "Item 1 has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ item1.getAttributeValue("AXTitle"),
+ "Item 1 is first element"
+ );
+
+ let reorder = waitForEvent(EVENT_REORDER, "radioGroup");
+ await SpecialPowers.spawn(browser, [], () => {
+ let d = content.document.createElement("div");
+ d.setAttribute("role", "radio");
+ content.document.getElementById("radioGroup").appendChild(d);
+ });
+ await reorder;
+
+ let radioGroup = getNativeInterface(accDoc, "radioGroup");
+ let groupMembers = radioGroup.getAttributeValue("AXChildren");
+ is(groupMembers.length, 2, "Radio group has two members");
+ let item2 = groupMembers[1];
+ item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let titleList = ["Option One", ""];
+
+ Assert.deepEqual(
+ titleList,
+ [item1, item2].map(c => c.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Item 1 has two linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item1),
+ titleList,
+ "Item one has correctly ordered linked elements"
+ );
+
+ linkedElems = item2.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Item 2 has two linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item2),
+ titleList,
+ "Item two has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] for single group
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label>
+ <input type="radio" id="dog" name="animal"><label for="dog">Dog</label>
+ <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+ let titleList = ["Cat", "Dog", "CatDog"];
+
+ Assert.deepEqual(
+ titleList,
+ [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Cat has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(cat),
+ titleList,
+ "Cat has correctly ordered linked elements"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Dog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(dog),
+ titleList,
+ "Dog has correctly ordered linked elements"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Catdog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(catdog),
+ titleList,
+ "catdog has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] for different groups
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="one"><label for="cat">Cat</label>
+ <input type="radio" id="dog" name="two"><label for="dog">Dog</label>
+ <input type="radio" id="catdog"><label for="catdog">CatDog</label>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Cat has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ cat.getAttributeValue("AXTitle"),
+ "Cat is only element"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Dog has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ dog.getAttributeValue("AXTitle"),
+ "Dog is only element"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 0, "Catdog has no linked UI elem");
+ }
+);
+
+/**
+ * Test input[type=radio] for single group across DOM
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label>
+ <div>
+ <span>
+ <input type="radio" id="dog" name="animal"><label for="dog">Dog</label>
+ </span>
+ </div>
+ <div>
+ <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>
+ </div>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+ let titleList = ["Cat", "Dog", "CatDog"];
+
+ Assert.deepEqual(
+ titleList,
+ [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Cat has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(cat),
+ titleList,
+ "cat has correctly ordered linked elements"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Dog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(dog),
+ titleList,
+ "dog has correctly ordered linked elements"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Catdog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(catdog),
+ titleList,
+ "catdog has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test dynamic add of input[type=radio] in a single group
+ */
+addAccessibleTask(
+ `<div id="container"><input type="radio" id="cat" name="animal"></div>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let container = getNativeInterface(accDoc, "container");
+
+ let containerChildren = container.getAttributeValue("AXChildren");
+ is(containerChildren.length, 1, "container has one button");
+ is(
+ containerChildren[0].getAttributeValue("AXRole"),
+ "AXRadioButton",
+ "Container child is radio button"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Cat has 1 linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ cat.getAttributeValue("AXTitle"),
+ "Cat is first element"
+ );
+ let reorder = waitForEvent(EVENT_REORDER, "container");
+ await SpecialPowers.spawn(browser, [], () => {
+ let input = content.document.createElement("input");
+ input.setAttribute("type", "radio");
+ input.setAttribute("name", "animal");
+ content.document.getElementById("container").appendChild(input);
+ });
+ await reorder;
+
+ container = getNativeInterface(accDoc, "container");
+ containerChildren = container.getAttributeValue("AXChildren");
+
+ is(containerChildren.length, 2, "container has two children");
+
+ Assert.deepEqual(
+ getChildRoles(container),
+ ["AXRadioButton", "AXRadioButton"],
+ "Both children are radio buttons"
+ );
+
+ linkedElems = containerChildren[0].getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Cat has 2 linked elements");
+
+ linkedElems = containerChildren[1].getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "New button has 2 linked elements");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js
new file mode 100644
index 0000000000..430e41d6ea
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_range.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Verify that the value of a slider input can be incremented/decremented
+ * Test input[type=range]
+ */
+addAccessibleTask(
+ `<input id="range" type="range" min="1" max="100" value="1" step="10">`,
+ async (browser, accDoc) => {
+ let range = getNativeInterface(accDoc, "range");
+ is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role");
+ is(range.getAttributeValue("AXValue"), 1, "Correct initial value");
+
+ let actions = range.actionNames;
+ ok(actions.includes("AXDecrement"), "Has decrement action");
+ ok(actions.includes("AXIncrement"), "Has increment action");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ range.performAction("AXIncrement");
+ await evt;
+ is(range.getAttributeValue("AXValue"), 11, "Correct increment value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ range.performAction("AXDecrement");
+ await evt;
+ is(range.getAttributeValue("AXValue"), 1, "Correct decrement value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ // Adjust value via script in content
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("range").value = 41;
+ });
+ await evt;
+ is(
+ range.getAttributeValue("AXValue"),
+ 41,
+ "Correct value from content change"
+ );
+ }
+);
+
+/**
+ * Verify that the value of a slider input can be set directly
+ * Test input[type=range]
+ */
+addAccessibleTask(
+ `<input id="range" type="range" min="1" max="100" value="1" step="10">`,
+ async (browser, accDoc) => {
+ let nextValue = 21;
+ let range = getNativeInterface(accDoc, "range");
+ is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role");
+ is(range.getAttributeValue("AXValue"), 1, "Correct initial value");
+
+ ok(range.isAttributeSettable("AXValue"), "Range AXValue is settable.");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ range.setAttributeValue("AXValue", nextValue);
+ await evt;
+ is(range.getAttributeValue("AXValue"), nextValue, "Correct updated value");
+ }
+);
+
+/**
+ * Verify that the value of a number input can be incremented/decremented
+ * Test input[type=number]
+ */
+addAccessibleTask(
+ `<input type="number" value="11" id="number" step=".05">`,
+ async (browser, accDoc) => {
+ let number = getNativeInterface(accDoc, "number");
+ is(
+ number.getAttributeValue("AXRole"),
+ "AXIncrementor",
+ "Correct AXIncrementor role"
+ );
+ is(number.getAttributeValue("AXValue"), 11, "Correct initial value");
+
+ let actions = number.actionNames;
+ ok(actions.includes("AXDecrement"), "Has decrement action");
+ ok(actions.includes("AXIncrement"), "Has increment action");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ number.performAction("AXIncrement");
+ await evt;
+ is(number.getAttributeValue("AXValue"), 11.05, "Correct increment value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ number.performAction("AXDecrement");
+ await evt;
+ is(number.getAttributeValue("AXValue"), 11, "Correct decrement value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ // Adjust value via script in content
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("number").value = 42;
+ });
+ await evt;
+ is(
+ number.getAttributeValue("AXValue"),
+ 42,
+ "Correct value from content change"
+ );
+ }
+);
+
+/**
+ * Test Min, Max, Orientation, ValueDescription
+ */
+addAccessibleTask(
+ `<input type="number" value="11" id="number">`,
+ async (browser, accDoc) => {
+ let nextValue = 21;
+ let number = getNativeInterface(accDoc, "number");
+ is(
+ number.getAttributeValue("AXRole"),
+ "AXIncrementor",
+ "Correct AXIncrementor role"
+ );
+ is(number.getAttributeValue("AXValue"), 11, "Correct initial value");
+
+ ok(number.isAttributeSettable("AXValue"), "Range AXValue is settable.");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ number.setAttributeValue("AXValue", nextValue);
+ await evt;
+ is(number.getAttributeValue("AXValue"), nextValue, "Correct updated value");
+ }
+);
+
+/**
+ * Verify that the value of a number input can be set directly
+ * Test input[type=number]
+ */
+addAccessibleTask(
+ `<div aria-valuetext="High" id="slider" aria-orientation="horizontal" role="slider" aria-valuenow="2" aria-valuemin="0" aria-valuemax="3"></div>`,
+ async (browser, accDoc) => {
+ let slider = getNativeInterface(accDoc, "slider");
+ is(
+ slider.getAttributeValue("AXValueDescription"),
+ "High",
+ "Correct value description"
+ );
+ is(
+ slider.getAttributeValue("AXOrientation"),
+ "AXHorizontalOrientation",
+ "Correct orientation"
+ );
+ is(slider.getAttributeValue("AXMinValue"), 0, "Correct min value");
+ is(slider.getAttributeValue("AXMaxValue"), 3, "Correct max value");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("slider");
+ s.setAttribute("aria-valuetext", "Low");
+ });
+ await evt;
+ is(
+ slider.getAttributeValue("AXValueDescription"),
+ "Low",
+ "Correct value description"
+ );
+
+ evt = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "slider");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("slider");
+ s.setAttribute("aria-orientation", "vertical");
+ s.setAttribute("aria-valuemin", "-1");
+ s.setAttribute("aria-valuemax", "5");
+ });
+ await evt;
+ is(
+ slider.getAttributeValue("AXOrientation"),
+ "AXVerticalOrientation",
+ "Correct orientation"
+ );
+ is(slider.getAttributeValue("AXMinValue"), -1, "Correct min value");
+ is(slider.getAttributeValue("AXMaxValue"), 5, "Correct max value");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_required.js b/accessible/tests/browser/mac/browser_required.js
new file mode 100644
index 0000000000..2109d265ab
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_required.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test required and aria-required attributes on checkboxes
+ * and radio buttons.
+ */
+addAccessibleTask(
+ `
+ <form>
+ <input type="checkbox" id="checkbox" required>
+ <br>
+ <input type="radio" id="radio" required>
+ <br>
+ <input type="checkbox" id="ariaCheckbox" aria-required="true">
+ <br>
+ <input type="radio" id="ariaRadio" aria-required="true">
+ </form>
+ `,
+ async (browser, accDoc) => {
+ // Check initial AXRequired values are correct
+ let radio = getNativeInterface(accDoc, "radio");
+ is(
+ radio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for radio"
+ );
+
+ let ariaRadio = getNativeInterface(accDoc, "ariaRadio");
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for ariaRadio"
+ );
+
+ let checkbox = getNativeInterface(accDoc, "checkbox");
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for checkbox"
+ );
+
+ let ariaCheckbox = getNativeInterface(accDoc, "ariaCheckbox");
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .setAttribute("aria-required", "false");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after false set for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .setAttribute("aria-required", "true");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required after true set for ariaCheckbox"
+ );
+
+ // Remove aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .removeAttribute("aria-required");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .setAttribute("aria-required", "false");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after false set for ariaRadio"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .setAttribute("aria-required", "true");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required after true set for ariaRadio"
+ );
+
+ // Remove aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .removeAttribute("aria-required");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for ariaRadio"
+ );
+
+ // Remove required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "checkbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("checkbox").removeAttribute("required");
+ });
+ await stateChanged;
+
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for checkbox"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "radio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("radio").removeAttribute("required");
+ });
+ await stateChanged;
+
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for radio"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_rich_listbox.js b/accessible/tests/browser/mac/browser_rich_listbox.js
new file mode 100644
index 0000000000..97dd6785bb
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rich_listbox.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "mac/doc_rich_listbox.xhtml",
+ async (browser, accDoc) => {
+ const categories = getNativeInterface(accDoc, "categories");
+ const categoriesChildren = categories.getAttributeValue("AXChildren");
+ is(categoriesChildren.length, 4, "Found listbox and 4 items");
+
+ const general = getNativeInterface(accDoc, "general");
+ is(
+ general.getAttributeValue("AXTitle"),
+ "general",
+ "general has appropriate title"
+ );
+ is(
+ categoriesChildren[0].getAttributeValue("AXTitle"),
+ general.getAttributeValue("AXTitle"),
+ "Found general listitem"
+ );
+ is(
+ general.getAttributeValue("AXEnabled"),
+ 1,
+ "general is enabled, not dimmed"
+ );
+
+ const home = getNativeInterface(accDoc, "home");
+ is(home.getAttributeValue("AXTitle"), "home", "home has appropriate title");
+ is(
+ categoriesChildren[1].getAttributeValue("AXTitle"),
+ home.getAttributeValue("AXTitle"),
+ "Found home listitem"
+ );
+ is(home.getAttributeValue("AXEnabled"), 1, "Home is enabled, not dimmed");
+
+ const search = getNativeInterface(accDoc, "search");
+ is(
+ search.getAttributeValue("AXTitle"),
+ "search",
+ "search has appropriate title"
+ );
+ is(
+ categoriesChildren[2].getAttributeValue("AXTitle"),
+ search.getAttributeValue("AXTitle"),
+ "Found search listitem"
+ );
+ is(
+ search.getAttributeValue("AXEnabled"),
+ 1,
+ "search is enabled, not dimmed"
+ );
+
+ const privacy = getNativeInterface(accDoc, "privacy");
+ is(
+ privacy.getAttributeValue("AXTitle"),
+ "privacy",
+ "privacy has appropriate title"
+ );
+ is(
+ categoriesChildren[3].getAttributeValue("AXTitle"),
+ privacy.getAttributeValue("AXTitle"),
+ "Found privacy listitem"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_roles_elements.js b/accessible/tests/browser/mac/browser_roles_elements.js
new file mode 100644
index 0000000000..791598fed6
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_roles_elements.js
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test different HTML elements for their roles and subroles
+ */
+function testRoleAndSubRole(accDoc, id, axRole, axSubRole, axRoleDescription) {
+ let el = getNativeInterface(accDoc, id);
+ if (axRole) {
+ is(
+ el.getAttributeValue("AXRole"),
+ axRole,
+ "AXRole for " + id + " is " + axRole
+ );
+ }
+ if (axSubRole) {
+ is(
+ el.getAttributeValue("AXSubrole"),
+ axSubRole,
+ "Subrole for " + id + " is " + axSubRole
+ );
+ }
+ if (axRoleDescription) {
+ is(
+ el.getAttributeValue("AXRoleDescription"),
+ axRoleDescription,
+ "Subrole for " + id + " is " + axRoleDescription
+ );
+ }
+}
+
+addAccessibleTask(
+ `
+ <!-- WAI-ARIA landmark roles -->
+ <div id="application" role="application"></div>
+ <div id="banner" role="banner"></div>
+ <div id="complementary" role="complementary"></div>
+ <div id="contentinfo" role="contentinfo"></div>
+ <div id="form" role="form"></div>
+ <div id="main" role="main"></div>
+ <div id="navigation" role="navigation"></div>
+ <div id="search" role="search"></div>
+ <div id="searchbox" role="searchbox"></div>
+
+ <!-- DPub landmarks -->
+ <div id="dPubNavigation" role="doc-index"></div>
+ <div id="dPubRegion" role="doc-introduction"></div>
+
+ <!-- Other WAI-ARIA widget roles -->
+ <div id="alert" role="alert"></div>
+ <div id="alertdialog" role="alertdialog"></div>
+ <div id="article" role="article"></div>
+ <div id="code" role="code"></div>
+ <div id="dialog" role="dialog"></div>
+ <div id="ariaDocument" role="document"></div>
+ <div id="log" role="log"></div>
+ <div id="marquee" role="marquee"></div>
+ <div id="ariaMath" role="math"></div>
+ <div id="note" role="note"></div>
+ <div id="ariaRegion" aria-label="region" role="region"></div>
+ <div id="ariaStatus" role="status"></div>
+ <div id="switch" role="switch"></div>
+ <div id="timer" role="timer"></div>
+ <div id="tooltip" role="tooltip"></div>
+ <input type="radio" role="menuitemradio" id="menuitemradio">
+ <input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox">
+ <input type="datetime-local" id="datetime">
+
+ <!-- text entries -->
+ <div id="textbox_multiline" role="textbox" aria-multiline="true"></div>
+ <div id="textbox_singleline" role="textbox" aria-multiline="false"></div>
+ <textarea id="textArea"></textarea>
+ <input id="textInput">
+
+ <!-- True HTML5 search box -->
+ <input type="search" id="htmlSearch" />
+
+ <!-- A button morphed into a toggle via ARIA -->
+ <button id="toggle" aria-pressed="false"></button>
+
+ <!-- A button with a 'banana' role description -->
+ <button id="banana" aria-roledescription="banana"></button>
+
+ <!-- Other elements -->
+ <del id="deletion">Deleted text</del>
+ <dl id="dl"><dt id="dt">term</dt><dd id="dd">definition</dd></dl>
+ <hr id="hr" />
+ <ins id="insertion">Inserted text</ins>
+ <meter id="meter" min="0" max="100" value="24">meter text here</meter>
+ <sub id="sub">sub text here</sub>
+ <sup id="sup">sup text here</sup>
+
+ <!-- Some SVG stuff -->
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="g">
+ <title>g</title>
+ </g>
+ <rect width="300" height="100" id="rect"
+ style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)">
+ <title>rect</title>
+ </rect>
+ <circle cx="100" cy="50" r="40" stroke="black" id="circle"
+ stroke-width="2" fill="red">
+ <title>circle</title>
+ </circle>
+ <ellipse cx="300" cy="80" rx="100" ry="50" id="ellipse"
+ style="fill:yellow;stroke:purple;stroke-width:2">
+ <title>ellipse</title>
+ </ellipse>
+ <line x1="0" y1="0" x2="200" y2="200" id="line"
+ style="stroke:rgb(255,0,0);stroke-width:2">
+ <title>line</title>
+ </line>
+ <polygon points="200,10 250,190 160,210" id="polygon"
+ style="fill:lime;stroke:purple;stroke-width:1">
+ <title>polygon</title>
+ </polygon>
+ <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" id="polyline"
+ style="fill:none;stroke:black;stroke-width:3" >
+ <title>polyline</title>
+ </polyline>
+ <path d="M150 0 L75 200 L225 200 Z" id="path">
+ <title>path</title>
+ </path>
+ <image x1="25" y1="80" width="50" height="20" id="image"
+ xlink:href="../moz.png">
+ <title>image</title>
+ </image>
+ </svg>`,
+ (browser, accDoc) => {
+ // WAI-ARIA landmark subroles, regardless of AXRole
+ testRoleAndSubRole(accDoc, "application", null, "AXLandmarkApplication");
+ testRoleAndSubRole(accDoc, "banner", null, "AXLandmarkBanner");
+ testRoleAndSubRole(
+ accDoc,
+ "complementary",
+ null,
+ "AXLandmarkComplementary"
+ );
+ testRoleAndSubRole(accDoc, "contentinfo", null, "AXLandmarkContentInfo");
+ testRoleAndSubRole(accDoc, "form", null, "AXLandmarkForm");
+ testRoleAndSubRole(accDoc, "main", null, "AXLandmarkMain");
+ testRoleAndSubRole(accDoc, "navigation", null, "AXLandmarkNavigation");
+ testRoleAndSubRole(accDoc, "search", null, "AXLandmarkSearch");
+ testRoleAndSubRole(accDoc, "searchbox", null, "AXSearchField");
+
+ // DPub roles map into two categories, sample one of each
+ testRoleAndSubRole(
+ accDoc,
+ "dPubNavigation",
+ "AXGroup",
+ "AXLandmarkNavigation"
+ );
+ testRoleAndSubRole(accDoc, "dPubRegion", "AXGroup", "AXLandmarkRegion");
+
+ // ARIA widget roles
+ testRoleAndSubRole(accDoc, "alert", null, "AXApplicationAlert");
+ testRoleAndSubRole(
+ accDoc,
+ "alertdialog",
+ "AXGroup",
+ "AXApplicationAlertDialog",
+ "alert dialog"
+ );
+ testRoleAndSubRole(accDoc, "article", null, "AXDocumentArticle");
+ testRoleAndSubRole(accDoc, "code", "AXGroup", "AXCodeStyleGroup");
+ testRoleAndSubRole(accDoc, "dialog", null, "AXApplicationDialog", "dialog");
+ testRoleAndSubRole(accDoc, "ariaDocument", null, "AXDocument");
+ testRoleAndSubRole(accDoc, "log", null, "AXApplicationLog");
+ testRoleAndSubRole(accDoc, "marquee", null, "AXApplicationMarquee");
+ testRoleAndSubRole(accDoc, "ariaMath", null, "AXDocumentMath");
+ testRoleAndSubRole(accDoc, "note", null, "AXDocumentNote");
+ testRoleAndSubRole(accDoc, "ariaRegion", null, "AXLandmarkRegion");
+ testRoleAndSubRole(accDoc, "ariaStatus", "AXGroup", "AXApplicationStatus");
+ testRoleAndSubRole(accDoc, "switch", "AXCheckBox", "AXSwitch");
+ testRoleAndSubRole(accDoc, "timer", null, "AXApplicationTimer");
+ testRoleAndSubRole(accDoc, "tooltip", "AXGroup", "AXUserInterfaceTooltip");
+ testRoleAndSubRole(accDoc, "menuitemradio", "AXMenuItem", null);
+ testRoleAndSubRole(accDoc, "menuitemcheckbox", "AXMenuItem", null);
+ testRoleAndSubRole(accDoc, "datetime", "AXGroup", null);
+ // XXX for datetime elements, we spoof the role via the title, since
+ // providing the correct role results in the internal elements being
+ // unreachable by VO
+ is(
+ getNativeInterface(accDoc, "datetime").getAttributeValue("AXTitle"),
+ "date field"
+ );
+
+ // Text boxes
+ testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea");
+ testRoleAndSubRole(accDoc, "textbox_singleline", "AXTextField");
+ testRoleAndSubRole(accDoc, "textArea", "AXTextArea");
+ testRoleAndSubRole(accDoc, "textInput", "AXTextField");
+
+ // True HTML5 search field
+ testRoleAndSubRole(accDoc, "htmlSearch", "AXTextField", "AXSearchField");
+
+ // A button morphed into a toggle by ARIA
+ testRoleAndSubRole(accDoc, "toggle", "AXCheckBox", "AXToggle");
+
+ // A banana button
+ testRoleAndSubRole(accDoc, "banana", "AXButton", null, "banana");
+
+ // Other elements
+ testRoleAndSubRole(accDoc, "deletion", "AXGroup", "AXDeleteStyleGroup");
+ testRoleAndSubRole(accDoc, "dl", "AXList", "AXDescriptionList");
+ testRoleAndSubRole(accDoc, "dt", "AXGroup", "AXTerm");
+ testRoleAndSubRole(accDoc, "dd", "AXGroup", "AXDescription");
+ testRoleAndSubRole(accDoc, "hr", "AXSplitter", "AXContentSeparator");
+ testRoleAndSubRole(accDoc, "insertion", "AXGroup", "AXInsertStyleGroup");
+ testRoleAndSubRole(
+ accDoc,
+ "meter",
+ "AXLevelIndicator",
+ "AXMeter",
+ "level indicator"
+ );
+ testRoleAndSubRole(accDoc, "sub", "AXGroup", "AXSubscriptStyleGroup");
+ testRoleAndSubRole(accDoc, "sup", "AXGroup", "AXSuperscriptStyleGroup");
+
+ // Some SVG stuff
+ testRoleAndSubRole(accDoc, "svg", "AXImage");
+ testRoleAndSubRole(accDoc, "g", "AXGroup");
+ testRoleAndSubRole(accDoc, "rect", "AXImage");
+ testRoleAndSubRole(accDoc, "circle", "AXImage");
+ testRoleAndSubRole(accDoc, "ellipse", "AXImage");
+ testRoleAndSubRole(accDoc, "line", "AXImage");
+ testRoleAndSubRole(accDoc, "polygon", "AXImage");
+ testRoleAndSubRole(accDoc, "polyline", "AXImage");
+ testRoleAndSubRole(accDoc, "path", "AXImage");
+ testRoleAndSubRole(accDoc, "image", "AXImage");
+ }
+);
+
+addAccessibleTask(
+ `
+ <figure id="figure">
+ <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" alt="Logo">
+ <p>Non-image figure content</p>
+ <figcaption id="figcaption">Old Mozilla logo</figcaption>
+ </figure>`,
+ (browser, accDoc) => {
+ let figure = getNativeInterface(accDoc, "figure");
+ ok(!figure.getAttributeValue("AXTitle"), "Figure should not have a title");
+ is(
+ figure.getAttributeValue("AXDescription"),
+ "Old Mozilla logo",
+ "Correct figure label"
+ );
+ is(figure.getAttributeValue("AXRole"), "AXGroup", "Correct figure role");
+ is(
+ figure.getAttributeValue("AXRoleDescription"),
+ "figure",
+ "Correct figure role description"
+ );
+
+ let img = getNativeInterface(accDoc, "img");
+ ok(!img.getAttributeValue("AXTitle"), "img should not have a title");
+ is(img.getAttributeValue("AXDescription"), "Logo", "Correct img label");
+ is(img.getAttributeValue("AXRole"), "AXImage", "Correct img role");
+ is(
+ img.getAttributeValue("AXRoleDescription"),
+ "image",
+ "Correct img role description"
+ );
+
+ let figcaption = getNativeInterface(accDoc, "figcaption");
+ ok(
+ !figcaption.getAttributeValue("AXTitle"),
+ "figcaption should not have a title"
+ );
+ ok(
+ !figcaption.getAttributeValue("AXDescription"),
+ "figcaption should not have a label"
+ );
+ is(
+ figcaption.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct figcaption role"
+ );
+ is(
+ figcaption.getAttributeValue("AXRoleDescription"),
+ "group",
+ "Correct figcaption role description"
+ );
+ }
+);
+
+addAccessibleTask(`<button>hello world</button>`, async (browser, accDoc) => {
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "web area should be an AXWebArea"
+ );
+ ok(
+ !webArea.attributeNames.includes("AXSubrole"),
+ "AXWebArea should not have a subrole"
+ );
+
+ let roleChanged = waitForMacEvent("AXMozRoleChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.setAttribute("role", "application");
+ });
+ await roleChanged;
+
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "web area should retain AXWebArea role"
+ );
+ ok(
+ !webArea.attributeNames.includes("AXSubrole"),
+ "AXWebArea should not have a subrole"
+ );
+
+ let rootGroup = webArea.getAttributeValue("AXChildren")[0];
+ is(rootGroup.getAttributeValue("AXRole"), "AXGroup");
+ is(rootGroup.getAttributeValue("AXSubrole"), "AXLandmarkApplication");
+});
diff --git a/accessible/tests/browser/mac/browser_rootgroup.js b/accessible/tests/browser/mac/browser_rootgroup.js
new file mode 100644
index 0000000000..a8f4297d64
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rootgroup.js
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test document with no single group child
+ */
+addAccessibleTask(
+ `<p id="p1">hello</p><p>world</p>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ is(
+ rootGroup.getAttributeValue("AXChildren").length,
+ 2,
+ "Root group has two children"
+ );
+
+ // From bottom-up
+ let p1 = getNativeInterface(accDoc, "p1");
+ rootGroup = p1.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ }
+);
+
+/**
+ * Test document with a top-level group
+ */
+addAccessibleTask(
+ `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXDOMIdentifier"),
+ "group",
+ "Root group is a document element"
+ );
+
+ // Adding an 'application' role to the body should
+ // create a root group with an application subrole.
+ let evt = waitForMacEvent("AXMozRoleChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.setAttribute("role", "application");
+ });
+ await evt;
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "doc still has web area role"
+ );
+ is(
+ doc.getAttributeValue("AXRoleDescription"),
+ "HTML Content",
+ "doc has correct role description"
+ );
+ ok(
+ !doc.attributeNames.includes("AXSubrole"),
+ "sub role not available on web area"
+ );
+
+ rootGroup = doc.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRole"),
+ "AXGroup",
+ "root group has AXGroup role"
+ );
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXLandmarkApplication",
+ "root group has application subrole"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRoleDescription"),
+ "application",
+ "root group has application role description"
+ );
+ }
+);
+
+/**
+ * Test document with body[role=application] and a top-level group
+ */
+addAccessibleTask(
+ `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "doc still has web area role"
+ );
+ is(
+ doc.getAttributeValue("AXRoleDescription"),
+ "HTML Content",
+ "doc has correct role description"
+ );
+ ok(
+ !doc.attributeNames.includes("AXSubrole"),
+ "sub role not available on web area"
+ );
+
+ let rootGroup = doc.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRole"),
+ "AXGroup",
+ "root group has AXGroup role"
+ );
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXLandmarkApplication",
+ "root group has application subrole"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRoleDescription"),
+ "application",
+ "root group has application role description"
+ );
+ },
+ { contentDocBodyAttrs: { role: "application" } }
+);
+
+/**
+ * Test document with a single button
+ */
+addAccessibleTask(
+ `<button id="button">I am a button</button>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ let rootGroupChildren = rootGroup.getAttributeValue("AXChildren");
+ is(rootGroupChildren.length, 1, "Root group has one children");
+
+ is(
+ rootGroupChildren[0].getAttributeValue("AXRole"),
+ "AXButton",
+ "Button is child of root group"
+ );
+
+ // From bottom-up
+ let button = getNativeInterface(accDoc, "button");
+ rootGroup = button.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ }
+);
+
+/**
+ * Test document with dialog role and heading
+ */
+addAccessibleTask(
+ `<body role="dialog" aria-labelledby="h">
+ <h1 id="h">
+ We're building a richer search experience
+ </h1>
+ </body>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ is(rootGroup.getAttributeValue("AXRole"), "AXGroup", "Inherits role");
+
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXApplicationDialog",
+ "Inherits subrole"
+ );
+ let rootGroupChildren = rootGroup.getAttributeValue("AXChildren");
+ is(rootGroupChildren.length, 1, "Root group has one child");
+
+ is(
+ rootGroupChildren[0].getAttributeValue("AXRole"),
+ "AXHeading",
+ "Heading is child of root group"
+ );
+
+ // From bottom-up
+ let heading = getNativeInterface(accDoc, "h");
+ rootGroup = heading.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Parent is generated root group"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js
new file mode 100644
index 0000000000..3f13506757
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rotor.js
@@ -0,0 +1,1752 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+/**
+ * Test rotor with heading
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, headingCount, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ hello.getAttributeValue("AXTitle"),
+ headings[0].getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ world.getAttributeValue("AXTitle"),
+ headings[1].getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with heading and empty search text
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXSearchText: "",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(headingCount, 2, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ headings[0].getAttributeValue("AXTitle"),
+ hello.getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ headings[1].getAttributeValue("AXTitle"),
+ world.getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with articles
+ */
+addAccessibleTask(
+ `<article id="google">
+ <h2>Google Chrome</h2>
+ <p>Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!</p>
+ </article>
+
+ <article id="moz">
+ <h2>Mozilla Firefox</h2>
+ <p>Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.</p>
+ </article>
+
+ <article id="microsoft">
+ <h2>Microsoft Edge</h2>
+ <p>Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.</p>
+ </article> `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXArticleSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const articleCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(3, articleCount, "Found three articles");
+
+ const articles = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const google = getNativeInterface(accDoc, "google");
+ const moz = getNativeInterface(accDoc, "moz");
+ const microsoft = getNativeInterface(accDoc, "microsoft");
+
+ is(
+ google.getAttributeValue("AXTitle"),
+ articles[0].getAttributeValue("AXTitle"),
+ "Found correct first article"
+ );
+ is(
+ moz.getAttributeValue("AXTitle"),
+ articles[1].getAttributeValue("AXTitle"),
+ "Found correct second article"
+ );
+ is(
+ microsoft.getAttributeValue("AXTitle"),
+ articles[2].getAttributeValue("AXTitle"),
+ "Found correct third article"
+ );
+ }
+);
+
+/**
+ * Test rotor with tables
+ */
+addAccessibleTask(
+ `
+ <table id="shapes">
+ <tr>
+ <th>Shape</th>
+ <th>Color</th>
+ <th>Do I like it?</th>
+ </tr>
+ <tr>
+ <td>Triangle</td>
+ <td>Green</td>
+ <td>No</td>
+ </tr>
+ <tr>
+ <td>Square</td>
+ <td>Red</td>
+ <td>Yes</td>
+ </tr>
+ </table>
+ <br>
+ <table id="food">
+ <tr>
+ <th>Grocery Item</th>
+ <th>Quantity</th>
+ </tr>
+ <tr>
+ <td>Onions</td>
+ <td>2</td>
+ </tr>
+ <tr>
+ <td>Yogurt</td>
+ <td>1</td>
+ </tr>
+ <tr>
+ <td>Spinach</td>
+ <td>1</td>
+ </tr>
+ <tr>
+ <td>Cherries</td>
+ <td>12</td>
+ </tr>
+ <tr>
+ <td>Carrots</td>
+ <td>5</td>
+ </tr>
+ </table>
+ <br>
+ <div role="table" id="ariaTable">
+ <div role="row">
+ <div role="cell">
+ I am a tiny aria table
+ </div>
+ </div>
+ </div>
+ <br>
+ <table role="grid" id="grid">
+ <tr>
+ <th>A</th>
+ <th>B</th>
+ <th>C</th>
+ <th>D</th>
+ <th>E</th>
+ </tr>
+ <tr>
+ <th>F</th>
+ <th>G</th>
+ <th>H</th>
+ <th>I</th>
+ <th>J</th>
+ </tr>
+ </table>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXTableSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const tableCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, tableCount, "Found four tables");
+
+ const tables = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const shapes = getNativeInterface(accDoc, "shapes");
+ const food = getNativeInterface(accDoc, "food");
+ const ariaTable = getNativeInterface(accDoc, "ariaTable");
+ const grid = getNativeInterface(accDoc, "grid");
+
+ is(
+ shapes.getAttributeValue("AXColumnCount"),
+ tables[0].getAttributeValue("AXColumnCount"),
+ "Found correct first table"
+ );
+ is(
+ food.getAttributeValue("AXColumnCount"),
+ tables[1].getAttributeValue("AXColumnCount"),
+ "Found correct second table"
+ );
+ is(
+ ariaTable.getAttributeValue("AXColumnCount"),
+ tables[2].getAttributeValue("AXColumnCount"),
+ "Found correct third table"
+ );
+ is(
+ grid.getAttributeValue("AXColumnCount"),
+ tables[3].getAttributeValue("AXColumnCount"),
+ "Found correct fourth table"
+ );
+ }
+);
+
+/**
+ * Test rotor with landmarks
+ */
+addAccessibleTask(
+ `
+ <header id="header">
+ <h1>This is a heading within a header</h1>
+ </header>
+
+ <nav id="nav">
+ <a href="example.com">I am a link in a nav</a>
+ </nav>
+
+ <main id="main">
+ I am some text in a main element
+ </main>
+
+ <footer id="footer">
+ <h2>Heading in footer</h2>
+ </footer>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXLandmarkSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const landmarkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, landmarkCount, "Found four landmarks");
+
+ const landmarks = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const header = getNativeInterface(accDoc, "header");
+ const nav = getNativeInterface(accDoc, "nav");
+ const main = getNativeInterface(accDoc, "main");
+ const footer = getNativeInterface(accDoc, "footer");
+
+ is(
+ header.getAttributeValue("AXSubrole"),
+ landmarks[0].getAttributeValue("AXSubrole"),
+ "Found correct first landmark"
+ );
+ is(
+ nav.getAttributeValue("AXSubrole"),
+ landmarks[1].getAttributeValue("AXSubrole"),
+ "Found correct second landmark"
+ );
+ is(
+ main.getAttributeValue("AXSubrole"),
+ landmarks[2].getAttributeValue("AXSubrole"),
+ "Found correct third landmark"
+ );
+ is(
+ footer.getAttributeValue("AXSubrole"),
+ landmarks[3].getAttributeValue("AXSubrole"),
+ "Found correct fourth landmark"
+ );
+ }
+);
+
+/**
+ * Test rotor with aria landmarks
+ */
+addAccessibleTask(
+ `
+ <div id="banner" role="banner">
+ <h1>This is a heading within a banner</h1>
+ </div>
+
+ <div id="nav" role="navigation">
+ <a href="example.com">I am a link in a nav</a>
+ </div>
+
+ <div id="main" role="main">
+ I am some text in a main element
+ </div>
+
+ <div id="contentinfo" role="contentinfo">
+ <h2>Heading in contentinfo</h2>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXLandmarkSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const landmarkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, landmarkCount, "Found four landmarks");
+
+ const landmarks = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const banner = getNativeInterface(accDoc, "banner");
+ const nav = getNativeInterface(accDoc, "nav");
+ const main = getNativeInterface(accDoc, "main");
+ const contentinfo = getNativeInterface(accDoc, "contentinfo");
+
+ is(
+ banner.getAttributeValue("AXSubrole"),
+ landmarks[0].getAttributeValue("AXSubrole"),
+ "Found correct first landmark"
+ );
+ is(
+ nav.getAttributeValue("AXSubrole"),
+ landmarks[1].getAttributeValue("AXSubrole"),
+ "Found correct second landmark"
+ );
+ is(
+ main.getAttributeValue("AXSubrole"),
+ landmarks[2].getAttributeValue("AXSubrole"),
+ "Found correct third landmark"
+ );
+ is(
+ contentinfo.getAttributeValue("AXSubrole"),
+ landmarks[3].getAttributeValue("AXSubrole"),
+ "Found correct fourth landmark"
+ );
+ }
+);
+
+/**
+ * Test rotor with buttons
+ */
+addAccessibleTask(
+ `
+ <button id="button">hello world</button><br>
+
+ <input type="button" value="another kinda button" id="input"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXButtonSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const buttonCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, buttonCount, "Found two buttons");
+
+ const buttons = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const button = getNativeInterface(accDoc, "button");
+ const input = getNativeInterface(accDoc, "input");
+
+ is(
+ button.getAttributeValue("AXRole"),
+ buttons[0].getAttributeValue("AXRole"),
+ "Found correct button"
+ );
+ is(
+ input.getAttributeValue("AXRole"),
+ buttons[1].getAttributeValue("AXRole"),
+ "Found correct input button"
+ );
+ }
+);
+
+/**
+ * Test rotor with heading
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, headingCount, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ hello.getAttributeValue("AXTitle"),
+ headings[0].getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ world.getAttributeValue("AXTitle"),
+ headings[1].getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with buttons
+ */
+addAccessibleTask(
+ `
+ <form>
+ <h2>input[type=button]</h2>
+ <input type="button" value="apply" id="button1">
+
+ <h2>input[type=submit]</h2>
+ <input type="submit" value="submit now" id="submit">
+
+ <h2>input[type=image]</h2>
+ <input type="image" src="sample.jpg" alt="submit image" id="image">
+
+ <h2>input[type=reset]</h2>
+ <input type="reset" value="reset now" id="reset">
+
+ <h2>button element</h2>
+ <button id="button2">Submit button</button>
+ </form>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(5, controlsCount, "Found 5 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const button1 = getNativeInterface(accDoc, "button1");
+ const submit = getNativeInterface(accDoc, "submit");
+ const image = getNativeInterface(accDoc, "image");
+ const reset = getNativeInterface(accDoc, "reset");
+ const button2 = getNativeInterface(accDoc, "button2");
+
+ is(
+ button1.getAttributeValue("AXTitle"),
+ controls[0].getAttributeValue("AXTitle"),
+ "Found correct first control"
+ );
+ is(
+ submit.getAttributeValue("AXTitle"),
+ controls[1].getAttributeValue("AXTitle"),
+ "Found correct second control"
+ );
+ is(
+ image.getAttributeValue("AXTitle"),
+ controls[2].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ is(
+ reset.getAttributeValue("AXTitle"),
+ controls[3].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ is(
+ button2.getAttributeValue("AXTitle"),
+ controls[4].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ }
+);
+
+/**
+ * Test rotor with inputs
+ */
+addAccessibleTask(
+ `
+ <input type="text" value="I'm a text field." id="text"><br>
+ <input type="text" value="me too" id="implText"><br>
+ <textarea id="textarea">this is some text in a text area</textarea><br>
+ <input type="tel" value="0000000000" id="tel"><br>
+ <input type="url" value="https://example.com" id="url"><br>
+ <input type="email" value="hi@example.com" id="email"><br>
+ <input type="password" value="blah" id="password"><br>
+ <input type="month" value="2020-01" id="month"><br>
+ <input type="week" value="2020-W01" id="week"><br>
+ <input type="number" value="12" id="number"><br>
+ <input type="range" value="12" min="0" max="20" id="range"><br>
+ <input type="date" value="2020-01-01" id="date"><br>
+ <input type="time" value="10:10:10" id="time"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(13, controlsCount, "Found 13 controls");
+ // the extra controls here come from our time control
+ // we can't filter out its internal buttons/incrementors
+ // like we do with the date entry because the time entry
+ // doesn't have its own specific role -- its just a grouping.
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const text = getNativeInterface(accDoc, "text");
+ const implText = getNativeInterface(accDoc, "implText");
+ const textarea = getNativeInterface(accDoc, "textarea");
+ const tel = getNativeInterface(accDoc, "tel");
+ const url = getNativeInterface(accDoc, "url");
+ const email = getNativeInterface(accDoc, "email");
+ const password = getNativeInterface(accDoc, "password");
+ const month = getNativeInterface(accDoc, "month");
+ const week = getNativeInterface(accDoc, "week");
+ const number = getNativeInterface(accDoc, "number");
+ const range = getNativeInterface(accDoc, "range");
+
+ const toCheck = [
+ text,
+ implText,
+ textarea,
+ tel,
+ url,
+ email,
+ password,
+ month,
+ week,
+ number,
+ range,
+ ];
+
+ for (let i = 0; i < toCheck.length; i++) {
+ is(
+ toCheck[i].getAttributeValue("AXValue"),
+ controls[i].getAttributeValue("AXValue"),
+ "Found correct input control"
+ );
+ }
+
+ const date = getNativeInterface(accDoc, "date");
+ const time = getNativeInterface(accDoc, "time");
+
+ is(
+ date.getAttributeValue("AXRole"),
+ controls[11].getAttributeValue("AXRole"),
+ "Found corrent date editor"
+ );
+
+ is(
+ time.getAttributeValue("AXRole"),
+ controls[12].getAttributeValue("AXRole"),
+ "Found corrent time editor"
+ );
+ }
+);
+
+/**
+ * Test rotor with groupings
+ */
+addAccessibleTask(
+ `
+ <fieldset>
+ <legend>Radios</legend>
+ <div role="radiogroup" id="radios">
+ <input id="radio1" type="radio" name="g1" checked="checked"> Radio 1
+ <input id="radio2" type="radio" name="g1"> Radio 2
+ </div>
+ </fieldset>
+
+ <fieldset id="checkboxes">
+ <legend>Checkboxes</legend>
+ <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1
+ <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2
+ </fieldset>
+
+ <fieldset id="switches">
+ <legend>Switches</legend>
+ <input id="switch1" name="g3" role="switch" type="checkbox">Switch 1
+ <input checked="checked" id="switch2" name="g3" role="switch" type="checkbox">Switch 2
+ </fieldset>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(9, controlsCount, "Found 9 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const radios = getNativeInterface(accDoc, "radios");
+ const radio1 = getNativeInterface(accDoc, "radio1");
+ const radio2 = getNativeInterface(accDoc, "radio2");
+
+ is(
+ radios.getAttributeValue("AXRole"),
+ controls[0].getAttributeValue("AXRole"),
+ "Found correct group of radios"
+ );
+ is(
+ radio1.getAttributeValue("AXRole"),
+ controls[1].getAttributeValue("AXRole"),
+ "Found correct radio 1"
+ );
+ is(
+ radio2.getAttributeValue("AXRole"),
+ controls[2].getAttributeValue("AXRole"),
+ "Found correct radio 2"
+ );
+
+ const checkboxes = getNativeInterface(accDoc, "checkboxes");
+ const checkbox1 = getNativeInterface(accDoc, "checkbox1");
+ const checkbox2 = getNativeInterface(accDoc, "checkbox2");
+
+ is(
+ checkboxes.getAttributeValue("AXRole"),
+ controls[3].getAttributeValue("AXRole"),
+ "Found correct group of checkboxes"
+ );
+ is(
+ checkbox1.getAttributeValue("AXRole"),
+ controls[4].getAttributeValue("AXRole"),
+ "Found correct checkbox 1"
+ );
+ is(
+ checkbox2.getAttributeValue("AXRole"),
+ controls[5].getAttributeValue("AXRole"),
+ "Found correct checkbox 2"
+ );
+
+ const switches = getNativeInterface(accDoc, "switches");
+ const switch1 = getNativeInterface(accDoc, "switch1");
+ const switch2 = getNativeInterface(accDoc, "switch2");
+
+ is(
+ switches.getAttributeValue("AXRole"),
+ controls[6].getAttributeValue("AXRole"),
+ "Found correct group of switches"
+ );
+ is(
+ switch1.getAttributeValue("AXRole"),
+ controls[7].getAttributeValue("AXRole"),
+ "Found correct switch 1"
+ );
+ is(
+ switch2.getAttributeValue("AXRole"),
+ controls[8].getAttributeValue("AXRole"),
+ "Found correct switch 2"
+ );
+ }
+);
+
+/**
+ * Test rotor with misc controls
+ */
+addAccessibleTask(
+ `
+ <input role="spinbutton" id="spinbutton" type="number" value="25">
+
+ <details id="details">
+ <summary>Hello</summary>
+ world
+ </details>
+
+ <ul role="tree" id="tree">
+ <li role="treeitem">item1</li>
+ <li role="treeitem">item1</li>
+ </ul>
+
+ <a id="buttonMenu" role="button">Click Me</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, controlsCount, "Found 4 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const spin = getNativeInterface(accDoc, "spinbutton");
+ const details = getNativeInterface(accDoc, "details");
+ const tree = getNativeInterface(accDoc, "tree");
+ const buttonMenu = getNativeInterface(accDoc, "buttonMenu");
+
+ is(
+ spin.getAttributeValue("AXRole"),
+ controls[0].getAttributeValue("AXRole"),
+ "Found correct spinbutton"
+ );
+ is(
+ details.getAttributeValue("AXRole"),
+ controls[1].getAttributeValue("AXRole"),
+ "Found correct details element"
+ );
+ is(
+ tree.getAttributeValue("AXRole"),
+ controls[2].getAttributeValue("AXRole"),
+ "Found correct tree"
+ );
+ is(
+ buttonMenu.getAttributeValue("AXRole"),
+ controls[3].getAttributeValue("AXRole"),
+ "Found correct button menu"
+ );
+ }
+);
+
+/**
+ * Test rotor with links
+ */
+addAccessibleTask(
+ `
+ <a href="" id="empty">empty link</a>
+ <a href="http://www.example.com/" id="href">Example link</a>
+ <a id="noHref">link without href</a>
+ `,
+ async (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, linkCount, "Found two links");
+
+ let links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const empty = getNativeInterface(accDoc, "empty");
+ const href = getNativeInterface(accDoc, "href");
+
+ is(
+ empty.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct first link"
+ );
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[1].getAttributeValue("AXTitle"),
+ "Found correct second link"
+ );
+
+ // unvisited links
+
+ searchPred = {
+ AXSearchKey: "AXUnvisitedLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, linkCount, "Found two links");
+
+ links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ empty.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct first link"
+ );
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[1].getAttributeValue("AXTitle"),
+ "Found correct second link"
+ );
+
+ // visited links
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "href");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await PlacesTestUtils.addVisits(["http://www.example.com/"]);
+
+ await stateChanged;
+
+ searchPred = {
+ AXSearchKey: "AXVisitedLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(1, linkCount, "Found one link");
+
+ links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct visited link"
+ );
+
+ // Ensure history is cleared before running again
+ await PlacesUtils.history.clear();
+ }
+);
+
+/*
+ * Test AXAnyTypeSearchKey with root group
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(results.length, 1, "One result for root group");
+ is(
+ results[0].getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ searchPred.AXStartElement = results[0];
+ results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(results.length, 0, "No more results past root group");
+
+ searchPred.AXDirection = "AXDirectionPrevious";
+ results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(
+ results.length,
+ 0,
+ "Searching backwards from root group should yield no results"
+ );
+
+ const rootGroup = webArea.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ results = rootGroup.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ results[0].getAttributeValue("AXRole"),
+ "AXHeading",
+ "Is first heading child"
+ );
+ }
+);
+
+/**
+ * Test rotor with checkboxes
+ */
+addAccessibleTask(
+ `
+ <fieldset id="checkboxes">
+ <legend>Checkboxes</legend>
+ <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1
+ <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2
+ <div id="checkbox3" role="checkbox">Checkbox 3</div>
+ <div id="checkbox4" role="checkbox" aria-checked="true">Checkbox 4</div>
+ </fieldset>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXCheckBoxSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const checkboxCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, checkboxCount, "Found 4 checkboxes");
+
+ const checkboxes = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const checkbox1 = getNativeInterface(accDoc, "checkbox1");
+ const checkbox2 = getNativeInterface(accDoc, "checkbox2");
+ const checkbox3 = getNativeInterface(accDoc, "checkbox3");
+ const checkbox4 = getNativeInterface(accDoc, "checkbox4");
+
+ is(
+ checkbox1.getAttributeValue("AXValue"),
+ checkboxes[0].getAttributeValue("AXValue"),
+ "Found correct checkbox 1"
+ );
+ is(
+ checkbox2.getAttributeValue("AXValue"),
+ checkboxes[1].getAttributeValue("AXValue"),
+ "Found correct checkbox 2"
+ );
+ is(
+ checkbox3.getAttributeValue("AXValue"),
+ checkboxes[2].getAttributeValue("AXValue"),
+ "Found correct checkbox 3"
+ );
+ is(
+ checkbox4.getAttributeValue("AXValue"),
+ checkboxes[3].getAttributeValue("AXValue"),
+ "Found correct checkbox 4"
+ );
+ }
+);
+
+/**
+ * Test rotor with radiogroups
+ */
+addAccessibleTask(
+ `
+ <div role="radiogroup" id="radios" aria-labelledby="desc">
+ <h1 id="desc">some radio buttons</h1>
+ <div id="radio1" role="radio"> Radio 1</div>
+ <div id="radio2" role="radio"> Radio 2</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXRadioGroupSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const radiogroupCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(1, radiogroupCount, "Found 1 radio group");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const radios = getNativeInterface(accDoc, "radios");
+
+ is(
+ radios.getAttributeValue("AXDescription"),
+ controls[0].getAttributeValue("AXDescription"),
+ "Found correct group of radios"
+ );
+ }
+);
+
+/*
+ * Test rotor with inputs
+ */
+addAccessibleTask(
+ `
+ <input type="text" value="I'm a text field." id="text"><br>
+ <input type="text" value="me too" id="implText"><br>
+ <textarea id="textarea">this is some text in a text area</textarea><br>
+ <input type="tel" value="0000000000" id="tel"><br>
+ <input type="url" value="https://example.com" id="url"><br>
+ <input type="email" value="hi@example.com" id="email"><br>
+ <input type="password" value="blah" id="password"><br>
+ <input type="month" value="2020-01" id="month"><br>
+ <input type="week" value="2020-W01" id="week"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXTextFieldSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textfieldCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(9, textfieldCount, "Found 9 fields");
+
+ const fields = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const text = getNativeInterface(accDoc, "text");
+ const implText = getNativeInterface(accDoc, "implText");
+ const textarea = getNativeInterface(accDoc, "textarea");
+ const tel = getNativeInterface(accDoc, "tel");
+ const url = getNativeInterface(accDoc, "url");
+ const email = getNativeInterface(accDoc, "email");
+ const password = getNativeInterface(accDoc, "password");
+ const month = getNativeInterface(accDoc, "month");
+ const week = getNativeInterface(accDoc, "week");
+
+ const toCheck = [
+ text,
+ implText,
+ textarea,
+ tel,
+ url,
+ email,
+ password,
+ month,
+ week,
+ ];
+
+ for (let i = 0; i < toCheck.length; i++) {
+ is(
+ toCheck[i].getAttributeValue("AXValue"),
+ fields[i].getAttributeValue("AXValue"),
+ "Found correct input control"
+ );
+ }
+ }
+);
+
+/**
+ * Test rotor with static text
+ */
+addAccessibleTask(
+ `
+ <h1>Hello I am a heading</h1>
+ This is some regular text.<p>this is some paragraph text</p><br>
+ This is a list:<ul>
+ <li>List item one</li>
+ <li>List item two</li>
+ </ul>
+
+ <a href="http://example.com">This is a link</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXStaticTextSearchKey",
+ AXImmediateDescendants: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(7, textCount, "Found 7 pieces of text");
+
+ const text = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ "Hello I am a heading",
+ text[0].getAttributeValue("AXValue"),
+ "Found correct text node for heading"
+ );
+ is(
+ "This is some regular text.",
+ text[1].getAttributeValue("AXValue"),
+ "Found correct text node"
+ );
+ is(
+ "this is some paragraph text",
+ text[2].getAttributeValue("AXValue"),
+ "Found correct text node for paragraph"
+ );
+ is(
+ "This is a list:",
+ text[3].getAttributeValue("AXValue"),
+ "Found correct text node for pre-list text node"
+ );
+ is(
+ "List item one",
+ text[4].getAttributeValue("AXValue"),
+ "Found correct text node for list item one"
+ );
+ is(
+ "List item two",
+ text[5].getAttributeValue("AXValue"),
+ "Found correct text node for list item two"
+ );
+ is(
+ "This is a link",
+ text[6].getAttributeValue("AXValue"),
+ "Found correct text node for link"
+ );
+ }
+);
+
+/**
+ * Test rotor with lists
+ */
+addAccessibleTask(
+ `
+ <ul id="unordered">
+ <li>hello</li>
+ <li>world</li>
+ </ul>
+
+ <ol id="ordered">
+ <li>item one</li>
+ <li>item two</li>
+ </ol>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXListSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const listCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, listCount, "Found 2 lists");
+
+ const lists = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const ordered = getNativeInterface(accDoc, "ordered");
+ const unordered = getNativeInterface(accDoc, "unordered");
+
+ is(
+ unordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ lists[0].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ "Found correct unordered list"
+ );
+ is(
+ ordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ lists[1].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ "Found correct ordered list"
+ );
+ }
+);
+
+/*
+ * Test rotor with images
+ */
+addAccessibleTask(
+ `
+ <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br>
+ <a href="http://example.com">
+ <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+ <img src="" id="img3">
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXImageSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let images = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(images.length, 3, "Found three images");
+
+ const img1 = getNativeInterface(accDoc, "img1");
+ const img2 = getNativeInterface(accDoc, "img2");
+ const img3 = getNativeInterface(accDoc, "img3");
+
+ is(
+ img1.getAttributeValue("AXDescription"),
+ images[0].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img2.getAttributeValue("AXDescription"),
+ images[1].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img3.getAttributeValue("AXDescription"),
+ images[2].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+ }
+);
+
+/**
+ * Test rotor with frames
+ */
+addAccessibleTask(
+ `
+ <iframe id="frame1" src="data:text/html,<h1>hello</h1>world"></iframe>
+ <iframe id="frame2" src="data:text/html,<iframe id='frame3' src='data:text/html,<h1>goodbye</h1>'>"></iframe>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXFrameSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const frameCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(3, frameCount, "Found 3 frames");
+ }
+);
+
+/**
+ * Test rotor with static text
+ */
+addAccessibleTask(
+ `
+ <h1>Hello I am a heading</h1>
+ This is some regular text.<p>this is some paragraph text</p><br>
+ This is a list:<ul>
+ <li>List item one</li>
+ <li>List item two</li>
+ </ul>
+
+ <a href="http://example.com">This is a link</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXStaticTextSearchKey",
+ AXImmediateDescendants: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(7, textCount, "Found 7 pieces of text");
+
+ const text = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ "Hello I am a heading",
+ text[0].getAttributeValue("AXValue"),
+ "Found correct text node for heading"
+ );
+ is(
+ "This is some regular text.",
+ text[1].getAttributeValue("AXValue"),
+ "Found correct text node"
+ );
+ is(
+ "this is some paragraph text",
+ text[2].getAttributeValue("AXValue"),
+ "Found correct text node for paragraph"
+ );
+ is(
+ "This is a list:",
+ text[3].getAttributeValue("AXValue"),
+ "Found correct text node for pre-list text node"
+ );
+ is(
+ "List item one",
+ text[4].getAttributeValue("AXValue"),
+ "Found correct text node for list item one"
+ );
+ is(
+ "List item two",
+ text[5].getAttributeValue("AXValue"),
+ "Found correct text node for list item two"
+ );
+ is(
+ "This is a link",
+ text[6].getAttributeValue("AXValue"),
+ "Found correct text node for link"
+ );
+ }
+);
+
+/**
+ * Test search with non-webarea root
+ */
+addAccessibleTask(
+ `
+ <div id="searchroot"><p id="p1">hello</p><p id="p2">world</p></div>
+ <div><p>goodybe</p></div>
+ `,
+ async (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const searchRoot = getNativeInterface(accDoc, "searchroot");
+ const resultCount = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(resultCount, 2, "Found 2 items");
+
+ const p1 = getNativeInterface(accDoc, "p1");
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXStartElement: p1,
+ };
+
+ let results = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["p2"],
+ "Result is next group sibling"
+ );
+
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionPrevious",
+ };
+
+ results = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["p2", "p1"],
+ "A reverse search should return groups in reverse"
+ );
+ }
+);
+
+/**
+ * Test search text
+ */
+addAccessibleTask(
+ `
+ <p>It's about the future, isn't it?</p>
+ <p>Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.</p>
+ <ul>
+ <li>I could hang out, you could show me around.</li>
+ <li>There's that word again, heavy.</li>
+ </ul>
+ `,
+ async (browser, f, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXSearchText: "could",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textSearchCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(textSearchCount, 2, "Found 2 matching items in text search");
+
+ const results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ info(results.map(r => r.getAttributeValue("AXMozDebugDescription")));
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXValue")),
+ [
+ "Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.",
+ "I could hang out, you could show me around.",
+ ],
+ "Correct text search results"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js
new file mode 100644
index 0000000000..af88c2e136
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_selectables.js
@@ -0,0 +1,363 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getSelectedIds(selectable) {
+ return selectable
+ .getAttributeValue("AXSelectedChildren")
+ .map(c => c.getAttributeValue("AXDOMIdentifier"));
+}
+
+/**
+ * Test aria tabs
+ */
+addAccessibleTask("mac/doc_aria_tabs.html", async (browser, accDoc) => {
+ let tablist = getNativeInterface(accDoc, "tablist");
+ is(
+ tablist.getAttributeValue("AXRole"),
+ "AXTabGroup",
+ "Correct role for tablist"
+ );
+
+ let tabMacAccs = tablist.getAttributeValue("AXTabs");
+ is(tabMacAccs.length, 3, "3 items in AXTabs");
+
+ let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+
+ let tab = selectedTabs[0];
+ is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
+ is(
+ tab.getAttributeValue("AXSubrole"),
+ "AXTabButton",
+ "Correct subrole for tab"
+ );
+ is(tab.getAttributeValue("AXTitle"), "First Tab", "Correct title for tab");
+
+ let tabToSelect = tabMacAccs[1];
+ is(
+ tabToSelect.getAttributeValue("AXTitle"),
+ "Second Tab",
+ "Correct title for tab"
+ );
+
+ let actions = tabToSelect.actionNames;
+ ok(true, actions);
+ ok(actions.includes("AXPress"), "Has switch action");
+
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ tabToSelect.performAction("AXPress");
+ await evt;
+
+ selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+ is(
+ selectedTabs[0].getAttributeValue("AXTitle"),
+ "Second Tab",
+ "Correct title for tab"
+ );
+});
+
+addAccessibleTask('<p id="p">hello</p>', async (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+ ok(
+ p.attributeNames.includes("AXSelected"),
+ "html element includes 'AXSelected' attribute"
+ );
+ is(p.getAttributeValue("AXSelected"), 0, "AX selected is 'false'");
+});
+
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a number" multiple>
+ <option id="one" selected>One</option>
+ <option id="two">Two</option>
+ <option id="three">Three</option>
+ <option id="four" disabled>Four</option>
+ </select>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+ let three = getNativeInterface(accDoc, "three");
+ let four = getNativeInterface(accDoc, "four");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a number",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+
+ is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
+ is(
+ one.getAttributeValue("AXValue"),
+ "One",
+ "Option should have correct value"
+ );
+ is(
+ one.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
+
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ one.setAttributeValue("AXSelected", false);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ three.setAttributeValue("AXSelected", true);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "one" && ids[1] == "two";
+ }, "Got correct selected children");
+
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "two" && ids[1] == "three";
+ }, "Got correct selected children");
+
+ ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled");
+ }
+);
+
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a thing" multiple>
+ <optgroup label="Fruits">
+ <option id="banana" selected>Banana</option>
+ <option id="apple">Apple</option>
+ <option id="orange">Orange</option>
+ </optgroup>
+ <optgroup label="Vegetables">
+ <option id="lettuce" selected>Lettuce</option>
+ <option id="tomato">Tomato</option>
+ <option id="onion">Onion</option>
+ </optgroup>
+ <optgroup label="Spices">
+ <option id="cumin">Cumin</option>
+ <option id="coriander">Coriander</option>
+ <option id="allspice" selected>Allspice</option>
+ </optgroup>
+ <option id="everything">Everything</option>
+ </select>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a thing",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+ let childValueSelectablePairs = select
+ .getAttributeValue("AXChildren")
+ .map(c => [
+ c.getAttributeValue("AXValue"),
+ c.isAttributeSettable("AXSelected"),
+ c.getAttributeValue("AXEnabled"),
+ ]);
+ [
+ ["​", false, 0],
+ ["Fruits", false, 0],
+ ["Banana", true, 1],
+ ["Apple", true, 1],
+ ["Orange", true, 1],
+ ["​", false, 0],
+ ["Vegetables", false, 0],
+ ["Lettuce", true, 1],
+ ["Tomato", true, 1],
+ ["Onion", true, 1],
+ ["​", false, 0],
+ ["Spices", false, 0],
+ ["Cumin", true, 1],
+ ["Coriander", true, 1],
+ ["Allspice", true, 1],
+ ["Everything", true, 1],
+ ];
+ Assert.deepEqual(
+ childValueSelectablePairs,
+ [
+ ["​", false, false],
+ ["Fruits", false, false],
+ ["Banana", true, true],
+ ["Apple", true, true],
+ ["Orange", true, true],
+ ["​", false, false],
+ ["Vegetables", false, false],
+ ["Lettuce", true, true],
+ ["Tomato", true, true],
+ ["Onion", true, true],
+ ["​", false, false],
+ ["Spices", false, false],
+ ["Cumin", true, true],
+ ["Coriander", true, true],
+ ["Allspice", true, true],
+ ["Everything", true, true],
+ ],
+ "Options are selectable, group labels are not"
+ );
+
+ let allspice = getNativeInterface(accDoc, "allspice");
+ is(
+ allspice.getAttributeValue("AXTitle"),
+ "",
+ "Option should not have a title"
+ );
+ is(
+ allspice.getAttributeValue("AXValue"),
+ "Allspice",
+ "Option should have a value"
+ );
+ is(
+ allspice.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(
+ allspice.isAttributeSettable("AXSelected"),
+ "Option can have AXSelected set"
+ );
+ is(
+ allspice
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of nested option"
+ );
+
+ let groupLabel = select.getAttributeValue("AXChildren")[1];
+ ok(
+ !groupLabel.isAttributeSettable("AXSelected"),
+ "Group label should not be selectable"
+ );
+ is(
+ groupLabel.getAttributeValue("AXValue"),
+ "Fruits",
+ "Group label should have a value"
+ );
+ is(
+ groupLabel.getAttributeValue("AXTitle"),
+ null,
+ "Group label should not have a title"
+ );
+ is(
+ groupLabel.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Group label should have AXStaticText role"
+ );
+ is(
+ groupLabel
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of group label"
+ );
+
+ Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]);
+ }
+);
+
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true">
+ <div role="option" id="one" aria-selected="true">One</div>
+ <div role="option" id="two">Two</div>
+ <div role="option" id="three">Three</div>
+ <div role="option" id="four" aria-disabled="true">Four</div>
+</div>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+ let three = getNativeInterface(accDoc, "three");
+ let four = getNativeInterface(accDoc, "four");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a number",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+
+ is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
+ is(
+ one.getAttributeValue("AXValue"),
+ "One",
+ "Option should have correct value"
+ );
+ is(
+ one.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
+
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ // Change selection from content.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("one").removeAttribute("aria-selected");
+ });
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ three.setAttributeValue("AXSelected", true);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "one" && ids[1] == "two";
+ }, "Got correct selected children");
+
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "two" && ids[1] == "three";
+ }, "Got correct selected children");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_table.js b/accessible/tests/browser/mac/browser_table.js
new file mode 100644
index 0000000000..50ae697deb
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_table.js
@@ -0,0 +1,629 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Helper function to test table consistency.
+ */
+function testTableConsistency(table, expectedRowCount, expectedColumnCount) {
+ is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table");
+
+ let tableChildren = table.getAttributeValue("AXChildren");
+ // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers
+ // if we're trying to match Safari.
+ is(
+ tableChildren.length,
+ expectedRowCount + expectedColumnCount,
+ "Table has children = rows (4) + cols (3)"
+ );
+ for (let i = 0; i < tableChildren.length; i++) {
+ let currChild = tableChildren[i];
+ if (i < expectedRowCount) {
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXRow",
+ "Correct role for row"
+ );
+ } else {
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXColumn",
+ "Correct role for col"
+ );
+ is(
+ currChild.getAttributeValue("AXRoleDescription"),
+ "column",
+ "Correct role desc for col"
+ );
+ }
+ }
+
+ is(
+ table.getAttributeValue("AXColumnCount"),
+ expectedColumnCount,
+ "Table has correct column count."
+ );
+ is(
+ table.getAttributeValue("AXRowCount"),
+ expectedRowCount,
+ "Table has correct row count."
+ );
+
+ let cols = table.getAttributeValue("AXColumns");
+ is(cols.length, expectedColumnCount, "Table has col list of correct length");
+ for (let i = 0; i < cols.length; i++) {
+ let currCol = cols[i];
+ let currChildren = currCol.getAttributeValue("AXChildren");
+ is(
+ currChildren.length,
+ expectedRowCount,
+ "Column has correct number of cells"
+ );
+ for (let j = 0; j < currChildren.length; j++) {
+ let currChild = currChildren[j];
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXCell",
+ "Column child is cell"
+ );
+ }
+ }
+
+ let rows = table.getAttributeValue("AXRows");
+ is(rows.length, expectedRowCount, "Table has row list of correct length");
+ for (let i = 0; i < rows.length; i++) {
+ let currRow = rows[i];
+ let currChildren = currRow.getAttributeValue("AXChildren");
+ is(
+ currChildren.length,
+ expectedColumnCount,
+ "Row has correct number of cells"
+ );
+ for (let j = 0; j < currChildren.length; j++) {
+ let currChild = currChildren[j];
+ is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell");
+ }
+ }
+}
+
+/**
+ * Test table, columns, rows
+ */
+addAccessibleTask(
+ `<table id="customers">
+ <tbody>
+ <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr>
+ <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr>
+ <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr>
+ <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr>
+ </tbody>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "customers");
+ testTableConsistency(table, 4, 3);
+
+ const rowText = [
+ "Madrigal Electromotive GmbH",
+ "Lydia Rodarte-Quayle",
+ "Germany",
+ ];
+ let reorder = waitForEvent(EVENT_REORDER, "customers");
+ await SpecialPowers.spawn(browser, [rowText], _rowText => {
+ let tr = content.document.createElement("tr");
+ for (let t of _rowText) {
+ let td = content.document.createElement("td");
+ td.textContent = t;
+ tr.appendChild(td);
+ }
+ content.document.getElementById("customers").appendChild(tr);
+ });
+ await reorder;
+
+ let cols = table.getAttributeValue("AXColumns");
+ is(cols.length, 3, "Table has col list of correct length");
+ for (let i = 0; i < cols.length; i++) {
+ let currCol = cols[i];
+ let currChildren = currCol.getAttributeValue("AXChildren");
+ is(currChildren.length, 5, "Column has correct number of cells");
+ let lastCell = currChildren[currChildren.length - 1];
+ let cellChildren = lastCell.getAttributeValue("AXChildren");
+ is(cellChildren.length, 1, "Cell has a single text child");
+ is(
+ cellChildren[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Correct role for cell child"
+ );
+ is(
+ cellChildren[0].getAttributeValue("AXValue"),
+ rowText[i],
+ "Correct text for cell"
+ );
+ }
+
+ reorder = waitForEvent(EVENT_REORDER, "firstrow");
+ await SpecialPowers.spawn(browser, [], () => {
+ let td = content.document.createElement("td");
+ td.textContent = "Ticker";
+ content.document.getElementById("firstrow").appendChild(td);
+ });
+ await reorder;
+
+ cols = table.getAttributeValue("AXColumns");
+ is(cols.length, 4, "Table has col list of correct length");
+ is(
+ cols[cols.length - 1].getAttributeValue("AXChildren").length,
+ 1,
+ "Last column has single child"
+ );
+
+ reorder = waitForEvent(
+ EVENT_REORDER,
+ e => e.accessible.role == ROLE_DOCUMENT
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("customers").remove();
+ });
+ await reorder;
+
+ try {
+ cols[0].getAttributeValue("AXChildren");
+ ok(false, "Getting children from column of expired table should fail");
+ } catch (e) {
+ ok(true, "Getting children from column of expired table should fail");
+ }
+ }
+);
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <th colspan="2" id="header1">Header 1</th>
+ <th id="header2">Header 2</th>
+ </tr>
+ <tr>
+ <td id="cell1">one</td>
+ <td id="cell2" rowspan="2">two</td>
+ <td id="cell3">three</td>
+ </tr>
+ <tr>
+ <td id="cell4">four</td>
+ <td id="cell5">five</td>
+ </tr>
+ </table>`,
+ (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+
+ let getCellAt = (col, row) =>
+ table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]);
+
+ function testCell(cell, expectedId, expectedColRange, expectedRowRange) {
+ is(
+ cell.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct DOM Identifier"
+ );
+ Assert.deepEqual(
+ cell.getAttributeValue("AXColumnIndexRange"),
+ expectedColRange,
+ "Correct column range"
+ );
+ Assert.deepEqual(
+ cell.getAttributeValue("AXRowIndexRange"),
+ expectedRowRange,
+ "Correct row range"
+ );
+ }
+
+ testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]);
+ testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]);
+ testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]);
+
+ testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]);
+ testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]);
+ testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]);
+
+ testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]);
+ testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]);
+ testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]);
+
+ let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements");
+ Assert.deepEqual(
+ colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")),
+ ["header1", "header1", "header2"],
+ "Correct column headers"
+ );
+ }
+);
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <td>Foo</td>
+ </tr>
+ </table>`,
+ (browser, accDoc) => {
+ // Make sure we guess this table to be a layout table.
+ testAttrs(
+ findAccessibleChildByID(accDoc, "table"),
+ { "layout-guess": "true" },
+ true
+ );
+
+ let table = getNativeInterface(accDoc, "table");
+ is(
+ table.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct role (AXGroup) for layout table"
+ );
+
+ let children = table.getAttributeValue("AXChildren");
+ is(
+ children.length,
+ 1,
+ "Layout table has single child (no additional columns)"
+ );
+ }
+);
+
+addAccessibleTask(
+ `<div id="table" role="table">
+ <span style="display: block;">
+ <div role="row">
+ <div role="cell">Cell 1</div>
+ <div role="cell">Cell 2</div>
+ </div>
+ </span>
+ <span style="display: block;">
+ <div role="row">
+ <span style="display: block;">
+ <div role="cell">Cell 3</div>
+ <div role="cell">Cell 4</div>
+ </span>
+ </div>
+ </span>
+ </div>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ testTableConsistency(table, 2, 2);
+ }
+);
+
+/*
+ * After executing function 'change' which operates on 'elem', verify the specified
+ * 'event' (if not null) is fired on elem. After the event, check if the given
+ * native accessible 'table' is a layout or data table by role using 'isLayout'.
+ */
+async function testIsLayout(table, elem, event, change, isLayout) {
+ info(
+ "Changing " +
+ elem +
+ ", expecting table change to " +
+ (isLayout ? "AXGroup" : "AXTable")
+ );
+ const toWait = event ? waitForEvent(event, elem) : null;
+ await change();
+ if (toWait) {
+ await toWait;
+ }
+ let intendedRole = isLayout ? "AXGroup" : "AXTable";
+ await untilCacheIs(
+ () => table.getAttributeValue("AXRole"),
+ intendedRole,
+ "Table role correct after change"
+ );
+}
+
+/*
+ * The following attributes should fire an attribute changed
+ * event, which in turn invalidates the layout-table cache
+ * associated with the given table. After adding and removing
+ * each attr, verify the table is a data or layout table,
+ * appropriately. Attrs: summary, abbr, scope, headers
+ */
+addAccessibleTask(
+ `<table id="table" summary="example summary">
+ <tr role="presentation">
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td id="cellThree">cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ // summary attr should take precedence over role="presentation" to make this
+ // a data table
+ is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table");
+
+ info("Removing summary attr");
+ // after summary is removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "table",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("table").removeAttribute("summary");
+ });
+ },
+ true
+ );
+
+ info("Setting abbr attr");
+ // after abbr is set we should have a data table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("abbr", "hello world");
+ });
+ },
+ false
+ );
+
+ info("Removing abbr attr");
+ // after abbr is removed we should have a layout table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("cellThree").removeAttribute("abbr");
+ });
+ },
+ true
+ );
+
+ info("Setting scope attr");
+ // after scope is set we should have a data table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("scope", "col");
+ });
+ },
+ false
+ );
+
+ info("Removing scope attr");
+ // remove scope should give layout
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("cellThree").removeAttribute("scope");
+ });
+ },
+ true
+ );
+
+ info("Setting headers attr");
+ // add headers attr should give data
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("headers", "cellOne");
+ });
+ },
+ false
+ );
+
+ info("Removing headers attr");
+ // remove headers attr should give layout
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .removeAttribute("headers");
+ });
+ },
+ true
+ );
+ }
+);
+
+/*
+ * The following style changes should fire a table style changed
+ * event, which in turn invalidates the layout-table cache
+ * associated with the given table.
+ */
+addAccessibleTask(
+ `<table id="table">
+ <tr id="rowOne">
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td>cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ // we should start as a layout table
+ is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table");
+
+ info("Adding cell border");
+ // after cell border added, we should have a data table
+ await testIsLayout(
+ table,
+ "cellOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellOne")
+ .style.setProperty("border", "5px solid green");
+ });
+ },
+ false
+ );
+
+ info("Removing cell border");
+ // after cell border removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "cellOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellOne")
+ .style.removeProperty("border");
+ });
+ },
+ true
+ );
+
+ info("Adding row background");
+ // after row background added, we should have a data table
+ await testIsLayout(
+ table,
+ "rowOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("rowOne")
+ .style.setProperty("background-color", "green");
+ });
+ },
+ false
+ );
+
+ info("Removing row background");
+ // after row background removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "rowOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("rowOne")
+ .style.removeProperty("background-color");
+ });
+ },
+ true
+ );
+ }
+);
+
+/*
+ * thead/tbody elements with click handlers should:
+ * (a) render as AXGroup elements
+ * (b) expose their rows as part of their parent table's AXRows array
+ */
+addAccessibleTask(
+ `<table id="table">
+ <thead id="thead">
+ <tr><td>head row</td></tr>
+ </thead>
+ <tbody id="tbody">
+ <tr><td>body row</td></tr>
+ <tr><td>another body row</td></tr>
+ </tbody>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+
+ // No click handlers present on thead/tbody
+ let tableChildren = table.getAttributeValue("AXChildren");
+ let tableRows = table.getAttributeValue("AXRows");
+
+ is(tableChildren.length, 4, "Table has four children (3 row + 1 col)");
+ is(tableRows.length, 3, "Table has three rows");
+
+ for (let i = 0; i < tableChildren.length; i++) {
+ const child = tableChildren[i];
+ if (i < 3) {
+ is(
+ child.getAttributeValue("AXRole"),
+ "AXRow",
+ "Table's first 3 children are rows"
+ );
+ } else {
+ is(
+ child.getAttributeValue("AXRole"),
+ "AXColumn",
+ "Table's last child is a column"
+ );
+ }
+ }
+ const reorder = waitForEvent(EVENT_REORDER);
+ await invokeContentTask(browser, [], () => {
+ const head = content.document.getElementById("thead");
+ const body = content.document.getElementById("tbody");
+
+ head.addEventListener("click", function () {});
+ body.addEventListener("click", function () {});
+ });
+ await reorder;
+
+ // Click handlers present
+ tableChildren = table.getAttributeValue("AXChildren");
+
+ is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)");
+ is(
+ tableChildren[0].getAttributeValue("AXRole"),
+ "AXGroup",
+ "Child one is a group"
+ );
+ is(
+ tableChildren[0].getAttributeValue("AXChildren").length,
+ 1,
+ "Child one has one child"
+ );
+
+ is(
+ tableChildren[1].getAttributeValue("AXRole"),
+ "AXGroup",
+ "Child two is a group"
+ );
+ is(
+ tableChildren[1].getAttributeValue("AXChildren").length,
+ 2,
+ "Child two has two children"
+ );
+
+ is(
+ tableChildren[2].getAttributeValue("AXRole"),
+ "AXColumn",
+ "Child three is a col"
+ );
+
+ tableRows = table.getAttributeValue("AXRows");
+ is(tableRows.length, 3, "Table has three rows");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_text_basics.js b/accessible/tests/browser/mac/browser_text_basics.js
new file mode 100644
index 0000000000..21637d0524
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_basics.js
@@ -0,0 +1,426 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testRangeAtMarker(macDoc, marker, attribute, expected, msg) {
+ let range = macDoc.getParameterizedAttributeValue(attribute, marker);
+ is(stringForRange(macDoc, range), expected, msg);
+}
+
+function testUIElement(
+ macDoc,
+ marker,
+ msg,
+ expectedRole,
+ expectedValue,
+ expectedRange
+) {
+ let elem = macDoc.getParameterizedAttributeValue(
+ "AXUIElementForTextMarker",
+ marker
+ );
+ is(
+ elem.getAttributeValue("AXRole"),
+ expectedRole,
+ `${msg}: element role matches`
+ );
+ is(elem.getAttributeValue("AXValue"), expectedValue, `${msg}: element value`);
+ let elemRange = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ elem
+ );
+ is(
+ stringForRange(macDoc, elemRange),
+ expectedRange,
+ `${msg}: element range matches element value`
+ );
+}
+
+function testStyleRun(macDoc, marker, msg, expectedStyleRun) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXStyleTextMarkerRangeForTextMarker",
+ expectedStyleRun,
+ `${msg}: style run matches`
+ );
+}
+
+function testParagraph(macDoc, marker, msg, expectedParagraph) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXParagraphTextMarkerRangeForTextMarker",
+ expectedParagraph,
+ `${msg}: paragraph matches`
+ );
+}
+
+function testWords(macDoc, marker, msg, expectedLeft, expectedRight) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ expectedLeft,
+ `${msg}: left word matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXRightWordTextMarkerRangeForTextMarker",
+ expectedRight,
+ `${msg}: right word matches`
+ );
+}
+
+function testLines(
+ macDoc,
+ marker,
+ msg,
+ expectedLine,
+ expectedLeft,
+ expectedRight
+) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLineTextMarkerRangeForTextMarker",
+ expectedLine,
+ `${msg}: line matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLeftLineTextMarkerRangeForTextMarker",
+ expectedLeft,
+ `${msg}: left line matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXRightLineTextMarkerRangeForTextMarker",
+ expectedRight,
+ `${msg}: right line matches`
+ );
+}
+
+function* markerIterator(macDoc, reverse = false) {
+ let m = macDoc.getAttributeValue(
+ reverse ? "AXEndTextMarker" : "AXStartTextMarker"
+ );
+ let c = 0;
+ while (m) {
+ yield [m, c++];
+ m = macDoc.getParameterizedAttributeValue(
+ reverse
+ ? "AXPreviousTextMarkerForTextMarker"
+ : "AXNextTextMarkerForTextMarker",
+ m
+ );
+ }
+}
+
+// Tests consistency in text markers between:
+// 1. "Linked list" forward navagation
+// 2. Getting markers by index
+// 3. "Linked list" reverse navagation
+// For each iteration method check that the returned index is consistent
+function testMarkerIntegrity(accDoc, expectedMarkerValues) {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ // Iterate forward with "AXNextTextMarkerForTextMarker"
+ let prevMarker;
+ let count = 0;
+ for (let [marker, index] of markerIterator(macDoc)) {
+ count++;
+ let markerIndex = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ markerIndex,
+ index,
+ `Correct index in "AXNextTextMarkerForTextMarker": ${index}`
+ );
+ if (prevMarker) {
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [prevMarker, marker]
+ );
+ is(
+ macDoc.getParameterizedAttributeValue(
+ "AXLengthForTextMarkerRange",
+ range
+ ),
+ 1,
+ `[${index}] marker moved one character`
+ );
+ }
+ prevMarker = marker;
+
+ testWords(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].words
+ );
+ testLines(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].lines
+ );
+ testUIElement(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].element
+ );
+ testParagraph(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ expectedMarkerValues[index].paragraph
+ );
+ testStyleRun(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ expectedMarkerValues[index].style
+ );
+ }
+
+ is(expectedMarkerValues.length, count, `Correct marker count: ${count}`);
+
+ // Use "AXTextMarkerForIndex" to retrieve all text markers
+ for (let i = 0; i < count; i++) {
+ let marker = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerForIndex",
+ i
+ );
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`);
+
+ if (i == count - 1) {
+ ok(
+ !macDoc.getParameterizedAttributeValue(
+ "AXNextTextMarkerForTextMarker",
+ marker
+ ),
+ "Iterated through all markers"
+ );
+ }
+ }
+
+ count = expectedMarkerValues.length;
+
+ // Iterate backward with "AXPreviousTextMarkerForTextMarker"
+ for (let [marker] of markerIterator(macDoc, true)) {
+ if (count <= 0) {
+ ok(false, "Exceeding marker count");
+ break;
+ }
+ count--;
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ index,
+ count,
+ `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}`
+ );
+ }
+
+ is(count, 0, "Iterated backward through all text markers");
+}
+
+// Run tests with old word segmenter
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", false],
+ ["layout.word_select.stop_at_punctuation", true], // This is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(false, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter without stop_at_punctuation.
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", false],
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, false);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter with stop_at_punctuation
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", true], // this is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test text marker lesser-than operator
+addAccessibleTask(
+ `<p id="p">hello <a id="a" href="#">goodbye</a> world</p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let start = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerForIndex",
+ 1
+ );
+ let end = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", 10);
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [end, start]
+ );
+ is(stringForRange(macDoc, range), "ello good");
+ }
+);
+
+addAccessibleTask(
+ `<input id="input" value=""><a href="#">goodbye</a>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let input = getNativeInterface(accDoc, "input");
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ input
+ );
+
+ is(stringForRange(macDoc, range), "", "string value is correct");
+ }
+);
+
+addAccessibleTask(
+ `<div role="listbox" id="box">
+ <input type="radio" name="test" role="option" title="First item"/>
+ <input type="radio" name="test" role="option" title="Second item"/>
+ </div>`,
+ async (browser, accDoc) => {
+ let box = getNativeInterface(accDoc, "box");
+ const children = box.getAttributeValue("AXChildren");
+ is(children.length, 2, "Listbox contains two items");
+ is(children[0].getAttributeValue("AXValue"), "First item");
+ is(children[1].getAttributeValue("AXValue"), "Second item");
+ }
+);
+
+addAccessibleTask(
+ `<div id="t">
+ A link <b>should</b> explain <u>clearly</u> what information the <i>reader</i> will get by clicking on that link.
+ </div>`,
+ async (browser, accDoc) => {
+ let t = getNativeInterface(accDoc, "t");
+ const children = t.getAttributeValue("AXChildren");
+ const expectedTitles = [
+ "A link ",
+ "should",
+ " explain ",
+ "clearly",
+ " what information the ",
+ "reader",
+ " will get by clicking on that link. ",
+ ];
+ is(children.length, 7, "container has seven children");
+ children.forEach((child, index) => {
+ is(child.getAttributeValue("AXValue"), expectedTitles[index]);
+ });
+ }
+);
+
+addAccessibleTask(
+ `<a href="#">link</a> <input id="input" value="hello">`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let input = getNativeInterface(accDoc, "input");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ input
+ );
+
+ let firstMarkerInInput = macDoc.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ range
+ );
+
+ let leftWordRange = macDoc.getParameterizedAttributeValue(
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ firstMarkerInInput
+ );
+ let str = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ leftWordRange
+ );
+ is(str, "hello", "Left word at start of input should be right word");
+ }
+);
+
+addAccessibleTask(`<p id="p">hello world</p>`, async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let p = getNativeInterface(accDoc, "p");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ p
+ );
+
+ let bounds = macDoc.getParameterizedAttributeValue(
+ "AXBoundsForTextMarkerRange",
+ range
+ );
+
+ ok(bounds.origin && bounds.size, "Returned valid bounds");
+});
diff --git a/accessible/tests/browser/mac/browser_text_input.js b/accessible/tests/browser/mac/browser_text_input.js
new file mode 100644
index 0000000000..11a9dc25f1
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_input.js
@@ -0,0 +1,657 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function testValueChangedEventData(
+ macIface,
+ data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+) {
+ is(
+ data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct AXTextChangeElement"
+ );
+ is(
+ data.AXTextStateChangeType,
+ AXTextStateChangeTypeEdit,
+ "Correct AXTextStateChangeType"
+ );
+
+ let changeValues = data.AXTextChangeValues;
+ is(changeValues.length, 1, "One element in AXTextChangeValues");
+ is(
+ changeValues[0].AXTextChangeValue,
+ expectedChangeValue,
+ "Correct AXTextChangeValue"
+ );
+ is(
+ changeValues[0].AXTextEditType,
+ expectedEditType,
+ "Correct AXTextEditType"
+ );
+
+ let textMarker = changeValues[0].AXTextChangeValueStartMarker;
+ ok(textMarker, "There is a AXTextChangeValueStartMarker");
+ let range = macIface.getParameterizedAttributeValue(
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ textMarker
+ );
+ let str = macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range,
+ "correct word before caret"
+ );
+ is(str, expectedWordAtLeft);
+}
+
+// Return true if the first given object a subset of the second
+function isSubset(subset, superset) {
+ if (typeof subset != "object" || typeof superset != "object") {
+ return superset == subset;
+ }
+
+ for (let [prop, val] of Object.entries(subset)) {
+ if (!isSubset(val, superset[prop])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function matchWebArea(expectedId, expectedInfo) {
+ return (iface, data) => {
+ if (!data) {
+ return false;
+ }
+
+ let textChangeElemID =
+ data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier");
+
+ return (
+ iface.getAttributeValue("AXRole") == "AXWebArea" &&
+ textChangeElemID == expectedId &&
+ isSubset(expectedInfo, data)
+ );
+ };
+}
+
+function matchInput(expectedId, expectedInfo) {
+ return (iface, data) => {
+ if (!data) {
+ return false;
+ }
+
+ return (
+ iface.getAttributeValue("AXDOMIdentifier") == expectedId &&
+ isSubset(expectedInfo, data)
+ );
+ };
+}
+
+async function synthKeyAndTestSelectionChanged(
+ synthKey,
+ synthEvent,
+ expectedId,
+ expectedSelectionString,
+ expectedSelectionInfo
+) {
+ let selectionChangedEvents = Promise.all([
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchWebArea(expectedId, expectedSelectionInfo)
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchInput(expectedId, expectedSelectionInfo)
+ ),
+ ]);
+
+ EventUtils.synthesizeKey(synthKey, synthEvent);
+ let [webareaEvent, inputEvent] = await selectionChangedEvents;
+ is(
+ inputEvent.data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct AXTextChangeElement"
+ );
+
+ let rangeString = inputEvent.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ inputEvent.data.AXSelectedTextMarkerRange
+ );
+ is(
+ rangeString,
+ expectedSelectionString,
+ `selection has correct value (${expectedSelectionString})`
+ );
+
+ is(
+ webareaEvent.macIface.getAttributeValue("AXDOMIdentifier"),
+ "body",
+ "Input event target is top-level WebArea"
+ );
+ rangeString = webareaEvent.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ inputEvent.data.AXSelectedTextMarkerRange
+ );
+ is(
+ rangeString,
+ expectedSelectionString,
+ `selection has correct value (${expectedSelectionString}) via top document`
+ );
+
+ return inputEvent;
+}
+
+function testSelectionEventLeftChar(event, expectedChar) {
+ const selStart = event.macIface.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ event.data.AXSelectedTextMarkerRange
+ );
+ const selLeft = event.macIface.getParameterizedAttributeValue(
+ "AXPreviousTextMarkerForTextMarker",
+ selStart
+ );
+ const leftCharRange = event.macIface.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [selLeft, selStart]
+ );
+ const leftCharString = event.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ leftCharRange
+ );
+ is(leftCharString, expectedChar, "Left character is correct");
+}
+
+function testSelectionEventLine(event, expectedLine) {
+ const selStart = event.macIface.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ event.data.AXSelectedTextMarkerRange
+ );
+ const lineRange = event.macIface.getParameterizedAttributeValue(
+ "AXLineTextMarkerRangeForTextMarker",
+ selStart
+ );
+ const lineString = event.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ lineRange
+ );
+ is(lineString, expectedLine, "Line is correct");
+}
+
+async function synthKeyAndTestValueChanged(
+ synthKey,
+ synthEvent,
+ expectedId,
+ expectedTextSelectionId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+) {
+ let valueChangedEvents = Promise.all([
+ waitForMacEvent(
+ "AXSelectedTextChanged",
+ matchWebArea(expectedTextSelectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEvent(
+ "AXSelectedTextChanged",
+ matchInput(expectedTextSelectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXValueChanged",
+ matchWebArea(expectedId, {
+ AXTextStateChangeType: AXTextStateChangeTypeEdit,
+ AXTextChangeValues: [
+ {
+ AXTextChangeValue: expectedChangeValue,
+ AXTextEditType: expectedEditType,
+ },
+ ],
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXValueChanged",
+ matchInput(expectedId, {
+ AXTextStateChangeType: AXTextStateChangeTypeEdit,
+ AXTextChangeValues: [
+ {
+ AXTextChangeValue: expectedChangeValue,
+ AXTextEditType: expectedEditType,
+ },
+ ],
+ })
+ ),
+ ]);
+
+ EventUtils.synthesizeKey(synthKey, synthEvent);
+ let [, , webareaEvent, inputEvent] = await valueChangedEvents;
+
+ testValueChangedEventData(
+ webareaEvent.macIface,
+ webareaEvent.data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+ );
+ testValueChangedEventData(
+ inputEvent.macIface,
+ inputEvent.data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+ );
+}
+
+async function focusIntoInput(accDoc, inputId, innerContainerId) {
+ let selectionId = innerContainerId ? innerContainerId : inputId;
+ let input = getNativeInterface(accDoc, inputId);
+ ok(!input.getAttributeValue("AXFocused"), "input is not focused");
+ ok(input.isAttributeSettable("AXFocused"), "input is focusable");
+ let events = Promise.all([
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXDOMIdentifier") == inputId
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchWebArea(selectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchInput(selectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ ]);
+ input.setAttributeValue("AXFocused", true);
+ await events;
+}
+
+async function focusIntoInputAndType(accDoc, inputId, innerContainerId) {
+ let selectionId = innerContainerId ? innerContainerId : inputId;
+ await focusIntoInput(accDoc, inputId, innerContainerId);
+
+ async function testTextInput(
+ synthKey,
+ expectedChangeValue,
+ expectedWordAtLeft
+ ) {
+ await synthKeyAndTestValueChanged(
+ synthKey,
+ null,
+ inputId,
+ selectionId,
+ expectedChangeValue,
+ AXTextEditTypeTyping,
+ expectedWordAtLeft
+ );
+ }
+
+ await testTextInput("h", "h", "h");
+ await testTextInput("e", "e", "he");
+ await testTextInput("l", "l", "hel");
+ await testTextInput("l", "l", "hell");
+ await testTextInput("o", "o", "hello");
+ await testTextInput(" ", " ", "hello");
+ // You would expect this to be useless but this is what VO
+ // consumes. I guess it concats the inserted text data to the
+ // word to the left of the marker.
+ await testTextInput("w", "w", " ");
+ await testTextInput("o", "o", "wo");
+ await testTextInput("r", "r", "wor");
+ await testTextInput("l", "l", "worl");
+ await testTextInput("d", "d", "world");
+
+ async function testTextDelete(expectedChangeValue, expectedWordAtLeft) {
+ await synthKeyAndTestValueChanged(
+ "KEY_Backspace",
+ null,
+ inputId,
+ selectionId,
+ expectedChangeValue,
+ AXTextEditTypeDelete,
+ expectedWordAtLeft
+ );
+ }
+
+ await testTextDelete("d", "worl");
+ await testTextDelete("l", "wor");
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true },
+ selectionId,
+ "o",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true },
+ selectionId,
+ "wo",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true, metaKey: true },
+ selectionId,
+ "hello ",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionBeginning,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ { shiftKey: true, altKey: true },
+ selectionId,
+ "hello",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityWord,
+ }
+ );
+}
+
+// Test text input
+addAccessibleTask(
+ `<a href="#">link</a> <input id="input">`,
+ async (browser, accDoc) => {
+ await focusIntoInputAndType(accDoc, "input");
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test content editable
+addAccessibleTask(
+ `<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`,
+ async (browser, accDoc) => {
+ const inner = getNativeInterface(accDoc, "inner");
+ const editableAncestor = inner.getAttributeValue("AXEditableAncestor");
+ is(
+ editableAncestor.getAttributeValue("AXDOMIdentifier"),
+ "input",
+ "Editable ancestor is input"
+ );
+ await focusIntoInputAndType(accDoc, "input");
+ }
+);
+
+// Test input that gets role::EDITCOMBOBOX
+addAccessibleTask(`<input type="text" id="box">`, async (browser, accDoc) => {
+ const box = getNativeInterface(accDoc, "box");
+ const editableAncestor = box.getAttributeValue("AXEditableAncestor");
+ is(
+ editableAncestor.getAttributeValue("AXDOMIdentifier"),
+ "box",
+ "Editable ancestor is box itself"
+ );
+ await focusIntoInputAndType(accDoc, "box");
+});
+
+// Test multiline caret control in a text area
+addAccessibleTask(
+ `<textarea id="input" cols="15">one two three four five six seven eight</textarea>`,
+ async (browser, accDoc) => {
+ await focusIntoInput(accDoc, "input");
+
+ await synthKeyAndTestSelectionChanged("KEY_ArrowRight", null, "input", "", {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ });
+
+ await synthKeyAndTestSelectionChanged("KEY_ArrowDown", null, "input", "", {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ });
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { metaKey: true },
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionBeginning,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ { metaKey: true },
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionEnd,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the caret returns the correct marker when it is positioned after
+ * the last character (to facilitate appending text).
+ */
+addAccessibleTask(
+ `<input id="input" value="abc">`,
+ async function (browser, docAcc) {
+ await focusIntoInput(docAcc, "input");
+
+ let event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "a");
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "b");
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "c");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test that the caret returns the correct line when the caret is at the start
+ * of the line.
+ */
+addAccessibleTask(
+ `
+<textarea id="hard">ab
+cd
+ef
+
+gh
+</textarea>
+<div role="textbox" id="wrapped" contenteditable style="width: 1ch;">a b c</div>
+ `,
+ async function (browser, docAcc) {
+ let hard = getNativeInterface(docAcc, "hard");
+ await focusIntoInput(docAcc, "hard");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 0);
+ let event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "cd");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 1);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "ef");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 2);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 3);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "gh");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 4);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 5);
+
+ let wrapped = getNativeInterface(docAcc, "wrapped");
+ await focusIntoInput(docAcc, "wrapped");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 0);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "wrapped",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "b ");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 1);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "wrapped",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "c");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 2);
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/mac/browser_text_leaf.js b/accessible/tests/browser/mac/browser_text_leaf.js
new file mode 100644
index 0000000000..21deed6212
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_leaf.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test accessibles aren't created for linebreaks.
+ */
+addAccessibleTask(
+ `hello<br>world`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ let children = rootGroup.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The root group contains 2 children");
+
+ // verify first child is correct
+ is(
+ children[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "First child is a text node"
+ );
+ is(
+ children[0].getAttributeValue("AXValue"),
+ "hello",
+ "First child is hello text"
+ );
+
+ // verify second child is correct
+ is(
+ children[1].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Second child is a text node"
+ );
+
+ is(
+ children[1].getAttributeValue("AXValue"),
+ gIsIframe && !gIsRemoteIframe ? "world" : "world ",
+ "Second child is world text"
+ );
+ // we have a trailing space in here due to bug 1577028
+ // but this appears fixed in non-remote iframes
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+addAccessibleTask(
+ `<p id="p">hello, this is a test</p>`,
+ async (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+ let textLeaf = p.getAttributeValue("AXChildren")[0];
+ ok(textLeaf, "paragraph has a text leaf");
+
+ let str = textLeaf.getParameterizedAttributeValue(
+ "AXStringForRange",
+ NSRange(3, 6)
+ );
+
+ is(str, "lo, th", "AXStringForRange matches.");
+
+ let smallBounds = textLeaf.getParameterizedAttributeValue(
+ "AXBoundsForRange",
+ NSRange(3, 6)
+ );
+
+ let largeBounds = textLeaf.getParameterizedAttributeValue(
+ "AXBoundsForRange",
+ NSRange(3, 8)
+ );
+
+ ok(smallBounds.size[0] < largeBounds.size[0], "longer range is wider");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/mac/browser_text_selection.js b/accessible/tests/browser/mac/browser_text_selection.js
new file mode 100644
index 0000000000..a914adba8e
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_selection.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test simple text selection
+ */
+addAccessibleTask(`<p id="p">Hello World</p>`, async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let startMarker = macDoc.getAttributeValue("AXStartTextMarker");
+ let endMarker = macDoc.getAttributeValue("AXEndTextMarker");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [startMarker, endMarker]
+ );
+ is(stringForRange(macDoc, range), "Hello World");
+
+ let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let p = content.document.getElementById("p");
+ let r = new content.Range();
+ r.setStart(p.firstChild, 1);
+ r.setEnd(p.firstChild, 8);
+
+ let s = content.getSelection();
+ s.addRange(r);
+ });
+ await evt;
+
+ range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "ello Wo");
+
+ let firstWordRange = macDoc.getParameterizedAttributeValue(
+ "AXRightWordTextMarkerRangeForTextMarker",
+ startMarker
+ );
+ is(stringForRange(macDoc, firstWordRange), "Hello");
+
+ evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ macDoc.setAttributeValue("AXSelectedTextMarkerRange", firstWordRange);
+ await evt;
+ range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "Hello");
+
+ // Collapse selection
+ evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionMove &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let s = content.getSelection();
+ s.collapseToEnd();
+ });
+ await evt;
+});
+
+/**
+ * Test text selection events caused by focus change
+ */
+addAccessibleTask(
+ `<p>
+ Hello <a href="#" id="link">World</a>,
+ I <a href="#" style="user-select: none;" id="unselectable_link">love</a>
+ <button id="button">you</button></p>`,
+ async (browser, accDoc) => {
+ // Set up an AXSelectedTextChanged listener here. It will get resolved
+ // on the first non-root event it encounters, so if we test its data at the end
+ // of this test it will show us the first text-selectable object that was focused,
+ // which is "link".
+ let selTextChanged = waitForMacEvent(
+ "AXSelectedTextChanged",
+ e => e.getAttributeValue("AXDOMIdentifier") != "body"
+ );
+
+ let focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("unselectable_link").focus();
+ });
+ let focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "unselectable_link",
+ "Correct event target"
+ );
+
+ focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("button").focus();
+ });
+ focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "button",
+ "Correct event target"
+ );
+
+ focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link").focus();
+ });
+ focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "Correct event target"
+ );
+
+ let selTextChangedTarget = await selTextChanged;
+ is(
+ selTextChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "Correct event target"
+ );
+ }
+);
+
+/**
+ * Test text selection with focus change
+ */
+addAccessibleTask(
+ `<p id="p">Hello <input id="input"></p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let p = content.document.getElementById("p");
+ let r = new content.Range();
+ r.setStart(p.firstChild, 1);
+ r.setEnd(p.firstChild, 3);
+
+ let s = content.getSelection();
+ s.addRange(r);
+ });
+ await evt;
+
+ let range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "el");
+
+ let events = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ waitForMacEventWithInfo("AXSelectedTextChanged"),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ let [, { data }] = await events;
+ ok(
+ data.AXTextSelectionChangedFocus,
+ "have AXTextSelectionChangedFocus in event info"
+ );
+ ok(!data.AXTextStateSync, "no AXTextStateSync in editables");
+ is(
+ data.AXTextSelectionDirection,
+ AXTextSelectionDirectionDiscontiguous,
+ "discontigous direction"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_toggle_radio_check.js b/accessible/tests/browser/mac/browser_toggle_radio_check.js
new file mode 100644
index 0000000000..1695d73b0d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_toggle_radio_check.js
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test input[type=checkbox]
+ */
+addAccessibleTask(
+ `<input type="checkbox" id="vehicle"><label for="vehicle"> Bike</label>`,
+ async (browser, accDoc) => {
+ let checkbox = getNativeInterface(accDoc, "vehicle");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test aria-pressed toggle buttons
+ */
+addAccessibleTask(
+ `<button id="toggle" aria-pressed="false">toggle</button>`,
+ async (browser, accDoc) => {
+ // Set up a callback to change the toggle value
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("toggle").onclick = e => {
+ let curVal = e.target.getAttribute("aria-pressed");
+ let nextVal = curVal == "false" ? "true" : "false";
+ e.target.setAttribute("aria-pressed", nextVal);
+ };
+ });
+
+ let toggle = getNativeInterface(accDoc, "toggle");
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = toggle.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "toggle");
+ toggle.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "toggle");
+ toggle.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test aria-checked with tri state
+ */
+addAccessibleTask(
+ `<button role="checkbox" id="checkbox" aria-checked="false">toggle</button>`,
+ async (browser, accDoc) => {
+ // Set up a callback to change the toggle value
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("checkbox").onclick = e => {
+ const states = ["false", "true", "mixed"];
+ let currState = e.target.getAttribute("aria-checked");
+ let nextState = states[(states.indexOf(currState) + 1) % states.length];
+ e.target.setAttribute("aria-checked", nextState);
+ };
+ });
+ let checkbox = getNativeInterface(accDoc, "checkbox");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "checkbox");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ // Changing from checked to mixed fires two events. Make sure we wait until
+ // the second so we're asserting based on the latest state.
+ evt = waitForMacEvent("AXValueChanged", (iface, data) => {
+ return (
+ iface.getAttributeValue("AXDOMIdentifier") == "checkbox" &&
+ iface.getAttributeValue("AXValue") == 2
+ );
+ });
+ checkbox.performAction("AXPress");
+ await evt;
+ is(checkbox.getAttributeValue("AXValue"), 2, "Correct checked value");
+ }
+);
+
+/**
+ * Test input[type=radio]
+ */
+addAccessibleTask(
+ `<input type="radio" id="huey" name="drone" value="huey" checked>
+ <label for="huey">Huey</label>
+ <input type="radio" id="dewey" name="drone" value="dewey">
+ <label for="dewey">Dewey</label>`,
+ async (browser, accDoc) => {
+ let huey = getNativeInterface(accDoc, "huey");
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 1,
+ "Correct initial value for huey"
+ );
+
+ let dewey = getNativeInterface(accDoc, "dewey");
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value for dewey"
+ );
+
+ let actions = dewey.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = Promise.all([
+ waitForMacEvent("AXValueChanged", "huey"),
+ waitForMacEvent("AXValueChanged", "dewey"),
+ ]);
+ dewey.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value for dewey"
+ );
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value for huey"
+ );
+ }
+);
+
+/**
+ * Test role=switch
+ */
+addAccessibleTask(
+ `<div role="switch" aria-checked="false" id="sw">hello</div>`,
+ async (browser, accDoc) => {
+ let sw = getNativeInterface(accDoc, "sw");
+ await untilCacheIs(
+ () => sw.getAttributeValue("AXValue"),
+ 0,
+ "Initially switch is off"
+ );
+ is(sw.getAttributeValue("AXRole"), "AXCheckBox", "Has correct role");
+ is(sw.getAttributeValue("AXSubrole"), "AXSwitch", "Has correct subrole");
+
+ let stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "sw"),
+ waitForStateChange("sw", STATE_CHECKED, true),
+ ]);
+
+ // We should get a state change event, and a value change.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("sw")
+ .setAttribute("aria-checked", "true");
+ });
+
+ await stateChanged;
+
+ await untilCacheIs(
+ () => sw.getAttributeValue("AXValue"),
+ 1,
+ "Switch is now on"
+ );
+ }
+);
+
+/**
+ * Test input[type=checkbox] with role=menuitemcheckbox
+ */
+addAccessibleTask(
+ `<input type="checkbox" role="menuitemcheckbox" id="vehicle"><label for="vehicle"> Bike</label>`,
+ async (browser, accDoc) => {
+ let checkbox = getNativeInterface(accDoc, "vehicle");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] with role=menuitemradio
+ */
+addAccessibleTask(
+ `<input type="radio" role="menuitemradio" id="huey" name="drone" value="huey" checked>
+ <label for="huey">Huey</label>
+ <input type="radio" role="menuitemradio" id="dewey" name="drone" value="dewey">
+ <label for="dewey">Dewey</label>`,
+ async (browser, accDoc) => {
+ let huey = getNativeInterface(accDoc, "huey");
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 1,
+ "Correct initial value for huey"
+ );
+
+ let dewey = getNativeInterface(accDoc, "dewey");
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value for dewey"
+ );
+
+ let actions = dewey.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = Promise.all([
+ waitForMacEvent("AXValueChanged", "huey"),
+ waitForMacEvent("AXValueChanged", "dewey"),
+ ]);
+ dewey.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value for dewey"
+ );
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value for huey"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_webarea.js b/accessible/tests/browser/mac/browser_webarea.js
new file mode 100644
index 0000000000..ac6122de14
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_webarea.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Test web area role and AXLoadComplete event
+addAccessibleTask(``, async (browser, accDoc) => {
+ let evt = waitForMacEvent("AXLoadComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "webarea test";
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location = "data:text/html,<title>webarea test</title>";
+ });
+ let doc = await evt;
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "document has AXWebArea role"
+ );
+ is(doc.getAttributeValue("AXValue"), "", "document has no AXValue");
+ is(doc.getAttributeValue("AXTitle"), null, "document has no AXTitle");
+
+ is(doc.getAttributeValue("AXLoaded"), 1, "document has finished loading");
+});
+
+// Test iframe web area role and AXLayoutComplete event
+addAccessibleTask(`<title>webarea test</title>`, async (browser, accDoc) => {
+ // If the iframe loads before the top level document finishes loading, we'll
+ // get both an AXLayoutComplete event for the iframe and an AXLoadComplete
+ // event for the document. Otherwise, if the iframe loads after the
+ // document, we'll get one AXLoadComplete event.
+ let eventPromise = Promise.race([
+ waitForMacEvent("AXLayoutComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "iframe document";
+ }),
+ waitForMacEvent("AXLoadComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "webarea test";
+ }),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = "data:text/html,<title>iframe document</title>hello world";
+ content.document.body.appendChild(iframe);
+ });
+ let doc = await eventPromise;
+
+ if (doc.getAttributeValue("AXTitle")) {
+ // iframe should have no title, so if we get a title here
+ // we've got the main document and need to get the iframe from
+ // the main doc
+ doc = doc.getAttributeValue("AXChildren")[0];
+ }
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "iframe document has AXWebArea role"
+ );
+ is(doc.getAttributeValue("AXValue"), "", "iframe document has no AXValue");
+ is(doc.getAttributeValue("AXTitle"), null, "iframe document has no AXTitle");
+ is(
+ doc.getAttributeValue("AXDescription"),
+ "iframe document",
+ "test has correct label"
+ );
+
+ is(
+ doc.getAttributeValue("AXLoaded"),
+ 1,
+ "iframe document has finished loading"
+ );
+});
diff --git a/accessible/tests/browser/mac/doc_aria_tabs.html b/accessible/tests/browser/mac/doc_aria_tabs.html
new file mode 100644
index 0000000000..0c8f2afd6f
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_aria_tabs.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+
+ <style type="text/css">
+ .tabs {
+ padding: 1em;
+ }
+
+ [role="tablist"] {
+ margin-bottom: -1px;
+ }
+
+ [role="tab"] {
+ position: relative;
+ z-index: 1;
+ background: white;
+ border-radius: 5px 5px 0 0;
+ border: 1px solid grey;
+ border-bottom: 0;
+ padding: 0.2em;
+ }
+
+ [role="tab"][aria-selected="true"] {
+ z-index: 3;
+ }
+
+ [role="tabpanel"] {
+ position: relative;
+ padding: 0 0.5em 0.5em 0.7em;
+ border: 1px solid grey;
+ border-radius: 0 0 5px 5px;
+ background: white;
+ z-index: 2;
+ }
+
+ [role="tabpanel"]:focus {
+ border-color: orange;
+ outline: 1px solid orange;
+ }
+ </style>
+ <script>
+ 'use strict';
+ /* exported changeTabs */
+ function changeTabs(target) {
+ const parent = target.parentNode;
+ const grandparent = parent.parentNode;
+
+ // Remove all current selected tabs
+ parent
+ .querySelectorAll('[aria-selected="true"]')
+ .forEach(t => t.setAttribute("aria-selected", false));
+
+ // Set this tab as selected
+ target.setAttribute("aria-selected", true);
+
+ // Hide all tab panels
+ grandparent
+ .querySelectorAll('[role="tabpanel"]')
+ .forEach(p => (p.hidden = true));
+
+ // Show the selected panel
+ grandparent.parentNode
+ .querySelector(`#${target.getAttribute("aria-controls")}`)
+ .removeAttribute("hidden");
+ }
+ </script>
+ <title>ARIA: tab role - Example - code sample</title>
+</head>
+<body id="body">
+
+ <div class="tabs">
+ <div id="tablist" role="tablist" aria-label="Sample Tabs">
+ <button onclick="changeTabs(this)" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
+ First Tab
+ </button>
+ <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
+ Second Tab
+ </button>
+ <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3">
+ Third Tab
+ </button>
+ </div>
+ <div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
+ <p>Content for the first panel</p>
+ </div>
+ <div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden="">
+ <p>Content for the second panel</p>
+ </div>
+ <div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden="">
+ <p>Content for the third panel</p>
+ </div>
+ </div>
+</body></html>
diff --git a/accessible/tests/browser/mac/doc_menulist.xhtml b/accessible/tests/browser/mac/doc_menulist.xhtml
new file mode 100644
index 0000000000..d6751bc8f4
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_menulist.xhtml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox>
+ <label control="defaultZoom" value="Zoom"/>
+ <hbox>
+ <menulist id="defaultZoom">
+ <menupopup>
+ <menuitem label="50%" value="50"/>
+ <menuitem label="100%" value="100"/>
+ <menuitem label="150%" value="150"/>
+ <menuitem label="200%" value="200"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/browser/mac/doc_rich_listbox.xhtml b/accessible/tests/browser/mac/doc_rich_listbox.xhtml
new file mode 100644
index 0000000000..3acaf3bff8
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_rich_listbox.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <richlistbox id="categories">
+ <richlistitem id="general">
+ <label value="general"/>
+ </richlistitem>
+
+ <richlistitem id="home">
+ <label value="home"/>
+ </richlistitem>
+
+ <richlistitem id="search">
+ <label value="search"/>
+ </richlistitem>
+
+ <richlistitem id="privacy">
+ <label value="privacy"/>
+ </richlistitem>
+ </richlistbox>
+</window>
diff --git a/accessible/tests/browser/mac/doc_textmarker_test.html b/accessible/tests/browser/mac/doc_textmarker_test.html
new file mode 100644
index 0000000000..9584d28e09
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_textmarker_test.html
@@ -0,0 +1,2427 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ </head>
+ <body id="body">
+ <p>Bob Loblaw Lobs Law Bomb</p>
+ <p>I love all of my <a href="#">children</a> equally</p>
+ <p>This is the <b>best</b> free scr<a href="#">apbook</a>ing class I have ever taken</p>
+ <ul>
+ <li>Fried cheese with club sauce</li>
+ <li>Popcorn shrimp with club sauce</li>
+ <li>Chicken fingers with <i>spicy</i> club sauce</li>
+ </ul>
+ <ul style="list-style: none;"><li>Do not order the Skip's Scramble</li></ul>
+ <p style="width: 1rem">These are my awards, Mother. From Army.</p>
+ <p>I <input value="deceived you">, mom.</p>
+ <script>
+ "use strict";
+ // eslint-disable-next-line no-unused-vars
+ function getExpected(useNewSegmenter, stopAtPunctuation) {
+ return [
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "Bob Loblaw Lobs Law Bomb",
+ "I love all of my children equally"],
+ words: ["Bomb", ""],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["I", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "of"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["of", "of"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["of", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "my"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["my", "my"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["my", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "children"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", " "],
+ element: ["AXStaticText", "children", "children"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "I love all of my children equally",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["equally", ""],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "is"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["is", "is"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["is", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "best"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", " "],
+ element: ["AXStaticText", "best", "best"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", " "],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "I"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["I", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "This is the best free scrapbooking class I have ever taken",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["taken", ""],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", ""],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", ""],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "spicy"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", " "],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "Do not order the Skip's Scramble"],
+ words: ["sauce", ""],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", "Do"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? [" ", "Skip's"] : [" ", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "s"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", " "] : ["s", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ",
+ "Do not order the Skip's Scramble",
+ "These "],
+ words: ["Scramble", ""],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "These ", "are "],
+ words: [" ", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "are ", "my "],
+ words: [" ", "my"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "my ", "my "],
+ words: ["my", "my"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "my ", "my "],
+ words: ["my", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "my ", "awards, "],
+ words: [" ", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "awards, ", "Mother. "],
+ words: [" ", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "Mother. ", "From "],
+ words: [" ", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "From ", "Army."],
+ words: [" ", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "Army.", "I deceived you, mom."],
+ words: ["Army.", ""],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "I ",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["I", " "],
+ element: ["AXStaticText", "I ", "I "] },
+ { style: "I ",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXStaticText", "I ", "I "] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", " "],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: [" ", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["you", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["you", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["", ""],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: [",", " "],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: [" ", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", ""],
+ element: ["AXStaticText", ", mom.", ", mom."] }];
+ }
+ </script>
+ </body>
+</html>
diff --git a/accessible/tests/browser/mac/doc_tree.xhtml b/accessible/tests/browser/mac/doc_tree.xhtml
new file mode 100644
index 0000000000..d043fa8923
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_tree.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <tree id="tree" hidecolumnpicker="true">
+ <treecols>
+ <treecol primary="true" label="Groceries"/>
+ </treecols>
+ <treechildren id="internalTree">
+ <treeitem id="fruits" container="true" open="true">
+ <treerow>
+ <treecell label="Fruits"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="apple">
+ <treerow>
+ <treecell label="Apple"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="orange">
+ <treerow>
+ <treecell label="Orange"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem id="veggies" container="true" open="true">
+ <treerow>
+ <treecell label="Veggies"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="greenVeggies" container="true" open="true">
+ <treerow>
+ <treecell label="Green Veggies"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="spinach">
+ <treerow>
+ <treecell label="Spinach"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="peas">
+ <treerow>
+ <treecell label="Peas"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem id="squash">
+ <treerow>
+ <treecell label="Squash"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/accessible/tests/browser/mac/head.js b/accessible/tests/browser/mac/head.js
new file mode 100644
index 0000000000..f33f86288b
--- /dev/null
+++ b/accessible/tests/browser/mac/head.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported getNativeInterface, waitForMacEventWithInfo, waitForMacEvent, waitForStateChange,
+ NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit,
+ AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove,
+ AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown,
+ AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext,
+ AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown,
+ AXTextSelectionDirectionBeginning, AXTextSelectionDirectionEnd,
+ AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord,
+ AXTextSelectionGranularityLine */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+// AXTextStateChangeType enum values
+const AXTextStateChangeTypeEdit = 1;
+const AXTextStateChangeTypeSelectionMove = 2;
+const AXTextStateChangeTypeSelectionExtend = 3;
+
+// AXTextEditType enum values
+const AXTextEditTypeDelete = 1;
+const AXTextEditTypeTyping = 3;
+
+// AXTextSelectionDirection enum values
+const AXTextSelectionDirectionUnknown = 0;
+const AXTextSelectionDirectionBeginning = 1;
+const AXTextSelectionDirectionEnd = 2;
+const AXTextSelectionDirectionPrevious = 3;
+const AXTextSelectionDirectionNext = 4;
+const AXTextSelectionDirectionDiscontiguous = 5;
+
+// AXTextSelectionGranularity enum values
+const AXTextSelectionGranularityUnknown = 0;
+const AXTextSelectionGranularityCharacter = 1;
+const AXTextSelectionGranularityWord = 2;
+const AXTextSelectionGranularityLine = 3;
+
+function getNativeInterface(accDoc, id) {
+ return findAccessibleChildByID(accDoc, id).nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+}
+
+function waitForMacEventWithInfo(notificationType, filter) {
+ let filterFunc = (macIface, data) => {
+ if (!filter) {
+ return true;
+ }
+
+ if (typeof filter == "function") {
+ return filter(macIface, data);
+ }
+
+ return macIface.getAttributeValue("AXDOMIdentifier") == filter;
+ };
+
+ return new Promise(resolve => {
+ let eventObserver = {
+ observe(subject, topic, data) {
+ let macEvent = subject.QueryInterface(Ci.nsIAccessibleMacEvent);
+ if (
+ data === notificationType &&
+ filterFunc(macEvent.macIface, macEvent.data)
+ ) {
+ Services.obs.removeObserver(this, "accessible-mac-event");
+ resolve(macEvent);
+ }
+ },
+ };
+ Services.obs.addObserver(eventObserver, "accessible-mac-event");
+ });
+}
+
+function waitForMacEvent(notificationType, filter) {
+ return waitForMacEventWithInfo(notificationType, filter).then(
+ e => e.macIface
+ );
+}
+
+function NSRange(location, length) {
+ return {
+ valueType: "NSRange",
+ value: [location, length],
+ };
+}
+
+function NSDictionary(dict) {
+ return {
+ objectType: "NSDictionary",
+ object: dict,
+ };
+}
+
+function stringForRange(macDoc, range) {
+ if (!range) {
+ return "";
+ }
+
+ let str = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range
+ );
+
+ let attrStr = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ // This is a fly-by test to make sure our attributed strings
+ // always match our flat strings.
+ is(
+ attrStr.map(({ string }) => string).join(""),
+ str,
+ "attributed text matches non-attributed text"
+ );
+
+ return str;
+}
diff --git a/accessible/tests/browser/pivot/browser.toml b/accessible/tests/browser/pivot/browser.toml
new file mode 100644
index 0000000000..f6552741e5
--- /dev/null
+++ b/accessible/tests/browser/pivot/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/browser/shared-head.js",
+ "head.js",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_pivot.js"]
diff --git a/accessible/tests/browser/pivot/browser_pivot.js b/accessible/tests/browser/pivot/browser_pivot.js
new file mode 100644
index 0000000000..bd46ae4933
--- /dev/null
+++ b/accessible/tests/browser/pivot/browser_pivot.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";
+
+/**
+ * Tests Pivot API
+ */
+addAccessibleTask(
+ `
+ <h1 id="heading-1-1">Main Title</h1>
+ <h2 id="heading-2-1" aria-hidden="true">First Section Title</h2>
+ <p id="paragraph-1">
+ Lorem ipsum <strong>dolor</strong> sit amet. Integer vitae urna
+ leo, id <a href="#">semper</a> nulla.
+ </p>
+ <h2 id="heading-2-2" aria-hidden="undefined">Second Section Title</h2>
+ <p id="paragraph-2" aria-hidden="">
+ Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.</p>
+ <p id="paragraph-3" aria-hidden="true">
+ <a href="#" id="hidden-link">Maybe</a> it was the other <i>George Michael</i>.
+ You know, the <a href="#">singer-songwriter</a>.
+ </p>
+ <p style="opacity: 0;" id="paragraph-4">
+ This is completely transparent
+ </p>
+ <iframe
+ src="data:text/html,<html><body>An <i>embedded</i> document.</body></html>">
+ </iframe>
+ <div id="hide-me">Hide me</div>
+ <p id="links" aria-hidden="false">
+ <a href="http://mozilla.org" title="Link 1 title">Link 1</a>
+ <a href="http://mozilla.org" title="Link 2 title">Link 2</a>
+ <a href="http://mozilla.org" title="Link 3 title">Link 3</a>
+ </p>
+ <ul>
+ <li>Hello<span> </span></li>
+ <li>World</li>
+ </ul>
+ `,
+ async function (browser, docAcc) {
+ let pivot = gAccService.createAccessiblePivot(docAcc);
+ testPivotSequence(pivot, HeadersTraversalRule, [
+ "heading-1-1",
+ "heading-2-2",
+ ]);
+
+ testPivotSequence(pivot, ObjectTraversalRule, [
+ "Main Title",
+ "Lorem ipsum ",
+ "dolor",
+ " sit amet. Integer vitae urna leo, id ",
+ "semper",
+ " nulla. ",
+ "Second Section Title",
+ "Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.",
+ "An ",
+ "embedded",
+ " document.",
+ "Hide me",
+ "Link 1",
+ "Link 2",
+ "Link 3",
+ "Hello",
+ "World",
+ ]);
+
+ let hideMeAcc = findAccessibleChildByID(docAcc, "hide-me");
+ let onHide = waitForEvent(EVENT_HIDE, hideMeAcc);
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("hide-me").remove();
+ });
+
+ await onHide;
+ testFailsWithNotInTree(
+ () => pivot.next(hideMeAcc, ObjectTraversalRule),
+ "moveNext from defunct accessible should fail"
+ );
+
+ let linksAcc = findAccessibleChildByID(docAcc, "links");
+
+ let removedRootPivot = gAccService.createAccessiblePivot(linksAcc);
+ onHide = waitForEvent(EVENT_HIDE, linksAcc);
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("links").remove();
+ });
+
+ await onHide;
+ testFailsWithNotInTree(
+ () => removedRootPivot.last(ObjectTraversalRule),
+ "moveLast with pivot with defunct root should fail"
+ );
+
+ let [x, y] = getBounds(findAccessibleChildByID(docAcc, "heading-1-1"));
+ let hitacc = pivot.atPoint(x + 1, y + 1, HeadersTraversalRule);
+ is(getIdOrName(hitacc), "heading-1-1", "Matching accessible at point");
+
+ hitacc = pivot.atPoint(x - 1, y - 1, HeadersTraversalRule);
+ ok(!hitacc, "No heading at given point");
+ },
+ { iframe: true, remoteIframe: true, topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/pivot/head.js b/accessible/tests/browser/pivot/head.js
new file mode 100644
index 0000000000..8389190a69
--- /dev/null
+++ b/accessible/tests/browser/pivot/head.js
@@ -0,0 +1,122 @@
+/* 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 HeadersTraversalRule, ObjectTraversalRule, testPivotSequence, testFailsWithNotInTree */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+/* import-globals-from ../../mochitest/layout.js */
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "layout.js", dir: MOCHITESTS_DIR }
+);
+
+const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH;
+const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE;
+const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
+
+const NS_ERROR_NOT_IN_TREE = 0x80780026;
+
+// //////////////////////////////////////////////////////////////////////////////
+// Traversal rules
+
+/**
+ * Rule object to traverse all focusable nodes and text nodes.
+ */
+const HeadersTraversalRule = {
+ match(acc) {
+ return acc.role == ROLE_HEADING ? FILTER_MATCH : FILTER_IGNORE;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]),
+};
+
+/**
+ * Traversal rule for all focusable nodes or leafs.
+ */
+const ObjectTraversalRule = {
+ match(acc) {
+ let [state, extstate] = getStates(acc);
+ if (state & STATE_INVISIBLE) {
+ return FILTER_IGNORE;
+ }
+
+ if ((extstate & EXT_STATE_OPAQUE) == 0) {
+ return FILTER_IGNORE | FILTER_IGNORE_SUBTREE;
+ }
+
+ let rv = FILTER_IGNORE;
+ let role = acc.role;
+ if (
+ hasState(acc, STATE_FOCUSABLE) &&
+ role != ROLE_DOCUMENT &&
+ role != ROLE_INTERNAL_FRAME
+ ) {
+ rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH;
+ } else if (
+ acc.childCount == 0 &&
+ role != ROLE_LISTITEM_MARKER &&
+ acc.name.trim()
+ ) {
+ rv = FILTER_MATCH;
+ }
+
+ return rv;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]),
+};
+
+function getIdOrName(acc) {
+ let id = getAccessibleDOMNodeID(acc);
+ if (id) {
+ return id;
+ }
+ return acc.name;
+}
+
+function* pivotNextGenerator(pivot, rule) {
+ for (let acc = pivot.first(rule); acc; acc = pivot.next(acc, rule)) {
+ yield acc;
+ }
+}
+
+function* pivotPreviousGenerator(pivot, rule) {
+ for (let acc = pivot.last(rule); acc; acc = pivot.prev(acc, rule)) {
+ yield acc;
+ }
+}
+
+function testPivotSequence(pivot, rule, expectedSequence) {
+ is(
+ JSON.stringify([...pivotNextGenerator(pivot, rule)].map(getIdOrName)),
+ JSON.stringify(expectedSequence),
+ "Forward pivot sequence is correct"
+ );
+ is(
+ JSON.stringify([...pivotPreviousGenerator(pivot, rule)].map(getIdOrName)),
+ JSON.stringify([...expectedSequence].reverse()),
+ "Reverse pivot sequence is correct"
+ );
+}
+
+function testFailsWithNotInTree(func, msg) {
+ try {
+ func();
+ ok(false, msg);
+ } catch (x) {
+ is(x.result, NS_ERROR_NOT_IN_TREE, `Expecting NOT_IN_TREE: ${msg}`);
+ }
+}
diff --git a/accessible/tests/browser/python_runner_wsh.py b/accessible/tests/browser/python_runner_wsh.py
new file mode 100644
index 0000000000..488051240f
--- /dev/null
+++ b/accessible/tests/browser/python_runner_wsh.py
@@ -0,0 +1,88 @@
+# 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/.
+
+"""A pywebsocket3 handler which runs arbitrary Python code and returns the
+result.
+This is used to test OS specific accessibility APIs which can't be tested in JS.
+It is intended to be called from JS browser tests.
+"""
+
+import json
+import os
+import sys
+import traceback
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ def send(*args):
+ """Send a response to the client as a JSON array."""
+ msgutil.send_message(request, json.dumps(args))
+
+ cleanNamespace = {}
+ testDir = None
+ if sys.platform == "win32":
+ testDir = "windows"
+ elif sys.platform == "linux":
+ testDir = "atk"
+ if testDir:
+ sys.path.append(
+ os.path.join(
+ os.getcwd(), "browser", "accessible", "tests", "browser", testDir
+ )
+ )
+ try:
+ import a11y_setup
+
+ cleanNamespace = a11y_setup.__dict__
+ setupExc = None
+ except Exception:
+ setupExc = traceback.format_exc()
+ sys.path.pop()
+
+ def info(message):
+ """Log an info message."""
+ send("info", str(message))
+
+ cleanNamespace["info"] = info
+ namespace = cleanNamespace.copy()
+
+ # Keep handling messages until the WebSocket is closed.
+ while True:
+ code = msgutil.receive_message(request)
+ if not code:
+ return
+ if code == "__reset__":
+ namespace = cleanNamespace.copy()
+ continue
+
+ if setupExc:
+ # a11y_setup failed. Report an exception immediately.
+ send("exception", setupExc)
+ continue
+
+ # Wrap the code in a function called run(). This allows the code to
+ # return a result by simply using the return statement.
+ if "\n" not in code and not code.lstrip().startswith("return "):
+ # Single line without return. Assume this is an expression. We use
+ # a lambda to return the result.
+ code = f"run = lambda: {code}"
+ else:
+ lines = ["def run():"]
+ # Indent each line inside the function.
+ lines.extend(f" {line}" for line in code.splitlines())
+ code = "\n".join(lines)
+ try:
+ # Execute this Python code, which will define the run() function.
+ exec(code, namespace)
+ # Run the function we just defined.
+ ret = namespace["run"]()
+ send("return", ret)
+ except Exception:
+ send("exception", traceback.format_exc())
diff --git a/accessible/tests/browser/role/browser.toml b/accessible/tests/browser/role/browser.toml
new file mode 100644
index 0000000000..ac593eb0e6
--- /dev/null
+++ b/accessible/tests/browser/role/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.mjs",
+]
+prefs = [
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+ "dom.element.popover.enabled=true"
+]
+
+["browser_computedARIARole.js"]
+["browser_minimumRole.js"]
diff --git a/accessible/tests/browser/role/browser_computedARIARole.js b/accessible/tests/browser/role/browser_computedARIARole.js
new file mode 100644
index 0000000000..50cfe43c98
--- /dev/null
+++ b/accessible/tests/browser/role/browser_computedARIARole.js
@@ -0,0 +1,88 @@
+/* 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 id="ariaButton" role="button">ARIA button</div>
+<div id="ariaLog" role="log">ARIA log</div>
+<div id="ariaMain" role="main">ARIA main</div>
+<div id="ariaRegion" role="region" aria-label="ARIA region">ARIA region</div>
+<nav id="ariaUnnamedRegion" role="region">ARIA unnamed region</nav>
+<div id="ariaDirectory" role="directory">ARIA directory</div>
+<div id="ariaAlertdialog" role="alertdialog">ARIA alertdialog</div>
+<div id="ariaFeed" role="feed">ARIA feed</div>
+<div id="ariaRowgroup" role="rowgroup">ARIA rowgroup</div>
+<div id="ariaSearchbox" role="searchbox">ARIA searchbox</div>
+<div id="ariaUnknown" role="unknown">unknown ARIA role</div>
+<button id="htmlButton">HTML button</button>
+<button id="toggleButton" aria-pressed="true">toggle button</button>
+<main id="htmlMain">HTML main</main>
+<header id="htmlHeader">HTML header</header>
+<section id="htmlSection">
+ <header id="htmlSectionHeader">HTML header inside section</header>
+</section>
+<section id="htmlRegion" aria-label="HTML region">HTML region</section>
+<fieldset id="htmlFieldset">HTML fieldset</fieldset>
+<table>
+ <tbody id="htmlTbody" tabindex="-1"><tr><th>HTML tbody</th></tr></tbody>
+</table>
+<table role="grid">
+ <tr>
+ <td id="htmlGridcell">HTML implicit gridcell</td>
+ </tr>
+</table>
+<div id="htmlDiv">HTML div</div>
+<span id="htmlSpan" aria-label="HTML span">HTML span</span>
+<iframe id="iframe"></iframe>
+ `,
+ async function (browser, docAcc) {
+ function testComputedARIARole(id, role) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ is(acc.computedARIARole, role, `computedARIARole for ${id} is correct`);
+ }
+
+ testComputedARIARole("ariaButton", "button");
+ testComputedARIARole("ariaLog", "log");
+ // Landmarks map to a single Gecko role.
+ testComputedARIARole("ariaMain", "main");
+ testComputedARIARole("ariaRegion", "region");
+ // Unnamed ARIA regions should ignore the ARIA role.
+ testComputedARIARole("ariaUnnamedRegion", "navigation");
+ // The directory ARIA role is an alias of list.
+ testComputedARIARole("ariaDirectory", "list");
+ // alertdialog, feed, rowgroup and searchbox map to a Gecko role, but it
+ // isn't unique.
+ testComputedARIARole("ariaAlertdialog", "alertdialog");
+ testComputedARIARole("ariaFeed", "feed");
+ testComputedARIARole("ariaRowgroup", "rowgroup");
+ testComputedARIARole("ariaSearchbox", "searchbox");
+ testComputedARIARole("ariaUnknown", "generic");
+ testComputedARIARole("htmlButton", "button");
+ // There is only a single ARIA role for buttons, but Gecko uses different
+ // roles depending on states.
+ testComputedARIARole("toggleButton", "button");
+ testComputedARIARole("htmlMain", "main");
+ testComputedARIARole("htmlHeader", "banner");
+ // <section> only maps to the region ARIA role if it has a label.
+ testComputedARIARole("htmlSection", "generic");
+ // <header> only maps to the banner role if it is not a child of a
+ // sectioning element.
+ testComputedARIARole("htmlSectionHeader", "generic");
+ testComputedARIARole("htmlRegion", "region");
+ // Gecko doesn't have a rowgroup role. Ensure we differentiate for
+ // computedARIARole.
+ testComputedARIARole("htmlFieldset", "group");
+ testComputedARIARole("htmlTbody", "rowgroup");
+ // <td> inside <table role="grid"> implicitly maps to ARIA gridcell.
+ testComputedARIARole("htmlGridcell", "gridcell");
+ // Test generics.
+ testComputedARIARole("htmlDiv", "generic");
+ testComputedARIARole("htmlSpan", "generic");
+ // Some roles can't be mapped to ARIA role tokens.
+ testComputedARIARole("iframe", "");
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/role/browser_minimumRole.js b/accessible/tests/browser/role/browser_minimumRole.js
new file mode 100644
index 0000000000..c02c35bc9c
--- /dev/null
+++ b/accessible/tests/browser/role/browser_minimumRole.js
@@ -0,0 +1,59 @@
+/* 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 that popover gets a minimum role.
+ */
+addAccessibleTask(
+ `
+<div id="generic" popover>generic</div>
+<div id="alert" role="alert" popover>alert</div>
+<blockquote id="blockquote" popover>blockquote</div>
+ `,
+ async function testPopover(browser, docAcc) {
+ let generic = findAccessibleChildByID(docAcc, "generic");
+ ok(!generic, "generic doesn't have an Accessible");
+ info("Showing generic");
+ let shown = waitForEvent(EVENT_SHOW, "generic");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("generic").showPopover();
+ });
+ generic = (await shown).accessible;
+ testRole(generic, ROLE_GROUPING, "generic has minimum role group");
+ info("Setting popover to null on generic");
+ // Setting popover to null causes the Accessible to be recreated.
+ shown = waitForEvent(EVENT_SHOW, "generic");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("generic").popover = null;
+ });
+ generic = (await shown).accessible;
+ testRole(generic, ROLE_SECTION, "generic has generic role");
+
+ let alert = findAccessibleChildByID(docAcc, "alert");
+ ok(!alert, "alert doesn't have an Accessible");
+ info("Showing alert");
+ shown = waitForEvent(EVENT_SHOW, "alert");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("alert").showPopover();
+ });
+ alert = (await shown).accessible;
+ testRole(alert, ROLE_ALERT, "alert has role alert");
+
+ let blockquote = findAccessibleChildByID(docAcc, "blockquote");
+ ok(!blockquote, "blockquote doesn't have an Accessible");
+ info("Showing blockquote");
+ shown = waitForEvent(EVENT_SHOW, "blockquote");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("blockquote").showPopover();
+ });
+ blockquote = (await shown).accessible;
+ testRole(blockquote, ROLE_BLOCKQUOTE, "blockquote has role blockquote");
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/role/head.js b/accessible/tests/browser/role/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/role/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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/scroll/browser.toml b/accessible/tests/browser/scroll/browser.toml
new file mode 100644
index 0000000000..ef637fe9a5
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser.toml
@@ -0,0 +1,19 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_scrollToPoint.js"]
+
+["browser_test_scrollTo.js"]
+
+["browser_test_scroll_bounds.js"]
+
+["browser_test_scroll_substring.js"]
+
+["browser_test_zoom_text.js"]
diff --git a/accessible/tests/browser/scroll/browser_scrollToPoint.js b/accessible/tests/browser/scroll/browser_scrollToPoint.js
new file mode 100644
index 0000000000..810478fa76
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_scrollToPoint.js
@@ -0,0 +1,31 @@
+/* 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 });
+
+/**
+ * Test nsIAccessible::scrollToPoint.
+ */
+addAccessibleTask(
+ `
+<hr style="height: 100vh;">
+<p id="p">hi</p>
+<hr style="height: 100vh;">
+ `,
+ async function (browser, docAcc) {
+ const [docX, docY] = getPos(docAcc);
+ const p = findAccessibleChildByID(docAcc, "p");
+ const [pX] = getPos(p);
+ info("Scrolling p");
+ let scrolled = waitForEvent(EVENT_SCROLLING_END, docAcc);
+ p.scrollToPoint(COORDTYPE_SCREEN_RELATIVE, docX, docY);
+ await scrolled;
+ // We can only scroll this vertically.
+ testPos(p, [pX, docY]);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scrollTo.js b/accessible/tests/browser/scroll/browser_test_scrollTo.js
new file mode 100644
index 0000000000..c007c62d82
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scrollTo.js
@@ -0,0 +1,53 @@
+/* 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 getCenterY(acc) {
+ const y = {};
+ const h = {};
+ acc.getBounds({}, y, {}, h);
+ return y.value + h.value / 2;
+}
+
+/**
+ * Test nsIAccessible::scrollTo.
+ */
+addAccessibleTask(
+ `
+<div id="scroller" style="height: 100vh; overflow: scroll;">
+ <hr style="height: 100vh;">
+ <p id="p1" style="height: 10vh;">a</p>
+ <hr style="height: 100vh;">
+ <p id="p2" style="height: 10vh;">b</p>
+ <hr style="height: 100vh;">
+</div>
+ `,
+ async function (browser, docAcc) {
+ const scroller = findAccessibleChildByID(docAcc, "scroller");
+ const scrollerY = getCenterY(scroller);
+ // scroller can only show one of p1 or p2, not both.
+ const p1 = findAccessibleChildByID(docAcc, "p1");
+ info("scrollTo p1");
+ let scrolled = waitForEvent(
+ nsIAccessibleEvent.EVENT_SCROLLING_END,
+ scroller
+ );
+ p1.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p1), scrollerY, 10, "p1 scrolled to center");
+ const p2 = findAccessibleChildByID(docAcc, "p2");
+ info("scrollTo p2");
+ scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller);
+ p2.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p2), scrollerY, 10, "p2 scrolled to center");
+ info("scrollTo p1");
+ scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller);
+ p1.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p1), scrollerY, 10, "p1 scrolled to center");
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scroll_bounds.js b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js
new file mode 100644
index 0000000000..31de002cda
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js
@@ -0,0 +1,662 @@
+/* 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: "layout.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+requestLongerTimeout(2);
+
+const appUnitsPerDevPixel = 60;
+
+function testCachedScrollPosition(
+ acc,
+ expectedX,
+ expectedY,
+ shouldBeEmpty = false
+) {
+ let cachedPosition = "";
+ try {
+ cachedPosition = acc.cache.getStringProperty("scroll-position");
+ } catch (e) {
+ info("Cache was not populated");
+ // If the key doesn't exist, this frame is not scrollable.
+ return shouldBeEmpty;
+ }
+
+ // The value we retrieve from the cache is in app units, but the values
+ // passed in are in pixels. Since the retrieved value is a string,
+ // and harder to modify, adjust our expected x and y values to match its units.
+ return (
+ cachedPosition ==
+ `${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}`
+ );
+}
+
+function getCachedBounds(acc) {
+ let cachedBounds = "";
+ try {
+ cachedBounds = acc.cache.getStringProperty("relative-bounds");
+ } catch (e) {
+ ok(false, "Unable to fetch cached bounds from cache!");
+ }
+ return cachedBounds;
+}
+
+/**
+ * Test bounds of accessibles after scrolling
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
+ </div>
+
+ <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ ok(docAcc, "iframe document acc is present");
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("square").scrollIntoView();
+ });
+
+ await waitForContentPaint(browser);
+
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+
+ // Scroll rect into view, but also make it reflow so we can be sure the
+ // bounds are correct for reflowed frames.
+ await invokeContentTask(browser, [], () => {
+ const rect = content.document.getElementById("rect");
+ rect.scrollIntoView();
+ rect.style.width = "300px";
+ rect.offsetTop; // Flush layout.
+ rect.style.width = "200px";
+ rect.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test scroll offset on cached accessibles
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
+ </div>
+
+ <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ ok(docAcc, "iframe document acc is present");
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 0),
+ "Correct initial scroll position."
+ );
+ const rectAcc = findAccessibleChildByID(docAcc, "rect");
+ const rectInitialBounds = getCachedBounds(rectAcc);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("square").scrollIntoView();
+ });
+
+ await waitForContentPaint(browser);
+
+ // The only content to scroll over is `square`'s top margin
+ // so our scroll offset here should be 3000px
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 3000),
+ "Correct scroll position after first scroll."
+ );
+
+ // Scroll rect into view, but also make it reflow so we can be sure the
+ // bounds are correct for reflowed frames.
+ await invokeContentTask(browser, [], () => {
+ const rect = content.document.getElementById("rect");
+ rect.scrollIntoView();
+ rect.style.width = "300px";
+ rect.offsetTop;
+ rect.style.width = "200px";
+ });
+
+ await waitForContentPaint(browser);
+ // We have to scroll over `square`'s top margin (3000px),
+ // `square` itself (100px), and `square`'s bottom margin (4000px).
+ // This should give us a 7100px offset.
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 7100),
+ "Correct final scroll position."
+ );
+ await untilCacheIs(
+ () => getCachedBounds(rectAcc),
+ rectInitialBounds,
+ "Cached relative bounds don't change when scrolling"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset fixed-pos acc accs
+ */
+addAccessibleTask(
+ `
+ <div style="margin-top: 100px; margin-left: 75px; border: 1px solid;">
+ <div id="d" style="position:fixed;">
+ <button id="top">top</button>
+ </div>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const origTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ const origDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ const e = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ for (let i = 0; i < 1000; ++i) {
+ const div = content.document.createElement("div");
+ div.innerHTML = "<button>${i}</button>";
+ content.document.body.append(div);
+ }
+ });
+ await e;
+
+ await invokeContentTask(browser, [], () => {
+ // scroll to the bottom of the page
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+
+ await waitForContentPaint(browser);
+
+ let newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ let newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[1],
+ newTopBounds[1],
+ "y of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origDBounds[0],
+ newTopBounds[0],
+ "x of fixed elem container is unaffected by scrolling"
+ );
+ is(
+ origDBounds[1],
+ newDBounds[1],
+ "y of fixed elem container is unaffected by scrolling"
+ );
+ is(
+ origDBounds[2],
+ newDBounds[2],
+ "width of fixed container elem is unaffected by scrolling"
+ );
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of fixed container elem is unaffected by scrolling"
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // remove position styling
+ content.document.getElementById("d").style = "";
+ });
+
+ await waitForContentPaint(browser);
+
+ newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x of non-fixed element remains accurate."
+ );
+ ok(newTopBounds[1] < 0, "y coordinate shows item scrolled off page");
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width of non-fixed element remains accurate."
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height of non-fixed element remains accurate."
+ );
+ is(
+ origDBounds[0],
+ newDBounds[0],
+ "x of non-fixed container element remains accurate."
+ );
+ ok(newDBounds[1] < 0, "y coordinate shows container scrolled off page");
+ // Removing the position styling on this acc causes it to be bound by
+ // its parent's bounding box, which alters its width as a block element.
+ // We don't particularly care about width in this test, so skip it.
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of non-fixed container element remains accurate."
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // re-add position styling
+ content.document.getElementById("d").style = "position:fixed;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[1],
+ newTopBounds[1],
+ "y correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height correct when position:fixed is added."
+ );
+ is(
+ origDBounds[0],
+ newDBounds[0],
+ "x of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[1],
+ newDBounds[1],
+ "y of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[2],
+ newDBounds[2],
+ "width of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of container correct when position:fixed is added."
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test position: fixed for containers that would otherwise be pruned from the
+ * a11y tree.
+ */
+addAccessibleTask(
+ `
+<table id="fixed" role="presentation" style="position: fixed;">
+ <tr><th>fixed</th></tr>
+</table>
+<div id="mutate" role="presentation">mutate</div>
+<hr style="height: 200vh;">
+<p>bottom</p>
+ `,
+ async function (browser, docAcc) {
+ const fixed = findAccessibleChildByID(docAcc, "fixed");
+ ok(fixed, "fixed is accessible");
+ isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE");
+ ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
+ info("Setting position: fixed on mutate");
+ let shown = waitForEvent(EVENT_SHOW, "mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").style.position = "fixed";
+ });
+ await shown;
+ const origFixedBounds = await testBoundsWithContent(
+ docAcc,
+ "fixed",
+ browser
+ );
+ const origMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ info("Scrolling to bottom of page");
+ await invokeContentTask(browser, [], () => {
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+ await waitForContentPaint(browser);
+ const newFixedBounds = await testBoundsWithContent(
+ docAcc,
+ "fixed",
+ browser
+ );
+ Assert.deepEqual(
+ newFixedBounds,
+ origFixedBounds,
+ "fixed bounds are unchanged"
+ );
+ const newMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ Assert.deepEqual(
+ newMutateBounds,
+ origMutateBounds,
+ "mutate bounds are unchanged"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset on sticky-pos acc
+ */
+addAccessibleTask(
+ `
+ <div id="d" style="margin-top: 100px; margin-left: 75px; position:sticky; top:0px;">
+ <button id="top">top</button>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const containerBounds = await testBoundsWithContent(docAcc, "d", browser);
+ const e = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ for (let i = 0; i < 1000; ++i) {
+ const div = content.document.createElement("div");
+ div.innerHTML = "<button>${i}</button>";
+ content.document.body.append(div);
+ }
+ });
+ await e;
+ for (let id of ["d", "top"]) {
+ info(`Verifying bounds for acc with ID ${id}`);
+ const origBounds = await testBoundsWithContent(docAcc, id, browser);
+
+ info("Scrolling partially");
+ await invokeContentTask(browser, [], () => {
+ // scroll some of the window
+ content.window.scrollTo(0, 50);
+ });
+
+ await waitForContentPaint(browser);
+
+ let newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ ok(
+ origBounds[1] > newBounds[1] && newBounds[1] >= 0,
+ "sticky element scrolled, but not off the page"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Scrolling to bottom");
+ await invokeContentTask(browser, [], () => {
+ // scroll to the bottom of the page
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ // Subtract margin from container screen coords to get chrome height
+ // which is where our y pos should be
+ is(
+ newBounds[1],
+ containerBounds[1] - 100,
+ "Sticky element is top of screen"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Removing position style on container");
+ await invokeContentTask(browser, [], () => {
+ // remove position styling
+ content.document.getElementById("d").style =
+ "margin-top: 100px; margin-left: 75px;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of non-sticky element remains accurate.`
+ );
+ ok(newBounds[1] < 0, "y coordinate shows item scrolled off page");
+
+ // Removing the position styling on this acc causes it to be bound by
+ // its parent's bounding box, which alters its width as a block element.
+ // We don't particularly care about width in this test, so skip it.
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of non-sticky element remains accurate.`
+ );
+
+ info("Adding position style on container");
+ await invokeContentTask(browser, [], () => {
+ // re-add position styling
+ content.document.getElementById("d").style =
+ "margin-top: 100px; margin-left: 75px; position:sticky; top:0px;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ is(
+ newBounds[1],
+ containerBounds[1] - 100,
+ "Sticky element is top of screen"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Scrolling back up to test next ID");
+ await invokeContentTask(browser, [], () => {
+ // scroll some of the window
+ content.window.scrollTo(0, 0);
+ });
+ }
+ },
+ { chrome: false, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test position: sticky for containers that would otherwise be pruned from the
+ * a11y tree.
+ */
+addAccessibleTask(
+ `
+<hr style="height: 100vh;">
+<div id="stickyContainer">
+ <div id="sticky" role="presentation" style="position: sticky; top: 0px;">sticky</div>
+ <hr style="height: 100vh;">
+ <p id="stickyEnd">stickyEnd</p>
+</div>
+<div id="mutateContainer">
+ <div id="mutate" role="presentation" style="top: 0px;">mutate</div>
+ <hr style="height: 100vh;">
+ <p id="mutateEnd">mutateEnd</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible");
+ info("Scrolling to sticky");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("sticky").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const origStickyBounds = await testBoundsWithContent(
+ docAcc,
+ "sticky",
+ browser
+ );
+ info("Scrolling to stickyEnd");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("stickyEnd").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const newStickyBounds = await testBoundsWithContent(
+ docAcc,
+ "sticky",
+ browser
+ );
+ Assert.deepEqual(
+ newStickyBounds,
+ origStickyBounds,
+ "sticky bounds are unchanged"
+ );
+
+ ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
+ info("Setting position: sticky on mutate");
+ let shown = waitForEvent(EVENT_SHOW, "mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").style.position = "sticky";
+ });
+ await shown;
+ info("Scrolling to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const origMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ info("Scrolling to mutateEnd");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutateEnd").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const newMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset on non-scrollable accs
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width: 100px; background:green;'>hello world
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const square = findAccessibleChildByID(docAcc, "square");
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0, true),
+ "Square is not scrollable."
+ );
+
+ info("Adding more text content to square");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("square");
+ s.textContent =
+ "hello world I am some text and I should overflow this container because I am very long";
+ s.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0, true),
+ "Square is not scrollable (still has overflow:visible)."
+ );
+
+ info("Adding overflow:auto; styling");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("square");
+ s.setAttribute(
+ "style",
+ "overflow:auto; height:100px; width: 100px; background:green;"
+ );
+ s.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0),
+ "Square is scrollable."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scroll_substring.js b/accessible/tests/browser/scroll/browser_test_scroll_substring.js
new file mode 100644
index 0000000000..e8426d00ca
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scroll_substring.js
@@ -0,0 +1,67 @@
+/* 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 });
+
+/**
+ * Test nsIAccessibleText::scrollSubstringTo.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ height: 40px;
+ overflow-y: scroll;
+ }
+ </style>
+ <pre id="text">
+
+
+
+
+
+It's a jetpack, Michael. What could possibly go wrong?
+
+
+
+
+
+The only thing I found in the fridge was a dead dove in a bag.
+</pre>`,
+ async function (browser, docAcc) {
+ let text = findAccessibleChildByID(docAcc, "text", [nsIAccessibleText]);
+ let [, containerY, , containerHeight] = getBounds(text);
+ let getCharY = () => {
+ let objY = {};
+ text.getCharacterExtents(7, {}, objY, {}, {}, COORDTYPE_SCREEN_RELATIVE);
+ return objY.value;
+ };
+ ok(
+ containerHeight < getCharY(),
+ "Character is outside of container bounds"
+ );
+ text.scrollSubstringTo(7, 8, SCROLL_TYPE_TOP_EDGE);
+
+ await waitForContentPaint(browser);
+ await untilCacheIs(
+ getCharY,
+ containerY,
+ "Character is scrolled to top of container"
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
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..4fc0a56b43
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_zoom_text.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/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+async function runTests(browser, accDoc) {
+ await loadContentScripts(browser, {
+ script: "Layout.sys.mjs",
+ symbol: "Layout",
+ });
+
+ 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..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/scroll/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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/selectable/browser.toml b/accessible/tests/browser/selectable/browser.toml
new file mode 100644
index 0000000000..38a13c25f4
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_aria_select.js"]
+
+["browser_test_select.js"]
diff --git a/accessible/tests/browser/selectable/browser_test_aria_select.js b/accessible/tests/browser/selectable/browser_test_aria_select.js
new file mode 100644
index 0000000000..f52603d1cb
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser_test_aria_select.js
@@ -0,0 +1,164 @@
+/* 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/selectable.js */
+
+// ////////////////////////////////////////////////////////////////////////
+// role="tablist" role="listbox" role="grid" role="tree" role="treegrid"
+addAccessibleTask(
+ `<div role="tablist" id="tablist">
+ <div role="tab">tab1</div>
+ <div role="tab">tab2</div>
+ </div>
+ <div role="listbox" id="listbox">
+ <div role="option">item1</div>
+ <div role="option">item2</div>
+ </div>
+ <div role="grid" id="grid">
+ <div role="row">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ </div>
+ <div role="tree" id="tree">
+ <div role="treeitem">
+ item1
+ <div role="group">
+ <div role="treeitem">item1.1</div>
+ </div>
+ </div>
+ <div>item2</div>
+ </div>
+ <div role="treegrid" id="treegrid">
+ <div role="row" aria-level="1">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row" aria-level="2">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row" aria-level="1">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ </div>`,
+ async function (browser, docAcc) {
+ info(
+ 'role="tablist" role="listbox" role="grid" role="tree" role="treegrid"'
+ );
+ testSelectableSelection(findAccessibleChildByID(docAcc, "tablist"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "listbox"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "grid"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "tree"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "treegrid"), []);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="tablist" aria-multiselectable
+addAccessibleTask(
+ `<div role="tablist" id="tablist" aria-multiselectable="true">
+ <div role="tab" id="tab_multi1">tab1</div>
+ <div role="tab" id="tab_multi2">tab2</div>
+ </div>`,
+ async function (browser, docAcc) {
+ info('role="tablist" aria-multiselectable');
+ let tablist = findAccessibleChildByID(docAcc, "tablist", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(tablist, ["tab_multi1", "tab_multi2"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="listbox" aria-multiselectable
+addAccessibleTask(
+ `<div role="listbox" id="listbox" aria-multiselectable="true">
+ <div role="option" id="listbox2_item1">item1</div>
+ <div role="option" id="listbox2_item2">item2</div>
+ </div>`,
+ async function (browser, docAcc) {
+ info('role="listbox" aria-multiselectable');
+ let listbox = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(listbox, ["listbox2_item1", "listbox2_item2"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="grid" aria-multiselectable, selectable children in subtree
+addAccessibleTask(
+ `<table tabindex="0" border="2" cellspacing="0" id="grid" role="grid"
+ aria-multiselectable="true">
+ <thead>
+ <tr>
+ <th tabindex="-1" role="columnheader" id="grid_colhead1"
+ style="width:6em">Entry #</th>
+ <th tabindex="-1" role="columnheader" id="grid_colhead2"
+ style="width:10em">Date</th>
+ <th tabindex="-1" role="columnheader" id="grid_colhead3"
+ style="width:20em">Expense</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td tabindex="-1" role="rowheader" id="grid_rowhead"
+ aria-readonly="true">1</td>
+ <td tabindex="-1" role="gridcell" id="grid_cell1"
+ aria-selected="false">03/14/05</td>
+ <td tabindex="-1" role="gridcell" id="grid_cell2"
+ aria-selected="false">Conference Fee</td>
+ </tr>
+ </tobdy>
+ </table>`,
+ async function (browser, docAcc) {
+ info('role="grid" aria-multiselectable, selectable children in subtree');
+ let grid = findAccessibleChildByID(docAcc, "grid", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(grid, [
+ "grid_colhead1",
+ "grid_colhead2",
+ "grid_colhead3",
+ "grid_rowhead",
+ "grid_cell1",
+ "grid_cell2",
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/selectable/browser_test_select.js b/accessible/tests/browser/selectable/browser_test_select.js
new file mode 100644
index 0000000000..f86a371d81
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser_test_select.js
@@ -0,0 +1,329 @@
+/* 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/selectable.js */
+/* import-globals-from ../../mochitest/states.js */
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="1" aka combobox
+addAccessibleTask(
+ `<select id="combobox">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='1' aka combobox");
+ let combobox = findAccessibleChildByID(docAcc, "combobox");
+ let comboboxList = combobox.firstChild;
+ ok(
+ isAccessible(comboboxList, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+
+ let select = getAccessible(comboboxList, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, ["item1"]);
+
+ // select 2nd item
+ let promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, true),
+ waitForStateChange("item1", STATE_SELECTED, false),
+ ]);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ // unselect 2nd item, 1st item gets selected automatically
+ promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, false),
+ waitForStateChange("item1", STATE_SELECTED, true),
+ ]);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item1"], "removeItemFromSelection(1): ");
+
+ // doesn't change selection
+ is(select.selectAll(), false, "No way to select all items in combobox");
+ testSelectableSelection(select, ["item1"], "selectAll: ");
+
+ // doesn't change selection
+ select.unselectAll();
+ testSelectableSelection(select, ["item1"], "unselectAll: ");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="1" with optgroups
+addAccessibleTask(
+ `<select id="combobox">
+ <option id="item1">option1</option>
+ <optgroup>optgroup
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='1' with optgroups");
+ let combobox = findAccessibleChildByID(docAcc, "combobox");
+ let comboboxList = combobox.firstChild;
+ ok(
+ isAccessible(comboboxList, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+
+ let select = getAccessible(comboboxList, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, ["item1"]);
+
+ let promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, true),
+ waitForStateChange("item1", STATE_SELECTED, false),
+ ]);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, false),
+ waitForStateChange("item1", STATE_SELECTED, true),
+ ]);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item1"], "removeItemFromSelection(1): ");
+
+ is(select.selectAll(), false, "No way to select all items in combobox");
+ testSelectableSelection(select, ["item1"]);
+
+ select.unselectAll();
+ testSelectableSelection(select, ["item1"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" aka single selectable listbox
+addAccessibleTask(
+ `<select id="listbox" size="4">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' aka single selectable listbox");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ testSelectableSelection(select, []);
+
+ // select 2nd item
+ let promise = waitForStateChange("item2", STATE_SELECTED, true);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ // unselect 2nd item, 1st item gets selected automatically
+ promise = waitForStateChange("item2", STATE_SELECTED, false);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, [], "removeItemFromSelection(1): ");
+
+ // doesn't change selection
+ is(
+ select.selectAll(),
+ false,
+ "No way to select all items in single selectable listbox"
+ );
+ testSelectableSelection(select, [], "selectAll: ");
+
+ // doesn't change selection
+ select.unselectAll();
+ testSelectableSelection(select, [], "unselectAll: ");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" with optgroups, single selectable
+addAccessibleTask(
+ `<select id="listbox" size="4">
+ <option id="item1">option1</option>
+ <optgroup>optgroup>
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' with optgroups, single selectable");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ testSelectableSelection(select, []);
+
+ let promise = waitForStateChange("item2", STATE_SELECTED, true);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"]);
+
+ promise = waitForStateChange("item2", STATE_SELECTED, false);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, []);
+
+ is(
+ select.selectAll(),
+ false,
+ "No way to select all items in single selectable listbox"
+ );
+ testSelectableSelection(select, []);
+
+ select.unselectAll();
+ testSelectableSelection(select, []);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" multiselect aka listbox
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect aka listbox");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ ["item1", "item2"],
+ "select@size='4' multiselect aka listbox "
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" multiselect with optgroups
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <optgroup>optgroup>
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect with optgroups");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ ["item1", "item2"],
+ "select@size='4' multiselect aka listbox "
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// multiselect with coalesced selection event
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ <option id="item3">option3</option>
+ <option id="item4">option4</option>
+ <option id="item5">option5</option>
+ <option id="item6">option6</option>
+ <option id="item7">option7</option>
+ <option id="item8">option8</option>
+ <option id="item9">option9</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect with coalesced selection event");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ [
+ "item1",
+ "item2",
+ "item3",
+ "item4",
+ "item5",
+ "item6",
+ "item7",
+ "item8",
+ "item9",
+ ],
+ "select@size='4' multiselect with coalesced selection event "
+ );
+ },
+ {
+ chrome: false,
+ topLevel: true,
+ iframe: false,
+ remoteIframe: false,
+ }
+);
+
+/**
+ * Ensure that we don't assert when dealing with defunct items in selection
+ * events dropped due to coalescence (bug 1800755).
+ */
+addAccessibleTask(
+ `
+<form id="form">
+ <select id="select">
+ <option>
+ <optgroup id="optgroup">
+ <option>
+ </optgroup>
+ </select>
+</form>
+ `,
+ async function (browser, docAcc) {
+ let selected = waitForEvent(EVENT_SELECTION_WITHIN, "select");
+ await invokeContentTask(browser, [], () => {
+ const form = content.document.getElementById("form");
+ const select = content.document.getElementById("select");
+ const optgroup = content.document.getElementById("optgroup");
+ form.reset();
+ select.selectedIndex = 1;
+ select.add(optgroup);
+ select.item(0).remove();
+ });
+ await selected;
+ }
+);
diff --git a/accessible/tests/browser/selectable/head.js b/accessible/tests/browser/selectable/head.js
new file mode 100644
index 0000000000..ccf9e86f77
--- /dev/null
+++ b/accessible/tests/browser/selectable/head.js
@@ -0,0 +1,88 @@
+/* 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 testMultiSelectable */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+/* import-globals-from ../../mochitest/selectable.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "selectable.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+// Handle case where multiple selection change events are coalesced into
+// a SELECTION_WITHIN event. Promise resolves to true in that case.
+function multipleSelectionChanged(widget, changedChildren, selected) {
+ return Promise.race([
+ Promise.all(
+ changedChildren.map(id =>
+ waitForStateChange(id, STATE_SELECTED, selected)
+ )
+ ).then(() => false),
+ waitForEvent(EVENT_SELECTION_WITHIN, widget).then(() => true),
+ ]);
+}
+
+async function testMultiSelectable(widget, selectableChildren, msg = "") {
+ let isRemote = false;
+ try {
+ widget.DOMNode;
+ } catch (e) {
+ isRemote = true;
+ }
+
+ testSelectableSelection(widget, [], `${msg}: initial`);
+
+ let promise = waitForStateChange(selectableChildren[0], STATE_SELECTED, true);
+ widget.addItemToSelection(0);
+ await promise;
+ testSelectableSelection(
+ widget,
+ [selectableChildren[0]],
+ `${msg}: addItemToSelection(0)`
+ );
+
+ promise = waitForStateChange(selectableChildren[0], STATE_SELECTED, false);
+ widget.removeItemFromSelection(0);
+ await promise;
+ testSelectableSelection(widget, [], `${msg}: removeItemFromSelection(0)`);
+
+ promise = multipleSelectionChanged(widget, selectableChildren, true);
+ let success = widget.selectAll();
+ ok(success, `${msg}: selectAll success`);
+ await promise;
+ if (isRemote) {
+ await untilCacheIs(
+ () => widget.selectedItemCount,
+ selectableChildren.length,
+ "Selection cache updated"
+ );
+ }
+ testSelectableSelection(widget, selectableChildren, `${msg}: selectAll`);
+
+ promise = multipleSelectionChanged(widget, selectableChildren, false);
+ widget.unselectAll();
+ await promise;
+ if (isRemote) {
+ await untilCacheIs(
+ () => widget.selectedItemCount,
+ 0,
+ "Selection cache updated"
+ );
+ }
+ testSelectableSelection(widget, [], `${msg}: selectAll`);
+}
diff --git a/accessible/tests/browser/shared-head.js b/accessible/tests/browser/shared-head.js
new file mode 100644
index 0000000000..fe87a77765
--- /dev/null
+++ b/accessible/tests/browser/shared-head.js
@@ -0,0 +1,973 @@
+/* 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/layout.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, untilCacheIs,
+ untilCacheOk, testBoundsWithContent, waitForContentPaint,
+ runPython */
+
+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.
+ */
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+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() {
+ 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, symbol } of scripts) {
+ let contentScript = `${CURRENT_DIR}${script}`;
+ 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, symbol],
+ async (_contentScript, importSymbol) => {
+ let module = ChromeUtils.importESModule(_contentScript);
+ content.window[importSymbol] = module[importSymbol];
+ }
+ );
+ 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) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ 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",
+ `<!doctype 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 = `<!doctype html>
+ <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(
+ `<!doctype html>
+ <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");
+ }
+ if (gPythonSocket) {
+ // Remove any globals set by Python code run in this test.
+ runPython(`__reset__`);
+ }
+ });
+
+ 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,
+ // For chrome, we need a non-remote browser.
+ opening: !options.chrome
+ ? url
+ : () => {
+ // Passing forceNotRemote: true still sets maychangeremoteness,
+ // which will cause data: URIs to load remotely. There's no way to
+ // avoid this with gBrowser or BrowserTestUtils. Therefore, we
+ // load a blank document initially and replace it below.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank",
+ {
+ forceNotRemote: true,
+ }
+ );
+ },
+ },
+ async function (browser) {
+ registerCleanupFunction(() => {
+ if (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab && !tab.closing && tab.linkedBrowser) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ });
+
+ if (options.chrome) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+ // Ensure this never becomes a remote browser.
+ browser.removeAttribute("maychangeremoteness");
+ // Now we can load our page without it becoming remote.
+ browser.setAttribute("src", url);
+ }
+
+ await SimpleTest.promiseFocus(browser);
+
+ 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 loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+
+ 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. Although url can be a markup
+ * snippet, a snippet cannot be used for XUL content. To load XUL,
+ * specify a relative URL to a XUL document. In that case, toplevel
+ * should usually be set to false, since XUL documents don't work in
+ * content processes.
+ * - {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") {
+ let acc = findAccessibleChildByID(accDoc, id);
+
+ if (!acc) {
+ const onShow = waitForEvent(EVENT_SHOW, id);
+ acc = (await onShow).accessible;
+ }
+
+ if (acc.firstChild) {
+ return;
+ }
+
+ const onReorder = waitForEvent(EVENT_REORDER, id);
+ // Wave over image map
+ await invokeContentTask(browser, [id], contentId => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ 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.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ return LayoutUtils.getBoundsForDOMElm(contentId, content.document);
+ });
+}
+
+const CACHE_WAIT_TIMEOUT_MS = 5000;
+
+/**
+ * Wait for a predicate to be true after cache ticks.
+ * This function takes two callbacks, the condition is evaluated
+ * by calling the first callback with the arguments returned by the second.
+ * This allows us to asynchronously return the arguments as a result if the condition
+ * of the first callback is met, or if it times out. The returned arguments can then
+ * be used to record a pass or fail in the test.
+ */
+function untilCacheCondition(conditionFunc, argsFunc) {
+ return new Promise((resolve, reject) => {
+ let args = argsFunc();
+ if (conditionFunc(...args)) {
+ resolve(args);
+ return;
+ }
+
+ let cacheObserver = {
+ observe(subject) {
+ args = argsFunc();
+ if (conditionFunc(...args)) {
+ clearTimeout(this.timer);
+ Services.obs.removeObserver(this, "accessible-cache");
+ resolve(args);
+ }
+ },
+
+ timeout() {
+ ok(false, "Timeout while waiting for cache update");
+ Services.obs.removeObserver(this, "accessible-cache");
+ args = argsFunc();
+ resolve(args);
+ },
+ };
+
+ cacheObserver.timer = setTimeout(
+ cacheObserver.timeout.bind(cacheObserver),
+ CACHE_WAIT_TIMEOUT_MS
+ );
+ Services.obs.addObserver(cacheObserver, "accessible-cache");
+ });
+}
+
+function untilCacheOk(conditionFunc, message) {
+ return untilCacheCondition(
+ (v, _unusedMessage) => v,
+ () => [conditionFunc(), message]
+ ).then(([v, msg]) => ok(v, msg));
+}
+
+function untilCacheIs(retrievalFunc, expected, message) {
+ return untilCacheCondition(
+ (a, b, _unusedMessage) => Object.is(a, b),
+ () => [retrievalFunc(), expected, message]
+ ).then(([got, exp, msg]) => is(got, exp, msg));
+}
+
+async function waitForContentPaint(browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(function (r) {
+ content.requestAnimationFrame(() => content.setTimeout(r));
+ });
+ });
+}
+
+// Returns true if both number arrays match within `FUZZ`.
+function areBoundsFuzzyEqual(actual, expected) {
+ const FUZZ = 1;
+ return actual
+ .map((val, i) => Math.abs(val - expected[i]) <= FUZZ)
+ .reduce((a, b) => a && b, true);
+}
+
+function assertBoundsFuzzyEqual(actual, expected) {
+ ok(
+ areBoundsFuzzyEqual(actual, expected),
+ `${actual} fuzzily matches expected ${expected}`
+ );
+}
+
+async function testBoundsWithContent(iframeDocAcc, id, browser) {
+ // Retrieve layout bounds from content
+ let expectedBounds = await invokeContentTask(browser, [id], _id => {
+ const { Layout: LayoutUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ return LayoutUtils.getBoundsForDOMElm(_id, content.document);
+ });
+
+ function isWithinExpected(bounds) {
+ return areBoundsFuzzyEqual(bounds, expectedBounds);
+ }
+
+ const acc = findAccessibleChildByID(iframeDocAcc, id);
+ let [accBounds] = await untilCacheCondition(isWithinExpected, () => [
+ getBounds(acc),
+ ]);
+
+ assertBoundsFuzzyEqual(accBounds, expectedBounds);
+
+ return accBounds;
+}
+
+let gPythonSocket = null;
+
+/**
+ * Run some Python code. This is useful for testing OS APIs.
+ * This function returns a Promise which is resolved or rejected when the Python
+ * code completes. The Python code can return a result with the return
+ * statement, as long as the result can be serialized to JSON. For convenience,
+ * if the code is a single line which does not begin with return, it will be
+ * treated as an expression and its result will be returned. The JS Promise will
+ * be resolved with the deserialized result. If the Python code raises an
+ * exception, the JS Promise will be rejected with the Python traceback.
+ * An info() function is provided in Python to log an info message.
+ * See windows/a11y_setup.py for other things available in the Python
+ * environment.
+ */
+function runPython(code) {
+ if (!gPythonSocket) {
+ // Keep the socket open across calls to avoid repeated setup overhead.
+ gPythonSocket = new WebSocket(
+ "ws://mochi.test:8888/browser/accessible/tests/browser/python_runner"
+ );
+ if (gPythonSocket.readyState != WebSocket.OPEN) {
+ gPythonSocket.onopen = evt => {
+ gPythonSocket.send(code);
+ gPythonSocket.onopen = null;
+ };
+ }
+ }
+ return new Promise((resolve, reject) => {
+ gPythonSocket.onmessage = evt => {
+ const message = JSON.parse(evt.data);
+ if (message[0] == "return") {
+ gPythonSocket.onmessage = null;
+ resolve(message[1]);
+ } else if (message[0] == "exception") {
+ gPythonSocket.onmessage = null;
+ reject(new Error(message[1]));
+ } else if (message[0] == "info") {
+ info(message[1]);
+ }
+ };
+ // If gPythonSocket isn't open yet, we'll send the message when .onopen is
+ // called. If it's open, we can send it immediately.
+ if (gPythonSocket.readyState == WebSocket.OPEN) {
+ gPythonSocket.send(code);
+ }
+ });
+}
diff --git a/accessible/tests/browser/states/browser.toml b/accessible/tests/browser/states/browser.toml
new file mode 100644
index 0000000000..fe29597d27
--- /dev/null
+++ b/accessible/tests/browser/states/browser.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_link.js"]
+https_first_disabled = true
+skip-if = ["verify"]
+
+["browser_test_select_visibility.js"]
+https_first_disabled = true
+
+["browser_test_visibility.js"]
+https_first_disabled = true
+
+["browser_test_visibility_2.js"]
+https_first_disabled = true
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..0a3e8a9975
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_link.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";
+
+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 newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#link_traversed",
+ 1,
+ 1,
+ { ctrlKey: !MAC, metaKey: MAC },
+ browser
+ );
+
+ await onStateChanged;
+ testStates(getAcc("link_traversed"), STATE_TRAVERSED);
+
+ let newTab = await newTabOpened;
+ gBrowser.removeTab(newTab);
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ // The URL doesn't really matter, just the fact that it isn't in the history
+ // initially. We append ms since epoch to the URL so it will never be visited
+ // initially, regardless of other tests (even this one) that ran before.
+ `
+ <a id="link_traversed"
+ href="https://www.example.com/${Date.now()}" target="_top">
+ example.com
+ </a>`,
+ runTests
+);
diff --git a/accessible/tests/browser/states/browser_test_select_visibility.js b/accessible/tests/browser/states/browser_test_select_visibility.js
new file mode 100644
index 0000000000..89b4df67f7
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_select_visibility.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 selects and options
+addAccessibleTask(
+ `<select id="select">
+ <option id="o1">hello</option>
+ <option id="o2">world</option>
+ </select>`,
+ async function (browser, accDoc) {
+ const select = findAccessibleChildByID(accDoc, "select");
+ ok(
+ isAccessible(select.firstChild, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+ await untilCacheOk(
+ () => testVisibility(select, false, false),
+ "select should be on screen and visible"
+ );
+
+ if (!browser.isRemoteBrowser) {
+ await untilCacheOk(
+ () => testVisibility(select.firstChild, false, true),
+ "combobox list should be on screen and invisible"
+ );
+ } else {
+ // XXX: When the cache is used, states::INVISIBLE is
+ // incorrect. Test OFFSCREEN anyway.
+ await untilCacheOk(() => {
+ const [states] = getStates(select.firstChild);
+ return (states & STATE_OFFSCREEN) == 0;
+ }, "combobox list should be on screen");
+ }
+
+ const o1 = findAccessibleChildByID(accDoc, "o1");
+ const o2 = findAccessibleChildByID(accDoc, "o2");
+
+ await untilCacheOk(
+ () => testVisibility(o1, false, false),
+ "option one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(o2, true, false),
+ "option two should be off screen and visible"
+ );
+
+ // Select the second option (drop-down collapsed).
+ const p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "o2"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("o2", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("o1", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("select").selectedIndex = 1;
+ });
+ await p;
+
+ await untilCacheOk(() => {
+ const [states] = getStates(o1);
+ return (states & STATE_OFFSCREEN) != 0;
+ }, "option 1 should be off screen");
+ await untilCacheOk(() => {
+ const [states] = getStates(o2);
+ return (states & STATE_OFFSCREEN) == 0;
+ }, "option 2 should be on screen");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
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..25bd903ed4
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_visibility.js
@@ -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";
+
+async function runTest(browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("div"), false, false),
+ "Div should be on screen"
+ );
+
+ let input = getAcc("input_scrolledoff");
+ await untilCacheOk(
+ () => testVisibility(input, true, false),
+ "Input should be offscreen"
+ );
+
+ // scrolled off item (twice)
+ let lastLi = getAcc("li_last");
+ await untilCacheOk(
+ () => testVisibility(lastLi, true, false),
+ "Last list item should be offscreen"
+ );
+
+ // scroll into view the item
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("li_last").scrollIntoView(true);
+ });
+ await untilCacheOk(
+ () => testVisibility(lastLi, false, false),
+ "Last list item should no longer be offscreen"
+ );
+
+ // first item is scrolled off now (testcase for bug 768786)
+ let firstLi = getAcc("li_first");
+ await untilCacheOk(
+ () => testVisibility(firstLi, true, false),
+ "First listitem should now be offscreen"
+ );
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), false, false),
+ "iframe should initially be onscreen"
+ );
+
+ let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "iframeDoc");
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("iframe").src =
+ 'data:text/html,<body id="iframeDoc"><p id="p">hi</p></body>';
+ });
+
+ const iframeDoc = (await loaded).accessible;
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), false, false),
+ "iframe outer doc should now be on screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, false, false),
+ "iframe inner doc should be on screen"
+ );
+ const iframeP = findAccessibleChildByID(iframeDoc, "p");
+ await untilCacheOk(
+ () => testVisibility(iframeP, false, false),
+ "iframe content should also be on screen"
+ );
+
+ // scroll into view the div
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").scrollIntoView(true);
+ });
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), true, false),
+ "iframe outer doc should now be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, true, false),
+ "iframe inner doc should now be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeP, true, false),
+ "iframe content should now be off screen"
+ );
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ // Accessibles in background tab should have offscreen state and no
+ // invisible state.
+ await untilCacheOk(
+ () => testVisibility(getAcc("div"), true, false),
+ "Accs in background tab should be offscreen but not invisible."
+ );
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), true, false),
+ "iframe outer doc should still be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, true, false),
+ "iframe inner doc should still be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeP, true, false),
+ "iframe content should still be off screen"
+ );
+
+ 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>
+ <iframe id="frame"></iframe>
+ `,
+ runTest,
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test div containers are reported as onscreen, even if some of their contents are
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <div id="outer" style="width:200vw; background: green; overflow:scroll;"><div id="inner"><div style="display:inline-block; width:100vw; background:red;" id="on">on screen</div><div style="background:blue; display:inline;" id="off">offscreen</div></div></div>
+ `,
+ async function (browser, accDoc) {
+ const outer = findAccessibleChildByID(accDoc, "outer");
+ const inner = findAccessibleChildByID(accDoc, "inner");
+ const on = findAccessibleChildByID(accDoc, "on");
+ const off = findAccessibleChildByID(accDoc, "off");
+
+ await untilCacheOk(
+ () => testVisibility(outer, false, false),
+ "outer should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(inner, false, false),
+ "inner should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(on, false, false),
+ "on should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(off, true, false),
+ "off should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// test dynamic translation
+addAccessibleTask(
+ `<div id="container" style="position: absolute; left: -300px; top: 100px;">Hello</div><button id="b" onclick="container.style.transform = 'translateX(400px)'">Move</button>`,
+ async function (browser, accDoc) {
+ const container = findAccessibleChildByID(accDoc, "container");
+ await untilCacheOk(
+ () => testVisibility(container, true, false),
+ "container should be off screen and visible"
+ );
+ await invokeContentTask(browser, [], () => {
+ let b = content.document.getElementById("b");
+ b.click();
+ });
+
+ await waitForContentPaint(browser);
+ await untilCacheOk(
+ () => testVisibility(container, false, false),
+ "container should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/states/browser_test_visibility_2.js b/accessible/tests/browser/states/browser_test_visibility_2.js
new file mode 100644
index 0000000000..ead134069a
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_visibility_2.js
@@ -0,0 +1,131 @@
+/* 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 tables, table rows are reported on screen, even if some cells of a given row are
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <table id="table" style="width:150vw;" border><tr id="row"><td id="one" style="width:50vw;">one</td><td style="width:50vw;" id="two">two</td><td id="three">three</td></tr></table>
+ `,
+ async function (browser, accDoc) {
+ const table = findAccessibleChildByID(accDoc, "table");
+ const row = findAccessibleChildByID(accDoc, "row");
+ const one = findAccessibleChildByID(accDoc, "one");
+ const two = findAccessibleChildByID(accDoc, "two");
+ const three = findAccessibleChildByID(accDoc, "three");
+
+ await untilCacheOk(
+ () => testVisibility(table, false, false),
+ "table should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(row, false, false),
+ "row should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(one, false, false),
+ "one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(two, false, false),
+ "two should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(three, true, false),
+ "three should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rows and cells outside of the viewport are reported as offscreen.
+ */
+addAccessibleTask(
+ `
+ <table id="table" style="height:150vh;" border><tr style="height:100vh;" id="rowA"><td id="one">one</td></tr><tr id="rowB"><td id="two">two</td></tr></table>
+ `,
+ async function (browser, accDoc) {
+ const table = findAccessibleChildByID(accDoc, "table");
+ const rowA = findAccessibleChildByID(accDoc, "rowA");
+ const one = findAccessibleChildByID(accDoc, "one");
+ const rowB = findAccessibleChildByID(accDoc, "rowB");
+ const two = findAccessibleChildByID(accDoc, "two");
+
+ await untilCacheOk(
+ () => testVisibility(table, false, false),
+ "table should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(rowA, false, false),
+ "rowA should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(one, false, false),
+ "one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(rowB, true, false),
+ "rowB should be off screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(two, true, false),
+ "two should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+addAccessibleTask(
+ `
+ <div id="div">hello</div>
+ `,
+ async function (browser, accDoc) {
+ let textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
+ await untilCacheOk(
+ () => testVisibility(textLeaf, false, false),
+ "text should be on screen and visible"
+ );
+ let p = waitForEvent(EVENT_TEXT_INSERTED, "div");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").textContent = "goodbye";
+ });
+ await p;
+ textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
+ await untilCacheOk(
+ () => testVisibility(textLeaf, false, false),
+ "text should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Overlapping, opaque divs with the same bounds should not be considered
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <style>div { height: 5px; width: 5px; background: green; }</style>
+ <div id="outer" role="group"><div style="background:blue;" id="inner" role="group">hi</div></div>
+ `,
+ async function (browser, accDoc) {
+ const outer = findAccessibleChildByID(accDoc, "outer");
+ const inner = findAccessibleChildByID(accDoc, "inner");
+
+ await untilCacheOk(
+ () => testVisibility(outer, false, false),
+ "outer should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(inner, false, false),
+ "inner should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/states/head.js b/accessible/tests/browser/states/head.js
new file mode 100644
index 0000000000..10c616cb80
--- /dev/null
+++ b/accessible/tests/browser/states/head.js
@@ -0,0 +1,91 @@
+/* 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, testVisibility */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.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
+ );
+}
+
+function testVisibility(acc, shouldBeOffscreen, shouldBeInvisible) {
+ const [states] = getStates(acc);
+ let looksGood = shouldBeOffscreen == ((states & STATE_OFFSCREEN) != 0);
+ looksGood &= shouldBeInvisible == ((states & STATE_INVISIBLE) != 0);
+ return looksGood;
+}
diff --git a/accessible/tests/browser/telemetry/browser.toml b/accessible/tests/browser/telemetry/browser.toml
new file mode 100644
index 0000000000..3606d1c099
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+subsuite = "a11y"
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["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..60ac3be82f
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js
@@ -0,0 +1,407 @@
+/* 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";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/preferences/tests/head.js",
+ this
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+registerCleanupFunction(() => {
+ reset();
+});
+
+function reset() {
+ // This (manually) runs after every task in this test suite.
+ // We have to add this in because the initial state of
+ // `document_color_use` affects the initial state of
+ // `foreground_color`/`background_color` which can change our
+ // starting telem samples. This ensures each tasks makes no lasting
+ // state changes.
+ Services.prefs.clearUserPref("browser.display.document_color_use");
+ Services.prefs.clearUserPref("browser.display.permit_backplate");
+ Services.prefs.clearUserPref("browser.display.use_system_colors");
+ Services.prefs.clearUserPref("layout.css.always_underline_links");
+ Services.telemetry.clearEvents();
+ TelemetryTestUtils.assertNumberOfEvents(0);
+ Services.prefs.clearUserPref("browser.display.foreground_color");
+ Services.prefs.clearUserPref("browser.display.background_color");
+}
+
+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");
+ const button = dialogWin.document
+ .getElementById("ColorsDialog")
+ .getButton("accept");
+ button.focus();
+ button.doCommand();
+ return dialogClosed;
+}
+
+function verifyBackplate(expectedValue) {
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent", false, true),
+ "a11y.backplate",
+ expectedValue,
+ "Backplate scalar is logged as " + expectedValue
+ );
+}
+
+function verifyUseSystemColors(expectedValue) {
+ const snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ ok("a11y.use_system_colors" in snapshot, "System color usage was logged.");
+ TelemetryTestUtils.assertScalar(
+ snapshot,
+ "a11y.use_system_colors",
+ expectedValue,
+ "System colors scalar is logged as " + expectedValue
+ );
+}
+
+async function verifyAlwaysUnderlineLinks(expectedValue) {
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ ok(
+ "a11y.always_underline_links" in snapshot,
+ "Always underline links was logged."
+ );
+ await TestUtils.waitForCondition(() => {
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ return snapshot["a11y.always_underline_links"] == expectedValue;
+ }, "Always underline links has expected value " + expectedValue);
+}
+
+// The magic numbers below are the uint32_t values representing RGB white
+// and RGB black respectively. They're directly captured as nsColors and
+// follow the same bit-shift pattern.
+function testIsWhite(pref, snapshot) {
+ ok(pref in snapshot, "Scalar must be present.");
+ is(snapshot[pref], 4294967295, "Scalar is logged as white");
+}
+
+function testIsBlack(pref, snapshot) {
+ ok(pref in snapshot, "Scalar must be present.");
+ is(snapshot[pref], 4278190080, "Scalar is logged as black");
+}
+
+async function setForegroundColor(color) {
+ // Note: we set the foreground and background colors by modifying this pref
+ // instead of setting the value attribute on the color input direclty.
+ // This is because setting the value of the input with setAttribute
+ // doesn't generate the correct event to save the new value to the prefs
+ // store, so we have to do it ourselves.
+ Services.prefs.setStringPref("browser.display.foreground_color", color);
+}
+
+async function setBackgroundColor(color) {
+ Services.prefs.setStringPref("browser.display.background_color", color);
+}
+
+add_task(async function testInit() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+ if (AppConstants.platform == "win") {
+ is(
+ Services.prefs.getBoolPref("browser.display.use_system_colors"),
+ true,
+ "Use system colours pref is init'd correctly"
+ );
+ verifyUseSystemColors(true);
+
+ is(
+ menulistHCM.value,
+ "0",
+ "HCM menulist should be set to only with HCM theme on startup for windows"
+ );
+
+ // Verify correct default value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "default",
+ false
+ );
+ } else {
+ is(
+ Services.prefs.getBoolPref("browser.display.use_system_colors"),
+ false,
+ "Use system colours pref is init'd correctly"
+ );
+ verifyUseSystemColors(false);
+
+ is(
+ menulistHCM.value,
+ "1",
+ "HCM menulist should be set to never on startup for non-windows platforms"
+ );
+
+ // Verify correct default value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "always",
+ false
+ );
+
+ await closeColorsDialog(dialogWin);
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+ }
+
+ reset();
+ 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
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(snapshot, "a11y.theme", "never", false);
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ // We should have logged the default foreground and background colors
+ testIsWhite("a11y.HCM_background", snapshot);
+ testIsBlack("a11y.HCM_foreground", snapshot);
+
+ // If we change the colors, our probes update on non-windows platforms.
+ // On windows, useSystemColors is on by default, and so the values we set here
+ // will not be written to our telemetry probes, because they capture
+ // used colors, not the values of browser.foreground/background_color directly.
+
+ setBackgroundColor("#000000");
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (AppConstants.platform == "win") {
+ testIsWhite("a11y.HCM_background", snapshot);
+ } else {
+ testIsBlack("a11y.HCM_background", snapshot);
+ }
+
+ setForegroundColor("#ffffff");
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (AppConstants.platform == "win") {
+ testIsBlack("a11y.HCM_foreground", snapshot);
+ } else {
+ testIsWhite("a11y.HCM_foreground", snapshot);
+ }
+
+ reset();
+ 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
+ );
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated anywhere
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ reset();
+ 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
+ );
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated anywhere
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+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);
+});
+
+add_task(async function testSystemColors() {
+ let expectedInitVal = false;
+ if (AppConstants.platform == "win") {
+ expectedInitVal = true;
+ }
+
+ const dialogWin = await openColorsDialog();
+ const checkbox = dialogWin.document.getElementById("browserUseSystemColors");
+ checkbox.click();
+
+ is(
+ checkbox.checked,
+ !expectedInitVal,
+ "System colors checkbox should be modified"
+ );
+
+ await closeColorsDialog(dialogWin);
+
+ verifyUseSystemColors(!expectedInitVal);
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testAlwaysUnderlineLinks() {
+ const expectedInitVal = false;
+ await verifyAlwaysUnderlineLinks(expectedInitVal);
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const checkbox = gBrowser.selectedBrowser.contentDocument.getElementById(
+ "alwaysUnderlineLinks"
+ );
+ is(
+ checkbox.checked,
+ expectedInitVal,
+ "Always underline links checkbox has correct initial state"
+ );
+ checkbox.click();
+
+ is(
+ checkbox.checked,
+ !expectedInitVal,
+ "Always underline links checkbox should be modified"
+ );
+ is(
+ Services.prefs.getBoolPref("layout.css.always_underline_links"),
+ !expectedInitVal,
+ "Always underline links pref reflects new value."
+ );
+ await verifyAlwaysUnderlineLinks(!expectedInitVal);
+ reset();
+ gBrowser.removeCurrentTab();
+});
diff --git a/accessible/tests/browser/text/browser.toml b/accessible/tests/browser/text/browser.toml
new file mode 100644
index 0000000000..c04531b126
--- /dev/null
+++ b/accessible/tests/browser/text/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_editabletext.js"]
+
+["browser_text.js"]
+
+["browser_text_caret.js"]
+
+["browser_text_paragraph_boundary.js"]
+
+["browser_text_selection.js"]
+
+["browser_text_spelling.js"]
+skip-if = ["true"] # Bug 1800400
+
+["browser_textleafpoint.js"]
diff --git a/accessible/tests/browser/text/browser_editabletext.js b/accessible/tests/browser/text/browser_editabletext.js
new file mode 100644
index 0000000000..0310122deb
--- /dev/null
+++ b/accessible/tests/browser/text/browser_editabletext.js
@@ -0,0 +1,173 @@
+/* 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 testEditable(browser, acc, aBefore = "", aAfter = "") {
+ async function resetInput() {
+ if (acc.childCount <= 1) {
+ return;
+ }
+
+ let emptyInputEvent = waitForEvent(EVENT_TEXT_VALUE_CHANGE, "input");
+ await invokeContentTask(browser, [], async () => {
+ content.document.getElementById("input").innerHTML = "";
+ });
+
+ await emptyInputEvent;
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // insertText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testInsertText(acc, "ma ", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ma hello", aAfter]);
+ await testInsertText(acc, "ma", 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "mama hello", aAfter]);
+ await testInsertText(acc, " hello", 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [
+ aBefore,
+ "mama hello hello",
+ aAfter,
+ ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // deleteText
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello hello", aAfter]);
+ await testDeleteText(acc, 5, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hellohello", aAfter]);
+ await testDeleteText(acc, 5, 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ // XXX: clipboard operation tests don't work well with editable documents.
+ if (acc.role == ROLE_DOCUMENT) {
+ return;
+ }
+
+ await resetInput();
+
+ // copyText and pasteText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+
+ await testCopyText(acc, 0, 1, aBefore.length, browser, "h");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhello", aAfter]);
+
+ await testCopyText(acc, 5, 6, aBefore.length, browser, "o");
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhelloo", aAfter]);
+
+ await testCopyText(acc, 2, 3, aBefore.length, browser, "e");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hehelloo", aAfter]);
+
+ // cut & paste
+ await testCutText(acc, 0, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehelloo", aAfter]);
+ await testPasteText(acc, 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhelloo", aAfter]);
+
+ await testCutText(acc, 3, 4, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloo", aAfter]);
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloeo", aAfter]);
+
+ await testCutText(acc, 0, 8, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ await resetInput();
+
+ // ////////////////////////////////////////////////////////////////////////
+ // setTextContents
+ await testSetTextContents(acc, "hello", aBefore.length, [
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testSetTextContents(acc, "katze", aBefore.length, [
+ EVENT_TEXT_REMOVED,
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "katze", aAfter]);
+}
+
+addAccessibleTask(
+ `<input id="input"/>`,
+ async function (browser, docAcc) {
+ await testEditable(browser, findAccessibleChildByID(docAcc, "input"));
+ },
+ { chrome: true, topLevel: true }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::after {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "",
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "before";
+ }
+ #input::after {
+ content: "after";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "before",
+ "after"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ ``,
+ async function (browser, docAcc) {
+ await testEditable(browser, docAcc);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ contentDocBodyAttrs: { contentEditable: "true" },
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text.js b/accessible/tests/browser/text/browser_text.js
new file mode 100644
index 0000000000..79909ee412
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text.js
@@ -0,0 +1,326 @@
+/* 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/text.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test line and word offsets for various cases for both local and remote
+ * Accessibles. There is more extensive coverage in ../../mochitest/text. These
+ * tests don't need to duplicate all of that, since much of the underlying code
+ * is unified. They should ensure that the cache works as expected and that
+ * there is consistency between local and remote.
+ */
+addAccessibleTask(
+ `
+<p id="br">ab cd<br>ef gh</p>
+<pre id="pre">ab cd
+ef gh</pre>
+<p id="linksStartEnd"><a href="https://example.com/">a</a>b<a href="https://example.com/">c</a></p>
+<p id="linksBreaking">a<a href="https://example.com/">b<br>c</a>d</p>
+<p id="p">a<br role="presentation">b</p>
+<p id="leafThenWrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span>a</span>bc</p>
+ `,
+ async function (browser, docAcc) {
+ for (const id of ["br", "pre"]) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ testCharacterCount([acc], 11);
+ testTextAtOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd\n", 0, 6],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ef gh", 6, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "ab cd", 0, 5],
+ [6, 11, "\nef gh", 5, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd", 0, 5],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "\nef gh", 5, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "ab ", 0, 3],
+ [3, 5, "cd\n", 3, 6],
+ [6, 8, "ef ", 6, 9],
+ [9, 11, "gh", 9, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab ", 0, 3],
+ [6, 8, "cd\n", 3, 6],
+ [9, 11, "ef ", 6, 9],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "cd\n", 3, 6],
+ [3, 5, "ef ", 6, 9],
+ [6, 8, "gh", 9, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_END, [
+ [0, 1, "ab", 0, 2],
+ [2, 4, " cd", 2, 5],
+ [5, 7, "\nef", 5, 8],
+ [8, 11, " gh", 8, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab", 0, 2],
+ // See below for offset 6.
+ [7, 8, " cd", 2, 5],
+ [9, 11, "\nef", 5, 8],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [[6, 6, " cd", 2, 5]]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, " cd", 2, 5],
+ [3, 5, "\nef", 5, 8],
+ [6, 8, " gh", 8, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_PARAGRAPH, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ }
+ const linksStartEnd = findAccessibleChildByID(docAcc, "linksStartEnd");
+ testTextAtOffset(linksStartEnd, BOUNDARY_LINE_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ testTextAtOffset(linksStartEnd, BOUNDARY_WORD_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ const linksBreaking = findAccessibleChildByID(docAcc, "linksBreaking");
+ testTextAtOffset(linksBreaking, BOUNDARY_LINE_START, [
+ [0, 0, `a${kEmbedChar}`, 0, 2],
+ [1, 1, `a${kEmbedChar}d`, 0, 3],
+ [2, 3, `${kEmbedChar}d`, 1, 3],
+ ]);
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [
+ [0, 0, "a", 0, 1],
+ [1, 2, "b", 1, 2],
+ ]);
+ testTextAtOffset(p, BOUNDARY_PARAGRAPH, [[0, 2, "ab", 0, 2]]);
+ const leafThenWrap = findAccessibleChildByID(docAcc, "leafThenWrap");
+ testTextAtOffset(leafThenWrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "c", 2, 3],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test line offsets after text mutation.
+ */
+addAccessibleTask(
+ `
+<p id="initBr"><br></p>
+<p id="rewrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span id="rewrap1">ac</span>def</p>
+ `,
+ async function (browser, docAcc) {
+ const initBr = findAccessibleChildByID(docAcc, "initBr");
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 0, "\n", 0, 1],
+ [1, 1, "", 1, 1],
+ ]);
+ info("initBr: Inserting text before br");
+ let reordered = waitForEvent(EVENT_REORDER, initBr);
+ await invokeContentTask(browser, [], () => {
+ const initBrNode = content.document.getElementById("initBr");
+ initBrNode.insertBefore(
+ content.document.createTextNode("a"),
+ initBrNode.firstElementChild
+ );
+ });
+ await reordered;
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 1, "a\n", 0, 2],
+ [2, 2, "", 2, 2],
+ ]);
+
+ const rewrap = findAccessibleChildByID(docAcc, "rewrap");
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ac", 0, 2],
+ [2, 3, "de", 2, 4],
+ [4, 5, "f", 4, 5],
+ ]);
+ info("rewrap: Changing ac to abc");
+ reordered = waitForEvent(EVENT_REORDER, rewrap);
+ await invokeContentTask(browser, [], () => {
+ const rewrap1 = content.document.getElementById("rewrap1");
+ rewrap1.textContent = "abc";
+ });
+ await reordered;
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "cd", 2, 4],
+ [4, 6, "ef", 4, 6],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test retrieval of text offsets when an invalid offset is given.
+ */
+addAccessibleTask(
+ `<p id="p">test</p>`,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextBeforeOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextAfterOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ },
+ {
+ // The old HyperTextAccessible implementation doesn't crash, but it returns
+ // different offsets. This doesn't matter because they're invalid either
+ // way. Since the new HyperTextAccessibleBase implementation is all we will
+ // have soon, just test that.
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods.
+ */
+addAccessibleTask(
+ `<div id="container">a<a id="link" href="https://example.com/">b</a>c</div>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container", [
+ nsIAccessibleHyperText,
+ ]);
+ is(container.linkCount, 1, "container linkCount is 1");
+ let link = container.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible, nsIAccessibleHyperText]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(container.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 1, "link's startIndex is 1");
+ is(link.endIndex, 2, "link's endIndex is 2");
+ is(container.getLinkIndexAtOffset(1), 0, "getLinkIndexAtOffset(1) is 0");
+ is(container.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ is(link.linkCount, 0, "link linkCount is 0");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods near a list bullet.
+ */
+addAccessibleTask(
+ `<ul><li id="li"><a id="link" href="https://example.com/">a</a></li></ul>`,
+ async function (browser, docAcc) {
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleHyperText]);
+ let link = li.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(li.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 2, "link's startIndex is 2");
+ is(li.getLinkIndexAtOffset(2), 0, "getLinkIndexAtOffset(2) is 0");
+ is(li.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/**
+ * Test text attribute methods.
+ */
+addAccessibleTask(
+ `
+<p id="plain">ab</p>
+<p id="bold" style="font-weight: bold;">ab</p>
+<p id="partialBold">ab<b>cd</b>ef</p>
+<p id="consecutiveBold">ab<b>cd</b><b>ef</b>gh</p>
+<p id="embeddedObjs">ab<a href="https://example.com/">cd</a><a href="https://example.com/">ef</a><a href="https://example.com/">gh</a>ij</p>
+<p id="empty"></p>
+<p id="fontFamilies" style="font-family: sans-serif;">ab<span style="font-family: monospace;">cd</span><span style="font-family: monospace;">ef</span>gh</p>
+ `,
+ async function (browser, docAcc) {
+ let defAttrs = {
+ "text-position": "baseline",
+ "font-style": "normal",
+ "font-weight": "400",
+ };
+
+ const plain = findAccessibleChildByID(docAcc, "plain");
+ testDefaultTextAttrs(plain, defAttrs, true);
+ for (let offset = 0; offset <= 2; ++offset) {
+ testTextAttrs(plain, offset, {}, defAttrs, 0, 2, true);
+ }
+
+ const bold = findAccessibleChildByID(docAcc, "bold");
+ defAttrs["font-weight"] = "700";
+ testDefaultTextAttrs(bold, defAttrs, true);
+ testTextAttrs(bold, 0, {}, defAttrs, 0, 2, true);
+
+ const partialBold = findAccessibleChildByID(docAcc, "partialBold");
+ defAttrs["font-weight"] = "400";
+ testDefaultTextAttrs(partialBold, defAttrs, true);
+ testTextAttrs(partialBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(partialBold, 2, boldAttrs, defAttrs, 2, 4, true);
+ testTextAttrs(partialBold, 4, {}, defAttrs, 4, 6, true);
+
+ const consecutiveBold = findAccessibleChildByID(docAcc, "consecutiveBold");
+ testDefaultTextAttrs(consecutiveBold, defAttrs, true);
+ testTextAttrs(consecutiveBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(consecutiveBold, 2, boldAttrs, defAttrs, 2, 6, true);
+ testTextAttrs(consecutiveBold, 6, {}, defAttrs, 6, 8, true);
+
+ const embeddedObjs = findAccessibleChildByID(docAcc, "embeddedObjs");
+ testDefaultTextAttrs(embeddedObjs, defAttrs, true);
+ testTextAttrs(embeddedObjs, 0, {}, defAttrs, 0, 2, true);
+ for (let offset = 2; offset <= 4; ++offset) {
+ // attrs and defAttrs should be completely empty, so we pass
+ // false for aSkipUnexpectedAttrs.
+ testTextAttrs(embeddedObjs, offset, {}, {}, 2, 5, false);
+ }
+ testTextAttrs(embeddedObjs, 5, {}, defAttrs, 5, 7, true);
+
+ const empty = findAccessibleChildByID(docAcc, "empty");
+ testDefaultTextAttrs(empty, defAttrs, true);
+ testTextAttrs(empty, 0, {}, defAttrs, 0, 0, true);
+
+ const fontFamilies = findAccessibleChildByID(docAcc, "fontFamilies", [
+ nsIAccessibleHyperText,
+ ]);
+ testDefaultTextAttrs(fontFamilies, defAttrs, true);
+ testTextAttrs(fontFamilies, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(fontFamilies, 2, {}, defAttrs, 2, 6, true);
+ testTextAttrs(fontFamilies, 6, {}, defAttrs, 6, 8, true);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_caret.js b/accessible/tests/browser/text/browser_text_caret.js
new file mode 100644
index 0000000000..e0cea334d6
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_caret.js
@@ -0,0 +1,452 @@
+/* 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/text.js */
+
+/**
+ * Test caret retrieval.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea"
+ spellcheck="false"
+ style="scrollbar-width: none; font-family: 'Liberation Mono', monospace;"
+ cols="6">ab cd e</textarea>
+<textarea id="empty"></textarea>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ let evt = await caretMoved;
+ is(textarea.caretOffset, 0, "Initial caret offset is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "a",
+ 0,
+ 1,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 1, "Caret offset is 1 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "b",
+ 1,
+ 2,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 2, "Caret offset is 2 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 2,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 3, "Caret offset is 3 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "c",
+ 3,
+ 4,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 4, "Caret offset is 4 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "d",
+ 4,
+ 5,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 5, "Caret offset is 5 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 5,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset is 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 6,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset remains 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ // Caret is at start of second line.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 7, "Caret offset is 7 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ // Caret is at end of textarea.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 7,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ const empty = findAccessibleChildByID(docAcc, "empty", [nsIAccessibleText]);
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, empty);
+ empty.takeFocus();
+ evt = await caretMoved;
+ is(empty.caretOffset, 0, "Caret offset in empty textarea is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test setting the caret.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab\nc</textarea>
+<div id="editable" contenteditable>
+ <p id="p">a<a id="link" href="https://example.com/">b</a></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("textarea: Set caret offset to 0");
+ let focused = waitForEvent(EVENT_FOCUS, textarea);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(textarea.caretOffset, 0, "textarea caret correct");
+ // Test setting caret to another line.
+ info("textarea: Set caret offset to 3");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 3;
+ await caretMoved;
+ is(textarea.caretOffset, 3, "textarea caret correct");
+ // Test setting caret to the end.
+ info("textarea: Set caret offset to 4 (end)");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 4;
+ await caretMoved;
+ is(textarea.caretOffset, 4, "textarea caret correct");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ focused = waitForEvent(EVENT_FOCUS, editable);
+ editable.takeFocus();
+ await focused;
+ const p = findAccessibleChildByID(docAcc, "p", [nsIAccessibleText]);
+ info("p: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p);
+ p.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(p.caretOffset, 0, "p caret correct");
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ info("link: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, link);
+ link.caretOffset = 0;
+ await caretMoved;
+ is(link.caretOffset, 0, "link caret correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/text/browser_text_paragraph_boundary.js b/accessible/tests/browser/text/browser_text_paragraph_boundary.js
new file mode 100644
index 0000000000..04e64520e8
--- /dev/null
+++ b/accessible/tests/browser/text/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/text/browser_text_selection.js b/accessible/tests/browser/text/browser_text_selection.js
new file mode 100644
index 0000000000..3b47d5f36e
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_selection.js
@@ -0,0 +1,344 @@
+/* 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/text.js */
+
+function waitForSelectionChange(selectionAcc, caretAcc) {
+ if (!caretAcc) {
+ caretAcc = selectionAcc;
+ }
+ return waitForEvents(
+ [
+ [EVENT_TEXT_SELECTION_CHANGED, selectionAcc],
+ // We must swallow the caret events as well to avoid confusion with later,
+ // unrelated caret events.
+ [EVENT_TEXT_CARET_MOVED, caretAcc],
+ ],
+ true
+ );
+}
+
+function changeDomSelection(
+ browser,
+ anchorId,
+ anchorOffset,
+ focusId,
+ focusOffset
+) {
+ return invokeContentTask(
+ browser,
+ [anchorId, anchorOffset, focusId, focusOffset],
+ (
+ contentAnchorId,
+ contentAnchorOffset,
+ contentFocusId,
+ contentFocusOffset
+ ) => {
+ // We want the text node, so we use firstChild.
+ content.window
+ .getSelection()
+ .setBaseAndExtent(
+ content.document.getElementById(contentAnchorId).firstChild,
+ contentAnchorOffset,
+ content.document.getElementById(contentFocusId).firstChild,
+ contentFocusOffset
+ );
+ }
+ );
+}
+
+function testSelectionRange(
+ browser,
+ root,
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+) {
+ let selRange = root.selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
+ testTextRange(
+ selRange,
+ getAccessibleDOMNodeID(root),
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+ );
+}
+
+/**
+ * Test text selection via keyboard.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab</textarea>
+<div id="editable" contenteditable>
+ <p id="p1">a</p>
+ <p id="p2">bc</p>
+ <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ queryInterfaces(docAcc, [nsIAccessibleText]);
+
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in textarea");
+ let selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Selecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 2);
+ testTextGetSelection(textarea, 0, 2, 0);
+
+ info("Unselecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Unselecting a in textarea");
+ // We don't fire selection changed when the selection collapses.
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]);
+ info("Focusing editable, caret to start");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 0);
+ await caretMoved;
+ testSelectionRange(browser, editable, p1, 0, p1, 0);
+ is(editable.selectionCount, 0, "editable selectionCount is 0");
+ is(p1.selectionCount, 0, "p1 selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p1, 1);
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ const p2 = findAccessibleChildByID(docAcc, "p2", [nsIAccessibleText]);
+ if (browser.isRemoteBrowser) {
+ is(p2.selectionCount, 0, "p2 selectionCount is 0");
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+
+ // Selecting across two Accessibles with only a partial selection in the
+ // second.
+ info("Selecting ab in editable");
+ selChanged = waitForSelectionChange(editable, p2);
+ await changeDomSelection(browser, "p1", 0, "p2", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p2, 1);
+ testTextGetSelection(editable, 0, 2, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ testTextGetSelection(p2, 0, 1, 0);
+
+ const pWithLink = findAccessibleChildByID(docAcc, "pWithLink", [
+ nsIAccessibleText,
+ ]);
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ // Selecting both text and a link.
+ info("Selecting de in editable");
+ selChanged = waitForSelectionChange(pWithLink, link);
+ await changeDomSelection(browser, "pWithLink", 0, "link", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, link, 1);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 2, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Selecting a link and text on either side.
+ info("Selecting def in editable");
+ selChanged = waitForSelectionChange(pWithLink, pWithLink);
+ await changeDomSelection(browser, "pWithLink", 0, "textAfterLink", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, pWithLink, 3);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 3, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Noncontiguous selection.
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ info("Adding c to selection in editable");
+ selChanged = waitForSelectionChange(p2);
+ await invokeContentTask(browser, [], () => {
+ const r = content.document.createRange();
+ const p2text = content.document.getElementById("p2").firstChild;
+ r.setStart(p2text, 0);
+ r.setEnd(p2text, 1);
+ content.window.getSelection().addRange(r);
+ });
+ await selChanged;
+ let selRanges = editable.selectionRanges;
+ is(selRanges.length, 2, "2 selection ranges");
+ testTextRange(
+ selRanges.queryElementAt(0, nsIAccessibleTextRange),
+ "range 0",
+ p1,
+ 0,
+ p1,
+ 1
+ );
+ testTextRange(
+ selRanges.queryElementAt(1, nsIAccessibleTextRange),
+ "range 1",
+ p2,
+ 0,
+ p2,
+ 1
+ );
+ is(editable.selectionCount, 2, "editable selectionCount is 2");
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(editable, 1, 2, 1);
+ if (browser.isRemoteBrowser) {
+ is(p1.selectionCount, 1, "p1 selectionCount is 1");
+ testTextGetSelection(p1, 0, 1, 0);
+ is(p2.selectionCount, 1, "p2 selectionCount is 1");
+ testTextGetSelection(p2, 0, 1, 0);
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Tabbing to an input selects all its text. Test that the cached selection
+ *reflects this. This has to be done separately from the other selection tests
+ * because prior contentEditable selection changes the events that get fired.
+ */
+addAccessibleTask(
+ `
+<button id="before">Before</button>
+<input id="input" value="test">
+ `,
+ async function (browser, docAcc) {
+ // The tab order is different when there's an iframe, so focus a control
+ // before the input to make tab consistent.
+ info("Focusing before");
+ const before = findAccessibleChildByID(docAcc, "before");
+ // Focusing a button fires a selection event. We must swallow this to
+ // avoid confusing the later test.
+ let events = waitForOrderedEvents([
+ [EVENT_FOCUS, before],
+ [EVENT_TEXT_SELECTION_CHANGED, docAcc],
+ ]);
+ before.takeFocus();
+ await events;
+
+ const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
+ info("Tabbing to input");
+ events = waitForEvents(
+ {
+ expected: [
+ [EVENT_FOCUS, input],
+ [EVENT_TEXT_SELECTION_CHANGED, input],
+ ],
+ unexpected: [[EVENT_TEXT_SELECTION_CHANGED, docAcc]],
+ },
+ "input",
+ false,
+ (args, task) => invokeContentTask(browser, args, task)
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ await events;
+ testSelectionRange(browser, input, input, 0, input, 4);
+ testTextGetSelection(input, 0, 4, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test text selection via API.
+ */
+addAccessibleTask(
+ `
+ <p id="paragraph">hello world</p>
+ <ol>
+ <li id="li">Number one</li>
+ </ol>
+ `,
+ async function (browser, docAcc) {
+ const paragraph = findAccessibleChildByID(docAcc, "paragraph", [
+ nsIAccessibleText,
+ ]);
+
+ let selChanged = waitForSelectionChange(paragraph);
+ paragraph.setSelectionBounds(0, 2, 4);
+ await selChanged;
+ testTextGetSelection(paragraph, 2, 4, 0);
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.addSelection(6, 10);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 1);
+ is(paragraph.selectionCount, 2, "paragraph selectionCount is 2");
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.removeSelection(0);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 0);
+ is(paragraph.selectionCount, 1, "paragraph selectionCount is 1");
+
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
+
+ selChanged = waitForSelectionChange(li);
+ li.setSelectionBounds(0, 1, 8);
+ await selChanged;
+ testTextGetSelection(li, 3, 8, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_spelling.js b/accessible/tests/browser/text/browser_text_spelling.js
new file mode 100644
index 0000000000..14c5c16be4
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_spelling.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";
+
+/* import-globals-from ../../mochitest/text.js */
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts(
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/*
+ * Given a text accessible and a list of ranges
+ * check if those ranges match the misspelled ranges in the accessible.
+ */
+function misspelledRangesMatch(acc, ranges) {
+ let offset = 0;
+ let expectedRanges = [...ranges];
+ let charCount = acc.characterCount;
+ while (offset < charCount) {
+ let start = {};
+ let end = {};
+ let attributes = acc.getTextAttributes(false, offset, start, end);
+ offset = end.value;
+ try {
+ if (attributes.getStringProperty("invalid") == "spelling") {
+ let expected = expectedRanges.shift();
+ if (
+ !expected ||
+ expected[0] != start.value ||
+ expected[1] != end.value
+ ) {
+ return false;
+ }
+ }
+ } catch (err) {}
+ }
+
+ return !expectedRanges.length;
+}
+
+/*
+ * Returns a promise that resolves after a text attribute changed event
+ * brings us to a state where the misspelled ranges match.
+ */
+async function waitForMisspelledRanges(acc, ranges) {
+ await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED);
+ await untilCacheOk(
+ () => misspelledRangesMatch(acc, ranges),
+ `Misspelled ranges match: ${JSON.stringify(ranges)}`
+ );
+}
+
+/**
+ * Test spelling errors.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea" spellcheck="true">test tset tset test</textarea>
+<div contenteditable id="editable" spellcheck="true">plain<span> ts</span>et <b>bold</b></div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ textarea.takeFocus();
+ await spellingChanged;
+
+ // Test removal of a spelling error.
+ info('textarea: Changing first "tset" to "test"');
+ // setTextRange fires multiple EVENT_TEXT_ATTRIBUTE_CHANGED, so replace by
+ // selecting and typing instead.
+ spellingChanged = waitForMisspelledRanges(textarea, [[10, 14]]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("test");
+ // Move the cursor to trigger spell check.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Test addition of a spelling error.
+ info('textarea: Changing it back to "tset"');
+ spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("tset");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Ensure that changing the text without changing any spelling errors
+ // correctly updates offsets.
+ info('textarea: Changing first "test" to "the"');
+ // Spelling errors don't change, so we won't get
+ // EVENT_TEXT_ATTRIBUTE_CHANGED. We change the text, wait for the insertion
+ // and then select a character so we know when the change is done.
+ let inserted = waitForEvent(EVENT_TEXT_INSERTED, textarea);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(0, 4);
+ });
+ EventUtils.sendString("the");
+ await inserted;
+ let selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selected;
+ const expectedRanges = [
+ [4, 8],
+ [9, 13],
+ ];
+ await untilCacheOk(
+ () => misspelledRangesMatch(textarea, expectedRanges),
+ `Misspelled ranges match: ${JSON.stringify(expectedRanges)}`
+ );
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing editable");
+ spellingChanged = waitForMisspelledRanges(editable, [[6, 10]]);
+ editable.takeFocus();
+ await spellingChanged;
+ // Test normal text and spelling errors crossing text nodes.
+ testTextAttrs(editable, 0, {}, {}, 0, 6, true); // "plain "
+ // Ensure we detect the spelling error even though there is a style change
+ // after it.
+ testTextAttrs(editable, 6, { invalid: "spelling" }, {}, 6, 10, true); // "tset"
+ testTextAttrs(editable, 10, {}, {}, 10, 11, true); // " "
+ // Ensure a style change is still detected in the presence of a spelling
+ // error.
+ testTextAttrs(editable, 11, boldAttrs, {}, 11, 15, true); // "bold"
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_textleafpoint.js b/accessible/tests/browser/text/browser_textleafpoint.js
new file mode 100644
index 0000000000..344a01e2d3
--- /dev/null
+++ b/accessible/tests/browser/text/browser_textleafpoint.js
@@ -0,0 +1,524 @@
+/* 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/text.js */
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+h<a href="#">id i</a>n
+the <strong>big</strong> rug.</p>
+`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ let charSequence = [
+ ...textBoundaryGenerator(firstPoint, BOUNDARY_CHAR, DIRECTION_NEXT),
+ ];
+
+ testPointEqual(
+ firstPoint,
+ charSequence[0],
+ "Point constructed via container and offset 0 is first character point."
+ );
+ testPointEqual(
+ lastPoint,
+ charSequence[charSequence.length - 1],
+ "Point constructed via container and kTextEndOffset is last character point."
+ );
+
+ const expectedCharSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 1],
+ ["A bug\nh", 2],
+ ["A bug\nh", 3],
+ ["A bug\nh", 4],
+ ["A bug\nh", 5],
+ ["A bug\nh", 6],
+ ["id i", 0],
+ ["id i", 1],
+ ["id i", 2],
+ ["id i", 3],
+ ["n\nthe ", 0],
+ ["n\nthe ", 1],
+ ["n\nthe ", 2],
+ ["n\nthe ", 3],
+ ["n\nthe ", 4],
+ ["n\nthe ", 5],
+ ["big", 0],
+ ["big", 1],
+ ["big", 2],
+ [" rug.", 0],
+ [" rug.", 1],
+ [" rug.", 2],
+ [" rug.", 3],
+ [" rug.", 4],
+ [" rug.", 5],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR sequence is correct"
+ );
+
+ const expectedWordStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 2],
+ ["A bug\nh", 6],
+ ["id i", 3],
+ ["n\nthe ", 2],
+ ["big", 0],
+ [" rug.", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ const expectedWordEndSequence = [
+ ["A bug\nh", 1],
+ ["A bug\nh", 5],
+ ["id i", 2],
+ ["n\nthe ", 1],
+ ["n\nthe ", 5],
+ [" rug.", 0],
+ [" rug.", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedWordEndSequence].reverse(),
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ const expectedLineStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 6],
+ ["n\nthe ", 2],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ const expectedLineEndSequence = [
+ ["A bug\nh", 5],
+ ["n\nthe ", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineEndSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_END sequence is correct",
+ { todo: true }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedLineEndSequence].reverse(),
+ "Backward BOUNDARY_LINE_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `<p id="p">
+ Rob ca<input id="i1" value="n m">op up.
+ </p>`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 1],
+ ["Rob ca", 2],
+ ["Rob ca", 3],
+ ["Rob ca", 4],
+ ["Rob ca", 5],
+ ["n m", 0],
+ ["n m", 1],
+ ["n m", 2],
+ ["n m", 3],
+ ],
+ "Forward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [
+ ["op up. ", 7],
+ ["op up. ", 6],
+ ["op up. ", 5],
+ ["op up. ", 4],
+ ["op up. ", 3],
+ ["op up. ", 2],
+ ["op up. ", 1],
+ ["op up. ", 0],
+ ["n m", 2],
+ ["n m", 1],
+ ["n m", 0],
+ ],
+ "Backward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 4],
+ ["n m", 2],
+ ],
+ "Forward BOUNDARY_WORD_START sequence when stopping in editable is correct",
+ {
+ flags: BOUNDARY_FLAG_STOP_IN_EDITABLE,
+ todo: true, // Shouldn't consider end of input a word start
+ }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+on a <span style="display: block;">rug</span></p>
+ <p id="p2">
+ Numbers:
+ </p>
+ <ul>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedParagraphStart = [
+ ["A bug\non a ", 0],
+ ["A bug\non a ", 6],
+ ["rug", 0],
+ ["Numbers: ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+
+ const paragraphStart = createTextLeafPoint(
+ findAccessibleChildByID(docAcc, "p2").firstChild,
+ 0
+ );
+ const wordEnd = paragraphStart.findBoundary(
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ testPointEqual(
+ wordEnd,
+ paragraphStart,
+ "The word end from the previous block is the first point in this block"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+// Test for skipping list item bullets.
+addAccessibleTask(
+ `<ul>
+ <li>One</li>
+ <li>Two</li>
+ <li style="white-space: pre-line;">Three
+Four</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const firstNonMarkerPoint = firstPoint.findBoundary(
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ Assert.deepEqual(
+ readablePoint(firstNonMarkerPoint),
+ ["One", 0],
+ "First non-marker point is correct"
+ );
+
+ const expectedParagraphStart = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_PREVIOUS,
+ [...expectedParagraphStart].reverse(),
+ "Backward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedCharSequence = [
+ ["One", 0],
+ ["One", 1],
+ ["One", 2],
+ ["Two", 0],
+ ["Two", 1],
+ ["Two", 2],
+ ["Three\nFour", 0],
+ ["Three\nFour", 1],
+ ["Three\nFour", 2],
+ ["Three\nFour", 3],
+ ["Three\nFour", 4],
+ ["Three\nFour", 5],
+ ["Three\nFour", 6],
+ ["Three\nFour", 7],
+ ["Three\nFour", 8],
+ ["Three\nFour", 9],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordEndSequence = [
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 5],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [
+ readablePoint(firstNonMarkerPoint),
+ ...expectedWordEndSequence,
+ ].reverse(),
+ "Backward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedLineStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ // Add last point in doc
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test the paragraph boundary on tables.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th>a</th><td>b</td></tr>
+ <tr><td>c</td><td>d</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [["a", 0], ["b", 0], ["c", 0], ["d", 0], readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/*
+ * Test the word boundary with punctuation character
+ */
+addAccessibleTask(
+ `
+<p>ab'cd</p>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedWordStartSequence = [
+ ["ab'cd", 0],
+ ["ab'cd", 3],
+ ["ab'cd", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ expectedWordStartSequence,
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+ const expectedWordEndSequence = [
+ ["ab'cd", 5],
+ ["ab'cd", 3],
+ ["ab'cd", 0],
+ ];
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ expectedWordEndSequence,
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
diff --git a/accessible/tests/browser/text/head.js b/accessible/tests/browser/text/head.js
new file mode 100644
index 0000000000..fa4b095892
--- /dev/null
+++ b/accessible/tests/browser/text/head.js
@@ -0,0 +1,276 @@
+/* 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 createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS,
+ BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN,
+ BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER,
+ readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence,
+ isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText,
+ testCopyText, testPasteText, testCutText, testSetTextContents */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+
+/* import-globals-from ../../mochitest/role.js */
+
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+const DIRECTION_NEXT = Ci.nsIAccessibleTextLeafPoint.DIRECTION_NEXT;
+const DIRECTION_PREVIOUS = Ci.nsIAccessibleTextLeafPoint.DIRECTION_PREVIOUS;
+
+const BOUNDARY_FLAG_DEFAULT =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_DEFAULT;
+const BOUNDARY_FLAG_INCLUDE_ORIGIN =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_INCLUDE_ORIGIN;
+const BOUNDARY_FLAG_STOP_IN_EDITABLE =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_STOP_IN_EDITABLE;
+const BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER;
+
+function createTextLeafPoint(acc, offset) {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ nsIAccessibilityService
+ );
+
+ return accService.createTextLeafPoint(acc, offset);
+}
+
+// Converts an nsIAccessibleTextLeafPoint into a human/machine
+// readable tuple with a readable accessible and the offset within it.
+// For a point text leaf it would look like this: ["hello", 2],
+// For a point in an empty input it would look like this ["input#name", 0]
+function readablePoint(point) {
+ const readableLeaf = acc => {
+ let tagName = getAccessibleTagName(acc);
+ if (tagName && !tagName.startsWith("_moz_generated")) {
+ let domNodeID = getAccessibleDOMNodeID(acc);
+ if (domNodeID) {
+ return `${tagName}#${domNodeID}`;
+ }
+ return tagName;
+ }
+
+ return acc.name;
+ };
+
+ return [readableLeaf(point.accessible), point.offset];
+}
+
+function sequenceEqual(val, expected, msg) {
+ Assert.deepEqual(val, expected, msg);
+}
+
+// eslint-disable-next-line camelcase
+function sequenceEqualTodo(val, expected, msg) {
+ todo_is(JSON.stringify(val), JSON.stringify(expected), msg);
+}
+
+function pointsEqual(pointA, pointB) {
+ return (
+ pointA.offset == pointB.offset && pointA.accessible == pointB.accessible
+ );
+}
+
+function testPointEqual(pointA, pointB, msg) {
+ is(pointA.offset, pointB.offset, `Offset mismatch - ${msg}`);
+ is(pointA.accessible, pointB.accessible, `Accessible mismatch - ${msg}`);
+}
+
+function* textBoundaryGenerator(
+ firstPoint,
+ boundaryType,
+ direction,
+ flags = BOUNDARY_FLAG_DEFAULT
+) {
+ // Our start point should be inclusive of the given point.
+ let nextLeafPoint = firstPoint.findBoundary(
+ boundaryType,
+ direction,
+ flags | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ let textLeafPoint = null;
+
+ do {
+ textLeafPoint = nextLeafPoint;
+ yield textLeafPoint;
+ nextLeafPoint = textLeafPoint.findBoundary(boundaryType, direction, flags);
+ } while (!pointsEqual(textLeafPoint, nextLeafPoint));
+}
+
+// This function takes FindBoundary arguments and an expected sequence
+// of boundary points formatted with readablePoint.
+// For example, word starts would look like this:
+// [["one two", 0], ["one two", 4], ["one two", 7]]
+function testBoundarySequence(
+ startPoint,
+ boundaryType,
+ direction,
+ expectedSequence,
+ msg,
+ options = {}
+) {
+ let sequence = [
+ ...textBoundaryGenerator(
+ startPoint,
+ boundaryType,
+ direction,
+ options.flags ? options.flags : BOUNDARY_FLAG_DEFAULT
+ ),
+ ];
+ (options.todo ? sequenceEqualTodo : sequenceEqual)(
+ sequence.map(readablePoint),
+ expectedSequence,
+ msg
+ );
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Editable text
+
+async function waitForCopy(browser) {
+ await BrowserTestUtils.waitForContentEvent(browser, "copy", false, evt => {
+ return true;
+ });
+
+ let clipboardData = await invokeContentTask(browser, [], async () => {
+ let text = await content.navigator.clipboard.readText();
+ return text;
+ });
+
+ return clipboardData;
+}
+
+async function isFinalValueCorrect(
+ browser,
+ acc,
+ expectedTextLeafs,
+ msg = "Value is correct"
+) {
+ let value =
+ acc.role == ROLE_ENTRY
+ ? acc.value
+ : await invokeContentTask(browser, [], () => {
+ return content.document.body.textContent;
+ });
+
+ let [before, text, after] = expectedTextLeafs;
+ let finalValue =
+ before && after && !text
+ ? [before, after].join(" ")
+ : [before, text, after].join("");
+
+ is(value.replace("\xa0", " "), finalValue, msg);
+}
+
+function waitForTextChangeEvents(acc, eventSeq) {
+ let events = eventSeq.map(eventType => {
+ return [eventType, acc];
+ });
+
+ if (acc.role == ROLE_ENTRY) {
+ events.push([EVENT_TEXT_VALUE_CHANGE, acc]);
+ }
+
+ return waitForEvents(events);
+}
+
+async function testSetTextContents(acc, text, staticContentOffset, events) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, events);
+ acc.setTextContents(text);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset);
+}
+
+async function testInsertText(
+ acc,
+ textToInsert,
+ insertOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.insertText(textToInsert, staticContentOffset + insertOffset);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testDeleteText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.deleteText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}
+
+async function testCopyText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset,
+ browser,
+ aExpectedClipboard = null
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let copied = waitForCopy(browser);
+ acc.copyText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let clipboardText = await copied;
+ if (aExpectedClipboard != null) {
+ is(clipboardText, aExpectedClipboard, "Correct text in clipboard");
+ }
+}
+
+async function testPasteText(acc, insertOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.pasteText(staticContentOffset + insertOffset);
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ // XXX: In non-headless mode pasting text produces several text leaves
+ // and the offset is not what we expect.
+ // is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testCutText(acc, startOffset, endOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.cutText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}
diff --git a/accessible/tests/browser/tree/browser.toml b/accessible/tests/browser/tree/browser.toml
new file mode 100644
index 0000000000..64be6853d1
--- /dev/null
+++ b/accessible/tests/browser/tree/browser.toml
@@ -0,0 +1,31 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_aria_owns.js"]
+skip-if = [
+ "true", #Bug 1445513
+ "verify && !debug && os == 'linux'",
+]
+
+["browser_browser_element.js"]
+
+["browser_css_content_visibility.js"]
+
+["browser_general.js"]
+
+["browser_lazy_tabs.js"]
+
+["browser_link.js"]
+
+["browser_searchbar.js"]
+
+["browser_shadowdom.js"]
+
+["browser_test_nsIAccessibleDocument_URL.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..0ca55ed357
--- /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_browser_element.js b/accessible/tests/browser/tree/browser_browser_element.js
new file mode 100644
index 0000000000..82be24d93c
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_browser_element.js
@@ -0,0 +1,16 @@
+/* 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 });
+
+// Test that the tree is correct for browser elements containing remote
+// documents.
+addAccessibleTask(`test`, async function (browser, docAcc) {
+ // testAccessibleTree also verifies childCount, indexInParent and parent.
+ testAccessibleTree(browser, {
+ INTERNAL_FRAME: [{ DOCUMENT: [{ TEXT_LEAF: [] }] }],
+ });
+});
diff --git a/accessible/tests/browser/tree/browser_css_content_visibility.js b/accessible/tests/browser/tree/browser_css_content_visibility.js
new file mode 100644
index 0000000000..798e409d86
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_css_content_visibility.js
@@ -0,0 +1,121 @@
+/* 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 snippet1 = `
+ <style>
+ #container {
+ width: 150px;
+ height: 150px;
+ background: lightblue;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .auto {
+ content-visibility: auto;
+ }
+ </style>
+ <div id="container">
+ <div class="hidden" id="hidden-target">
+ hidden target
+ <div id="hidden-child">
+ hidden child
+ </div>
+ </div>
+ <div class="auto" id="auto-target">
+ auto target
+ <div id="auto-child">
+ auto child
+ </div>
+ </div>
+ </div>
+ `;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.content-visibility.enabled", true]],
+ });
+});
+
+// Check if the element specified with `content-visibility` property is accessible
+addAccessibleTask(
+ snippet1,
+ async function (browser, accDoc) {
+ const container = findAccessibleChildByID(accDoc, "container");
+ ok(
+ findAccessibleChildByID(container, "hidden-target"),
+ "hidden-target is accessible"
+ );
+ ok(
+ findAccessibleChildByID(container, "auto-target"),
+ "auto-target is accessible"
+ );
+
+ // The test checks if the child element of the element specified with
+ // `content-visibility: hidden` is ignored from a11y tree
+ let target = findAccessibleChildByID(accDoc, "hidden-target");
+ ok(
+ !findAccessibleChildByID(target, "hidden-child"),
+ "Children of hidden-target is not accessible"
+ );
+
+ // The test checks if the child element of the element specified with
+ // `content-visibility: auto` is showen in a11y tree
+ target = findAccessibleChildByID(accDoc, "auto-target");
+ ok(
+ findAccessibleChildByID(target, "auto-child"),
+ "Children of auto-target is accessible"
+ );
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+// Check if the element having `display: contents` and a child of `content-visibility: hidden` element isn't accessible
+const snippet2 = `
+ <style>
+ #target {
+ width: 150px;
+ height: 150px;
+ background-color: lightblue;
+ }
+ #child {
+ width: 100px;
+ height: 100px;
+ background-color: lightgreen;
+ }
+ #grandchild {
+ width: 50px;
+ height: 50px;
+ background-color: red;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .display-contents {
+ display: contents;
+ }
+ </style>
+ <div id="target" class="hidden">
+ <div id="child" class="display-contents">
+ <div id="grandchild"></div>
+ </div>
+ </div>
+ `;
+
+addAccessibleTask(
+ snippet2,
+ async function (browser, accDoc) {
+ const target = findAccessibleChildByID(accDoc, "target");
+ ok(
+ !findAccessibleChildByID(target, "child"),
+ "Element having `display: contents` and a child of `content-visibility: hidden` element isn't accessible"
+ );
+ testAccessibleTree(target, { SECTION: [] });
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
diff --git a/accessible/tests/browser/tree/browser_general.js b/accessible/tests/browser/tree/browser_general.js
new file mode 100644
index 0000000000..0d16271a36
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_general.js
@@ -0,0 +1,128 @@
+/* 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 });
+
+/**
+ * Verify adding `overflow:hidden;` styling to a div causes it to
+ * get an accessible.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:hidden styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:hidden;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
+
+/**
+ * Verify adding `overflow:scroll;` styling to a div causes
+ * it to get an accessible.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:scroll styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:scroll;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
+
+/**
+ * Verify adding `overflow:auto;` styling to a div causes
+ * it to get an accessible, but `overflow:visible` does not.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:visible styling to div");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:visible;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:auto styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:auto;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
diff --git a/accessible/tests/browser/tree/browser_lazy_tabs.js b/accessible/tests/browser/tree/browser_lazy_tabs.js
new file mode 100644
index 0000000000..f7aa9bdeb2
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_lazy_tabs.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that lazy background tabs aren't unintentionally loaded when building
+// the a11y tree (bug 1700708).
+addAccessibleTask(``, async function (browser, accDoc) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.restore_on_demand", true]],
+ });
+
+ info("Opening a new window");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Window is opened with a blank tab.
+ info("Loading second tab");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url: "data:text/html,2",
+ });
+ info("Loading third tab");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url: "data:text/html,3",
+ });
+ info("Closing the window");
+ await BrowserTestUtils.closeWindow(win);
+
+ is(SessionStore.getClosedWindowCount(), 1, "Should have a window to restore");
+ info("Restoring the window");
+ win = SessionStore.undoCloseWindow(0);
+ await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady");
+ await BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+ is(win.gBrowser.tabs.length, 3, "3 tabs restored");
+ ok(win.gBrowser.tabs[2].selected, "Third tab selected");
+ ok(getAccessible(win.gBrowser.tabs[1]), "Second tab has accessible");
+ ok(!win.gBrowser.browsers[1].isConnected, "Second tab is lazy");
+ info("Closing the restored window");
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/accessible/tests/browser/tree/browser_link.js b/accessible/tests/browser/tree/browser_link.js
new file mode 100644
index 0000000000..b0ff992365
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_link.js
@@ -0,0 +1,208 @@
+/* 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 });
+
+/**
+ * Verify that an anchor element reports a generic role without an href
+ * attribute and reports a LINK role with it present. Verify that these roles
+ * change as the attribute appears and disappears.
+ */
+addAccessibleTask(
+ `
+<a id="link">test</a>
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of anchor element without href");
+
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Adding an href to the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").setAttribute("href", "#");
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_LINK, "Checking role of anchor element with href");
+
+ onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Removing the href from the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").removeAttribute("href");
+ });
+ await onHideAndShow;
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of anchor element without href");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an anchor element reports a generic role without a click listener
+ * and reports a LINK role with it present. Verify that these roles change as
+ * the click listener appears.
+ */
+addAccessibleTask(
+ `
+<a id="link">test</a>
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_TEXT,
+ "Checking role of anchor element without click listener"
+ );
+
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Adding a click listener to the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("link")
+ .addEventListener("click", () => {});
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_LINK,
+ "Checking role of anchor element with click listener"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an area element reports a generic role without an href
+ * attribute and reports a LINK role with it present. Verify that these roles
+ * change as the attribute appears and disappears.
+ */
+addAccessibleTask(
+ `
+<map name="map">
+ <area id="link">
+</map>
+<img id="img" usemap="#map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+`,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of area element without href");
+
+ let img = findAccessibleChildByID(accDoc, "img");
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Adding an href to the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").setAttribute("href", "#");
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_LINK, "Checking role of area element with href");
+
+ img = findAccessibleChildByID(accDoc, "img");
+ onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Removing the href from the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").removeAttribute("href");
+ });
+ await onHideAndShow;
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of area element without href");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an area element reports a generic role without a click listener
+ * and reports a LINK role with it present. Verify that these roles change as
+ * the click listener appears.
+ */
+addAccessibleTask(
+ `
+<map name="map">
+ <area id="link">
+</map>
+<img id="img" usemap="#map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_TEXT,
+ "Checking role of area element without click listener"
+ );
+
+ let img = findAccessibleChildByID(accDoc, "img");
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Adding a click listener to the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("link")
+ .addEventListener("click", () => {});
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_LINK,
+ "Checking role of area element with click listener"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/tree/browser_searchbar.js b/accessible/tests/browser/tree/browser_searchbar.js
new file mode 100644
index 0000000000..600c14e143
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_searchbar.js
@@ -0,0 +1,96 @@
+"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]],
+ });
+
+ // This used to rely on the implied 100ms initial timer of
+ // TestUtils.waitForCondition. See bug 1700735.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ 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 = Promise.all([
+ BrowserTestUtils.waitForEvent(popup, "popupshown", false),
+ waitForEvent(EVENT_SHOW, popup),
+ ]);
+ searchbar.textbox.openPopup();
+ await promise;
+
+ let TREE = {
+ role: ROLE_EDITCOMBOBOX,
+
+ children: [
+ // image button toggling the results list
+ {
+ role: ROLE_BUTTONMENU,
+ 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);
+
+ promise = Promise.all([
+ BrowserTestUtils.waitForEvent(popup, "popuphidden", false),
+ waitForEvent(EVENT_HIDE, popup),
+ ]);
+ searchbar.textbox.closePopup();
+ await promise;
+
+ TREE = {
+ role: ROLE_EDITCOMBOBOX,
+
+ children: [
+ // image button toggling the results list
+ {
+ role: ROLE_BUTTONMENU,
+ children: [],
+ },
+
+ // input element
+ {
+ role: ROLE_ENTRY,
+ children: [],
+ },
+
+ // context menu
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [],
+ },
+
+ // the result list should be removed from the tree on popuphidden
+ ],
+ };
+
+ 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..6d9f06f9ff
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_shadowdom.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 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"]);
+});
+
+/**
+ * Ensure that changing the slot on the body while moving the body doesn't
+ * try to remove the DocAccessible. We test this here instead of in
+ * accessible/tests/mochitest/treeupdate/test_shadow_slots.html because this
+ * messes with the body element and we don't want that to impact other tests.
+ */
+addAccessibleTask(
+ `
+<div id="host"></div>
+<script>
+ const host = document.getElementById("host");
+ host.attachShadow({ mode: "open" });
+ const emptyScript = document.createElement("script");
+ emptyScript.id = "emptyScript";
+ document.head.append(emptyScript);
+</script>
+ `,
+ async function (browser, docAcc) {
+ info("Moving body and setting slot on body");
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ const host = content.document.getElementById("host");
+ const emptyScript = content.document.getElementById("emptyScript");
+ const body = content.document.body;
+ emptyScript.append(host);
+ host.append(body);
+ body.slot = "";
+ });
+ await reordered;
+ is(docAcc.childCount, 0, "document has no children after body move");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js b/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js
new file mode 100644
index 0000000000..623f2640f0
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function promiseEventDocumentLoadComplete(expectedURL) {
+ return new Promise(resolve => {
+ waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => {
+ try {
+ if (
+ event.accessible.QueryInterface(nsIAccessibleDocument).URL ==
+ expectedURL
+ ) {
+ resolve(event.accessible.QueryInterface(nsIAccessibleDocument));
+ return true;
+ }
+ return false;
+ } catch (e) {
+ return false;
+ }
+ });
+ });
+}
+
+add_task(async function testInDataURI() {
+ const kURL = "data:text/html,Some text";
+ const waitForDocumentLoadComplete = promiseEventDocumentLoadComplete("");
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ is(
+ (await waitForDocumentLoadComplete).URL,
+ "",
+ "nsIAccessibleDocument.URL shouldn't return data URI"
+ );
+ });
+});
+
+add_task(async function testInHTTPSURIContainingPrivateThings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.auth.confirmAuth.enabled", false]],
+ });
+ const kURL =
+ "https://username:password@example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref";
+ const kURLWithoutUserPass =
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref";
+ const waitForDocumentLoadComplete =
+ promiseEventDocumentLoadComplete(kURLWithoutUserPass);
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ is(
+ (await waitForDocumentLoadComplete).URL,
+ kURLWithoutUserPass,
+ "nsIAccessibleDocument.URL shouldn't contain user/pass section"
+ );
+ });
+});
diff --git a/accessible/tests/browser/tree/head.js b/accessible/tests/browser/tree/head.js
new file mode 100644
index 0000000000..b9c787e9e2
--- /dev/null
+++ b/accessible/tests/browser/tree/head.js
@@ -0,0 +1,33 @@
+/* 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.
+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.`
+ );
+}
diff --git a/accessible/tests/browser/windows/a11y_setup.py b/accessible/tests/browser/windows/a11y_setup.py
new file mode 100644
index 0000000000..d6dc19f0fb
--- /dev/null
+++ b/accessible/tests/browser/windows/a11y_setup.py
@@ -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/.
+
+"""Python environment for Windows a11y browser tests.
+"""
+
+import ctypes
+import os
+from ctypes import POINTER, byref
+from ctypes.wintypes import BOOL, HWND, LPARAM, POINT # noqa: F401
+
+import comtypes.client
+import psutil
+from comtypes import COMError, IServiceProvider
+
+user32 = ctypes.windll.user32
+oleacc = ctypes.oledll.oleacc
+oleaccMod = comtypes.client.GetModule("oleacc.dll")
+IAccessible = oleaccMod.IAccessible
+del oleaccMod
+OBJID_CLIENT = -4
+CHILDID_SELF = 0
+NAVRELATION_EMBEDS = 0x1009
+# This is the path if running locally.
+ia2Tlb = os.path.join(
+ os.getcwd(),
+ "..",
+ "..",
+ "..",
+ "accessible",
+ "interfaces",
+ "ia2",
+ "IA2Typelib.tlb",
+)
+if not os.path.isfile(ia2Tlb):
+ # This is the path if running in CI.
+ ia2Tlb = os.path.join(os.getcwd(), "ia2Typelib.tlb")
+ia2Mod = comtypes.client.GetModule(ia2Tlb)
+del ia2Tlb
+# Shove all the IAccessible* interfaces and IA2_* constants directly
+# into our namespace for convenience.
+globals().update((k, getattr(ia2Mod, k)) for k in ia2Mod.__all__)
+# We use this below. The linter doesn't understand our globals() update hack.
+IAccessible2 = ia2Mod.IAccessible2
+del ia2Mod
+
+uiaMod = comtypes.client.GetModule("UIAutomationCore.dll")
+globals().update((k, getattr(uiaMod, k)) for k in uiaMod.__all__)
+uiaClient = comtypes.CoCreateInstance(
+ uiaMod.CUIAutomation._reg_clsid_,
+ interface=uiaMod.IUIAutomation,
+ clsctx=comtypes.CLSCTX_INPROC_SERVER,
+)
+TreeScope_Descendants = uiaMod.TreeScope_Descendants
+UIA_AutomationIdPropertyId = uiaMod.UIA_AutomationIdPropertyId
+del uiaMod
+
+
+def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT):
+ p = POINTER(IAccessible)()
+ oleacc.AccessibleObjectFromWindow(
+ hwnd, objectID, byref(IAccessible._iid_), byref(p)
+ )
+ return p
+
+
+def getFirefoxHwnd():
+ """Search all top level windows for the Firefox instance being
+ tested.
+ We search by window class name and window title prefix.
+ """
+ # We can compare the grandparent process ids to find the Firefox started by
+ # the test harness.
+ commonPid = psutil.Process().parent().ppid()
+ # We need something mutable to store the result from the callback.
+ found = []
+
+ @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
+ def callback(hwnd, lParam):
+ name = ctypes.create_unicode_buffer(100)
+ user32.GetClassNameW(hwnd, name, 100)
+ if name.value != "MozillaWindowClass":
+ return True
+ pid = ctypes.wintypes.DWORD()
+ user32.GetWindowThreadProcessId(hwnd, byref(pid))
+ if psutil.Process(pid.value).parent().ppid() != commonPid:
+ return True # Not the Firefox being tested.
+ found.append(hwnd)
+ return False
+
+ user32.EnumWindows(callback, LPARAM(0))
+ if not found:
+ raise LookupError("Couldn't find Firefox HWND")
+ return found[0]
+
+
+def toIa2(obj):
+ serv = obj.QueryInterface(IServiceProvider)
+ return serv.QueryService(IAccessible2._iid_, IAccessible2)
+
+
+def getDocIa2():
+ """Get the IAccessible2 for the document being tested."""
+ hwnd = getFirefoxHwnd()
+ root = AccessibleObjectFromWindow(hwnd)
+ doc = root.accNavigate(NAVRELATION_EMBEDS, 0)
+ try:
+ child = toIa2(doc.accChild(1))
+ if "id:default-iframe-id;" in child.attributes:
+ # This is an iframe or remoteIframe test.
+ doc = child.accChild(1)
+ except COMError:
+ pass # No child.
+ return toIa2(doc)
+
+
+def findIa2ByDomId(root, id):
+ search = f"id:{id};"
+ # Child ids begin at 1.
+ for i in range(1, root.accChildCount + 1):
+ child = toIa2(root.accChild(i))
+ if search in child.attributes:
+ return child
+ descendant = findIa2ByDomId(child, id)
+ if descendant:
+ return descendant
+
+
+def getDocUia():
+ """Get the IUIAutomationElement for the document being tested."""
+ # We start with IAccessible2 because there's no efficient way to
+ # find the document we want with UIA.
+ ia2 = getDocIa2()
+ return uiaClient.ElementFromIAccessible(ia2, CHILDID_SELF)
+
+
+def findUiaByDomId(root, id):
+ cond = uiaClient.CreatePropertyCondition(UIA_AutomationIdPropertyId, id)
+ # FindFirst ignores elements in the raw tree, so we have to use
+ # FindFirstBuildCache to override that, even though we don't want to cache
+ # anything.
+ request = uiaClient.CreateCacheRequest()
+ request.TreeFilter = uiaClient.RawViewCondition
+ return root.FindFirstBuildCache(TreeScope_Descendants, cond, request)
diff --git a/accessible/tests/browser/windows/a11y_setup_requirements.txt b/accessible/tests/browser/windows/a11y_setup_requirements.txt
new file mode 100644
index 0000000000..ea131a6f92
--- /dev/null
+++ b/accessible/tests/browser/windows/a11y_setup_requirements.txt
@@ -0,0 +1 @@
+comtypes==1.2.0
diff --git a/accessible/tests/browser/windows/ia2/browser.toml b/accessible/tests/browser/windows/ia2/browser.toml
new file mode 100644
index 0000000000..d72b5f8a2d
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/browser.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'win'",
+ "headless",
+]
+support-files = ["head.js"]
+
+["browser_role.js"]
diff --git a/accessible/tests/browser/windows/ia2/browser_role.js b/accessible/tests/browser/windows/ia2/browser_role.js
new file mode 100644
index 0000000000..08e44c280f
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/browser_role.js
@@ -0,0 +1,39 @@
+/* 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 ROLE_SYSTEM_DOCUMENT = 15;
+const ROLE_SYSTEM_GROUPING = 20;
+const IA2_ROLE_PARAGRAPH = 1054;
+
+addAccessibleTask(
+ `
+<p id="p">p</p>
+ `,
+ async function (browser, docAcc) {
+ let role = await runPython(`
+ global doc
+ doc = getDocIa2()
+ return doc.accRole(CHILDID_SELF)
+ `);
+ is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct MSAA role");
+ role = await runPython(`doc.role()`);
+ is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct IA2 role");
+ ok(
+ await runPython(`
+ global p
+ p = findIa2ByDomId(doc, "p")
+ firstChild = toIa2(doc.accChild(1))
+ return p == firstChild
+ `),
+ "doc's first child is p"
+ );
+ role = await runPython(`p.accRole(CHILDID_SELF)`);
+ is(role, ROLE_SYSTEM_GROUPING, "p has correct MSAA role");
+ role = await runPython(`p.role()`);
+ is(role, IA2_ROLE_PARAGRAPH, "p has correct IA2 role");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/windows/ia2/head.js b/accessible/tests/browser/windows/ia2/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml
new file mode 100644
index 0000000000..f7974d69c7
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'win'",
+ "headless",
+]
+support-files = ["head.js"]
+
+["browser_controlType.js"]
+
+["browser_elementFromPoint.js"]
diff --git a/accessible/tests/browser/windows/uia/browser_controlType.js b/accessible/tests/browser/windows/uia/browser_controlType.js
new file mode 100644
index 0000000000..16db892581
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser_controlType.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";
+
+/* eslint-disable camelcase */
+const UIA_ButtonControlTypeId = 50000;
+const UIA_DocumentControlTypeId = 50030;
+/* eslint-enable camelcase */
+
+addAccessibleTask(
+ `
+<button id="button">button</button>
+ `,
+ async function (browser, docAcc) {
+ let controlType = await runPython(`
+ global doc
+ doc = getDocUia()
+ return doc.CurrentControlType
+ `);
+ is(controlType, UIA_DocumentControlTypeId, "doc has correct control type");
+ controlType = await runPython(`
+ button = findUiaByDomId(doc, "button")
+ return button.CurrentControlType
+ `);
+ is(controlType, UIA_ButtonControlTypeId, "button has correct control type");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/windows/uia/browser_elementFromPoint.js b/accessible/tests/browser/windows/uia/browser_elementFromPoint.js
new file mode 100644
index 0000000000..e2fda4ab30
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser_elementFromPoint.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";
+
+addAccessibleTask(
+ `
+<button id="button">button</p>
+<a id="a" href="#">a</a>
+ `,
+ async function (browser, docAcc) {
+ ok(
+ await runPython(`
+ global doc
+ doc = getDocUia()
+ button = findUiaByDomId(doc, "button")
+ rect = button.CurrentBoundingRectangle
+ found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
+ return uiaClient.CompareElements(button, found)
+ `),
+ "ElementFromPoint on button returns button"
+ );
+ ok(
+ await runPython(`
+ a = findUiaByDomId(doc, "a")
+ rect = a.CurrentBoundingRectangle
+ found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
+ return uiaClient.CompareElements(a, found)
+ `),
+ "ElementFromPoint on a returns a"
+ );
+ }
+);
diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/head.js
@@ -0,0 +1,18 @@
+/* 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.
+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 }
+);