From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- accessible/tests/browser/.eslintrc.js | 28 + accessible/tests/browser/Common.sys.mjs | 451 ++++ accessible/tests/browser/Layout.sys.mjs | 178 ++ accessible/tests/browser/bounds/browser.ini | 27 + .../browser/bounds/browser_accessible_moved.js | 49 + .../tests/browser/bounds/browser_caret_rect.js | 140 ++ .../tests/browser/bounds/browser_position.js | 103 + .../bounds/browser_test_display_contents.js | 44 + .../bounds/browser_test_iframe_transform.js | 209 ++ .../browser/bounds/browser_test_resolution.js | 72 + .../bounds/browser_test_simple_transform.js | 225 ++ .../tests/browser/bounds/browser_test_zoom.js | 65 + .../tests/browser/bounds/browser_test_zoom_text.js | 86 + .../tests/browser/bounds/browser_zero_area.js | 118 + accessible/tests/browser/bounds/head.js | 19 + accessible/tests/browser/browser.ini | 39 + .../browser/browser_shutdown_acc_reference.js | 64 + .../browser/browser_shutdown_doc_acc_reference.js | 56 + .../browser_shutdown_multi_acc_reference_doc.js | 76 + .../browser_shutdown_multi_acc_reference_obj.js | 76 + ...owser_shutdown_multi_proxy_acc_reference_doc.js | 90 + ...owser_shutdown_multi_proxy_acc_reference_obj.js | 90 + .../browser/browser_shutdown_multi_reference.js | 58 + .../browser_shutdown_parent_own_reference.js | 102 + accessible/tests/browser/browser_shutdown_pref.js | 72 + .../browser_shutdown_proxy_acc_reference.js | 76 + .../browser_shutdown_proxy_doc_acc_reference.js | 78 + .../browser_shutdown_remote_no_reference.js | 141 ++ .../tests/browser/browser_shutdown_remote_only.js | 59 + .../browser_shutdown_remote_own_reference.js | 181 ++ .../browser/browser_shutdown_scope_lifecycle.js | 28 + .../browser/browser_shutdown_start_restart.js | 51 + accessible/tests/browser/e10s/browser.ini | 80 + .../tests/browser/e10s/browser_caching_actions.js | 266 +++ .../browser/e10s/browser_caching_attributes.js | 717 ++++++ .../browser/e10s/browser_caching_description.js | 280 +++ .../browser/e10s/browser_caching_document_props.js | 80 + .../browser/e10s/browser_caching_domnodeid.js | 32 + .../browser/e10s/browser_caching_hyperlink.js | 228 ++ .../browser/e10s/browser_caching_innerHTML.js | 48 + .../browser/e10s/browser_caching_interfaces.js | 59 + .../browser/e10s/browser_caching_large_update.js | 66 + .../tests/browser/e10s/browser_caching_name.js | 539 +++++ .../tests/browser/e10s/browser_caching_position.js | 194 ++ .../browser/e10s/browser_caching_relations.js | 291 +++ .../browser/e10s/browser_caching_relations_002.js | 293 +++ .../tests/browser/e10s/browser_caching_states.js | 484 ++++ .../tests/browser/e10s/browser_caching_table.js | 532 +++++ .../browser/e10s/browser_caching_text_bounds.js | 696 ++++++ .../tests/browser/e10s/browser_caching_uniqueid.js | 30 + .../tests/browser/e10s/browser_caching_value.js | 415 ++++ .../browser/e10s/browser_events_announcement.js | 30 + .../tests/browser/e10s/browser_events_caretmove.js | 22 + .../tests/browser/e10s/browser_events_hide.js | 44 + .../tests/browser/e10s/browser_events_show.js | 22 + .../browser/e10s/browser_events_statechange.js | 71 + .../browser/e10s/browser_events_textchange.js | 119 + .../tests/browser/e10s/browser_events_vcchange.js | 87 + accessible/tests/browser/e10s/browser_language.js | 29 + accessible/tests/browser/e10s/browser_obj_group.js | 430 ++++ .../tests/browser/e10s/browser_obj_group_002.js | 390 ++++ .../browser/e10s/browser_treeupdate_ariadialog.js | 45 + .../browser/e10s/browser_treeupdate_ariaowns.js | 325 +++ .../browser/e10s/browser_treeupdate_canvas.js | 28 + .../browser_treeupdate_csscontentvisibility.js | 72 + .../browser/e10s/browser_treeupdate_cssoverflow.js | 60 + .../tests/browser/e10s/browser_treeupdate_doc.js | 320 +++ .../browser/e10s/browser_treeupdate_gencontent.js | 94 + .../browser/e10s/browser_treeupdate_hidden.js | 32 + .../tests/browser/e10s/browser_treeupdate_image.js | 192 ++ .../browser/e10s/browser_treeupdate_imagemap.js | 190 ++ .../tests/browser/e10s/browser_treeupdate_list.js | 52 + .../e10s/browser_treeupdate_list_editabledoc.js | 48 + .../browser/e10s/browser_treeupdate_listener.js | 38 + .../tests/browser/e10s/browser_treeupdate_move.js | 84 + .../browser/e10s/browser_treeupdate_optgroup.js | 100 + .../browser/e10s/browser_treeupdate_removal.js | 58 + .../e10s/browser_treeupdate_select_dropdown.js | 73 + .../tests/browser/e10s/browser_treeupdate_table.js | 48 + .../browser/e10s/browser_treeupdate_textleaf.js | 38 + .../browser/e10s/browser_treeupdate_visibility.js | 342 +++ .../browser/e10s/browser_treeupdate_whitespace.js | 69 + .../browser/e10s/doc_treeupdate_ariadialog.html | 23 + .../browser/e10s/doc_treeupdate_ariaowns.html | 44 + .../browser/e10s/doc_treeupdate_imagemap.html | 21 + .../browser/e10s/doc_treeupdate_removal.xhtml | 11 + .../browser/e10s/doc_treeupdate_visibility.html | 78 + .../browser/e10s/doc_treeupdate_whitespace.html | 10 + accessible/tests/browser/e10s/fonts/Ahem.sjs | 241 ++ accessible/tests/browser/e10s/head.js | 192 ++ accessible/tests/browser/events/browser.ini | 24 + .../events/browser_test_A11yUtils_announce.js | 57 + .../events/browser_test_caret_move_granularity.js | 102 + .../tests/browser/events/browser_test_docload.js | 122 + .../browser/events/browser_test_focus_browserui.js | 57 + .../browser/events/browser_test_focus_dialog.js | 76 + .../browser/events/browser_test_focus_urlbar.js | 438 ++++ .../tests/browser/events/browser_test_panel.js | 58 + .../tests/browser/events/browser_test_scrolling.js | 113 + .../events/browser_test_selection_urlbar.js | 61 + .../tests/browser/events/browser_test_textcaret.js | 58 + accessible/tests/browser/events/head.js | 18 + accessible/tests/browser/fission/browser.ini | 20 + .../tests/browser/fission/browser_content_tree.js | 75 + .../tests/browser/fission/browser_hidden_iframe.js | 70 + .../tests/browser/fission/browser_nested_iframe.js | 164 ++ .../tests/browser/fission/browser_reframe_root.js | 95 + .../browser/fission/browser_reframe_visibility.js | 116 + .../tests/browser/fission/browser_src_change.js | 62 + .../tests/browser/fission/browser_take_focus.js | 73 + accessible/tests/browser/fission/head.js | 18 + accessible/tests/browser/general/browser.ini | 12 + .../browser/general/browser_test_doc_creation.js | 55 + .../tests/browser/general/browser_test_urlbar.js | 40 + accessible/tests/browser/general/head.js | 71 + accessible/tests/browser/head.js | 146 ++ accessible/tests/browser/hittest/browser.ini | 18 + .../tests/browser/hittest/browser_test_browser.js | 68 + .../tests/browser/hittest/browser_test_general.js | 339 +++ .../browser/hittest/browser_test_scroll_hittest.js | 105 + .../browser/hittest/browser_test_shadowroot.js | 61 + .../tests/browser/hittest/browser_test_text.js | 84 + .../tests/browser/hittest/browser_test_zoom.js | 38 + .../browser/hittest/browser_test_zoom_text.js | 64 + accessible/tests/browser/hittest/head.js | 113 + accessible/tests/browser/mac/browser.ini | 58 + accessible/tests/browser/mac/browser_app.js | 351 +++ accessible/tests/browser/mac/browser_aria_busy.js | 44 + .../browser/mac/browser_aria_controls_flowto.js | 92 + .../tests/browser/mac/browser_aria_current.js | 58 + .../tests/browser/mac/browser_aria_expanded.js | 45 + .../tests/browser/mac/browser_aria_haspopup.js | 320 +++ .../tests/browser/mac/browser_attributed_text.js | 144 ++ accessible/tests/browser/mac/browser_bounds.js | 77 + .../tests/browser/mac/browser_details_summary.js | 69 + accessible/tests/browser/mac/browser_focus.js | 44 + accessible/tests/browser/mac/browser_heading.js | 77 + accessible/tests/browser/mac/browser_hierarchy.js | 75 + accessible/tests/browser/mac/browser_input.js | 225 ++ .../tests/browser/mac/browser_label_title.js | 111 + accessible/tests/browser/mac/browser_link.js | 231 ++ .../tests/browser/mac/browser_live_regions.js | 165 ++ accessible/tests/browser/mac/browser_mathml.js | 151 ++ accessible/tests/browser/mac/browser_menulist.js | 103 + accessible/tests/browser/mac/browser_navigate.js | 394 ++++ accessible/tests/browser/mac/browser_outline.js | 566 +++++ .../tests/browser/mac/browser_outline_xul.js | 274 +++ .../tests/browser/mac/browser_popupbutton.js | 166 ++ .../tests/browser/mac/browser_radio_position.js | 321 +++ accessible/tests/browser/mac/browser_range.js | 190 ++ accessible/tests/browser/mac/browser_required.js | 175 ++ .../tests/browser/mac/browser_rich_listbox.js | 73 + .../tests/browser/mac/browser_roles_elements.js | 334 +++ accessible/tests/browser/mac/browser_rootgroup.js | 246 ++ accessible/tests/browser/mac/browser_rotor.js | 1752 ++++++++++++++ .../tests/browser/mac/browser_selectables.js | 342 +++ accessible/tests/browser/mac/browser_table.js | 629 +++++ .../tests/browser/mac/browser_text_basics.js | 380 +++ accessible/tests/browser/mac/browser_text_input.js | 657 ++++++ accessible/tests/browser/mac/browser_text_leaf.js | 83 + .../tests/browser/mac/browser_text_selection.js | 187 ++ .../browser/mac/browser_toggle_radio_check.js | 304 +++ accessible/tests/browser/mac/browser_webarea.js | 77 + accessible/tests/browser/mac/doc_aria_tabs.html | 95 + accessible/tests/browser/mac/doc_menulist.xhtml | 19 + .../tests/browser/mac/doc_rich_listbox.xhtml | 22 + .../tests/browser/mac/doc_textmarker_test.html | 2424 ++++++++++++++++++++ accessible/tests/browser/mac/doc_tree.xhtml | 59 + accessible/tests/browser/mac/head.js | 133 ++ accessible/tests/browser/role/browser.ini | 11 + .../tests/browser/role/browser_computedARIARole.js | 88 + accessible/tests/browser/role/head.js | 18 + accessible/tests/browser/scroll/browser.ini | 15 + .../tests/browser/scroll/browser_test_scrollTo.js | 36 + .../browser/scroll/browser_test_scroll_bounds.js | 606 +++++ .../scroll/browser_test_scroll_substring.js | 67 + .../tests/browser/scroll/browser_test_zoom_text.js | 145 ++ accessible/tests/browser/scroll/head.js | 18 + accessible/tests/browser/selectable/browser.ini | 12 + .../browser/selectable/browser_test_aria_select.js | 164 ++ .../browser/selectable/browser_test_select.js | 329 +++ accessible/tests/browser/selectable/head.js | 88 + accessible/tests/browser/shared-head.js | 918 ++++++++ accessible/tests/browser/states/browser.ini | 19 + .../tests/browser/states/browser_test_link.js | 44 + .../states/browser_test_select_visibility.js | 76 + .../browser/states/browser_test_visibility.js | 181 ++ .../browser/states/browser_test_visibility_2.js | 131 ++ accessible/tests/browser/states/head.js | 91 + accessible/tests/browser/telemetry/browser.ini | 8 + .../browser/telemetry/browser_HCM_telemetry.js | 365 +++ accessible/tests/browser/text/browser.ini | 18 + .../tests/browser/text/browser_editabletext.js | 173 ++ accessible/tests/browser/text/browser_text.js | 326 +++ .../tests/browser/text/browser_text_caret.js | 452 ++++ .../text/browser_text_paragraph_boundary.js | 22 + .../tests/browser/text/browser_text_selection.js | 344 +++ .../tests/browser/text/browser_text_spelling.js | 151 ++ .../tests/browser/text/browser_textleafpoint.js | 485 ++++ accessible/tests/browser/text/head.js | 276 +++ accessible/tests/browser/tree/browser.ini | 19 + accessible/tests/browser/tree/browser_aria_owns.js | 278 +++ .../tests/browser/tree/browser_browser_element.js | 16 + .../browser/tree/browser_css_content_visibility.js | 121 + accessible/tests/browser/tree/browser_general.js | 128 ++ accessible/tests/browser/tree/browser_lazy_tabs.js | 43 + accessible/tests/browser/tree/browser_searchbar.js | 84 + accessible/tests/browser/tree/browser_shadowdom.js | 98 + .../tree/browser_test_nsIAccessibleDocument_URL.js | 54 + accessible/tests/browser/tree/head.js | 33 + 210 files changed, 34637 insertions(+) create mode 100644 accessible/tests/browser/.eslintrc.js create mode 100644 accessible/tests/browser/Common.sys.mjs create mode 100644 accessible/tests/browser/Layout.sys.mjs create mode 100644 accessible/tests/browser/bounds/browser.ini create mode 100644 accessible/tests/browser/bounds/browser_accessible_moved.js create mode 100644 accessible/tests/browser/bounds/browser_caret_rect.js create mode 100644 accessible/tests/browser/bounds/browser_position.js create mode 100644 accessible/tests/browser/bounds/browser_test_display_contents.js create mode 100644 accessible/tests/browser/bounds/browser_test_iframe_transform.js create mode 100644 accessible/tests/browser/bounds/browser_test_resolution.js create mode 100644 accessible/tests/browser/bounds/browser_test_simple_transform.js create mode 100644 accessible/tests/browser/bounds/browser_test_zoom.js create mode 100644 accessible/tests/browser/bounds/browser_test_zoom_text.js create mode 100644 accessible/tests/browser/bounds/browser_zero_area.js create mode 100644 accessible/tests/browser/bounds/head.js create mode 100644 accessible/tests/browser/browser.ini create mode 100644 accessible/tests/browser/browser_shutdown_acc_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_doc_acc_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js create mode 100644 accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js create mode 100644 accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js create mode 100644 accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js create mode 100644 accessible/tests/browser/browser_shutdown_multi_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_parent_own_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_pref.js create mode 100644 accessible/tests/browser/browser_shutdown_proxy_acc_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_remote_no_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_remote_only.js create mode 100644 accessible/tests/browser/browser_shutdown_remote_own_reference.js create mode 100644 accessible/tests/browser/browser_shutdown_scope_lifecycle.js create mode 100644 accessible/tests/browser/browser_shutdown_start_restart.js create mode 100644 accessible/tests/browser/e10s/browser.ini create mode 100644 accessible/tests/browser/e10s/browser_caching_actions.js create mode 100644 accessible/tests/browser/e10s/browser_caching_attributes.js create mode 100644 accessible/tests/browser/e10s/browser_caching_description.js create mode 100644 accessible/tests/browser/e10s/browser_caching_document_props.js create mode 100644 accessible/tests/browser/e10s/browser_caching_domnodeid.js create mode 100644 accessible/tests/browser/e10s/browser_caching_hyperlink.js create mode 100644 accessible/tests/browser/e10s/browser_caching_innerHTML.js create mode 100644 accessible/tests/browser/e10s/browser_caching_interfaces.js create mode 100644 accessible/tests/browser/e10s/browser_caching_large_update.js create mode 100644 accessible/tests/browser/e10s/browser_caching_name.js create mode 100644 accessible/tests/browser/e10s/browser_caching_position.js create mode 100644 accessible/tests/browser/e10s/browser_caching_relations.js create mode 100644 accessible/tests/browser/e10s/browser_caching_relations_002.js create mode 100644 accessible/tests/browser/e10s/browser_caching_states.js create mode 100644 accessible/tests/browser/e10s/browser_caching_table.js create mode 100644 accessible/tests/browser/e10s/browser_caching_text_bounds.js create mode 100644 accessible/tests/browser/e10s/browser_caching_uniqueid.js create mode 100644 accessible/tests/browser/e10s/browser_caching_value.js create mode 100644 accessible/tests/browser/e10s/browser_events_announcement.js create mode 100644 accessible/tests/browser/e10s/browser_events_caretmove.js create mode 100644 accessible/tests/browser/e10s/browser_events_hide.js create mode 100644 accessible/tests/browser/e10s/browser_events_show.js create mode 100644 accessible/tests/browser/e10s/browser_events_statechange.js create mode 100644 accessible/tests/browser/e10s/browser_events_textchange.js create mode 100644 accessible/tests/browser/e10s/browser_events_vcchange.js create mode 100644 accessible/tests/browser/e10s/browser_language.js create mode 100644 accessible/tests/browser/e10s/browser_obj_group.js create mode 100644 accessible/tests/browser/e10s/browser_obj_group_002.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_canvas.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_doc.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_gencontent.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_hidden.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_image.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_imagemap.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_list.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_listener.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_move.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_optgroup.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_removal.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_table.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_textleaf.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_visibility.js create mode 100644 accessible/tests/browser/e10s/browser_treeupdate_whitespace.js create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_imagemap.html create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_visibility.html create mode 100644 accessible/tests/browser/e10s/doc_treeupdate_whitespace.html create mode 100644 accessible/tests/browser/e10s/fonts/Ahem.sjs create mode 100644 accessible/tests/browser/e10s/head.js create mode 100644 accessible/tests/browser/events/browser.ini create mode 100644 accessible/tests/browser/events/browser_test_A11yUtils_announce.js create mode 100644 accessible/tests/browser/events/browser_test_caret_move_granularity.js create mode 100644 accessible/tests/browser/events/browser_test_docload.js create mode 100644 accessible/tests/browser/events/browser_test_focus_browserui.js create mode 100644 accessible/tests/browser/events/browser_test_focus_dialog.js create mode 100644 accessible/tests/browser/events/browser_test_focus_urlbar.js create mode 100644 accessible/tests/browser/events/browser_test_panel.js create mode 100644 accessible/tests/browser/events/browser_test_scrolling.js create mode 100644 accessible/tests/browser/events/browser_test_selection_urlbar.js create mode 100644 accessible/tests/browser/events/browser_test_textcaret.js create mode 100644 accessible/tests/browser/events/head.js create mode 100644 accessible/tests/browser/fission/browser.ini create mode 100644 accessible/tests/browser/fission/browser_content_tree.js create mode 100644 accessible/tests/browser/fission/browser_hidden_iframe.js create mode 100644 accessible/tests/browser/fission/browser_nested_iframe.js create mode 100644 accessible/tests/browser/fission/browser_reframe_root.js create mode 100644 accessible/tests/browser/fission/browser_reframe_visibility.js create mode 100644 accessible/tests/browser/fission/browser_src_change.js create mode 100644 accessible/tests/browser/fission/browser_take_focus.js create mode 100644 accessible/tests/browser/fission/head.js create mode 100644 accessible/tests/browser/general/browser.ini create mode 100644 accessible/tests/browser/general/browser_test_doc_creation.js create mode 100644 accessible/tests/browser/general/browser_test_urlbar.js create mode 100644 accessible/tests/browser/general/head.js create mode 100644 accessible/tests/browser/head.js create mode 100644 accessible/tests/browser/hittest/browser.ini create mode 100644 accessible/tests/browser/hittest/browser_test_browser.js create mode 100644 accessible/tests/browser/hittest/browser_test_general.js create mode 100644 accessible/tests/browser/hittest/browser_test_scroll_hittest.js create mode 100644 accessible/tests/browser/hittest/browser_test_shadowroot.js create mode 100644 accessible/tests/browser/hittest/browser_test_text.js create mode 100644 accessible/tests/browser/hittest/browser_test_zoom.js create mode 100644 accessible/tests/browser/hittest/browser_test_zoom_text.js create mode 100644 accessible/tests/browser/hittest/head.js create mode 100644 accessible/tests/browser/mac/browser.ini create mode 100644 accessible/tests/browser/mac/browser_app.js create mode 100644 accessible/tests/browser/mac/browser_aria_busy.js create mode 100644 accessible/tests/browser/mac/browser_aria_controls_flowto.js create mode 100644 accessible/tests/browser/mac/browser_aria_current.js create mode 100644 accessible/tests/browser/mac/browser_aria_expanded.js create mode 100644 accessible/tests/browser/mac/browser_aria_haspopup.js create mode 100644 accessible/tests/browser/mac/browser_attributed_text.js create mode 100644 accessible/tests/browser/mac/browser_bounds.js create mode 100644 accessible/tests/browser/mac/browser_details_summary.js create mode 100644 accessible/tests/browser/mac/browser_focus.js create mode 100644 accessible/tests/browser/mac/browser_heading.js create mode 100644 accessible/tests/browser/mac/browser_hierarchy.js create mode 100644 accessible/tests/browser/mac/browser_input.js create mode 100644 accessible/tests/browser/mac/browser_label_title.js create mode 100644 accessible/tests/browser/mac/browser_link.js create mode 100644 accessible/tests/browser/mac/browser_live_regions.js create mode 100644 accessible/tests/browser/mac/browser_mathml.js create mode 100644 accessible/tests/browser/mac/browser_menulist.js create mode 100644 accessible/tests/browser/mac/browser_navigate.js create mode 100644 accessible/tests/browser/mac/browser_outline.js create mode 100644 accessible/tests/browser/mac/browser_outline_xul.js create mode 100644 accessible/tests/browser/mac/browser_popupbutton.js create mode 100644 accessible/tests/browser/mac/browser_radio_position.js create mode 100644 accessible/tests/browser/mac/browser_range.js create mode 100644 accessible/tests/browser/mac/browser_required.js create mode 100644 accessible/tests/browser/mac/browser_rich_listbox.js create mode 100644 accessible/tests/browser/mac/browser_roles_elements.js create mode 100644 accessible/tests/browser/mac/browser_rootgroup.js create mode 100644 accessible/tests/browser/mac/browser_rotor.js create mode 100644 accessible/tests/browser/mac/browser_selectables.js create mode 100644 accessible/tests/browser/mac/browser_table.js create mode 100644 accessible/tests/browser/mac/browser_text_basics.js create mode 100644 accessible/tests/browser/mac/browser_text_input.js create mode 100644 accessible/tests/browser/mac/browser_text_leaf.js create mode 100644 accessible/tests/browser/mac/browser_text_selection.js create mode 100644 accessible/tests/browser/mac/browser_toggle_radio_check.js create mode 100644 accessible/tests/browser/mac/browser_webarea.js create mode 100644 accessible/tests/browser/mac/doc_aria_tabs.html create mode 100644 accessible/tests/browser/mac/doc_menulist.xhtml create mode 100644 accessible/tests/browser/mac/doc_rich_listbox.xhtml create mode 100644 accessible/tests/browser/mac/doc_textmarker_test.html create mode 100644 accessible/tests/browser/mac/doc_tree.xhtml create mode 100644 accessible/tests/browser/mac/head.js create mode 100644 accessible/tests/browser/role/browser.ini create mode 100644 accessible/tests/browser/role/browser_computedARIARole.js create mode 100644 accessible/tests/browser/role/head.js create mode 100644 accessible/tests/browser/scroll/browser.ini create mode 100644 accessible/tests/browser/scroll/browser_test_scrollTo.js create mode 100644 accessible/tests/browser/scroll/browser_test_scroll_bounds.js create mode 100644 accessible/tests/browser/scroll/browser_test_scroll_substring.js create mode 100644 accessible/tests/browser/scroll/browser_test_zoom_text.js create mode 100644 accessible/tests/browser/scroll/head.js create mode 100644 accessible/tests/browser/selectable/browser.ini create mode 100644 accessible/tests/browser/selectable/browser_test_aria_select.js create mode 100644 accessible/tests/browser/selectable/browser_test_select.js create mode 100644 accessible/tests/browser/selectable/head.js create mode 100644 accessible/tests/browser/shared-head.js create mode 100644 accessible/tests/browser/states/browser.ini create mode 100644 accessible/tests/browser/states/browser_test_link.js create mode 100644 accessible/tests/browser/states/browser_test_select_visibility.js create mode 100644 accessible/tests/browser/states/browser_test_visibility.js create mode 100644 accessible/tests/browser/states/browser_test_visibility_2.js create mode 100644 accessible/tests/browser/states/head.js create mode 100644 accessible/tests/browser/telemetry/browser.ini create mode 100644 accessible/tests/browser/telemetry/browser_HCM_telemetry.js create mode 100644 accessible/tests/browser/text/browser.ini create mode 100644 accessible/tests/browser/text/browser_editabletext.js create mode 100644 accessible/tests/browser/text/browser_text.js create mode 100644 accessible/tests/browser/text/browser_text_caret.js create mode 100644 accessible/tests/browser/text/browser_text_paragraph_boundary.js create mode 100644 accessible/tests/browser/text/browser_text_selection.js create mode 100644 accessible/tests/browser/text/browser_text_spelling.js create mode 100644 accessible/tests/browser/text/browser_textleafpoint.js create mode 100644 accessible/tests/browser/text/head.js create mode 100644 accessible/tests/browser/tree/browser.ini create mode 100644 accessible/tests/browser/tree/browser_aria_owns.js create mode 100644 accessible/tests/browser/tree/browser_browser_element.js create mode 100644 accessible/tests/browser/tree/browser_css_content_visibility.js create mode 100644 accessible/tests/browser/tree/browser_general.js create mode 100644 accessible/tests/browser/tree/browser_lazy_tabs.js create mode 100644 accessible/tests/browser/tree/browser_searchbar.js create mode 100644 accessible/tests/browser/tree/browser_shadowdom.js create mode 100644 accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js create mode 100644 accessible/tests/browser/tree/head.js (limited to 'accessible/tests/browser') 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/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini new file mode 100644 index 0000000000..078659a97d --- /dev/null +++ b/accessible/tests/browser/bounds/browser.ini @@ -0,0 +1,27 @@ +[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_resolution.js] +skip-if = os == 'win' # bug 1372296 +[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] +[browser_test_display_contents.js] +[browser_test_simple_transform.js] +[browser_test_iframe_transform.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure 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( + ` +
+ `, + 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( + ` + + + `, + 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..18de7d8a76 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_position.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +/** + * Test changing the left/top CSS properties. + */ +addAccessibleTask( + ` +
+ test +
+ `, + 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( + ` +
+

1

+ +

3

+
+

noReflow

+ `, + 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( + ` +
+

1

+
+

2

+
+
+ `, + 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 } +); 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( + ` +
before +
    +
  • +

    item

    +
  • +
+
`, + 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( + `
hello world
`, + 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( + `
`, + 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( + ` +
    + +
+ `, + 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( + ` +

Tilimilitryamdiya

+

para 2

+ +`, + 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( + `

hello world

`, + 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( + ` +
+

hello

+

world

+
+ `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "p1", browser); + await testBoundsWithContent(docAcc, "p2", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// test basic rotation +addAccessibleTask( + `

hello world

`, + 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( + `

hello world

`, + 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( + ` +
+

hello

+

world

+
+
+
+

hello

+

world

+
+
+ `, + 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( + ` +
+
+

test

+
+
+ + + `, + 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( + ` +
+

test

+
+ `, + 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( + ` +
+
+

test

+
+
+ `, + 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( + ` +
+

hello world


+
+ `, + 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( + ` +

para 1

para 2

+ + mozilla.org + +`, + 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( + ` +

Tilimilitryamdiya

+

ل

+
`, + 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( + ` +
+
+
+ `, + 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( + ` +
+
+`, + 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( + ` + +`, + 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( + ` + + + `, + 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( + ` + +
Testing
+
+ `, + 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.ini b/accessible/tests/browser/browser.ini new file mode 100644 index 0000000000..c7bc851086 --- /dev/null +++ b/accessible/tests/browser/browser.ini @@ -0,0 +1,39 @@ +[DEFAULT] +skip-if = a11y_checks # 1534855 +subsuite = a11y +support-files = + !/accessible/tests/mochitest/*.js + *.sys.mjs + head.js + shared-head.js +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..c09c3f6bf1 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + +
+ `, + }, + async function (browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let acc = docAcc.getChildAt(0); + ok(acc, "Accessible proxy is created"); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible proxy. + acc = null; + ok(!acc, "Accessible proxy is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible document proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js new file mode 100644 index 0000000000..4b0b22f858 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + +
+ `, + }, + async function (browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let acc = docAcc.getChildAt(0); + ok(acc, "Accessible proxy is created"); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible proxy. + acc = null; + ok(!acc, "Accessible proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_reference.js b/accessible/tests/browser/browser_shutdown_multi_reference.js new file mode 100644 index 0000000000..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..472d5c9c95 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_parent_own_reference.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"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + 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]); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_pref.js b/accessible/tests/browser/browser_shutdown_pref.js new file mode 100644 index 0000000000..74cef28b03 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_pref.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled"; + +add_task(async function testForceDisable() { + ok( + !Services.appinfo.accessibilityEnabled, + "Accessibility is disabled by default" + ); + + info("Reset force disabled preference"); + Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED); + + info("Enable accessibility service via XPCOM"); + let [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled"); + + info("Force disable a11y service via preference"); + let [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1); + await a11yShutdown; + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); + + info("Attempt to get an instance of a11y service and call its method."); + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + try { + accService.getAccesssibleFor(document); + ok(false, "getAccesssibleFor should've triggered an exception."); + } catch (e) { + ok( + true, + "getAccesssibleFor triggers an exception as a11y service is shutdown." + ); + } + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); + + info("Reset force disabled preference"); + Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED); + + info("Create a11y service again"); + [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled"); + + info("Remove all references to a11y service"); + [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + accService = null; + forceGC(); + await a11yShutdown; + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); +}); diff --git a/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js new file mode 100644 index 0000000000..f0e93cc188 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + async function (browser) { + let onShow = waitForEvent(Ci.nsIAccessibleEvent.EVENT_SHOW, "div"); + await invokeSetStyle(browser, "div", "visibility", "visible"); + let showEvent = await onShow; + let divAcc = showEvent.accessible; + ok(divAcc, "Accessible proxy is created"); + // Remove unnecessary dangling references + onShow = null; + showEvent = null; + forceGC(); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible proxy. + divAcc = null; + ok(!divAcc, "Accessible proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js new file mode 100644 index 0000000000..6d417f71eb --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + async function (browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_no_reference.js b/accessible/tests/browser/browser_shutdown_remote_no_reference.js new file mode 100644 index 0000000000..dc2a7e33e9 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_no_reference.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + 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." + ) + ); + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_only.js b/accessible/tests/browser/browser_shutdown_remote_only.js new file mode 100644 index 0000000000..678031aac0 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_only.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"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + 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." + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_own_reference.js b/accessible/tests/browser/browser_shutdown_remote_own_reference.js new file mode 100644 index 0000000000..34fdeee3ab --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_own_reference.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"; + +add_task(async function () { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + + + + Accessibility Test + + + `, + }, + 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." + ) + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_scope_lifecycle.js b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js new file mode 100644 index 0000000000..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.ini b/accessible/tests/browser/e10s/browser.ini new file mode 100644 index 0000000000..b9a511bd35 --- /dev/null +++ b/accessible/tests/browser/e10s/browser.ini @@ -0,0 +1,80 @@ +[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 + +# 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_events_vcchange.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] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[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..8bf3542a03 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_actions.js @@ -0,0 +1,266 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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( + ` + + + + linkable textleaf accessible +
linkable textleaf accessible
+ + + image in link + + +
+ + +
+ +

p in clickable 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("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 + const link1Acc = findAccessibleChildByID(docAcc, "link1"); + is( + link1Acc.firstChild.getActionName(0), + "click ancestor", + "linkable child has click ancestor action" + ); + await invokeContentTask(browser, [], () => { + let link1 = content.document.getElementById("link1"); + link1.removeAttribute("href"); + }); + 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( + ` + + + `, + 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..7b20cff49f --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_attributes.js @@ -0,0 +1,717 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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( + ` + `, + 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( + ` +

text

+ + `, + 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( + ` + + + + + + + + + `, + 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( + ` +
+ a + +
+ `, + 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" + ); + }, + { chrome: true, topLevel: true, iframe: true, remoteIframe: true } +); + +/** + * Test that there is no display attribute on image map areas. + */ +addAccessibleTask( + ` + + + + + + + `, + 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( + ` +

content

+ + + + +
+ `, + 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( + ` +
currentTrue
+
currentFalse
+
currentPage
+
currentBlah
+
haspopup
+
+
+
positive
+
+
+
+
+
negative
+
+
+
+
+
invalid
+
+
+
foo
+
mutate
+ `, + 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( + ` +
knownRole
+
emptyRole
+
unknownRole
+
multiRole
+
landmarkMarkup
+
landmarkMarkupWithRole
+
landmarkMarkupWithEmptyRole
+
markup
+ +
markupWithEmptyRole
+ `, + 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( + ` +

noLive

+

liveMarkup

+

ariaLive

+

liveRole

+

nonLiveRole

+

other

+ `, + 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( + ` +

withId

+

noId

+ `, + 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( + ` +
+
+
+ `, + 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( + ` +
oneClass
+
multiClass
+
noClass
+
mutate
+ `, + 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( + ` + +alt + + `, + async function (browser, docAcc) { + const noAlt = findAccessibleChildByID(docAcc, "noAlt"); + testAttrs(noAlt, { src: kImgUrl }, true); + if (browser.isRemoteBrowser) { + // To avoid wasting memory, we don't cache src if there's a name. + const alt = findAccessibleChildByID(docAcc, "alt"); + testAbsentAttrs(alt, { src: "" }); + } + + 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( + ` + + + + + + + `, + 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 } +); 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( + ` +

aria description

+

another description

+ `, + 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( + ` +`, + 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..787e979045 --- /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 = "http://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 = "http://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( + '
', + 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( + ` +


Simple link:
Mozilla Foundation
ARIA link:
Mozilla Foundation Home
Invalid, non-focusable hyperlink:
Invalid link
Image map:

Empty link:

Link with embedded span
Heise Online
Named anchor, must not have "linked" state for it to be exposed correctly:
This should never be of state_linked +

+ `, + 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( + ` +

mozilla.org

+ `, + 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( + ` +

mozillamozilla text mozilla

+ `, + 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( + ` +

test

+xy + `, + 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"), + "xy", + "math cached html is correct" + ); + + info("Mutating math"); + await invokeContentTask(browser, [], () => { + content.document.querySelectorAll("mi")[1].textContent = "z"; + }); + await untilCacheIs( + () => math.cache.getStringProperty("html"), + "xz", + "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( + ` + + + + + +
hello
+ `, + 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( + `
`, + 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..0c1f419b97 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_name.js @@ -0,0 +1,539 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +/** + * Rules for name tests that are inspired by + * accessible/tests/mochitest/name/markuprules.xul + * + * Each element in the list of rules represents a name calculation rule for a + * particular test case. + * + * The rules have the following format: + * { attr } - calculated from attribute + * { elm } - calculated from another element + * { fromsubtree } - calculated from element's subtree + * + */ +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: ` + test2 + test3 + + `, + expected: ["test2 test3", "test1", "test4", "press me", "test5"], + }, + { + id: "btn", + ruleset: "HTMLInputButton", + markup: ` + test2 + test3 + + `, + expected: [ + "test2 test3", + "test1", + "test4", + "name from value", + "name from title", + ], + }, + { + id: "btn-submit", + ruleset: "HTMLInputSubmit", + markup: ` + test2 + test3 + + `, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-reset", + ruleset: "HTMLInputReset", + markup: ` + test2 + test3 + + `, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-image", + ruleset: "HTMLInputImage", + markup: ` + test2 + test3 + + `, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + "name from title", + ], + }, + { + id: "btn-image", + ruleset: "HTMLInputImageNoValidSrc", + markup: ` + test2 + test3 + + `, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + ], + }, + { + id: "opt", + ruleset: "HTMLOption", + markup: ` + test2 + test3 + `, + expected: ["test2 test3", "test1", "test4", "option1", "test5"], + }, + { + id: "img", + ruleset: "HTMLImg", + markup: ` + test2 + test3 + Mozilla logo`, + expected: [ + "test2 test3", + "Logo of Mozilla", + "Mozilla logo", + "This is a logo", + ], + }, + { + id: "tc", + ruleset: "HTMLElm", + markup: ` + test2 + test3 + + + + + +
+

This is a paragraph

+ This is a link +
    +
  • This is a list
  • +
+
`, + expected: ["test2 test3", "test1", "test5"], + }, + { + id: "gc", + ruleset: "HTMLARIAGridCell", + markup: ` + test2 + test3 + + + + + +
+

This is a paragraph

+ This is a link +
    +
  • Listitem1
  • +
  • Listitem2
  • +
+
`, + 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: ` + lby_tst6_1 + lby_tst6_2 + + + + + + + +
caption_tst6
cell1cell2
`, + expected: [ + "lby_tst6_1 lby_tst6_2", + "arialabel_tst6", + "caption_tst6", + "summary_tst6", + "title_tst6", + ], + }, + { + id: "btn", + ruleset: "CSSContent", + markup: ` +
+ + +
`, + expected: ["do not press me", "press me"], + }, + { + // TODO: uncomment when Bug-1256382 is resoved. + // id: 'li', + // ruleset: 'CSSContent', + // markup: ` + // + // `, + // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`] + // }, { + id: "a", + ruleset: "HTMLLink", + markup: ` + test2 + test3 + test5`, + expected: ["test2 test3", "test1", "test5", "test4"], + }, + { + id: "a-img", + ruleset: "HTMLLinkImage", + markup: ` + test2 + test3 + test5`, + 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( + ` + + + `, + 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( + ` + + + `, + async function (browser, docAcc) { + testStates(docAcc, STATE_FOCUSED); + } +); + +/** + * Test caching of the focused state in iframes. + */ +addAccessibleTask( + ` + + `, + 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( + ` + +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( + ` +
hello world
+ `, + 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( + `
`, + 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( + ``, + 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,'; + }); + 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( + ` +
+ + +
+
+
multiNoSel
+
+ `, + 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( + ``, + 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 } +); 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..fa978c08eb --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_table.js @@ -0,0 +1,532 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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( + ` + + + + + + + + +
abcd
eifjgh
k
+ `, + 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( + ` + + + + +
ab
cd
ef
+ `, + 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( + ` + +
outerCell +
a
+
+ + `, + 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( + ` + + + +
c1
a
+ + +
a
+ + + +
c3
a
+ `, + 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( + ` +
a
+
a
+
ab
+
+ `, + 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 = "ab"; + 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 = `
a
`; + }); + 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( + ` + +
ab
cd
cd
+ `, + 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( + ` +
+
+
a
b
+
+
+
c
d
+
+
+ `, + 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( + ` + + + +
ab
+
+ `, + 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( + ` +
+
a
+
+ `, + 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( + ` +
+
+
+
a
+
+
+
+ `, + 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( + `
`, + 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 we don't crash for authoring error like . + */ +addAccessibleTask( + `
`, + async function (browser, docAcc) { + const table = findAccessibleChildByID(docAcc, "table"); + ok(table, "Retrieved table Accessible"); + }, + { chrome: true, topLevel: true } +); + +/** + * Verify that we don't crash for authoring error like . + */ +addAccessibleTask( + ` +
+ + +
a
b
+ `, + 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..2d2a857c8f --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_text_bounds.js @@ -0,0 +1,696 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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.
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);
+  }
+}
+
+/**
+ * Test the text range boundary for simple LtR text
+ */
+addAccessibleTask(
+  `
+  

Tilimilitryamdiya

+

ل

+

Привіт Світ

+
a%0abcdef
+ `, + 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( + ` +

Tilimilitryamdiya

+

Привіт Світ

+ `, + 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( + ` +

Привіт Світ
Привіт Світ

+

Привіт Світ
Я ще трохи тексту в другому рядку

+

hello world I'm on line one
and I'm a separate line two with slightly more text

+

hello world
hello world

+ `, + 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( + ` +

Tilimilitryamdiya

+

ل

+

لل لللل لل

+
a%0abcdef
+ `, + 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( + ` +

لل لللل لل
لل لللل لل

+

لل لللل لل
لل لل لل لل ل لل لل لل

+

hello world I'm on line one
and I'm a separate line two with slightly more text

+

hello world
hello world

+ `, + 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( + ` +

Tilimilitryamdiya

+

لل لللل لل

+ `, + 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( + ` +
+

你好世界

+

hello world

+
+

こんにちは世界

+
+
+

你好世界

+

hello world

+
+

こんにちは世界

+
+ `, + 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( + ` +

你好世界
你好世界

+

hello world
hello world

+
+

你好世界
你好世界 你好世界

+

hello world
hello world hello world

+ `, + 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( + `

hello world

+

hello
world

+

hello world
+
hello world

+
oh

hello world
`, + 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( + `

a

`, + 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( + ``, + 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( + ` + +
XX
+XXX
`, + 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( + ` + +
XX
+XXX
`, + 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( + `

a

`, + 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( + ` + +
XX
+XXX
+XX
+X
`, + 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( + ` + +
hello
XX
+XXX
+XX
+X
`, + 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( + ` + + `, + 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( + ``, + 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( + ` + +

ab cd

+

+foo

+ `, + 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( + ` + +
XX
+XXX
+XX
+X
`, + 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, + } +); 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( + '
', + 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..0e3cda93af --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_value.js @@ -0,0 +1,415 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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/value.js */ +loadScripts( + { name: "states.js", dir: MOCHITESTS_DIR }, + { name: "value.js", dir: MOCHITESTS_DIR } +); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * id {String} given accessible DOMNode ID + * expected {String} expected value for a given accessible + * action {?AsyncFunction} an optional action that awaits a value change + * attrs {?Array} an optional list of attributes to update + * waitFor {?Number} an optional value change event to wait for + * } + */ +const valueTests = [ + { + desc: "Initially value is set to 1st element of select", + id: "select", + expected: "1st", + }, + { + desc: "Value should update to 3rd when 3 is pressed", + id: "select", + async action(browser) { + await invokeFocus(browser, "select"); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.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", + }, +]; + +/** + * Test caching of accessible object values + */ +addAccessibleTask( + ` +
slider
+ + + + +
Some rich text
`, + 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); + testValue(acc, ...expected); + } else { + is(acc.value, expected, `Correct value for ${prettyName(acc)}`); + } + } + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test caching of link URL values. + */ +addAccessibleTask( + `Test`, + async function (browser, docAcc) { + const 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"); + await invokeSetAttribute(browser, "link", "href"); + await untilCacheIs(() => link.value, "", "link value empty after removal"); + + info("Setting link href"); + await invokeSetAttribute(browser, "link", "href", "https://example.com/"); + 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( + ` + `, + 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( + ` +
value
+
+
+
value
+
+
+
+
value
+
+
foo +
+
bar
+
+
+ + + testing + +
+
+
value
+
+
+`, + 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( + `

abc

`, + 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( + '', + 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( + ` +
+ +
+ +
`, + 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( + '', + 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, + + + + Inner Iframe + + + `; + +/** + * Test state change event and its interface: + * - state + * - isExtraState + * - isEnabled + */ +addAccessibleTask( + ` + + `, + 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( + ` +

abc

+ `, + 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_events_vcchange.js b/accessible/tests/browser/e10s/browser_events_vcchange.js new file mode 100644 index 0000000000..3571d66212 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_vcchange.js @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +addAccessibleTask( + ` +

abc

+ `, + async function (browser) { + let onVCChanged = waitForEvent( + EVENT_VIRTUALCURSOR_CHANGED, + matchContentDoc + ); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.position = CommonUtils.getAccessible( + "p1", + null, + null, + null, + content.document + ); + }); + let vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + is(vccEvent.newAccessible.id, "p1", "New position is correct"); + is(vccEvent.newStartOffset, -1, "New start offset is correct"); + is(vccEvent.newEndOffset, -1, "New end offset is correct"); + ok(!vccEvent.isFromUserInput, "not user initiated"); + + onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.moveNextByText(Ci.nsIAccessiblePivot.CHAR_BOUNDARY); + }); + vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + is(vccEvent.newAccessible.id, vccEvent.oldAccessible.id, "Same position"); + is(vccEvent.newStartOffset, 0, "New start offset is correct"); + is(vccEvent.newEndOffset, 1, "New end offset is correct"); + ok(vccEvent.isFromUserInput, "user initiated"); + + onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.position = CommonUtils.getAccessible( + "input1", + null, + null, + null, + content.document + ); + }); + vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + isnot(vccEvent.oldAccessible, vccEvent.newAccessible, "positions differ"); + is(vccEvent.oldAccessible.id, "p1", "Old position is correct"); + is(vccEvent.newAccessible.id, "input1", "New position is correct"); + is(vccEvent.newStartOffset, -1, "New start offset is correct"); + is(vccEvent.newEndOffset, -1, "New end offset is correct"); + ok(!vccEvent.isFromUserInput, "not user initiated"); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_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( + ` + +
+
+
+
+
+ `, + 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( + ` + + + + `, + 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( + `
+ + +
+ + + + +
+ + + +
+ + `, + 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( + `
    +
  • Oranges
  • +
  • Apples
  • +
  • Bananas
  • +
+ +
    +
  1. Oranges
  2. +
  3. Apples
  4. +
  5. Bananas +
      +
    • Oranges
    • +
    • Apples
    • +
    • Bananas
    • +
    +
  6. +
+ + + Oranges + Apples + Bananas + + + + Oranges + Apples + Bananas + + Oranges + Apples + Bananas + + + + +
+
Item 1 +
+
Item 1A
+
Item 1B
+
+
+
Item 2 +
+
Item 2A
+
Item 2B
+
+
+
`, + 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( + ``, + 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( + `
    + + + +
`, + 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( + `
    + + + +
`, + 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( + ` + + + + + + + + + + + + + + + + + + + + + + + + +
vegetables
cucumber
carrot
mercedes
BMW
Audi
people
+ +
    +
  • Item 1 +
      +
    • Item 1A
    • +
    • Item 1B
    • +
    +
  • +
  • Item 2 +
      +
    • Item 2A
    • +
    • Item 2B
    • +
    +
  • + + +
    +
    Item 1
    +
    +
  • Item 1A
  • +
  • Item 1B
  • +
    +
    Item 2
    +
    +
    Item 2A
    +
    Item 2B
    +
    +
    `, + 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( + ` + + + + + + + + +
    cell1cell2
    cell3cell4
    `, + 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( + `
    +
    +
    cell1
    +
    cell2
    +
    +
    +
    cell1
    +
    cell2
    +
    +
    +
    cell1
    +
    cell2
    +
    +
    `, + 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( + `
    +

    heading1

    +

    heading2

    +

    heading3

    +

    heading4

    +
    heading5
    +
    heading6
    +
    ariaHeadingNoLevel
    +
    `, + 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( + `
      Password +
    • Xyzzy
    • +
    • Plughs
    • +
    • Shazaam
    • +
    • JoeSentMe
    • +
    `, + 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( + `
    +
    +
    cell
    +
    +
    `, + 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( + `
    +
    +
    +
    cell content
    +
    +
    +
    +
    +
    cell content
    +
    +
    +
    `, + 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( + `
    +
    Apples
    +
    Oranges
    +
    +
    Bananas
    `, + 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( + ` +
    + + +
    + + +
    + + + +
    `, + 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( + `
    Item 1
    Item 2
    `, + 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( + `
    +
    +
    treeitem 1
    +
    +
    +
    treeitem 2
    +
    +
    +
    +
    +
    option 1
    +
    +
    +
    option 2
    +
    +
    +
    +
    +
    listitem 1
    +
    +
    +
    listitem 2
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +`, + 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( + `
    +
    +
    first
    +
    +
    +
    second
    +
    +
    `, + 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..c8fb7e5488 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js @@ -0,0 +1,325 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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( + ` + +
    +
    +
    + +
    + + `, + async function (browser, accDoc) { + testAccessibleTree(accDoc, { + DOCUMENT: [ + { + // b + SECTION: [ + { + // c + SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }], + }, + ], + }, + ], + }); + } +); 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( + ` + + + `, + 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..0af583a96c --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.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/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const snippet = ` + + + `; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["layout.css.content-visibility.enabled", true]], + }); +}); + +async function setContentVisibility(browser, id, value) { + let onReorder = waitForEvent(EVENT_REORDER, id); + + // Change the value of `content-visibility` property for the target + info(`Setting content-visibility: ${value} on ${id}`); + await invokeSetAttribute(browser, id, "class", value); + await onReorder; +} + +addAccessibleTask( + snippet, + async function (browser, accDoc) { + const targetId = "target"; + const target = findAccessibleChildByID(accDoc, targetId); + + info("Initial Accessibility Structure Test"); + testAccessibleTree(target, { SECTION: [] }); + + await setContentVisibility(browser, targetId, "auto"); + testAccessibleTree(target, { SECTION: [{ SECTION: [] }, { SECTION: [] }] }); + + await setContentVisibility(browser, targetId, "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( + ` +
    `, + 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, + + + + Inner Iframe + + + `; + +addAccessibleTask( + ` + `, + 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('hello'); + 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('');`; + 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,` + ); + 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( + ` + +
    +
    text
    `, + 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( + '
    ', + 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( + ``, + 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( + ``, + 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( + `${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( + ``, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ` +
      +
    • item1
    • +
    `, + 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( + '
      ', + 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( + '', + 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( + ` + + `, + 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( + ` +
      + +
      +
      + `, + 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( + '', + 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 = ` + +`; + +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( + ` + + + + + +
      cell1cell2
      `, + 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( + ` +

      text

      +
      text
      `, + 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 @@ + + + + Tree Update ARIA Dialog Test + + + + + 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 @@ + + + + Tree Update ARIA Owns Test + + +
      +
      + +
      +
      +
      + +
      +
      +
      +
      + +
      + +
      + +
      +
      + + +
      + +
      +
      + + +
      + +
      + hey +
      +
      + + 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 @@ + + + + Tree Update Imagemap Test + + + + b + + +
      +
      + + 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 @@ + + + + Tree Update Removal Test + + + +
      + + + 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 @@ + + + + Tree Update Visibility Test + + + +
      +
      +
      text
      +
      +
      + + +
      +
      +
      +
      text
      +
      text
      +
      +
      +
      + + +
      +
      +
      text
      +
      +
      +
      text
      +
      +
      + + +
      + + + + +
      + +
      +
      + + +
      +
      + + + + +
      +
      text
      +
      +
      +
      + + +
      +
      + + + + +
      + + + + +
      +
      text
      +
      +
      +
      text
      +
      +
      + + diff --git a/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html new file mode 100644 index 0000000000..f17dbbd60e --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html @@ -0,0 +1,10 @@ + + + + Whitespace text accessible creation/destruction + + +
      +
      + + 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.ini b/accessible/tests/browser/events/browser.ini new file mode 100644 index 0000000000..576d3e53ab --- /dev/null +++ b/accessible/tests/browser/events/browser.ini @@ -0,0 +1,24 @@ +[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_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 == "linux" # Bug 1782783 + 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_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( + ``, + 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..11ba90db19 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_docload.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"; + +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.loadURIString( + browser, + `data:text/html;charset=utf-8, + + + ` + ); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:about")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURIString(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.loadURIString(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)], + ]); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.loadURIString(browser, "http://www.wronguri.wronguri/"); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("https://nocert.example.com/")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURIString(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(``, { + 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(``, 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( + ` + + + `, + 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..f2d74cc5f9 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_panel.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Verify we recieve 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: [ + { + TEXT_CONTAINER: [ + { 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..f1f4b07120 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_scrolling.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"; + +addAccessibleTask( + ` +
      one
      +
      two
      +
      +
      three
      +
      + + `, + 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; + } +); 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..d5065c81f3 --- /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.loadURIString(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.ini b/accessible/tests/browser/fission/browser.ini new file mode 100644 index 0000000000..c898fb54d2 --- /dev/null +++ b/accessible/tests/browser/fission/browser.ini @@ -0,0 +1,20 @@ +[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] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[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( + ` + + + + +
      cell1cell2
      +
        +
      • item1
      • +
      `, + 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( + ``, + 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", + ` + + + Accessibility Nested Iframe Frame Test + + + + + + + +
      cell1cell2
      +
        +
      • item1
      • +
      + + ` +); + +function getOsPid(browsingContext) { + return browsingContext.currentWindowGlobal.osPid; +} + +addAccessibleTask( + ` +
      + + `, + 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(`

      para 1

      para 2

      `, 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( + `

      hello world hello world

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

      hello world

      +

      this is a test

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

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

      +
      hello world
      I am some text
      `, + async (browser, accDoc) => { + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null"); + ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null"); + + // AXSize and AXPosition are composed of AXFrame components, so we + // test them here instead of calling AXFrame directly. + const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize"); + const [worldWidth, worldHeight] = world.getAttributeValue("AXSize"); + ok(helloWidth > 0, "Hello has a positive width"); + ok(helloHeight > 0, "Hello has a positive height"); + ok(worldWidth > 0, "World has a positive width"); + ok(worldHeight > 0, "World has a positive height"); + ok(helloHeight < worldHeight, "Hello has a smaller height than world"); + ok(helloWidth < worldWidth, "Hello has a smaller width than world"); + + // Note: these are mac screen coords, so our origin is bottom left + const [helloX, helloY] = hello.getAttributeValue("AXPosition"); + const [worldX, worldY] = world.getAttributeValue("AXPosition"); + ok(helloX < 0, "Hello has a negative X"); + ok(helloY > 0, "Hello has a positive Y"); + ok(worldX < 0, "World has a negative X"); + ok(worldY > 0, "World has a positive Y"); + ok(helloY > worldY, "Hello has a larger Y than world"); + ok(helloX == worldX, "Hello and world have the same X"); + } +); diff --git a/accessible/tests/browser/mac/browser_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js new file mode 100644 index 0000000000..6157707f79 --- /dev/null +++ b/accessible/tests/browser/mac/browser_details_summary.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test details/summary + */ +addAccessibleTask( + `
      Foo

      Bar

      `, + async (browser, accDoc) => { + let details = getNativeInterface(accDoc, "details"); + is( + details.getAttributeValue("AXRole"), + "AXGroup", + "Correct role for details" + ); + is( + details.getAttributeValue("AXSubrole"), + "AXDetails", + "Correct subrole for details" + ); + + let detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 1, "collapsed details has only one child"); + + let summary = detailsChildren[0]; + is( + summary.getAttributeValue("AXRole"), + "AXButton", + "Correct role for summary" + ); + is( + summary.getAttributeValue("AXSubrole"), + "AXSummary", + "Correct subrole for summary" + ); + is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed"); + + let actions = summary.actionNames; + ok(actions.includes("AXPress"), "Summary Has press action"); + + let stateChanged = waitForStateChange("summary", STATE_EXPANDED, true); + summary.performAction("AXPress"); + // The reorder gecko event notifies us of a tree change. + await stateChanged; + is(summary.getAttributeValue("AXExpanded"), 1, "Summary is expanded"); + + detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 2, "collapsed details has only one child"); + + stateChanged = waitForStateChange("summary", STATE_EXPANDED, false); + summary.performAction("AXPress"); + // The reorder gecko event notifies us of a tree change. + await stateChanged; + is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed 2"); + + detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 1, "collapsed details has only one child"); + } +); diff --git a/accessible/tests/browser/mac/browser_focus.js b/accessible/tests/browser/mac/browser_focus.js new file mode 100644 index 0000000000..6bceb06c6c --- /dev/null +++ b/accessible/tests/browser/mac/browser_focus.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test focusability + */ +addAccessibleTask( + ` +
      hello
      + `, + async (browser, accDoc) => { + let ariabutton = getNativeInterface(accDoc, "ariabutton"); + let button = getNativeInterface(accDoc, "button"); + + is( + ariabutton.getAttributeValue("AXFocused"), + 0, + "aria button is not focused" + ); + + is(button.getAttributeValue("AXFocused"), 0, "button is not focused"); + + ok( + !ariabutton.isAttributeSettable("AXFocused"), + "aria button should not be focusable" + ); + + ok(button.isAttributeSettable("AXFocused"), "button is focusable"); + + let evt = waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == "button" + ); + + button.setAttributeValue("AXFocused", true); + + await evt; + + is(button.getAttributeValue("AXFocused"), 1, "button is focused"); + } +); diff --git a/accessible/tests/browser/mac/browser_heading.js b/accessible/tests/browser/mac/browser_heading.js new file mode 100644 index 0000000000..fd8c12883d --- /dev/null +++ b/accessible/tests/browser/mac/browser_heading.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test whether line break code in text content will be removed + * and extra whitespaces will be trimmed. + */ +addAccessibleTask( + ` +

      We’re building a richer search experience

      +

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

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

      Hello world

      +

      Hello

      +

      Hello

      + `, + async (browser, accDoc) => { + const a = getNativeInterface(accDoc, "a"); + is( + a.getAttributeValue("AXTitle"), + "Hello world", + "Correct AXTitle for 'a'" + ); + ok( + !a.getAttributeValue("AXDescription"), + "'a' Should not have AXDescription" + ); + + const b = getNativeInterface(accDoc, "b"); + is(b.getAttributeValue("AXTitle"), "Hello", "Correct AXTitle for 'b'"); + ok( + !b.getAttributeValue("AXDescription"), + "'b' Should not have AXDescription" + ); + + const c = getNativeInterface(accDoc, "c"); + is( + c.getAttributeValue("AXDescription"), + "Goodbye", + "Correct AXDescription for 'c'" + ); + ok(!c.getAttributeValue("AXTitle"), "'c' Should not have AXTitle"); + } +); diff --git a/accessible/tests/browser/mac/browser_hierarchy.js b/accessible/tests/browser/mac/browser_hierarchy.js new file mode 100644 index 0000000000..8a97e55c07 --- /dev/null +++ b/accessible/tests/browser/mac/browser_hierarchy.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test AXIndexForChildUIElement + */ +addAccessibleTask( + `

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

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

      Elmer Fudd

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

      Bob Loblaw's

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

      I am that element

      +

      + I have a name + +

      I have no name and no ID

      +

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

      hello

      + world
      + I am a link +

      goodbye

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

      a

      +

      b

      +

      c

      +

      d

      +
      e
      +
      f
      +

      g

      +

      h

      +

      i

      +

      j

      +
      k
      +
      l
      + this is some regular text that should be ignored + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingLevel1SearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let h1Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h1Count, "Found two h1 items"); + + let h1s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const a = getNativeInterface(accDoc, "a"); + const g = getNativeInterface(accDoc, "g"); + + is( + a.getAttributeValue("AXValue"), + h1s[0].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + is( + g.getAttributeValue("AXValue"), + h1s[1].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel2SearchKey"; + + let h2Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h2Count, "Found two h2 items"); + + let h2s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const b = getNativeInterface(accDoc, "b"); + const h = getNativeInterface(accDoc, "h"); + + is( + b.getAttributeValue("AXValue"), + h2s[0].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + is( + h.getAttributeValue("AXValue"), + h2s[1].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel3SearchKey"; + + let h3Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h3Count, "Found two h3 items"); + + let h3s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const c = getNativeInterface(accDoc, "c"); + const i = getNativeInterface(accDoc, "i"); + + is( + c.getAttributeValue("AXValue"), + h3s[0].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + is( + i.getAttributeValue("AXValue"), + h3s[1].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel4SearchKey"; + + let h4Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h4Count, "Found two h4 items"); + + let h4s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const d = getNativeInterface(accDoc, "d"); + const j = getNativeInterface(accDoc, "j"); + + is( + d.getAttributeValue("AXValue"), + h4s[0].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + is( + j.getAttributeValue("AXValue"), + h4s[1].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel5SearchKey"; + + let h5Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h5Count, "Found two h5 items"); + + let h5s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const e = getNativeInterface(accDoc, "e"); + const k = getNativeInterface(accDoc, "k"); + + is( + e.getAttributeValue("AXValue"), + h5s[0].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + is( + k.getAttributeValue("AXValue"), + h5s[1].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel6SearchKey"; + + let h6Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h6Count, "Found two h6 items"); + + let h6s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const f = getNativeInterface(accDoc, "f"); + const l = getNativeInterface(accDoc, "l"); + + is( + f.getAttributeValue("AXValue"), + h6s[0].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + + is( + l.getAttributeValue("AXValue"), + h6s[1].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + } +); + +/* + * Test rotor with blockquotes + */ +addAccessibleTask( + ` +
      hello I am a blockquote
      +
      + I am also a blockquote of the same level +
      +
      but I have a different level
      +
      + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXBlockquoteSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let bquotes = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(bquotes.length, 3, "Found three blockquotes"); + + const first = getNativeInterface(accDoc, "first"); + const second = getNativeInterface(accDoc, "second"); + const third = getNativeInterface(accDoc, "third"); + console.log("values :"); + console.log(first.getAttributeValue("AXValue")); + is( + first.getAttributeValue("AXValue"), + bquotes[0].getAttributeValue("AXValue"), + "Found correct first blockquote" + ); + + is( + second.getAttributeValue("AXValue"), + bquotes[1].getAttributeValue("AXValue"), + "Found correct second blockquote" + ); + + is( + third.getAttributeValue("AXValue"), + bquotes[2].getAttributeValue("AXValue"), + "Found correct third blockquote" + ); + } +); + +/* + * Test rotor with graphics + */ +addAccessibleTask( + ` + image one
      + + image two + + + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXGraphicSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let images = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(images.length, 3, "Found three images"); + + const img1 = getNativeInterface(accDoc, "img1"); + const img2 = getNativeInterface(accDoc, "img2"); + const img3 = getNativeInterface(accDoc, "img3"); + + is( + img1.getAttributeValue("AXDescription"), + images[0].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img2.getAttributeValue("AXDescription"), + images[1].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img3.getAttributeValue("AXDescription"), + images[2].getAttributeValue("AXDescription"), + "Found correct image" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_outline.js b/accessible/tests/browser/mac/browser_outline.js new file mode 100644 index 0000000000..ba211fdf4b --- /dev/null +++ b/accessible/tests/browser/mac/browser_outline.js @@ -0,0 +1,566 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); + +/** + * Test outline, outline rows with computed properties + */ +addAccessibleTask( + ` +

      + Foods +

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

      + Foods +

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

      Non-image figure content

      +
      Old Mozilla logo
      +
      `, + (browser, accDoc) => { + let figure = getNativeInterface(accDoc, "figure"); + ok(!figure.getAttributeValue("AXTitle"), "Figure should not have a title"); + is( + figure.getAttributeValue("AXDescription"), + "Old Mozilla logo", + "Correct figure label" + ); + is(figure.getAttributeValue("AXRole"), "AXGroup", "Correct figure role"); + is( + figure.getAttributeValue("AXRoleDescription"), + "figure", + "Correct figure role description" + ); + + let img = getNativeInterface(accDoc, "img"); + ok(!img.getAttributeValue("AXTitle"), "img should not have a title"); + is(img.getAttributeValue("AXDescription"), "Logo", "Correct img label"); + is(img.getAttributeValue("AXRole"), "AXImage", "Correct img role"); + is( + img.getAttributeValue("AXRoleDescription"), + "image", + "Correct img role description" + ); + + let figcaption = getNativeInterface(accDoc, "figcaption"); + ok( + !figcaption.getAttributeValue("AXTitle"), + "figcaption should not have a title" + ); + ok( + !figcaption.getAttributeValue("AXDescription"), + "figcaption should not have a label" + ); + is( + figcaption.getAttributeValue("AXRole"), + "AXGroup", + "Correct figcaption role" + ); + is( + figcaption.getAttributeValue("AXRoleDescription"), + "group", + "Correct figcaption role description" + ); + } +); + +addAccessibleTask(``, async (browser, accDoc) => { + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should be an AXWebArea" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let roleChanged = waitForMacEvent("AXMozRoleChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("role", "application"); + }); + await roleChanged; + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should retain AXWebArea role" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let rootGroup = webArea.getAttributeValue("AXChildren")[0]; + is(rootGroup.getAttributeValue("AXRole"), "AXGroup"); + is(rootGroup.getAttributeValue("AXSubrole"), "AXLandmarkApplication"); +}); diff --git a/accessible/tests/browser/mac/browser_rootgroup.js b/accessible/tests/browser/mac/browser_rootgroup.js new file mode 100644 index 0000000000..a8f4297d64 --- /dev/null +++ b/accessible/tests/browser/mac/browser_rootgroup.js @@ -0,0 +1,246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test document with no single group child + */ +addAccessibleTask( + `

      hello

      world

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

      hello

      world

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

      hello

      world

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

      + We're building a richer search experience +

      + `, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + is(rootGroup.getAttributeValue("AXRole"), "AXGroup", "Inherits role"); + + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXApplicationDialog", + "Inherits subrole" + ); + let rootGroupChildren = rootGroup.getAttributeValue("AXChildren"); + is(rootGroupChildren.length, 1, "Root group has one child"); + + is( + rootGroupChildren[0].getAttributeValue("AXRole"), + "AXHeading", + "Heading is child of root group" + ); + + // From bottom-up + let heading = getNativeInterface(accDoc, "h"); + rootGroup = heading.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Parent is generated root group" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js new file mode 100644 index 0000000000..3f13506757 --- /dev/null +++ b/accessible/tests/browser/mac/browser_rotor.js @@ -0,0 +1,1752 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `

      hello


      world


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

      hello


      world


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

      Google Chrome

      +

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

      +
      + +
      +

      Mozilla Firefox

      +

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

      +
      + +
      +

      Microsoft Edge

      +

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

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

      Heading in footer

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

      hello


      world


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

      input[type=button]

      + + +

      input[type=submit]

      + + +

      input[type=image]

      + + +

      input[type=reset]

      + + +

      button element

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

      hello


      world


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

      some radio buttons

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

      Hello I am a heading

      + This is some regular text.

      this is some paragraph text


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

      Hello I am a heading

      + This is some regular text.

      this is some paragraph text


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

      hello

      world

      +

      goodybe

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

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

      +

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

      +
        +
      • I could hang out, you could show me around.
      • +
      • There's that word again, heavy.
      • +
      + `, + async (browser, f, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXSearchText: "could", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textSearchCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(textSearchCount, 2, "Found 2 matching items in text search"); + + const results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + info(results.map(r => r.getAttributeValue("AXMozDebugDescription"))); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXValue")), + [ + "Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.", + "I could hang out, you could show me around.", + ], + "Correct text search results" + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js new file mode 100644 index 0000000000..331cd7d21c --- /dev/null +++ b/accessible/tests/browser/mac/browser_selectables.js @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getSelectedIds(selectable) { + return selectable + .getAttributeValue("AXSelectedChildren") + .map(c => c.getAttributeValue("AXDOMIdentifier")); +} + +/** + * Test aria tabs + */ +addAccessibleTask("mac/doc_aria_tabs.html", async (browser, accDoc) => { + let tablist = getNativeInterface(accDoc, "tablist"); + is( + tablist.getAttributeValue("AXRole"), + "AXTabGroup", + "Correct role for tablist" + ); + + let tabMacAccs = tablist.getAttributeValue("AXTabs"); + is(tabMacAccs.length, 3, "3 items in AXTabs"); + + let selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + + let tab = selectedTabs[0]; + is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab"); + is( + tab.getAttributeValue("AXSubrole"), + "AXTabButton", + "Correct subrole for tab" + ); + is(tab.getAttributeValue("AXTitle"), "First Tab", "Correct title for tab"); + + let tabToSelect = tabMacAccs[1]; + is( + tabToSelect.getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); + + let actions = tabToSelect.actionNames; + ok(true, actions); + ok(actions.includes("AXPress"), "Has switch action"); + + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + tabToSelect.performAction("AXPress"); + await evt; + + selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + is( + selectedTabs[0].getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); +}); + +addAccessibleTask('

      hello

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

      hello goodbye world

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

      hello world

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

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

      hello, this is a test

      `, + async (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + let textLeaf = p.getAttributeValue("AXChildren")[0]; + ok(textLeaf, "paragraph has a text leaf"); + + let str = textLeaf.getParameterizedAttributeValue( + "AXStringForRange", + NSRange(3, 6) + ); + + is(str, "lo, th", "AXStringForRange matches."); + + let smallBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 6) + ); + + let largeBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 8) + ); + + ok(smallBounds.size[0] < largeBounds.size[0], "longer range is wider"); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/mac/browser_text_selection.js b/accessible/tests/browser/mac/browser_text_selection.js new file mode 100644 index 0000000000..a914adba8e --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_selection.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test simple text selection + */ +addAccessibleTask(`

      Hello World

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

      + Hello World, + I love +

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

      Hello

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

      Content for the first panel

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

      Bob Loblaw Lobs Law Bomb

      +

      I love all of my children equally

      +

      This is the best free scrapbooking class I have ever taken

      +
        +
      • Fried cheese with club sauce
      • +
      • Popcorn shrimp with club sauce
      • +
      • Chicken fingers with spicy club sauce
      • +
      +
      • Do not order the Skip's Scramble
      +

      These are my awards, Mother. From Army.

      +

      I , mom.

      + + + diff --git a/accessible/tests/browser/mac/doc_tree.xhtml b/accessible/tests/browser/mac/doc_tree.xhtml new file mode 100644 index 0000000000..d043fa8923 --- /dev/null +++ b/accessible/tests/browser/mac/doc_tree.xhtml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accessible/tests/browser/mac/head.js b/accessible/tests/browser/mac/head.js new file mode 100644 index 0000000000..f33f86288b --- /dev/null +++ b/accessible/tests/browser/mac/head.js @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported getNativeInterface, waitForMacEventWithInfo, waitForMacEvent, waitForStateChange, + NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit, + AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove, + AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown, + AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext, + AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, + AXTextSelectionDirectionBeginning, AXTextSelectionDirectionEnd, + AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord, + AXTextSelectionGranularityLine */ + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +// AXTextStateChangeType enum values +const AXTextStateChangeTypeEdit = 1; +const AXTextStateChangeTypeSelectionMove = 2; +const AXTextStateChangeTypeSelectionExtend = 3; + +// AXTextEditType enum values +const AXTextEditTypeDelete = 1; +const AXTextEditTypeTyping = 3; + +// AXTextSelectionDirection enum values +const AXTextSelectionDirectionUnknown = 0; +const AXTextSelectionDirectionBeginning = 1; +const AXTextSelectionDirectionEnd = 2; +const AXTextSelectionDirectionPrevious = 3; +const AXTextSelectionDirectionNext = 4; +const AXTextSelectionDirectionDiscontiguous = 5; + +// AXTextSelectionGranularity enum values +const AXTextSelectionGranularityUnknown = 0; +const AXTextSelectionGranularityCharacter = 1; +const AXTextSelectionGranularityWord = 2; +const AXTextSelectionGranularityLine = 3; + +function getNativeInterface(accDoc, id) { + return findAccessibleChildByID(accDoc, id).nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); +} + +function waitForMacEventWithInfo(notificationType, filter) { + let filterFunc = (macIface, data) => { + if (!filter) { + return true; + } + + if (typeof filter == "function") { + return filter(macIface, data); + } + + return macIface.getAttributeValue("AXDOMIdentifier") == filter; + }; + + return new Promise(resolve => { + let eventObserver = { + observe(subject, topic, data) { + let macEvent = subject.QueryInterface(Ci.nsIAccessibleMacEvent); + if ( + data === notificationType && + filterFunc(macEvent.macIface, macEvent.data) + ) { + Services.obs.removeObserver(this, "accessible-mac-event"); + resolve(macEvent); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-mac-event"); + }); +} + +function waitForMacEvent(notificationType, filter) { + return waitForMacEventWithInfo(notificationType, filter).then( + e => e.macIface + ); +} + +function NSRange(location, length) { + return { + valueType: "NSRange", + value: [location, length], + }; +} + +function NSDictionary(dict) { + return { + objectType: "NSDictionary", + object: dict, + }; +} + +function stringForRange(macDoc, range) { + if (!range) { + return ""; + } + + let str = macDoc.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range + ); + + let attrStr = macDoc.getParameterizedAttributeValue( + "AXAttributedStringForTextMarkerRange", + range + ); + + // This is a fly-by test to make sure our attributed strings + // always match our flat strings. + is( + attrStr.map(({ string }) => string).join(""), + str, + "attributed text matches non-attributed text" + ); + + return str; +} diff --git a/accessible/tests/browser/role/browser.ini b/accessible/tests/browser/role/browser.ini new file mode 100644 index 0000000000..751f24ecc4 --- /dev/null +++ b/accessible/tests/browser/role/browser.ini @@ -0,0 +1,11 @@ +[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 + +[browser_computedARIARole.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( + ` +
      ARIA button
      +
      ARIA log
      +
      ARIA main
      +
      ARIA region
      + +
      ARIA directory
      +
      ARIA alertdialog
      +
      ARIA feed
      +
      ARIA rowgroup
      + +
      unknown ARIA role
      + + +
      HTML main
      +
      HTML header
      +
      +
      HTML header inside section
      +
      +
      HTML region
      +
      HTML fieldset
      + + +
      HTML tbody
      + + + + +
      HTML implicit gridcell
      +
      HTML div
      +HTML span + + `, + 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"); + //
      only maps to the region ARIA role if it has a label. + testComputedARIARole("htmlSection", "generic"); + //
      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"); + // inside 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/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.ini b/accessible/tests/browser/scroll/browser.ini new file mode 100644 index 0000000000..0cae6a7c0e --- /dev/null +++ b/accessible/tests/browser/scroll/browser.ini @@ -0,0 +1,15 @@ +[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_zoom_text.js] +skip-if = os == 'win' # bug 1372296 +[browser_test_scroll_bounds.js] +[browser_test_scrollTo.js] +[browser_test_scroll_substring.js] 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..43a230b7b8 --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_scrollTo.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 nsIAccessible::scrollTo. + */ +addAccessibleTask( + ` +
      +

      a

      +

      b

      +
      + `, + async function (browser, docAcc) { + const scroller = findAccessibleChildByID(docAcc, "scroller"); + // scroller can only fit one of p1 or p2, not both. + // p1 is on screen already. + const p2 = findAccessibleChildByID(docAcc, "p2"); + info("scrollTo p2"); + let scrolled = waitForEvent( + nsIAccessibleEvent.EVENT_SCROLLING_END, + scroller + ); + p2.scrollTo(SCROLL_TYPE_ANYWHERE); + await scrolled; + const p1 = findAccessibleChildByID(docAcc, "p1"); + info("scrollTo p1"); + scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller); + p1.scrollTo(SCROLL_TYPE_ANYWHERE); + await scrolled; + }, + { 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..bd61340aa6 --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js @@ -0,0 +1,606 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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) { + let cachedPosition = ""; + try { + cachedPosition = acc.cache.getStringProperty("scroll-position"); + } catch (e) { + // If the key doesn't exist, this means 0, 0. + cachedPosition = "0, 0"; + } + + // 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( + ` +
      +
      + +
      +
      + `, + 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( + ` +
      +
      + +
      +
      + `, + 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( + ` +
      +
      + +
      +
      + `, + 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 = ""; + 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( + ` +
      + + + +
      +

      bottom

      + `, + 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( + ` +
      + +
      + `, + 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 = ""; + 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( + ` +
      +
      + +
      +

      stickyEnd

      +
      +
      + +
      +

      mutateEnd

      +
      + `, + 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 } +); 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( + ` + +
      +
      +
      +
      +
      +
      +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.
      +
      `, + 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( + ` +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +










      +

      + Пошел котик на торжок
      + Купил котик пирожок
      + Пошел котик на улочку
      + Купил котик булочку
      +

      +










      +









      +









      +









      +









      +










      +

      + Самому ли съесть
      + Либо Сашеньке снесть
      + Я и сам укушу
      + Я и Сашеньке снесу
      +

      +










      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      +









      `, + 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.ini b/accessible/tests/browser/selectable/browser.ini new file mode 100644 index 0000000000..45e34f82d3 --- /dev/null +++ b/accessible/tests/browser/selectable/browser.ini @@ -0,0 +1,12 @@ +[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( + `
      +
      tab1
      +
      tab2
      +
      +
      +
      item1
      +
      item2
      +
      +
      +
      + cell + cell +
      +
      + cell + cell +
      +
      +
      +
      + item1 +
      +
      item1.1
      +
      +
      +
      item2
      +
      +
      +
      + cell + cell +
      +
      + cell + cell +
      +
      + cell + cell +
      +
      `, + 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( + `
      + + +
      `, + 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( + `
      +
      item1
      +
      item2
      +
      `, + 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( + ` + + + + + + + + + + + + + + +
      Entry #DateExpense
      103/14/05Conference Fee
      `, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ``, + 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( + ` +
      + +
      + `, + 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..9037802728 --- /dev/null +++ b/accessible/tests/browser/shared-head.js @@ -0,0 +1,918 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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 */ + +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", + ` + + + + Accessibility Fission Test + + ${doc} + ` + ); + } + 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( + //, + `` + ); + } else { + doc = ` + ${doc}`; + } + + src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`; + } + + iframeAttrs = { + id: DEFAULT_IFRAME_ID, + src, + ...iframeAttrs, + }; + + return ` + `, + runTest, + { iframe: true, remoteIframe: true } +); + +/** + * Test div containers are reported as onscreen, even if some of their contents are + * offscreen. + */ +addAccessibleTask( + ` +
      on screen
      offscreen
      + `, + 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( + `
      Hello
      `, + 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( + ` +
      onetwothree
      + `, + 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( + ` +
      one
      two
      + `, + 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( + ` +
      hello
      + `, + 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( + ` + +
      hi
      + `, + 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.ini b/accessible/tests/browser/telemetry/browser.ini new file mode 100644 index 0000000000..07e44a348d --- /dev/null +++ b/accessible/tests/browser/telemetry/browser.ini @@ -0,0 +1,8 @@ +[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..23600d51c4 --- /dev/null +++ b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js @@ -0,0 +1,365 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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.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 + ); +} + +// 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(); +}); diff --git a/accessible/tests/browser/text/browser.ini b/accessible/tests/browser/text/browser.ini new file mode 100644 index 0000000000..1b0c5a3033 --- /dev/null +++ b/accessible/tests/browser/text/browser.ini @@ -0,0 +1,18 @@ +[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( + ``, + async function (browser, docAcc) { + await testEditable(browser, findAccessibleChildByID(docAcc, "input")); + }, + { chrome: true, topLevel: true } +); + +addAccessibleTask( + ` +
      `, + async function (browser, docAcc) { + await testEditable( + browser, + findAccessibleChildByID(docAcc, "input"), + "", + "pseudo element" + ); + }, + { chrome: true, topLevel: false /* bug 1834129 */ } +); + +addAccessibleTask( + ` +
      `, + async function (browser, docAcc) { + await testEditable( + browser, + findAccessibleChildByID(docAcc, "input"), + "pseudo element" + ); + }, + { chrome: true, topLevel: false /* bug 1834129 */ } +); + +addAccessibleTask( + ` +
      `, + 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( + ` +

      ab cd
      ef gh

      +
      ab cd
      +ef gh
      +

      abc

      +

      ab
      c
      d

      +

      a
      b

      +

      abc

      + `, + 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( + ` +


      +

      acdef

      + `, + 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( + `

      test

      `, + 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( + `
      abc
      `, + 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( + ``, + 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( + ` +

      ab

      +

      ab

      +

      abcdef

      +

      abcdefgh

      +

      abcdefghij

      +

      +

      abcdefgh

      + `, + 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( + ` + + + `, + 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( + ` + +
      +

      ab

      +
      + `, + 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( + ` + +
      +

      a

      +

      bc

      + +
      + `, + 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( + ` + + + `, + 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( + ` +

      hello world

      +
        +
      1. Number one
      2. +
      + `, + 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( + ` + +
      plain tset bold
      + `, + 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..894e982142 --- /dev/null +++ b/accessible/tests/browser/text/browser_textleafpoint.js @@ -0,0 +1,485 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If 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( + ` +

      A bug +hid in +the big rug.

      +`, + 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( + `

      + Rob caop up. +

      `, + 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( + ` +

      A bug +on a rug

      +

      + Numbers: +

      +
        +
      • One
      • +
      • Two
      • +
      • Three
      • +
      `, + 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( + `
        +
      • One
      • +
      • Two
      • +
      • Three +Four
      • +
      `, + 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( + ` + + + +
      ab
      cd
      + `, + 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 } +); 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.ini b/accessible/tests/browser/tree/browser.ini new file mode 100644 index 0000000000..bb4aedd149 --- /dev/null +++ b/accessible/tests/browser/tree/browser.ini @@ -0,0 +1,19 @@ +[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 || (verify && !debug && (os == 'linux')) #Bug 1445513 +[browser_browser_element.js] +[browser_css_content_visibility.js] +[browser_general.js] +[browser_lazy_tabs.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( + `
      • Test
      `, + 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( + `
      • Test
      `, + 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( + `
      • Test
      `, + 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( + ` +
        +
      • Test
      • +
      +
        +
          `, + 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 + + + `, + async function (browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + let selectAcc = findAccessibleChildByID(accDoc, "select"); + + testChildrenIds(containerAcc, []); + testChildrenIds(selectAcc.firstChild, ["a", "b"]); + } +); + +// Don't allow + + `, + 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 . +addAccessibleTask( + ` + + `, + 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 + + + + `, + 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( + ` + `, + async function (browser, accDoc) { + ok(true, "Did not crash"); + } +); + +addAccessibleTask( + ` +
            +
          • Test
          • +
          • Test 2
          • +
          • Test 3
          • +
          +
            `, + 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( + `
            `, + 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( + `
            Hello
            +
            There
            There
            `, + 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 = ` + +
            + +
            + auto target +
            + auto child +
            +
            +
            + `; + +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 = ` + + + `; + +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(`

            hello world

            `, 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(`

            hello world

            `, 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(`

            hello world

            `, 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_searchbar.js b/accessible/tests/browser/tree/browser_searchbar.js new file mode 100644 index 0000000000..ef68307b91 --- /dev/null +++ b/accessible/tests/browser/tree/browser_searchbar.js @@ -0,0 +1,84 @@ +"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: [ + // 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: [ + // 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 = ` + + +`; + +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 = ` + +
            +`; + +addAccessibleTask(snippet2, async function (browser, accDoc) { + let container = findAccessibleChildByID(accDoc, "container"); + + await contentSpawnMutation(browser, REORDER, function () { + content.document.getElementById("container").innerHTML = ""; + }); + + 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( + ` +
            + + `, + 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.` + ); +} -- cgit v1.2.3