From 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:47:29 +0200 Subject: Adding upstream version 115.8.0esr. Signed-off-by: Daniel Baumann --- accessible/android/AccessibleWrap.cpp | 679 ++++ accessible/android/AccessibleWrap.h | 80 + accessible/android/ApplicationAccessibleWrap.h | 20 + accessible/android/DocAccessibleWrap.cpp | 79 + accessible/android/DocAccessibleWrap.h | 33 + accessible/android/HyperTextAccessibleWrap.h | 19 + accessible/android/Platform.cpp | 234 ++ accessible/android/RootAccessibleWrap.cpp | 69 + accessible/android/RootAccessibleWrap.h | 37 + accessible/android/SessionAccessibility.cpp | 807 ++++ accessible/android/SessionAccessibility.h | 122 + accessible/android/TraversalRule.cpp | 290 ++ accessible/android/TraversalRule.h | 58 + accessible/android/moz.build | 38 + accessible/aom/AccessibleNode.cpp | 173 + accessible/aom/AccessibleNode.h | 212 + accessible/aom/moz.build | 44 + accessible/atk/AccessibleWrap.cpp | 1510 ++++++++ accessible/atk/AccessibleWrap.h | 94 + accessible/atk/ApplicationAccessibleWrap.cpp | 146 + accessible/atk/ApplicationAccessibleWrap.h | 34 + accessible/atk/DOMtoATK.cpp | 151 + accessible/atk/DOMtoATK.h | 152 + accessible/atk/DocAccessibleWrap.cpp | 36 + accessible/atk/DocAccessibleWrap.h | 33 + accessible/atk/HyperTextAccessibleWrap.h | 20 + accessible/atk/InterfaceInitFuncs.h | 43 + accessible/atk/Platform.cpp | 271 ++ accessible/atk/RootAccessibleWrap.cpp | 25 + accessible/atk/RootAccessibleWrap.h | 32 + accessible/atk/UtilInterface.cpp | 347 ++ accessible/atk/moz.build | 66 + accessible/atk/nsMai.h | 112 + accessible/atk/nsMaiHyperlink.cpp | 216 ++ accessible/atk/nsMaiHyperlink.h | 49 + accessible/atk/nsMaiInterfaceAction.cpp | 83 + accessible/atk/nsMaiInterfaceComponent.cpp | 152 + accessible/atk/nsMaiInterfaceDocument.cpp | 106 + accessible/atk/nsMaiInterfaceEditableText.cpp | 100 + accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp | 32 + accessible/atk/nsMaiInterfaceHypertext.cpp | 63 + accessible/atk/nsMaiInterfaceImage.cpp | 61 + accessible/atk/nsMaiInterfaceSelection.cpp | 102 + accessible/atk/nsMaiInterfaceTable.cpp | 264 ++ accessible/atk/nsMaiInterfaceTableCell.cpp | 148 + accessible/atk/nsMaiInterfaceText.cpp | 564 +++ accessible/atk/nsMaiInterfaceValue.cpp | 98 + accessible/atk/nsStateMap.h | 116 + accessible/base/ARIAMap.cpp | 1628 ++++++++ accessible/base/ARIAMap.h | 335 ++ accessible/base/ARIAStateMap.cpp | 334 ++ accessible/base/ARIAStateMap.h | 66 + accessible/base/AccAttributes.cpp | 271 ++ accessible/base/AccAttributes.h | 294 ++ accessible/base/AccEvent.cpp | 301 ++ accessible/base/AccEvent.h | 629 +++ accessible/base/AccGroupInfo.cpp | 397 ++ accessible/base/AccGroupInfo.h | 101 + accessible/base/AccIterator.cpp | 362 ++ accessible/base/AccIterator.h | 328 ++ accessible/base/AccTypes.h | 98 + accessible/base/Asserts.cpp | 29 + accessible/base/CacheConstants.h | 98 + accessible/base/CachedTableAccessible.cpp | 430 +++ accessible/base/CachedTableAccessible.h | 294 ++ accessible/base/DocManager.cpp | 567 +++ accessible/base/DocManager.h | 193 + accessible/base/EmbeddedObjCollector.cpp | 62 + accessible/base/EmbeddedObjCollector.h | 68 + accessible/base/EventQueue.cpp | 431 +++ accessible/base/EventQueue.h | 83 + accessible/base/EventTree.cpp | 103 + accessible/base/EventTree.h | 61 + accessible/base/Filters.cpp | 45 + accessible/base/Filters.h | 46 + accessible/base/FocusManager.cpp | 461 +++ accessible/base/FocusManager.h | 169 + accessible/base/HTMLMarkupMap.h | 427 ++ accessible/base/IDSet.h | 129 + accessible/base/Logging.cpp | 993 +++++ accessible/base/Logging.h | 236 ++ accessible/base/MathMLMarkupMap.h | 113 + accessible/base/NotificationController.cpp | 1089 ++++++ accessible/base/NotificationController.h | 393 ++ accessible/base/Pivot.cpp | 669 ++++ accessible/base/Pivot.h | 152 + accessible/base/Platform.h | 149 + accessible/base/Relation.h | 105 + accessible/base/RelationType.h | 168 + accessible/base/RelationTypeMap.h | 90 + accessible/base/Role.h | 1105 ++++++ accessible/base/RoleMap.h | 2084 ++++++++++ accessible/base/SelectionManager.cpp | 247 ++ accessible/base/SelectionManager.h | 141 + accessible/base/States.h | 305 ++ accessible/base/Statistics.h | 42 + accessible/base/StyleInfo.cpp | 50 + accessible/base/StyleInfo.h | 36 + accessible/base/TextAttrs.cpp | 819 ++++ accessible/base/TextAttrs.h | 440 +++ accessible/base/TextLeafRange.cpp | 1957 ++++++++++ accessible/base/TextLeafRange.h | 354 ++ accessible/base/TextRange-inl.h | 26 + accessible/base/TextRange.cpp | 483 +++ accessible/base/TextRange.h | 185 + accessible/base/TextUpdater.cpp | 217 ++ accessible/base/TextUpdater.h | 95 + accessible/base/TreeWalker.cpp | 350 ++ accessible/base/TreeWalker.h | 142 + accessible/base/XULMap.h | 119 + accessible/base/moz.build | 119 + accessible/base/nsAccCache.h | 24 + accessible/base/nsAccUtils.cpp | 629 +++ accessible/base/nsAccUtils.h | 299 ++ accessible/base/nsAccessibilityService.cpp | 1901 +++++++++ accessible/base/nsAccessibilityService.h | 544 +++ accessible/base/nsAccessiblePivot.cpp | 523 +++ accessible/base/nsAccessiblePivot.h | 140 + accessible/base/nsCoreUtils.cpp | 623 +++ accessible/base/nsCoreUtils.h | 329 ++ accessible/base/nsEventShell.cpp | 81 + accessible/base/nsEventShell.h | 66 + accessible/base/nsTextEquivUtils.cpp | 367 ++ accessible/base/nsTextEquivUtils.h | 182 + accessible/basetypes/Accessible.cpp | 723 ++++ accessible/basetypes/Accessible.h | 725 ++++ accessible/basetypes/HyperTextAccessibleBase.cpp | 843 ++++ accessible/basetypes/HyperTextAccessibleBase.h | 310 ++ accessible/basetypes/TableAccessible.h | 172 + accessible/basetypes/TableCellAccessible.h | 68 + accessible/basetypes/moz.build | 25 + accessible/docs/Architecture.md | 65 + accessible/docs/ColorsAndHighContrastMode.md | 48 + accessible/docs/DocumentAccessibilityLifecycle.md | 104 + accessible/docs/GeckoViewThreadTopography.md | 51 + accessible/docs/index.rst | 15 + accessible/generic/ARIAGridAccessible.cpp | 79 + accessible/generic/ARIAGridAccessible.h | 35 + accessible/generic/ApplicationAccessible.cpp | 144 + accessible/generic/ApplicationAccessible.h | 109 + accessible/generic/BaseAccessibles.cpp | 149 + accessible/generic/BaseAccessibles.h | 136 + accessible/generic/DocAccessible-inl.h | 191 + accessible/generic/DocAccessible.cpp | 2771 +++++++++++++ accessible/generic/DocAccessible.h | 830 ++++ accessible/generic/FormControlAccessible.cpp | 84 + accessible/generic/FormControlAccessible.h | 65 + accessible/generic/HyperTextAccessible-inl.h | 48 + accessible/generic/HyperTextAccessible.cpp | 1216 ++++++ accessible/generic/HyperTextAccessible.h | 290 ++ accessible/generic/ImageAccessible.cpp | 262 ++ accessible/generic/ImageAccessible.h | 94 + accessible/generic/LocalAccessible-inl.h | 106 + accessible/generic/LocalAccessible.cpp | 4068 ++++++++++++++++++++ accessible/generic/LocalAccessible.h | 1021 +++++ accessible/generic/OuterDocAccessible.cpp | 226 ++ accessible/generic/OuterDocAccessible.h | 80 + accessible/generic/RootAccessible.cpp | 709 ++++ accessible/generic/RootAccessible.h | 93 + accessible/generic/TextLeafAccessible.cpp | 44 + accessible/generic/TextLeafAccessible.h | 46 + accessible/generic/moz.build | 72 + accessible/html/HTMLCanvasAccessible.cpp | 16 + accessible/html/HTMLCanvasAccessible.h | 35 + accessible/html/HTMLElementAccessibles.cpp | 233 ++ accessible/html/HTMLElementAccessibles.h | 160 + accessible/html/HTMLFormControlAccessible.cpp | 995 +++++ accessible/html/HTMLFormControlAccessible.h | 387 ++ accessible/html/HTMLImageMapAccessible.cpp | 190 + accessible/html/HTMLImageMapAccessible.h | 79 + accessible/html/HTMLLinkAccessible.cpp | 129 + accessible/html/HTMLLinkAccessible.h | 60 + accessible/html/HTMLListAccessible.cpp | 115 + accessible/html/HTMLListAccessible.h | 87 + accessible/html/HTMLSelectAccessible.cpp | 474 +++ accessible/html/HTMLSelectAccessible.h | 216 ++ accessible/html/HTMLTableAccessible.cpp | 769 ++++ accessible/html/HTMLTableAccessible.h | 190 + accessible/html/moz.build | 52 + accessible/interfaces/ia2/moz.build | 78 + accessible/interfaces/moz.build | 46 + accessible/interfaces/msaa/AccessibleMarshal.def | 11 + accessible/interfaces/msaa/AccessibleMarshal.rc | 5 + .../interfaces/msaa/AccessibleMarshalThunk.c | 17 + accessible/interfaces/msaa/ISimpleDOM.idl | 22 + accessible/interfaces/msaa/ISimpleDOMDocument.idl | 83 + accessible/interfaces/msaa/ISimpleDOMNode.idl | 175 + accessible/interfaces/msaa/ISimpleDOMText.idl | 78 + accessible/interfaces/msaa/moz.build | 57 + accessible/interfaces/nsIAccessibilityService.idl | 119 + accessible/interfaces/nsIAccessible.idl | 351 ++ .../interfaces/nsIAccessibleAnnouncementEvent.idl | 23 + accessible/interfaces/nsIAccessibleApplication.idl | 34 + .../interfaces/nsIAccessibleCaretMoveEvent.idl | 33 + accessible/interfaces/nsIAccessibleDocument.idl | 75 + .../interfaces/nsIAccessibleEditableText.idl | 57 + accessible/interfaces/nsIAccessibleEvent.idl | 482 +++ accessible/interfaces/nsIAccessibleHideEvent.idl | 28 + accessible/interfaces/nsIAccessibleHyperLink.idl | 86 + accessible/interfaces/nsIAccessibleHyperText.idl | 54 + accessible/interfaces/nsIAccessibleImage.idl | 30 + .../interfaces/nsIAccessibleMacInterface.idl | 87 + .../nsIAccessibleObjectAttributeChangedEvent.idl | 19 + accessible/interfaces/nsIAccessiblePivot.idl | 259 ++ accessible/interfaces/nsIAccessibleRelation.idl | 183 + accessible/interfaces/nsIAccessibleRole.idl | 1089 ++++++ .../interfaces/nsIAccessibleScrollingEvent.idl | 34 + accessible/interfaces/nsIAccessibleSelectable.idl | 59 + .../interfaces/nsIAccessibleStateChangeEvent.idl | 29 + accessible/interfaces/nsIAccessibleStates.idl | 76 + accessible/interfaces/nsIAccessibleTable.idl | 234 ++ .../interfaces/nsIAccessibleTableChangeEvent.idl | 20 + accessible/interfaces/nsIAccessibleText.idl | 242 ++ .../interfaces/nsIAccessibleTextChangeEvent.idl | 33 + .../interfaces/nsIAccessibleTextLeafRange.idl | 43 + accessible/interfaces/nsIAccessibleTextRange.idl | 72 + .../nsIAccessibleTextSelectionChangeEvent.idl | 21 + accessible/interfaces/nsIAccessibleTypes.idl | 80 + accessible/interfaces/nsIAccessibleValue.idl | 35 + .../nsIAccessibleVirtualCursorChangeEvent.idl | 54 + accessible/ipc/DocAccessibleChildBase.cpp | 348 ++ accessible/ipc/DocAccessibleChildBase.h | 144 + accessible/ipc/DocAccessibleParent.cpp | 1346 +++++++ accessible/ipc/DocAccessibleParent.h | 438 +++ accessible/ipc/DocAccessibleTypes.ipdlh | 19 + accessible/ipc/IPCTypes.h | 189 + accessible/ipc/RemoteAccessibleBase.cpp | 2121 ++++++++++ accessible/ipc/RemoteAccessibleBase.h | 532 +++ accessible/ipc/RemoteAccessibleShared.h | 22 + .../android/DocAccessiblePlatformExtChild.cpp | 75 + .../android/DocAccessiblePlatformExtChild.h | 44 + .../android/DocAccessiblePlatformExtParent.cpp | 16 + .../android/DocAccessiblePlatformExtParent.h | 18 + .../android/PDocAccessiblePlatformExt.ipdl | 30 + accessible/ipc/extension/android/moz.build | 28 + accessible/ipc/extension/moz.build | 12 + .../other/DocAccessiblePlatformExtChild.h | 20 + .../other/DocAccessiblePlatformExtParent.h | 18 + .../extension/other/PDocAccessiblePlatformExt.ipdl | 20 + accessible/ipc/extension/other/moz.build | 17 + accessible/ipc/moz.build | 71 + accessible/ipc/other/DocAccessibleChild.cpp | 87 + accessible/ipc/other/DocAccessibleChild.h | 68 + accessible/ipc/other/PDocAccessible.ipdl | 185 + accessible/ipc/other/RemoteAccessible.cpp | 43 + accessible/ipc/other/RemoteAccessible.h | 57 + accessible/ipc/other/moz.build | 51 + accessible/ipc/win/DocAccessibleChild.cpp | 66 + accessible/ipc/win/DocAccessibleChild.h | 39 + accessible/ipc/win/PDocAccessible.ipdl | 149 + accessible/ipc/win/RemoteAccessible.h | 51 + accessible/ipc/win/moz.build | 42 + accessible/mac/.clang-format | 11 + accessible/mac/AccessibleWrap.h | 95 + accessible/mac/AccessibleWrap.mm | 405 ++ accessible/mac/ApplicationAccessibleWrap.h | 21 + accessible/mac/DocAccessibleWrap.h | 46 + accessible/mac/DocAccessibleWrap.mm | 104 + accessible/mac/GeckoTextMarker.h | 138 + accessible/mac/GeckoTextMarker.mm | 537 +++ accessible/mac/HyperTextAccessibleWrap.h | 19 + accessible/mac/MOXAccessibleBase.h | 143 + accessible/mac/MOXAccessibleBase.mm | 575 +++ accessible/mac/MOXAccessibleProtocol.h | 534 +++ accessible/mac/MOXLandmarkAccessibles.h | 15 + accessible/mac/MOXLandmarkAccessibles.mm | 15 + accessible/mac/MOXMathAccessibles.h | 64 + accessible/mac/MOXMathAccessibles.mm | 117 + accessible/mac/MOXSearchInfo.h | 43 + accessible/mac/MOXSearchInfo.mm | 375 ++ accessible/mac/MOXTextMarkerDelegate.h | 169 + accessible/mac/MOXTextMarkerDelegate.mm | 527 +++ accessible/mac/MOXWebAreaAccessible.h | 105 + accessible/mac/MOXWebAreaAccessible.mm | 276 ++ accessible/mac/MacUtils.h | 62 + accessible/mac/MacUtils.mm | 162 + accessible/mac/Platform.mm | 246 ++ accessible/mac/PlatformExtTypes.h | 25 + accessible/mac/RootAccessibleWrap.h | 40 + accessible/mac/RootAccessibleWrap.mm | 51 + accessible/mac/RotorRules.h | 144 + accessible/mac/RotorRules.mm | 392 ++ accessible/mac/SelectorMapGen.py | 61 + accessible/mac/moz.build | 72 + accessible/mac/mozAccessible.h | 279 ++ accessible/mac/mozAccessible.mm | 982 +++++ accessible/mac/mozAccessibleProtocol.h | 65 + accessible/mac/mozActionElements.h | 108 + accessible/mac/mozActionElements.mm | 228 ++ accessible/mac/mozHTMLAccessible.h | 44 + accessible/mac/mozHTMLAccessible.mm | 83 + accessible/mac/mozRootAccessible.h | 58 + accessible/mac/mozRootAccessible.mm | 84 + accessible/mac/mozSelectableElements.h | 128 + accessible/mac/mozSelectableElements.mm | 330 ++ accessible/mac/mozTableAccessible.h | 177 + accessible/mac/mozTableAccessible.mm | 629 +++ accessible/mac/mozTextAccessible.h | 114 + accessible/mac/mozTextAccessible.mm | 423 ++ accessible/moz.build | 58 + accessible/other/AccessibleWrap.cpp | 19 + accessible/other/AccessibleWrap.h | 28 + accessible/other/ApplicationAccessibleWrap.h | 20 + accessible/other/DocAccessibleWrap.h | 23 + accessible/other/HyperTextAccessibleWrap.h | 19 + accessible/other/Platform.cpp | 35 + accessible/other/RootAccessibleWrap.h | 23 + accessible/other/moz.build | 24 + 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 + accessible/tests/crashtests/1072792.xhtml | 5 + accessible/tests/crashtests/1380199.html | 15 + accessible/tests/crashtests/1402999.html | 11 + accessible/tests/crashtests/1415667.html | 1 + accessible/tests/crashtests/1463962.html | 4 + accessible/tests/crashtests/1472024-1.html | 7 + accessible/tests/crashtests/1472024-2.html | 20 + accessible/tests/crashtests/1484778.html | 26 + accessible/tests/crashtests/1494707.html | 15 + accessible/tests/crashtests/1503964.html | 4 + accessible/tests/crashtests/1572811.html | 9 + accessible/tests/crashtests/1578282.html | 21 + accessible/tests/crashtests/1585851.html | 21 + accessible/tests/crashtests/1655983.html | 6 + accessible/tests/crashtests/448064.xhtml | 70 + accessible/tests/crashtests/884202.html | 21 + accessible/tests/crashtests/890760.html | 14 + accessible/tests/crashtests/893515.html | 15 + accessible/tests/crashtests/crashtests.list | 22 + .../crashtests/last_test_to_unload_testsuite.xhtml | 36 + accessible/tests/mochitest/.eslintrc.js | 24 + accessible/tests/mochitest/a11y.ini | 21 + accessible/tests/mochitest/actions.js | 231 ++ accessible/tests/mochitest/actions/a11y.ini | 17 + .../tests/mochitest/actions/test_anchors.html | 146 + accessible/tests/mochitest/actions/test_aria.html | 200 + .../tests/mochitest/actions/test_controls.html | 107 + .../tests/mochitest/actions/test_general.html | 105 + .../tests/mochitest/actions/test_general.xhtml | 167 + accessible/tests/mochitest/actions/test_keys.html | 57 + accessible/tests/mochitest/actions/test_keys.xhtml | 124 + accessible/tests/mochitest/actions/test_link.html | 145 + accessible/tests/mochitest/actions/test_media.html | 130 + .../tests/mochitest/actions/test_select.html | 67 + accessible/tests/mochitest/actions/test_tree.xhtml | 127 + .../tests/mochitest/actions/test_treegrid.xhtml | 190 + accessible/tests/mochitest/aom/a11y.ini | 3 + accessible/tests/mochitest/aom/test_general.html | 208 + accessible/tests/mochitest/attributes.js | 516 +++ accessible/tests/mochitest/attributes/a11y.ini | 14 + .../attributes/test_dpub_aria_xml-roles.html | 120 + .../attributes/test_graphics_aria_xml-roles.html | 48 + .../tests/mochitest/attributes/test_listbox.html | 82 + .../tests/mochitest/attributes/test_obj.html | 292 ++ .../tests/mochitest/attributes/test_obj_css.html | 225 ++ .../tests/mochitest/attributes/test_obj_group.html | 564 +++ .../mochitest/attributes/test_obj_group.xhtml | 215 ++ .../mochitest/attributes/test_obj_group_tree.xhtml | 84 + .../tests/mochitest/attributes/test_tag.html | 80 + .../tests/mochitest/attributes/test_xml-roles.html | 267 ++ accessible/tests/mochitest/autocomplete.js | 198 + accessible/tests/mochitest/bounds/a11y.ini | 5 + accessible/tests/mochitest/bounds/test_list.html | 78 + accessible/tests/mochitest/browser.js | 156 + accessible/tests/mochitest/common.js | 1046 +++++ accessible/tests/mochitest/dumbfile.zip | Bin 0 -> 22 bytes accessible/tests/mochitest/elm/a11y.ini | 14 + accessible/tests/mochitest/elm/test_HTMLSpec.html | 2024 ++++++++++ .../tests/mochitest/elm/test_MathMLSpec.html | 616 +++ accessible/tests/mochitest/elm/test_figure.html | 60 + accessible/tests/mochitest/elm/test_listbox.xhtml | 73 + .../tests/mochitest/elm/test_nsApplicationAcc.html | 67 + .../tests/mochitest/elm/test_shadowroot.html | 35 + .../mochitest/elm/test_shadowroot_subframe.html | 68 + accessible/tests/mochitest/events.js | 2660 +++++++++++++ accessible/tests/mochitest/events/a11y.ini | 68 + accessible/tests/mochitest/events/docload/a11y.ini | 13 + .../mochitest/events/docload/docload_wnd.html | 37 + .../events/docload/test_docload_aria.html | 75 + .../events/docload/test_docload_busy.html | 83 + .../events/docload/test_docload_embedded.html | 85 + .../events/docload/test_docload_iframe.html | 99 + .../events/docload/test_docload_root.html | 125 + .../events/docload/test_docload_shutdown.html | 142 + accessible/tests/mochitest/events/focus.html | 10 + accessible/tests/mochitest/events/scroll.html | 181 + accessible/tests/mochitest/events/slow_image.sjs | 55 + .../tests/mochitest/events/test_announcement.html | 61 + .../tests/mochitest/events/test_aria_alert.html | 84 + .../tests/mochitest/events/test_aria_menu.html | 267 ++ .../tests/mochitest/events/test_aria_objattr.html | 68 + .../tests/mochitest/events/test_aria_owns.html | 122 + .../mochitest/events/test_aria_statechange.html | 231 ++ .../tests/mochitest/events/test_attrchange.html | 107 + accessible/tests/mochitest/events/test_attrs.html | 85 + .../tests/mochitest/events/test_bug1322593-2.html | 77 + .../tests/mochitest/events/test_bug1322593.html | 74 + .../tests/mochitest/events/test_caretmove.html | 142 + .../tests/mochitest/events/test_coalescence.html | 817 ++++ .../tests/mochitest/events/test_contextmenu.html | 131 + .../tests/mochitest/events/test_descrchange.html | 142 + .../tests/mochitest/events/test_dragndrop.html | 106 + accessible/tests/mochitest/events/test_flush.html | 74 + .../events/test_focus_aria_activedescendant.html | 327 ++ .../mochitest/events/test_focus_autocomplete.html | 83 + .../mochitest/events/test_focus_autocomplete.xhtml | 507 +++ .../tests/mochitest/events/test_focus_canvas.html | 58 + .../mochitest/events/test_focus_contextmenu.xhtml | 98 + .../mochitest/events/test_focus_controls.html | 76 + .../tests/mochitest/events/test_focus_doc.html | 92 + .../tests/mochitest/events/test_focus_general.html | 176 + .../mochitest/events/test_focus_general.xhtml | 124 + .../mochitest/events/test_focus_listcontrols.xhtml | 153 + .../tests/mochitest/events/test_focus_menu.xhtml | 117 + .../tests/mochitest/events/test_focus_name.html | 116 + .../tests/mochitest/events/test_focus_removal.html | 95 + .../tests/mochitest/events/test_focus_selects.html | 173 + .../tests/mochitest/events/test_focus_tabbox.xhtml | 102 + .../tests/mochitest/events/test_focus_tree.xhtml | 117 + .../events/test_focusable_statechange.html | 128 + .../tests/mochitest/events/test_fromUserInput.html | 112 + accessible/tests/mochitest/events/test_label.xhtml | 178 + accessible/tests/mochitest/events/test_menu.xhtml | 200 + .../tests/mochitest/events/test_mutation.html | 580 +++ .../tests/mochitest/events/test_namechange.html | 185 + .../tests/mochitest/events/test_namechange.xhtml | 90 + .../tests/mochitest/events/test_scroll.xhtml | 107 + .../tests/mochitest/events/test_scroll_caret.xhtml | 91 + .../tests/mochitest/events/test_selection.html | 109 + .../tests/mochitest/events/test_selection.xhtml | 254 ++ .../mochitest/events/test_selection_aria.html | 122 + .../tests/mochitest/events/test_statechange.html | 582 +++ .../tests/mochitest/events/test_statechange.xhtml | 117 + accessible/tests/mochitest/events/test_text.html | 310 ++ .../tests/mochitest/events/test_text_alg.html | 249 ++ .../mochitest/events/test_textattrchange.html | 105 + .../tests/mochitest/events/test_textselchange.html | 82 + accessible/tests/mochitest/events/test_tree.xhtml | 358 ++ .../tests/mochitest/events/test_valuechange.html | 315 ++ accessible/tests/mochitest/focus/a11y.ini | 9 + .../tests/mochitest/focus/test_focus_radio.xhtml | 84 + .../tests/mochitest/focus/test_focusedChild.html | 81 + .../tests/mochitest/focus/test_takeFocus.html | 109 + .../tests/mochitest/focus/test_takeFocus.xhtml | 104 + accessible/tests/mochitest/formimage.png | Bin 0 -> 20105 bytes accessible/tests/mochitest/grid.js | 142 + accessible/tests/mochitest/hittest/a11y.ini | 13 + .../tests/mochitest/hittest/test_browser.html | 61 + .../tests/mochitest/hittest/test_general.html | 110 + accessible/tests/mochitest/hittest/test_menu.xhtml | 133 + .../tests/mochitest/hittest/test_shadowroot.html | 35 + .../hittest/test_shadowroot_subframe.html | 58 + accessible/tests/mochitest/hittest/test_zoom.html | 59 + .../tests/mochitest/hittest/test_zoom_text.html | 57 + .../tests/mochitest/hittest/test_zoom_tree.xhtml | 97 + accessible/tests/mochitest/hittest/zoom_tree.xhtml | 18 + accessible/tests/mochitest/hyperlink/a11y.ini | 7 + accessible/tests/mochitest/hyperlink/hyperlink.js | 46 + .../tests/mochitest/hyperlink/test_general.html | 279 ++ .../tests/mochitest/hyperlink/test_general.xhtml | 98 + accessible/tests/mochitest/hypertext/a11y.ini | 7 + .../tests/mochitest/hypertext/test_general.html | 150 + .../tests/mochitest/hypertext/test_update.html | 214 + accessible/tests/mochitest/layout.js | 390 ++ accessible/tests/mochitest/letters.gif | Bin 0 -> 5596 bytes accessible/tests/mochitest/longdesc_src.html | 8 + accessible/tests/mochitest/moz.build | 36 + accessible/tests/mochitest/moz.png | Bin 0 -> 1991 bytes accessible/tests/mochitest/name.js | 38 + accessible/tests/mochitest/name/a11y.ini | 18 + accessible/tests/mochitest/name/markup.js | 425 ++ accessible/tests/mochitest/name/markuprules.xml | 367 ++ .../mochitest/name/test_ARIACore_examples.html | 90 + .../tests/mochitest/name/test_browserui.xhtml | 85 + .../tests/mochitest/name/test_counterstyle.html | 150 + accessible/tests/mochitest/name/test_general.html | 732 ++++ accessible/tests/mochitest/name/test_general.xhtml | 343 ++ accessible/tests/mochitest/name/test_link.html | 87 + accessible/tests/mochitest/name/test_list.html | 103 + accessible/tests/mochitest/name/test_markup.html | 58 + accessible/tests/mochitest/name/test_svg.html | 53 + accessible/tests/mochitest/name/test_tree.xhtml | 207 + accessible/tests/mochitest/pivot.js | 664 ++++ accessible/tests/mochitest/pivot/a11y.ini | 8 + .../tests/mochitest/pivot/doc_virtualcursor.html | 38 + .../mochitest/pivot/doc_virtualcursor_text.html | 37 + .../tests/mochitest/pivot/test_virtualcursor.html | 119 + .../mochitest/pivot/test_virtualcursor_text.html | 271 ++ accessible/tests/mochitest/promisified-events.js | 328 ++ accessible/tests/mochitest/relations.js | 204 + accessible/tests/mochitest/relations/a11y.ini | 14 + .../tests/mochitest/relations/test_embeds.xhtml | 128 + .../tests/mochitest/relations/test_general.html | 456 +++ .../tests/mochitest/relations/test_general.xhtml | 237 ++ .../mochitest/relations/test_groupInfoUpdate.html | 57 + .../tests/mochitest/relations/test_shadowdom.html | 58 + .../mochitest/relations/test_tabbrowser.xhtml | 109 + .../tests/mochitest/relations/test_tree.xhtml | 105 + .../mochitest/relations/test_ui_modalprompt.html | 111 + .../tests/mochitest/relations/test_update.html | 213 + accessible/tests/mochitest/role.js | 200 + accessible/tests/mochitest/role/a11y.ini | 13 + .../mochitest/role/chrome_body_role_alert.xhtml | 6 + accessible/tests/mochitest/role/test_aria.html | 729 ++++ accessible/tests/mochitest/role/test_aria.xhtml | 65 + .../tests/mochitest/role/test_dpub_aria.html | 114 + accessible/tests/mochitest/role/test_general.html | 201 + accessible/tests/mochitest/role/test_general.xhtml | 59 + .../tests/mochitest/role/test_graphics_aria.html | 42 + accessible/tests/mochitest/role/test_svg.html | 93 + accessible/tests/mochitest/scroll/a11y.ini | 5 + accessible/tests/mochitest/scroll/test_zoom.html | 145 + accessible/tests/mochitest/selectable.js | 138 + accessible/tests/mochitest/selectable/a11y.ini | 10 + .../tests/mochitest/selectable/test_listbox.xhtml | 144 + .../tests/mochitest/selectable/test_menu.xhtml | 77 + .../tests/mochitest/selectable/test_menulist.xhtml | 95 + .../tests/mochitest/selectable/test_tabs.xhtml | 93 + .../tests/mochitest/selectable/test_tree.xhtml | 171 + accessible/tests/mochitest/states.js | 365 ++ accessible/tests/mochitest/states/a11y.ini | 36 + accessible/tests/mochitest/states/test_aria.html | 655 ++++ accessible/tests/mochitest/states/test_aria.xhtml | 70 + .../tests/mochitest/states/test_aria_imgmap.html | 75 + .../mochitest/states/test_aria_widgetitems.html | 152 + .../tests/mochitest/states/test_buttons.html | 83 + .../tests/mochitest/states/test_controls.html | 51 + .../tests/mochitest/states/test_controls.xhtml | 153 + accessible/tests/mochitest/states/test_doc.html | 95 + .../tests/mochitest/states/test_doc_busy.html | 130 + .../tests/mochitest/states/test_docarticle.html | 78 + .../tests/mochitest/states/test_editablebody.html | 44 + .../tests/mochitest/states/test_expandable.xhtml | 112 + accessible/tests/mochitest/states/test_frames.html | 93 + accessible/tests/mochitest/states/test_inputs.html | 268 ++ accessible/tests/mochitest/states/test_link.html | 85 + accessible/tests/mochitest/states/test_popup.xhtml | 54 + .../tests/mochitest/states/test_selects.html | 166 + accessible/tests/mochitest/states/test_stale.html | 108 + accessible/tests/mochitest/states/test_tabs.xhtml | 66 + .../tests/mochitest/states/test_textbox.xhtml | 78 + accessible/tests/mochitest/states/test_tree.xhtml | 146 + .../tests/mochitest/states/test_visibility.html | 75 + .../tests/mochitest/states/test_visibility.xhtml | 162 + accessible/tests/mochitest/states/z_frames.html | 11 + .../tests/mochitest/states/z_frames_article.html | 11 + .../tests/mochitest/states/z_frames_checkbox.html | 11 + .../tests/mochitest/states/z_frames_textbox.html | 11 + .../tests/mochitest/states/z_frames_update.html | 21 + accessible/tests/mochitest/table.js | 851 ++++ accessible/tests/mochitest/table/a11y.ini | 24 + .../tests/mochitest/table/test_css_tables.html | 114 + .../mochitest/table/test_headers_ariagrid.html | 183 + .../mochitest/table/test_headers_ariatable.html | 94 + .../tests/mochitest/table/test_headers_table.html | 756 ++++ .../tests/mochitest/table/test_headers_tree.xhtml | 100 + .../mochitest/table/test_indexes_ariagrid.html | 159 + .../tests/mochitest/table/test_indexes_table.html | 481 +++ .../tests/mochitest/table/test_indexes_tree.xhtml | 70 + .../tests/mochitest/table/test_layoutguess.html | 554 +++ accessible/tests/mochitest/table/test_mtable.html | 160 + .../tests/mochitest/table/test_sels_ariagrid.html | 147 + .../tests/mochitest/table/test_sels_table.html | 155 + .../tests/mochitest/table/test_sels_tree.xhtml | 76 + .../mochitest/table/test_struct_ariagrid.html | 163 + .../mochitest/table/test_struct_ariatreegrid.html | 74 + .../tests/mochitest/table/test_struct_table.html | 217 ++ .../tests/mochitest/table/test_struct_tree.xhtml | 73 + accessible/tests/mochitest/table/test_table_1.html | 107 + accessible/tests/mochitest/table/test_table_2.html | 87 + .../tests/mochitest/table/test_table_mutation.html | 100 + .../tests/mochitest/test_OuterDocAccessible.html | 87 + .../tests/mochitest/test_aria_token_attrs.html | 417 ++ accessible/tests/mochitest/test_bug420863.html | 99 + ...test_custom_element_accessibility_defaults.html | 383 ++ accessible/tests/mochitest/test_descr.html | 134 + .../mochitest/test_nsIAccessibleDocument.html | 94 + .../tests/mochitest/test_nsIAccessibleImage.html | 198 + accessible/tests/mochitest/text.js | 814 ++++ accessible/tests/mochitest/text/a11y.ini | 19 + accessible/tests/mochitest/text/doc.html | 9 + .../tests/mochitest/text/test_atcaretoffset.html | 425 ++ .../tests/mochitest/text/test_charboundary.html | 138 + accessible/tests/mochitest/text/test_doc.html | 40 + accessible/tests/mochitest/text/test_dynamic.html | 80 + accessible/tests/mochitest/text/test_general.xhtml | 79 + accessible/tests/mochitest/text/test_gettext.html | 135 + .../tests/mochitest/text/test_hypertext.html | 150 + .../tests/mochitest/text/test_lineboundary.html | 422 ++ .../mochitest/text/test_paragraphboundary.html | 148 + .../tests/mochitest/text/test_passwords.html | 72 + .../tests/mochitest/text/test_selection.html | 119 + .../mochitest/text/test_settext_input_event.html | 38 + .../tests/mochitest/text/test_textBounds.html | 36 + .../tests/mochitest/text/test_wordboundary.html | 361 ++ accessible/tests/mochitest/text/test_words.html | 131 + accessible/tests/mochitest/textattrs/a11y.ini | 11 + .../tests/mochitest/textattrs/test_general.html | 823 ++++ .../tests/mochitest/textattrs/test_general.xhtml | 51 + .../tests/mochitest/textattrs/test_invalid.html | 59 + .../tests/mochitest/textattrs/test_mathml.html | 47 + .../tests/mochitest/textattrs/test_spelling.html | 52 + accessible/tests/mochitest/textattrs/test_svg.html | 52 + accessible/tests/mochitest/textcaret/a11y.ini | 5 + .../tests/mochitest/textcaret/test_general.html | 174 + accessible/tests/mochitest/textrange/a11y.ini | 7 + .../tests/mochitest/textrange/test_general.html | 106 + .../tests/mochitest/textrange/test_selection.html | 144 + accessible/tests/mochitest/textselection/a11y.ini | 6 + .../mochitest/textselection/test_general.html | 221 ++ .../mochitest/textselection/test_userinput.html | 76 + accessible/tests/mochitest/tree/a11y.ini | 58 + accessible/tests/mochitest/tree/dockids.html | 32 + .../tests/mochitest/tree/test_applicationacc.xhtml | 73 + .../mochitest/tree/test_aria_display_contents.html | 173 + .../tests/mochitest/tree/test_aria_globals.html | 127 + .../tests/mochitest/tree/test_aria_grid.html | 318 ++ .../tests/mochitest/tree/test_aria_imgmap.html | 104 + .../tests/mochitest/tree/test_aria_list.html | 90 + .../tests/mochitest/tree/test_aria_menu.html | 91 + .../tests/mochitest/tree/test_aria_owns.html | 197 + .../mochitest/tree/test_aria_presentation.html | 176 + .../tests/mochitest/tree/test_aria_table.html | 101 + .../tests/mochitest/tree/test_brokencontext.html | 214 + accessible/tests/mochitest/tree/test_button.xhtml | 83 + accessible/tests/mochitest/tree/test_canvas.html | 53 + .../tests/mochitest/tree/test_combobox.xhtml | 116 + .../tests/mochitest/tree/test_cssflexbox.html | 78 + .../tests/mochitest/tree/test_cssoverflow.html | 135 + .../mochitest/tree/test_display_contents.html | 92 + accessible/tests/mochitest/tree/test_divs.html | 351 ++ .../tests/mochitest/tree/test_dochierarchy.html | 84 + accessible/tests/mochitest/tree/test_dockids.html | 62 + accessible/tests/mochitest/tree/test_filectrl.html | 56 + accessible/tests/mochitest/tree/test_formctrl.html | 125 + .../tests/mochitest/tree/test_formctrl.xhtml | 129 + .../tests/mochitest/tree/test_gencontent.html | 69 + .../tests/mochitest/tree/test_groupbox.xhtml | 63 + .../tests/mochitest/tree/test_html_in_mathml.html | 61 + accessible/tests/mochitest/tree/test_iframe.html | 50 + accessible/tests/mochitest/tree/test_image.xhtml | 58 + accessible/tests/mochitest/tree/test_img.html | 84 + .../tests/mochitest/tree/test_invalid_img.xhtml | 48 + .../mochitest/tree/test_invalidationlist.html | 56 + accessible/tests/mochitest/tree/test_list.html | 346 ++ accessible/tests/mochitest/tree/test_map.html | 81 + accessible/tests/mochitest/tree/test_media.html | 127 + accessible/tests/mochitest/tree/test_select.html | 121 + accessible/tests/mochitest/tree/test_svg.html | 127 + accessible/tests/mochitest/tree/test_tabbox.xhtml | 108 + .../tests/mochitest/tree/test_tabbrowser.xhtml | 261 ++ accessible/tests/mochitest/tree/test_table.html | 507 +++ accessible/tests/mochitest/tree/test_table_2.html | 242 ++ accessible/tests/mochitest/tree/test_table_3.html | 244 ++ accessible/tests/mochitest/tree/test_tree.xhtml | 182 + accessible/tests/mochitest/tree/test_txtcntr.html | 234 ++ accessible/tests/mochitest/tree/test_txtctrl.html | 171 + accessible/tests/mochitest/tree/test_txtctrl.xhtml | 86 + accessible/tests/mochitest/tree/wnd.xhtml | 8 + accessible/tests/mochitest/treeupdate/a11y.ini | 46 + .../mochitest/treeupdate/test_ariadialog.html | 113 + .../mochitest/treeupdate/test_ariahidden.html | 118 + .../tests/mochitest/treeupdate/test_ariaowns.html | 851 ++++ .../mochitest/treeupdate/test_bug1040735.html | 40 + .../mochitest/treeupdate/test_bug1175913.html | 95 + .../mochitest/treeupdate/test_bug1189277.html | 82 + .../mochitest/treeupdate/test_bug1276857.html | 131 + .../treeupdate/test_bug1276857_subframe.html | 33 + .../mochitest/treeupdate/test_bug852150.xhtml | 57 + .../mochitest/treeupdate/test_bug883708.xhtml | 31 + .../mochitest/treeupdate/test_bug884251.xhtml | 19 + .../tests/mochitest/treeupdate/test_bug895082.html | 49 + .../tests/mochitest/treeupdate/test_canvas.html | 87 + .../mochitest/treeupdate/test_contextmenu.xhtml | 315 ++ .../mochitest/treeupdate/test_cssoverflow.html | 149 + .../tests/mochitest/treeupdate/test_deck.xhtml | 154 + .../mochitest/treeupdate/test_delayed_removal.html | 500 +++ .../tests/mochitest/treeupdate/test_doc.html | 415 ++ .../mochitest/treeupdate/test_gencontent.html | 187 + .../tests/mochitest/treeupdate/test_general.html | 174 + .../tests/mochitest/treeupdate/test_hidden.html | 125 + .../tests/mochitest/treeupdate/test_imagemap.html | 402 ++ .../tests/mochitest/treeupdate/test_inert.html | 138 + .../mochitest/treeupdate/test_inner_reorder.html | 148 + .../tests/mochitest/treeupdate/test_list.html | 139 + .../treeupdate/test_list_editabledoc.html | 100 + .../mochitest/treeupdate/test_list_style.html | 181 + .../tests/mochitest/treeupdate/test_listbox.xhtml | 181 + .../tests/mochitest/treeupdate/test_menu.xhtml | 127 + .../mochitest/treeupdate/test_menubutton.xhtml | 141 + .../tests/mochitest/treeupdate/test_optgroup.html | 122 + .../mochitest/treeupdate/test_recreation.html | 93 + .../tests/mochitest/treeupdate/test_select.html | 191 + .../mochitest/treeupdate/test_shadow_slots.html | 554 +++ .../tests/mochitest/treeupdate/test_shutdown.xhtml | 131 + .../tests/mochitest/treeupdate/test_table.html | 74 + .../tests/mochitest/treeupdate/test_textleaf.html | 167 + .../tests/mochitest/treeupdate/test_tooltip.xhtml | 75 + .../mochitest/treeupdate/test_visibility.html | 411 ++ .../mochitest/treeupdate/test_whitespace.html | 200 + accessible/tests/mochitest/treeview.css | 15 + accessible/tests/mochitest/treeview.js | 273 ++ accessible/tests/mochitest/value.js | 52 + accessible/tests/mochitest/value/a11y.ini | 11 + .../tests/mochitest/value/test_ariavalue.html | 85 + .../tests/mochitest/value/test_datetime.html | 76 + accessible/tests/mochitest/value/test_general.html | 159 + accessible/tests/mochitest/value/test_meter.html | 82 + accessible/tests/mochitest/value/test_number.html | 56 + .../tests/mochitest/value/test_progress.html | 61 + accessible/tests/mochitest/value/test_range.html | 59 + accessible/windows/ia2/ia2Accessible.cpp | 608 +++ accessible/windows/ia2/ia2Accessible.h | 125 + accessible/windows/ia2/ia2AccessibleAction.cpp | 152 + accessible/windows/ia2/ia2AccessibleAction.h | 85 + .../windows/ia2/ia2AccessibleApplication.cpp | 94 + accessible/windows/ia2/ia2AccessibleApplication.h | 49 + accessible/windows/ia2/ia2AccessibleComponent.cpp | 106 + accessible/windows/ia2/ia2AccessibleComponent.h | 40 + .../windows/ia2/ia2AccessibleEditableText.cpp | 106 + accessible/windows/ia2/ia2AccessibleEditableText.h | 59 + accessible/windows/ia2/ia2AccessibleHyperlink.cpp | 164 + accessible/windows/ia2/ia2AccessibleHyperlink.h | 55 + accessible/windows/ia2/ia2AccessibleHypertext.cpp | 141 + accessible/windows/ia2/ia2AccessibleHypertext.h | 68 + accessible/windows/ia2/ia2AccessibleImage.cpp | 81 + accessible/windows/ia2/ia2AccessibleImage.h | 51 + accessible/windows/ia2/ia2AccessibleRelation.cpp | 94 + accessible/windows/ia2/ia2AccessibleRelation.h | 80 + accessible/windows/ia2/ia2AccessibleTable.cpp | 534 +++ accessible/windows/ia2/ia2AccessibleTable.h | 178 + accessible/windows/ia2/ia2AccessibleTableCell.cpp | 186 + accessible/windows/ia2/ia2AccessibleTableCell.h | 71 + accessible/windows/ia2/ia2AccessibleText.cpp | 481 +++ accessible/windows/ia2/ia2AccessibleText.h | 255 ++ accessible/windows/ia2/ia2AccessibleValue.cpp | 125 + accessible/windows/ia2/ia2AccessibleValue.h | 44 + accessible/windows/ia2/moz.build | 59 + accessible/windows/moz.build | 7 + accessible/windows/msaa/AccessibleWrap.cpp | 155 + accessible/windows/msaa/AccessibleWrap.h | 74 + .../windows/msaa/ApplicationAccessibleWrap.cpp | 43 + .../windows/msaa/ApplicationAccessibleWrap.h | 31 + accessible/windows/msaa/Compatibility.cpp | 284 ++ accessible/windows/msaa/Compatibility.h | 127 + accessible/windows/msaa/CompatibilityUIA.cpp | 345 ++ accessible/windows/msaa/DocAccessibleWrap.cpp | 100 + accessible/windows/msaa/DocAccessibleWrap.h | 39 + accessible/windows/msaa/EnumVariant.cpp | 90 + accessible/windows/msaa/EnumVariant.h | 62 + .../windows/msaa/HyperTextAccessibleWrap.cpp | 38 + accessible/windows/msaa/HyperTextAccessibleWrap.h | 34 + accessible/windows/msaa/IUnknownImpl.cpp | 36 + accessible/windows/msaa/IUnknownImpl.h | 173 + accessible/windows/msaa/LazyInstantiator.cpp | 764 ++++ accessible/windows/msaa/LazyInstantiator.h | 129 + accessible/windows/msaa/MsaaAccessible.cpp | 1397 +++++++ accessible/windows/msaa/MsaaAccessible.h | 203 + accessible/windows/msaa/MsaaDocAccessible.cpp | 156 + accessible/windows/msaa/MsaaDocAccessible.h | 71 + accessible/windows/msaa/MsaaIdGenerator.cpp | 87 + accessible/windows/msaa/MsaaIdGenerator.h | 48 + accessible/windows/msaa/MsaaRootAccessible.cpp | 67 + accessible/windows/msaa/MsaaRootAccessible.h | 50 + accessible/windows/msaa/MsaaXULMenuAccessible.cpp | 83 + accessible/windows/msaa/MsaaXULMenuAccessible.h | 30 + accessible/windows/msaa/NtUndoc.h | 85 + accessible/windows/msaa/Platform.cpp | 262 ++ accessible/windows/msaa/RootAccessibleWrap.cpp | 44 + accessible/windows/msaa/RootAccessibleWrap.h | 33 + accessible/windows/msaa/ServiceProvider.cpp | 106 + accessible/windows/msaa/ServiceProvider.h | 37 + accessible/windows/msaa/moz.build | 77 + accessible/windows/msaa/nsEventMap.h | 109 + accessible/windows/msaa/nsWinUtils.cpp | 167 + accessible/windows/msaa/nsWinUtils.h | 105 + accessible/windows/sdn/moz.build | 24 + accessible/windows/sdn/sdnAccessible-inl.h | 49 + accessible/windows/sdn/sdnAccessible.cpp | 522 +++ accessible/windows/sdn/sdnAccessible.h | 149 + accessible/windows/sdn/sdnDocAccessible.cpp | 117 + accessible/windows/sdn/sdnDocAccessible.h | 52 + accessible/windows/sdn/sdnTextAccessible.cpp | 166 + accessible/windows/sdn/sdnTextAccessible.h | 69 + accessible/windows/uia/moz.build | 22 + accessible/windows/uia/uiaRawElmProvider.cpp | 206 + accessible/windows/uia/uiaRawElmProvider.h | 75 + accessible/xpcom/AccEventGen.py | 256 ++ accessible/xpcom/AccEvents.conf | 21 + accessible/xpcom/moz.build | 80 + accessible/xpcom/nsAccessibleRelation.cpp | 60 + accessible/xpcom/nsAccessibleRelation.h | 46 + accessible/xpcom/xpcAccessibilityService.cpp | 311 ++ accessible/xpcom/xpcAccessibilityService.h | 68 + accessible/xpcom/xpcAccessible.cpp | 699 ++++ accessible/xpcom/xpcAccessible.h | 112 + accessible/xpcom/xpcAccessibleApplication.cpp | 60 + accessible/xpcom/xpcAccessibleApplication.h | 47 + accessible/xpcom/xpcAccessibleDocument.cpp | 191 + accessible/xpcom/xpcAccessibleDocument.h | 138 + accessible/xpcom/xpcAccessibleGeneric.cpp | 61 + accessible/xpcom/xpcAccessibleGeneric.h | 96 + accessible/xpcom/xpcAccessibleHyperLink.cpp | 95 + accessible/xpcom/xpcAccessibleHyperLink.h | 48 + accessible/xpcom/xpcAccessibleHyperText.cpp | 570 +++ accessible/xpcom/xpcAccessibleHyperText.h | 57 + accessible/xpcom/xpcAccessibleImage.cpp | 51 + accessible/xpcom/xpcAccessibleImage.h | 40 + accessible/xpcom/xpcAccessibleMacInterface.h | 104 + accessible/xpcom/xpcAccessibleMacInterface.mm | 539 +++ accessible/xpcom/xpcAccessibleSelectable.cpp | 121 + accessible/xpcom/xpcAccessibleSelectable.h | 50 + accessible/xpcom/xpcAccessibleTable.cpp | 363 ++ accessible/xpcom/xpcAccessibleTable.h | 74 + accessible/xpcom/xpcAccessibleTableCell.cpp | 141 + accessible/xpcom/xpcAccessibleTableCell.h | 52 + accessible/xpcom/xpcAccessibleTextLeafRange.cpp | 84 + accessible/xpcom/xpcAccessibleTextLeafRange.h | 45 + accessible/xpcom/xpcAccessibleTextRange.cpp | 159 + accessible/xpcom/xpcAccessibleTextRange.h | 81 + accessible/xpcom/xpcAccessibleValue.cpp | 99 + accessible/xpcom/xpcAccessibleValue.h | 42 + accessible/xul/XULAlertAccessible.cpp | 44 + accessible/xul/XULAlertAccessible.h | 40 + accessible/xul/XULComboboxAccessible.cpp | 142 + accessible/xul/XULComboboxAccessible.h | 43 + accessible/xul/XULElementAccessibles.cpp | 205 + accessible/xul/XULElementAccessibles.h | 108 + accessible/xul/XULFormControlAccessible.cpp | 450 +++ accessible/xul/XULFormControlAccessible.h | 186 + accessible/xul/XULListboxAccessible.cpp | 456 +++ accessible/xul/XULListboxAccessible.h | 136 + accessible/xul/XULMenuAccessible.cpp | 484 +++ accessible/xul/XULMenuAccessible.h | 113 + accessible/xul/XULSelectControlAccessible.cpp | 253 ++ accessible/xul/XULSelectControlAccessible.h | 47 + accessible/xul/XULTabAccessible.cpp | 218 ++ accessible/xul/XULTabAccessible.h | 98 + accessible/xul/XULTreeAccessible.cpp | 995 +++++ accessible/xul/XULTreeAccessible.h | 265 ++ accessible/xul/XULTreeGridAccessible.cpp | 666 ++++ accessible/xul/XULTreeGridAccessible.h | 193 + accessible/xul/moz.build | 56 + 1051 files changed, 195665 insertions(+) create mode 100644 accessible/android/AccessibleWrap.cpp create mode 100644 accessible/android/AccessibleWrap.h create mode 100644 accessible/android/ApplicationAccessibleWrap.h create mode 100644 accessible/android/DocAccessibleWrap.cpp create mode 100644 accessible/android/DocAccessibleWrap.h create mode 100644 accessible/android/HyperTextAccessibleWrap.h create mode 100644 accessible/android/Platform.cpp create mode 100644 accessible/android/RootAccessibleWrap.cpp create mode 100644 accessible/android/RootAccessibleWrap.h create mode 100644 accessible/android/SessionAccessibility.cpp create mode 100644 accessible/android/SessionAccessibility.h create mode 100644 accessible/android/TraversalRule.cpp create mode 100644 accessible/android/TraversalRule.h create mode 100644 accessible/android/moz.build create mode 100644 accessible/aom/AccessibleNode.cpp create mode 100644 accessible/aom/AccessibleNode.h create mode 100644 accessible/aom/moz.build create mode 100644 accessible/atk/AccessibleWrap.cpp create mode 100644 accessible/atk/AccessibleWrap.h create mode 100644 accessible/atk/ApplicationAccessibleWrap.cpp create mode 100644 accessible/atk/ApplicationAccessibleWrap.h create mode 100644 accessible/atk/DOMtoATK.cpp create mode 100644 accessible/atk/DOMtoATK.h create mode 100644 accessible/atk/DocAccessibleWrap.cpp create mode 100644 accessible/atk/DocAccessibleWrap.h create mode 100644 accessible/atk/HyperTextAccessibleWrap.h create mode 100644 accessible/atk/InterfaceInitFuncs.h create mode 100644 accessible/atk/Platform.cpp create mode 100644 accessible/atk/RootAccessibleWrap.cpp create mode 100644 accessible/atk/RootAccessibleWrap.h create mode 100644 accessible/atk/UtilInterface.cpp create mode 100644 accessible/atk/moz.build create mode 100644 accessible/atk/nsMai.h create mode 100644 accessible/atk/nsMaiHyperlink.cpp create mode 100644 accessible/atk/nsMaiHyperlink.h create mode 100644 accessible/atk/nsMaiInterfaceAction.cpp create mode 100644 accessible/atk/nsMaiInterfaceComponent.cpp create mode 100644 accessible/atk/nsMaiInterfaceDocument.cpp create mode 100644 accessible/atk/nsMaiInterfaceEditableText.cpp create mode 100644 accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp create mode 100644 accessible/atk/nsMaiInterfaceHypertext.cpp create mode 100644 accessible/atk/nsMaiInterfaceImage.cpp create mode 100644 accessible/atk/nsMaiInterfaceSelection.cpp create mode 100644 accessible/atk/nsMaiInterfaceTable.cpp create mode 100644 accessible/atk/nsMaiInterfaceTableCell.cpp create mode 100644 accessible/atk/nsMaiInterfaceText.cpp create mode 100644 accessible/atk/nsMaiInterfaceValue.cpp create mode 100644 accessible/atk/nsStateMap.h create mode 100644 accessible/base/ARIAMap.cpp create mode 100644 accessible/base/ARIAMap.h create mode 100644 accessible/base/ARIAStateMap.cpp create mode 100644 accessible/base/ARIAStateMap.h create mode 100644 accessible/base/AccAttributes.cpp create mode 100644 accessible/base/AccAttributes.h create mode 100644 accessible/base/AccEvent.cpp create mode 100644 accessible/base/AccEvent.h create mode 100644 accessible/base/AccGroupInfo.cpp create mode 100644 accessible/base/AccGroupInfo.h create mode 100644 accessible/base/AccIterator.cpp create mode 100644 accessible/base/AccIterator.h create mode 100644 accessible/base/AccTypes.h create mode 100644 accessible/base/Asserts.cpp create mode 100644 accessible/base/CacheConstants.h create mode 100644 accessible/base/CachedTableAccessible.cpp create mode 100644 accessible/base/CachedTableAccessible.h create mode 100644 accessible/base/DocManager.cpp create mode 100644 accessible/base/DocManager.h create mode 100644 accessible/base/EmbeddedObjCollector.cpp create mode 100644 accessible/base/EmbeddedObjCollector.h create mode 100644 accessible/base/EventQueue.cpp create mode 100644 accessible/base/EventQueue.h create mode 100644 accessible/base/EventTree.cpp create mode 100644 accessible/base/EventTree.h create mode 100644 accessible/base/Filters.cpp create mode 100644 accessible/base/Filters.h create mode 100644 accessible/base/FocusManager.cpp create mode 100644 accessible/base/FocusManager.h create mode 100644 accessible/base/HTMLMarkupMap.h create mode 100644 accessible/base/IDSet.h create mode 100644 accessible/base/Logging.cpp create mode 100644 accessible/base/Logging.h create mode 100644 accessible/base/MathMLMarkupMap.h create mode 100644 accessible/base/NotificationController.cpp create mode 100644 accessible/base/NotificationController.h create mode 100644 accessible/base/Pivot.cpp create mode 100644 accessible/base/Pivot.h create mode 100644 accessible/base/Platform.h create mode 100644 accessible/base/Relation.h create mode 100644 accessible/base/RelationType.h create mode 100644 accessible/base/RelationTypeMap.h create mode 100644 accessible/base/Role.h create mode 100644 accessible/base/RoleMap.h create mode 100644 accessible/base/SelectionManager.cpp create mode 100644 accessible/base/SelectionManager.h create mode 100644 accessible/base/States.h create mode 100644 accessible/base/Statistics.h create mode 100644 accessible/base/StyleInfo.cpp create mode 100644 accessible/base/StyleInfo.h create mode 100644 accessible/base/TextAttrs.cpp create mode 100644 accessible/base/TextAttrs.h create mode 100644 accessible/base/TextLeafRange.cpp create mode 100644 accessible/base/TextLeafRange.h create mode 100644 accessible/base/TextRange-inl.h create mode 100644 accessible/base/TextRange.cpp create mode 100644 accessible/base/TextRange.h create mode 100644 accessible/base/TextUpdater.cpp create mode 100644 accessible/base/TextUpdater.h create mode 100644 accessible/base/TreeWalker.cpp create mode 100644 accessible/base/TreeWalker.h create mode 100644 accessible/base/XULMap.h create mode 100644 accessible/base/moz.build create mode 100644 accessible/base/nsAccCache.h create mode 100644 accessible/base/nsAccUtils.cpp create mode 100644 accessible/base/nsAccUtils.h create mode 100644 accessible/base/nsAccessibilityService.cpp create mode 100644 accessible/base/nsAccessibilityService.h create mode 100644 accessible/base/nsAccessiblePivot.cpp create mode 100644 accessible/base/nsAccessiblePivot.h create mode 100644 accessible/base/nsCoreUtils.cpp create mode 100644 accessible/base/nsCoreUtils.h create mode 100644 accessible/base/nsEventShell.cpp create mode 100644 accessible/base/nsEventShell.h create mode 100644 accessible/base/nsTextEquivUtils.cpp create mode 100644 accessible/base/nsTextEquivUtils.h create mode 100644 accessible/basetypes/Accessible.cpp create mode 100644 accessible/basetypes/Accessible.h create mode 100644 accessible/basetypes/HyperTextAccessibleBase.cpp create mode 100644 accessible/basetypes/HyperTextAccessibleBase.h create mode 100644 accessible/basetypes/TableAccessible.h create mode 100644 accessible/basetypes/TableCellAccessible.h create mode 100644 accessible/basetypes/moz.build create mode 100644 accessible/docs/Architecture.md create mode 100644 accessible/docs/ColorsAndHighContrastMode.md create mode 100644 accessible/docs/DocumentAccessibilityLifecycle.md create mode 100644 accessible/docs/GeckoViewThreadTopography.md create mode 100644 accessible/docs/index.rst create mode 100644 accessible/generic/ARIAGridAccessible.cpp create mode 100644 accessible/generic/ARIAGridAccessible.h create mode 100644 accessible/generic/ApplicationAccessible.cpp create mode 100644 accessible/generic/ApplicationAccessible.h create mode 100644 accessible/generic/BaseAccessibles.cpp create mode 100644 accessible/generic/BaseAccessibles.h create mode 100644 accessible/generic/DocAccessible-inl.h create mode 100644 accessible/generic/DocAccessible.cpp create mode 100644 accessible/generic/DocAccessible.h create mode 100644 accessible/generic/FormControlAccessible.cpp create mode 100644 accessible/generic/FormControlAccessible.h create mode 100644 accessible/generic/HyperTextAccessible-inl.h create mode 100644 accessible/generic/HyperTextAccessible.cpp create mode 100644 accessible/generic/HyperTextAccessible.h create mode 100644 accessible/generic/ImageAccessible.cpp create mode 100644 accessible/generic/ImageAccessible.h create mode 100644 accessible/generic/LocalAccessible-inl.h create mode 100644 accessible/generic/LocalAccessible.cpp create mode 100644 accessible/generic/LocalAccessible.h create mode 100644 accessible/generic/OuterDocAccessible.cpp create mode 100644 accessible/generic/OuterDocAccessible.h create mode 100644 accessible/generic/RootAccessible.cpp create mode 100644 accessible/generic/RootAccessible.h create mode 100644 accessible/generic/TextLeafAccessible.cpp create mode 100644 accessible/generic/TextLeafAccessible.h create mode 100644 accessible/generic/moz.build create mode 100644 accessible/html/HTMLCanvasAccessible.cpp create mode 100644 accessible/html/HTMLCanvasAccessible.h create mode 100644 accessible/html/HTMLElementAccessibles.cpp create mode 100644 accessible/html/HTMLElementAccessibles.h create mode 100644 accessible/html/HTMLFormControlAccessible.cpp create mode 100644 accessible/html/HTMLFormControlAccessible.h create mode 100644 accessible/html/HTMLImageMapAccessible.cpp create mode 100644 accessible/html/HTMLImageMapAccessible.h create mode 100644 accessible/html/HTMLLinkAccessible.cpp create mode 100644 accessible/html/HTMLLinkAccessible.h create mode 100644 accessible/html/HTMLListAccessible.cpp create mode 100644 accessible/html/HTMLListAccessible.h create mode 100644 accessible/html/HTMLSelectAccessible.cpp create mode 100644 accessible/html/HTMLSelectAccessible.h create mode 100644 accessible/html/HTMLTableAccessible.cpp create mode 100644 accessible/html/HTMLTableAccessible.h create mode 100644 accessible/html/moz.build create mode 100644 accessible/interfaces/ia2/moz.build create mode 100644 accessible/interfaces/moz.build create mode 100644 accessible/interfaces/msaa/AccessibleMarshal.def create mode 100644 accessible/interfaces/msaa/AccessibleMarshal.rc create mode 100644 accessible/interfaces/msaa/AccessibleMarshalThunk.c create mode 100644 accessible/interfaces/msaa/ISimpleDOM.idl create mode 100644 accessible/interfaces/msaa/ISimpleDOMDocument.idl create mode 100644 accessible/interfaces/msaa/ISimpleDOMNode.idl create mode 100644 accessible/interfaces/msaa/ISimpleDOMText.idl create mode 100644 accessible/interfaces/msaa/moz.build create mode 100644 accessible/interfaces/nsIAccessibilityService.idl create mode 100644 accessible/interfaces/nsIAccessible.idl create mode 100644 accessible/interfaces/nsIAccessibleAnnouncementEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleApplication.idl create mode 100644 accessible/interfaces/nsIAccessibleCaretMoveEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleDocument.idl create mode 100644 accessible/interfaces/nsIAccessibleEditableText.idl create mode 100644 accessible/interfaces/nsIAccessibleEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleHideEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleHyperLink.idl create mode 100644 accessible/interfaces/nsIAccessibleHyperText.idl create mode 100644 accessible/interfaces/nsIAccessibleImage.idl create mode 100644 accessible/interfaces/nsIAccessibleMacInterface.idl create mode 100644 accessible/interfaces/nsIAccessibleObjectAttributeChangedEvent.idl create mode 100644 accessible/interfaces/nsIAccessiblePivot.idl create mode 100644 accessible/interfaces/nsIAccessibleRelation.idl create mode 100644 accessible/interfaces/nsIAccessibleRole.idl create mode 100644 accessible/interfaces/nsIAccessibleScrollingEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleSelectable.idl create mode 100644 accessible/interfaces/nsIAccessibleStateChangeEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleStates.idl create mode 100644 accessible/interfaces/nsIAccessibleTable.idl create mode 100644 accessible/interfaces/nsIAccessibleTableChangeEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleText.idl create mode 100644 accessible/interfaces/nsIAccessibleTextChangeEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleTextLeafRange.idl create mode 100644 accessible/interfaces/nsIAccessibleTextRange.idl create mode 100644 accessible/interfaces/nsIAccessibleTextSelectionChangeEvent.idl create mode 100644 accessible/interfaces/nsIAccessibleTypes.idl create mode 100644 accessible/interfaces/nsIAccessibleValue.idl create mode 100644 accessible/interfaces/nsIAccessibleVirtualCursorChangeEvent.idl create mode 100644 accessible/ipc/DocAccessibleChildBase.cpp create mode 100644 accessible/ipc/DocAccessibleChildBase.h create mode 100644 accessible/ipc/DocAccessibleParent.cpp create mode 100644 accessible/ipc/DocAccessibleParent.h create mode 100644 accessible/ipc/DocAccessibleTypes.ipdlh create mode 100644 accessible/ipc/IPCTypes.h create mode 100644 accessible/ipc/RemoteAccessibleBase.cpp create mode 100644 accessible/ipc/RemoteAccessibleBase.h create mode 100644 accessible/ipc/RemoteAccessibleShared.h create mode 100644 accessible/ipc/extension/android/DocAccessiblePlatformExtChild.cpp create mode 100644 accessible/ipc/extension/android/DocAccessiblePlatformExtChild.h create mode 100644 accessible/ipc/extension/android/DocAccessiblePlatformExtParent.cpp create mode 100644 accessible/ipc/extension/android/DocAccessiblePlatformExtParent.h create mode 100644 accessible/ipc/extension/android/PDocAccessiblePlatformExt.ipdl create mode 100644 accessible/ipc/extension/android/moz.build create mode 100644 accessible/ipc/extension/moz.build create mode 100644 accessible/ipc/extension/other/DocAccessiblePlatformExtChild.h create mode 100644 accessible/ipc/extension/other/DocAccessiblePlatformExtParent.h create mode 100644 accessible/ipc/extension/other/PDocAccessiblePlatformExt.ipdl create mode 100644 accessible/ipc/extension/other/moz.build create mode 100644 accessible/ipc/moz.build create mode 100644 accessible/ipc/other/DocAccessibleChild.cpp create mode 100644 accessible/ipc/other/DocAccessibleChild.h create mode 100644 accessible/ipc/other/PDocAccessible.ipdl create mode 100644 accessible/ipc/other/RemoteAccessible.cpp create mode 100644 accessible/ipc/other/RemoteAccessible.h create mode 100644 accessible/ipc/other/moz.build create mode 100644 accessible/ipc/win/DocAccessibleChild.cpp create mode 100644 accessible/ipc/win/DocAccessibleChild.h create mode 100644 accessible/ipc/win/PDocAccessible.ipdl create mode 100644 accessible/ipc/win/RemoteAccessible.h create mode 100644 accessible/ipc/win/moz.build create mode 100644 accessible/mac/.clang-format create mode 100644 accessible/mac/AccessibleWrap.h create mode 100644 accessible/mac/AccessibleWrap.mm create mode 100644 accessible/mac/ApplicationAccessibleWrap.h create mode 100644 accessible/mac/DocAccessibleWrap.h create mode 100644 accessible/mac/DocAccessibleWrap.mm create mode 100644 accessible/mac/GeckoTextMarker.h create mode 100644 accessible/mac/GeckoTextMarker.mm create mode 100644 accessible/mac/HyperTextAccessibleWrap.h create mode 100644 accessible/mac/MOXAccessibleBase.h create mode 100644 accessible/mac/MOXAccessibleBase.mm create mode 100644 accessible/mac/MOXAccessibleProtocol.h create mode 100644 accessible/mac/MOXLandmarkAccessibles.h create mode 100644 accessible/mac/MOXLandmarkAccessibles.mm create mode 100644 accessible/mac/MOXMathAccessibles.h create mode 100644 accessible/mac/MOXMathAccessibles.mm create mode 100644 accessible/mac/MOXSearchInfo.h create mode 100644 accessible/mac/MOXSearchInfo.mm create mode 100644 accessible/mac/MOXTextMarkerDelegate.h create mode 100644 accessible/mac/MOXTextMarkerDelegate.mm create mode 100644 accessible/mac/MOXWebAreaAccessible.h create mode 100644 accessible/mac/MOXWebAreaAccessible.mm create mode 100644 accessible/mac/MacUtils.h create mode 100644 accessible/mac/MacUtils.mm create mode 100644 accessible/mac/Platform.mm create mode 100644 accessible/mac/PlatformExtTypes.h create mode 100644 accessible/mac/RootAccessibleWrap.h create mode 100644 accessible/mac/RootAccessibleWrap.mm create mode 100644 accessible/mac/RotorRules.h create mode 100644 accessible/mac/RotorRules.mm create mode 100755 accessible/mac/SelectorMapGen.py create mode 100644 accessible/mac/moz.build create mode 100644 accessible/mac/mozAccessible.h create mode 100644 accessible/mac/mozAccessible.mm create mode 100644 accessible/mac/mozAccessibleProtocol.h create mode 100644 accessible/mac/mozActionElements.h create mode 100644 accessible/mac/mozActionElements.mm create mode 100644 accessible/mac/mozHTMLAccessible.h create mode 100644 accessible/mac/mozHTMLAccessible.mm create mode 100644 accessible/mac/mozRootAccessible.h create mode 100644 accessible/mac/mozRootAccessible.mm create mode 100644 accessible/mac/mozSelectableElements.h create mode 100644 accessible/mac/mozSelectableElements.mm create mode 100644 accessible/mac/mozTableAccessible.h create mode 100644 accessible/mac/mozTableAccessible.mm create mode 100644 accessible/mac/mozTextAccessible.h create mode 100644 accessible/mac/mozTextAccessible.mm create mode 100644 accessible/moz.build create mode 100644 accessible/other/AccessibleWrap.cpp create mode 100644 accessible/other/AccessibleWrap.h create mode 100644 accessible/other/ApplicationAccessibleWrap.h create mode 100644 accessible/other/DocAccessibleWrap.h create mode 100644 accessible/other/HyperTextAccessibleWrap.h create mode 100644 accessible/other/Platform.cpp create mode 100644 accessible/other/RootAccessibleWrap.h create mode 100644 accessible/other/moz.build 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 create mode 100644 accessible/tests/crashtests/1072792.xhtml create mode 100644 accessible/tests/crashtests/1380199.html create mode 100644 accessible/tests/crashtests/1402999.html create mode 100644 accessible/tests/crashtests/1415667.html create mode 100644 accessible/tests/crashtests/1463962.html create mode 100644 accessible/tests/crashtests/1472024-1.html create mode 100644 accessible/tests/crashtests/1472024-2.html create mode 100644 accessible/tests/crashtests/1484778.html create mode 100644 accessible/tests/crashtests/1494707.html create mode 100644 accessible/tests/crashtests/1503964.html create mode 100644 accessible/tests/crashtests/1572811.html create mode 100644 accessible/tests/crashtests/1578282.html create mode 100644 accessible/tests/crashtests/1585851.html create mode 100644 accessible/tests/crashtests/1655983.html create mode 100644 accessible/tests/crashtests/448064.xhtml create mode 100644 accessible/tests/crashtests/884202.html create mode 100644 accessible/tests/crashtests/890760.html create mode 100644 accessible/tests/crashtests/893515.html create mode 100644 accessible/tests/crashtests/crashtests.list create mode 100644 accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml create mode 100644 accessible/tests/mochitest/.eslintrc.js create mode 100644 accessible/tests/mochitest/a11y.ini create mode 100644 accessible/tests/mochitest/actions.js create mode 100644 accessible/tests/mochitest/actions/a11y.ini create mode 100644 accessible/tests/mochitest/actions/test_anchors.html create mode 100644 accessible/tests/mochitest/actions/test_aria.html create mode 100644 accessible/tests/mochitest/actions/test_controls.html create mode 100644 accessible/tests/mochitest/actions/test_general.html create mode 100644 accessible/tests/mochitest/actions/test_general.xhtml create mode 100644 accessible/tests/mochitest/actions/test_keys.html create mode 100644 accessible/tests/mochitest/actions/test_keys.xhtml create mode 100644 accessible/tests/mochitest/actions/test_link.html create mode 100644 accessible/tests/mochitest/actions/test_media.html create mode 100644 accessible/tests/mochitest/actions/test_select.html create mode 100644 accessible/tests/mochitest/actions/test_tree.xhtml create mode 100644 accessible/tests/mochitest/actions/test_treegrid.xhtml create mode 100644 accessible/tests/mochitest/aom/a11y.ini create mode 100644 accessible/tests/mochitest/aom/test_general.html create mode 100644 accessible/tests/mochitest/attributes.js create mode 100644 accessible/tests/mochitest/attributes/a11y.ini create mode 100644 accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html create mode 100644 accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html create mode 100644 accessible/tests/mochitest/attributes/test_listbox.html create mode 100644 accessible/tests/mochitest/attributes/test_obj.html create mode 100644 accessible/tests/mochitest/attributes/test_obj_css.html create mode 100644 accessible/tests/mochitest/attributes/test_obj_group.html create mode 100644 accessible/tests/mochitest/attributes/test_obj_group.xhtml create mode 100644 accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml create mode 100644 accessible/tests/mochitest/attributes/test_tag.html create mode 100644 accessible/tests/mochitest/attributes/test_xml-roles.html create mode 100644 accessible/tests/mochitest/autocomplete.js create mode 100644 accessible/tests/mochitest/bounds/a11y.ini create mode 100644 accessible/tests/mochitest/bounds/test_list.html create mode 100644 accessible/tests/mochitest/browser.js create mode 100644 accessible/tests/mochitest/common.js create mode 100644 accessible/tests/mochitest/dumbfile.zip create mode 100644 accessible/tests/mochitest/elm/a11y.ini create mode 100644 accessible/tests/mochitest/elm/test_HTMLSpec.html create mode 100644 accessible/tests/mochitest/elm/test_MathMLSpec.html create mode 100644 accessible/tests/mochitest/elm/test_figure.html create mode 100644 accessible/tests/mochitest/elm/test_listbox.xhtml create mode 100644 accessible/tests/mochitest/elm/test_nsApplicationAcc.html create mode 100644 accessible/tests/mochitest/elm/test_shadowroot.html create mode 100644 accessible/tests/mochitest/elm/test_shadowroot_subframe.html create mode 100644 accessible/tests/mochitest/events.js create mode 100644 accessible/tests/mochitest/events/a11y.ini create mode 100644 accessible/tests/mochitest/events/docload/a11y.ini create mode 100644 accessible/tests/mochitest/events/docload/docload_wnd.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_aria.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_busy.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_embedded.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_iframe.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_root.html create mode 100644 accessible/tests/mochitest/events/docload/test_docload_shutdown.html create mode 100644 accessible/tests/mochitest/events/focus.html create mode 100644 accessible/tests/mochitest/events/scroll.html create mode 100644 accessible/tests/mochitest/events/slow_image.sjs create mode 100644 accessible/tests/mochitest/events/test_announcement.html create mode 100644 accessible/tests/mochitest/events/test_aria_alert.html create mode 100644 accessible/tests/mochitest/events/test_aria_menu.html create mode 100644 accessible/tests/mochitest/events/test_aria_objattr.html create mode 100644 accessible/tests/mochitest/events/test_aria_owns.html create mode 100644 accessible/tests/mochitest/events/test_aria_statechange.html create mode 100644 accessible/tests/mochitest/events/test_attrchange.html create mode 100644 accessible/tests/mochitest/events/test_attrs.html create mode 100644 accessible/tests/mochitest/events/test_bug1322593-2.html create mode 100644 accessible/tests/mochitest/events/test_bug1322593.html create mode 100644 accessible/tests/mochitest/events/test_caretmove.html create mode 100644 accessible/tests/mochitest/events/test_coalescence.html create mode 100644 accessible/tests/mochitest/events/test_contextmenu.html create mode 100644 accessible/tests/mochitest/events/test_descrchange.html create mode 100644 accessible/tests/mochitest/events/test_dragndrop.html create mode 100644 accessible/tests/mochitest/events/test_flush.html create mode 100644 accessible/tests/mochitest/events/test_focus_aria_activedescendant.html create mode 100644 accessible/tests/mochitest/events/test_focus_autocomplete.html create mode 100644 accessible/tests/mochitest/events/test_focus_autocomplete.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_canvas.html create mode 100644 accessible/tests/mochitest/events/test_focus_contextmenu.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_controls.html create mode 100644 accessible/tests/mochitest/events/test_focus_doc.html create mode 100644 accessible/tests/mochitest/events/test_focus_general.html create mode 100644 accessible/tests/mochitest/events/test_focus_general.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_listcontrols.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_menu.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_name.html create mode 100644 accessible/tests/mochitest/events/test_focus_removal.html create mode 100644 accessible/tests/mochitest/events/test_focus_selects.html create mode 100644 accessible/tests/mochitest/events/test_focus_tabbox.xhtml create mode 100644 accessible/tests/mochitest/events/test_focus_tree.xhtml create mode 100644 accessible/tests/mochitest/events/test_focusable_statechange.html create mode 100644 accessible/tests/mochitest/events/test_fromUserInput.html create mode 100644 accessible/tests/mochitest/events/test_label.xhtml create mode 100644 accessible/tests/mochitest/events/test_menu.xhtml create mode 100644 accessible/tests/mochitest/events/test_mutation.html create mode 100644 accessible/tests/mochitest/events/test_namechange.html create mode 100644 accessible/tests/mochitest/events/test_namechange.xhtml create mode 100644 accessible/tests/mochitest/events/test_scroll.xhtml create mode 100644 accessible/tests/mochitest/events/test_scroll_caret.xhtml create mode 100644 accessible/tests/mochitest/events/test_selection.html create mode 100644 accessible/tests/mochitest/events/test_selection.xhtml create mode 100644 accessible/tests/mochitest/events/test_selection_aria.html create mode 100644 accessible/tests/mochitest/events/test_statechange.html create mode 100644 accessible/tests/mochitest/events/test_statechange.xhtml create mode 100644 accessible/tests/mochitest/events/test_text.html create mode 100644 accessible/tests/mochitest/events/test_text_alg.html create mode 100644 accessible/tests/mochitest/events/test_textattrchange.html create mode 100644 accessible/tests/mochitest/events/test_textselchange.html create mode 100644 accessible/tests/mochitest/events/test_tree.xhtml create mode 100644 accessible/tests/mochitest/events/test_valuechange.html create mode 100644 accessible/tests/mochitest/focus/a11y.ini create mode 100644 accessible/tests/mochitest/focus/test_focus_radio.xhtml create mode 100644 accessible/tests/mochitest/focus/test_focusedChild.html create mode 100644 accessible/tests/mochitest/focus/test_takeFocus.html create mode 100644 accessible/tests/mochitest/focus/test_takeFocus.xhtml create mode 100644 accessible/tests/mochitest/formimage.png create mode 100644 accessible/tests/mochitest/grid.js create mode 100644 accessible/tests/mochitest/hittest/a11y.ini create mode 100644 accessible/tests/mochitest/hittest/test_browser.html create mode 100644 accessible/tests/mochitest/hittest/test_general.html create mode 100644 accessible/tests/mochitest/hittest/test_menu.xhtml create mode 100644 accessible/tests/mochitest/hittest/test_shadowroot.html create mode 100644 accessible/tests/mochitest/hittest/test_shadowroot_subframe.html create mode 100644 accessible/tests/mochitest/hittest/test_zoom.html create mode 100644 accessible/tests/mochitest/hittest/test_zoom_text.html create mode 100644 accessible/tests/mochitest/hittest/test_zoom_tree.xhtml create mode 100644 accessible/tests/mochitest/hittest/zoom_tree.xhtml create mode 100644 accessible/tests/mochitest/hyperlink/a11y.ini create mode 100644 accessible/tests/mochitest/hyperlink/hyperlink.js create mode 100644 accessible/tests/mochitest/hyperlink/test_general.html create mode 100644 accessible/tests/mochitest/hyperlink/test_general.xhtml create mode 100644 accessible/tests/mochitest/hypertext/a11y.ini create mode 100644 accessible/tests/mochitest/hypertext/test_general.html create mode 100644 accessible/tests/mochitest/hypertext/test_update.html create mode 100644 accessible/tests/mochitest/layout.js create mode 100644 accessible/tests/mochitest/letters.gif create mode 100644 accessible/tests/mochitest/longdesc_src.html create mode 100644 accessible/tests/mochitest/moz.build create mode 100644 accessible/tests/mochitest/moz.png create mode 100644 accessible/tests/mochitest/name.js create mode 100644 accessible/tests/mochitest/name/a11y.ini create mode 100644 accessible/tests/mochitest/name/markup.js create mode 100644 accessible/tests/mochitest/name/markuprules.xml create mode 100644 accessible/tests/mochitest/name/test_ARIACore_examples.html create mode 100644 accessible/tests/mochitest/name/test_browserui.xhtml create mode 100644 accessible/tests/mochitest/name/test_counterstyle.html create mode 100644 accessible/tests/mochitest/name/test_general.html create mode 100644 accessible/tests/mochitest/name/test_general.xhtml create mode 100644 accessible/tests/mochitest/name/test_link.html create mode 100644 accessible/tests/mochitest/name/test_list.html create mode 100644 accessible/tests/mochitest/name/test_markup.html create mode 100644 accessible/tests/mochitest/name/test_svg.html create mode 100644 accessible/tests/mochitest/name/test_tree.xhtml create mode 100644 accessible/tests/mochitest/pivot.js create mode 100644 accessible/tests/mochitest/pivot/a11y.ini create mode 100644 accessible/tests/mochitest/pivot/doc_virtualcursor.html create mode 100644 accessible/tests/mochitest/pivot/doc_virtualcursor_text.html create mode 100644 accessible/tests/mochitest/pivot/test_virtualcursor.html create mode 100644 accessible/tests/mochitest/pivot/test_virtualcursor_text.html create mode 100644 accessible/tests/mochitest/promisified-events.js create mode 100644 accessible/tests/mochitest/relations.js create mode 100644 accessible/tests/mochitest/relations/a11y.ini create mode 100644 accessible/tests/mochitest/relations/test_embeds.xhtml create mode 100644 accessible/tests/mochitest/relations/test_general.html create mode 100644 accessible/tests/mochitest/relations/test_general.xhtml create mode 100644 accessible/tests/mochitest/relations/test_groupInfoUpdate.html create mode 100644 accessible/tests/mochitest/relations/test_shadowdom.html create mode 100644 accessible/tests/mochitest/relations/test_tabbrowser.xhtml create mode 100644 accessible/tests/mochitest/relations/test_tree.xhtml create mode 100644 accessible/tests/mochitest/relations/test_ui_modalprompt.html create mode 100644 accessible/tests/mochitest/relations/test_update.html create mode 100644 accessible/tests/mochitest/role.js create mode 100644 accessible/tests/mochitest/role/a11y.ini create mode 100644 accessible/tests/mochitest/role/chrome_body_role_alert.xhtml create mode 100644 accessible/tests/mochitest/role/test_aria.html create mode 100644 accessible/tests/mochitest/role/test_aria.xhtml create mode 100644 accessible/tests/mochitest/role/test_dpub_aria.html create mode 100644 accessible/tests/mochitest/role/test_general.html create mode 100644 accessible/tests/mochitest/role/test_general.xhtml create mode 100644 accessible/tests/mochitest/role/test_graphics_aria.html create mode 100644 accessible/tests/mochitest/role/test_svg.html create mode 100644 accessible/tests/mochitest/scroll/a11y.ini create mode 100644 accessible/tests/mochitest/scroll/test_zoom.html create mode 100644 accessible/tests/mochitest/selectable.js create mode 100644 accessible/tests/mochitest/selectable/a11y.ini create mode 100644 accessible/tests/mochitest/selectable/test_listbox.xhtml create mode 100644 accessible/tests/mochitest/selectable/test_menu.xhtml create mode 100644 accessible/tests/mochitest/selectable/test_menulist.xhtml create mode 100644 accessible/tests/mochitest/selectable/test_tabs.xhtml create mode 100644 accessible/tests/mochitest/selectable/test_tree.xhtml create mode 100644 accessible/tests/mochitest/states.js create mode 100644 accessible/tests/mochitest/states/a11y.ini create mode 100644 accessible/tests/mochitest/states/test_aria.html create mode 100644 accessible/tests/mochitest/states/test_aria.xhtml create mode 100644 accessible/tests/mochitest/states/test_aria_imgmap.html create mode 100644 accessible/tests/mochitest/states/test_aria_widgetitems.html create mode 100644 accessible/tests/mochitest/states/test_buttons.html create mode 100644 accessible/tests/mochitest/states/test_controls.html create mode 100644 accessible/tests/mochitest/states/test_controls.xhtml create mode 100644 accessible/tests/mochitest/states/test_doc.html create mode 100644 accessible/tests/mochitest/states/test_doc_busy.html create mode 100644 accessible/tests/mochitest/states/test_docarticle.html create mode 100644 accessible/tests/mochitest/states/test_editablebody.html create mode 100644 accessible/tests/mochitest/states/test_expandable.xhtml create mode 100644 accessible/tests/mochitest/states/test_frames.html create mode 100644 accessible/tests/mochitest/states/test_inputs.html create mode 100644 accessible/tests/mochitest/states/test_link.html create mode 100644 accessible/tests/mochitest/states/test_popup.xhtml create mode 100644 accessible/tests/mochitest/states/test_selects.html create mode 100644 accessible/tests/mochitest/states/test_stale.html create mode 100644 accessible/tests/mochitest/states/test_tabs.xhtml create mode 100644 accessible/tests/mochitest/states/test_textbox.xhtml create mode 100644 accessible/tests/mochitest/states/test_tree.xhtml create mode 100644 accessible/tests/mochitest/states/test_visibility.html create mode 100644 accessible/tests/mochitest/states/test_visibility.xhtml create mode 100644 accessible/tests/mochitest/states/z_frames.html create mode 100644 accessible/tests/mochitest/states/z_frames_article.html create mode 100644 accessible/tests/mochitest/states/z_frames_checkbox.html create mode 100644 accessible/tests/mochitest/states/z_frames_textbox.html create mode 100644 accessible/tests/mochitest/states/z_frames_update.html create mode 100644 accessible/tests/mochitest/table.js create mode 100644 accessible/tests/mochitest/table/a11y.ini create mode 100644 accessible/tests/mochitest/table/test_css_tables.html create mode 100644 accessible/tests/mochitest/table/test_headers_ariagrid.html create mode 100644 accessible/tests/mochitest/table/test_headers_ariatable.html create mode 100644 accessible/tests/mochitest/table/test_headers_table.html create mode 100644 accessible/tests/mochitest/table/test_headers_tree.xhtml create mode 100644 accessible/tests/mochitest/table/test_indexes_ariagrid.html create mode 100644 accessible/tests/mochitest/table/test_indexes_table.html create mode 100644 accessible/tests/mochitest/table/test_indexes_tree.xhtml create mode 100644 accessible/tests/mochitest/table/test_layoutguess.html create mode 100644 accessible/tests/mochitest/table/test_mtable.html create mode 100644 accessible/tests/mochitest/table/test_sels_ariagrid.html create mode 100644 accessible/tests/mochitest/table/test_sels_table.html create mode 100644 accessible/tests/mochitest/table/test_sels_tree.xhtml create mode 100644 accessible/tests/mochitest/table/test_struct_ariagrid.html create mode 100644 accessible/tests/mochitest/table/test_struct_ariatreegrid.html create mode 100644 accessible/tests/mochitest/table/test_struct_table.html create mode 100644 accessible/tests/mochitest/table/test_struct_tree.xhtml create mode 100644 accessible/tests/mochitest/table/test_table_1.html create mode 100644 accessible/tests/mochitest/table/test_table_2.html create mode 100644 accessible/tests/mochitest/table/test_table_mutation.html create mode 100644 accessible/tests/mochitest/test_OuterDocAccessible.html create mode 100644 accessible/tests/mochitest/test_aria_token_attrs.html create mode 100644 accessible/tests/mochitest/test_bug420863.html create mode 100644 accessible/tests/mochitest/test_custom_element_accessibility_defaults.html create mode 100644 accessible/tests/mochitest/test_descr.html create mode 100644 accessible/tests/mochitest/test_nsIAccessibleDocument.html create mode 100644 accessible/tests/mochitest/test_nsIAccessibleImage.html create mode 100644 accessible/tests/mochitest/text.js create mode 100644 accessible/tests/mochitest/text/a11y.ini create mode 100644 accessible/tests/mochitest/text/doc.html create mode 100644 accessible/tests/mochitest/text/test_atcaretoffset.html create mode 100644 accessible/tests/mochitest/text/test_charboundary.html create mode 100644 accessible/tests/mochitest/text/test_doc.html create mode 100644 accessible/tests/mochitest/text/test_dynamic.html create mode 100644 accessible/tests/mochitest/text/test_general.xhtml create mode 100644 accessible/tests/mochitest/text/test_gettext.html create mode 100644 accessible/tests/mochitest/text/test_hypertext.html create mode 100644 accessible/tests/mochitest/text/test_lineboundary.html create mode 100644 accessible/tests/mochitest/text/test_paragraphboundary.html create mode 100644 accessible/tests/mochitest/text/test_passwords.html create mode 100644 accessible/tests/mochitest/text/test_selection.html create mode 100644 accessible/tests/mochitest/text/test_settext_input_event.html create mode 100644 accessible/tests/mochitest/text/test_textBounds.html create mode 100644 accessible/tests/mochitest/text/test_wordboundary.html create mode 100644 accessible/tests/mochitest/text/test_words.html create mode 100644 accessible/tests/mochitest/textattrs/a11y.ini create mode 100644 accessible/tests/mochitest/textattrs/test_general.html create mode 100644 accessible/tests/mochitest/textattrs/test_general.xhtml create mode 100644 accessible/tests/mochitest/textattrs/test_invalid.html create mode 100644 accessible/tests/mochitest/textattrs/test_mathml.html create mode 100644 accessible/tests/mochitest/textattrs/test_spelling.html create mode 100644 accessible/tests/mochitest/textattrs/test_svg.html create mode 100644 accessible/tests/mochitest/textcaret/a11y.ini create mode 100644 accessible/tests/mochitest/textcaret/test_general.html create mode 100644 accessible/tests/mochitest/textrange/a11y.ini create mode 100644 accessible/tests/mochitest/textrange/test_general.html create mode 100644 accessible/tests/mochitest/textrange/test_selection.html create mode 100644 accessible/tests/mochitest/textselection/a11y.ini create mode 100644 accessible/tests/mochitest/textselection/test_general.html create mode 100644 accessible/tests/mochitest/textselection/test_userinput.html create mode 100644 accessible/tests/mochitest/tree/a11y.ini create mode 100644 accessible/tests/mochitest/tree/dockids.html create mode 100644 accessible/tests/mochitest/tree/test_applicationacc.xhtml create mode 100644 accessible/tests/mochitest/tree/test_aria_display_contents.html create mode 100644 accessible/tests/mochitest/tree/test_aria_globals.html create mode 100644 accessible/tests/mochitest/tree/test_aria_grid.html create mode 100644 accessible/tests/mochitest/tree/test_aria_imgmap.html create mode 100644 accessible/tests/mochitest/tree/test_aria_list.html create mode 100644 accessible/tests/mochitest/tree/test_aria_menu.html create mode 100644 accessible/tests/mochitest/tree/test_aria_owns.html create mode 100644 accessible/tests/mochitest/tree/test_aria_presentation.html create mode 100644 accessible/tests/mochitest/tree/test_aria_table.html create mode 100644 accessible/tests/mochitest/tree/test_brokencontext.html create mode 100644 accessible/tests/mochitest/tree/test_button.xhtml create mode 100644 accessible/tests/mochitest/tree/test_canvas.html create mode 100644 accessible/tests/mochitest/tree/test_combobox.xhtml create mode 100644 accessible/tests/mochitest/tree/test_cssflexbox.html create mode 100644 accessible/tests/mochitest/tree/test_cssoverflow.html create mode 100644 accessible/tests/mochitest/tree/test_display_contents.html create mode 100644 accessible/tests/mochitest/tree/test_divs.html create mode 100644 accessible/tests/mochitest/tree/test_dochierarchy.html create mode 100644 accessible/tests/mochitest/tree/test_dockids.html create mode 100644 accessible/tests/mochitest/tree/test_filectrl.html create mode 100644 accessible/tests/mochitest/tree/test_formctrl.html create mode 100644 accessible/tests/mochitest/tree/test_formctrl.xhtml create mode 100644 accessible/tests/mochitest/tree/test_gencontent.html create mode 100644 accessible/tests/mochitest/tree/test_groupbox.xhtml create mode 100644 accessible/tests/mochitest/tree/test_html_in_mathml.html create mode 100644 accessible/tests/mochitest/tree/test_iframe.html create mode 100644 accessible/tests/mochitest/tree/test_image.xhtml create mode 100644 accessible/tests/mochitest/tree/test_img.html create mode 100644 accessible/tests/mochitest/tree/test_invalid_img.xhtml create mode 100644 accessible/tests/mochitest/tree/test_invalidationlist.html create mode 100644 accessible/tests/mochitest/tree/test_list.html create mode 100644 accessible/tests/mochitest/tree/test_map.html create mode 100644 accessible/tests/mochitest/tree/test_media.html create mode 100644 accessible/tests/mochitest/tree/test_select.html create mode 100644 accessible/tests/mochitest/tree/test_svg.html create mode 100644 accessible/tests/mochitest/tree/test_tabbox.xhtml create mode 100644 accessible/tests/mochitest/tree/test_tabbrowser.xhtml create mode 100644 accessible/tests/mochitest/tree/test_table.html create mode 100644 accessible/tests/mochitest/tree/test_table_2.html create mode 100644 accessible/tests/mochitest/tree/test_table_3.html create mode 100644 accessible/tests/mochitest/tree/test_tree.xhtml create mode 100644 accessible/tests/mochitest/tree/test_txtcntr.html create mode 100644 accessible/tests/mochitest/tree/test_txtctrl.html create mode 100644 accessible/tests/mochitest/tree/test_txtctrl.xhtml create mode 100644 accessible/tests/mochitest/tree/wnd.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/a11y.ini create mode 100644 accessible/tests/mochitest/treeupdate/test_ariadialog.html create mode 100644 accessible/tests/mochitest/treeupdate/test_ariahidden.html create mode 100644 accessible/tests/mochitest/treeupdate/test_ariaowns.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug1040735.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug1175913.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug1189277.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug1276857.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html create mode 100644 accessible/tests/mochitest/treeupdate/test_bug852150.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_bug883708.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_bug884251.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_bug895082.html create mode 100644 accessible/tests/mochitest/treeupdate/test_canvas.html create mode 100644 accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_cssoverflow.html create mode 100644 accessible/tests/mochitest/treeupdate/test_deck.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_delayed_removal.html create mode 100644 accessible/tests/mochitest/treeupdate/test_doc.html create mode 100644 accessible/tests/mochitest/treeupdate/test_gencontent.html create mode 100644 accessible/tests/mochitest/treeupdate/test_general.html create mode 100644 accessible/tests/mochitest/treeupdate/test_hidden.html create mode 100644 accessible/tests/mochitest/treeupdate/test_imagemap.html create mode 100644 accessible/tests/mochitest/treeupdate/test_inert.html create mode 100644 accessible/tests/mochitest/treeupdate/test_inner_reorder.html create mode 100644 accessible/tests/mochitest/treeupdate/test_list.html create mode 100644 accessible/tests/mochitest/treeupdate/test_list_editabledoc.html create mode 100644 accessible/tests/mochitest/treeupdate/test_list_style.html create mode 100644 accessible/tests/mochitest/treeupdate/test_listbox.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_menu.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_menubutton.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_optgroup.html create mode 100644 accessible/tests/mochitest/treeupdate/test_recreation.html create mode 100644 accessible/tests/mochitest/treeupdate/test_select.html create mode 100644 accessible/tests/mochitest/treeupdate/test_shadow_slots.html create mode 100644 accessible/tests/mochitest/treeupdate/test_shutdown.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_table.html create mode 100644 accessible/tests/mochitest/treeupdate/test_textleaf.html create mode 100644 accessible/tests/mochitest/treeupdate/test_tooltip.xhtml create mode 100644 accessible/tests/mochitest/treeupdate/test_visibility.html create mode 100644 accessible/tests/mochitest/treeupdate/test_whitespace.html create mode 100644 accessible/tests/mochitest/treeview.css create mode 100644 accessible/tests/mochitest/treeview.js create mode 100644 accessible/tests/mochitest/value.js create mode 100644 accessible/tests/mochitest/value/a11y.ini create mode 100644 accessible/tests/mochitest/value/test_ariavalue.html create mode 100644 accessible/tests/mochitest/value/test_datetime.html create mode 100644 accessible/tests/mochitest/value/test_general.html create mode 100644 accessible/tests/mochitest/value/test_meter.html create mode 100644 accessible/tests/mochitest/value/test_number.html create mode 100644 accessible/tests/mochitest/value/test_progress.html create mode 100644 accessible/tests/mochitest/value/test_range.html create mode 100644 accessible/windows/ia2/ia2Accessible.cpp create mode 100644 accessible/windows/ia2/ia2Accessible.h create mode 100644 accessible/windows/ia2/ia2AccessibleAction.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleAction.h create mode 100644 accessible/windows/ia2/ia2AccessibleApplication.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleApplication.h create mode 100644 accessible/windows/ia2/ia2AccessibleComponent.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleComponent.h create mode 100644 accessible/windows/ia2/ia2AccessibleEditableText.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleEditableText.h create mode 100644 accessible/windows/ia2/ia2AccessibleHyperlink.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleHyperlink.h create mode 100644 accessible/windows/ia2/ia2AccessibleHypertext.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleHypertext.h create mode 100644 accessible/windows/ia2/ia2AccessibleImage.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleImage.h create mode 100644 accessible/windows/ia2/ia2AccessibleRelation.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleRelation.h create mode 100644 accessible/windows/ia2/ia2AccessibleTable.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleTable.h create mode 100644 accessible/windows/ia2/ia2AccessibleTableCell.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleTableCell.h create mode 100644 accessible/windows/ia2/ia2AccessibleText.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleText.h create mode 100644 accessible/windows/ia2/ia2AccessibleValue.cpp create mode 100644 accessible/windows/ia2/ia2AccessibleValue.h create mode 100644 accessible/windows/ia2/moz.build create mode 100644 accessible/windows/moz.build create mode 100644 accessible/windows/msaa/AccessibleWrap.cpp create mode 100644 accessible/windows/msaa/AccessibleWrap.h create mode 100644 accessible/windows/msaa/ApplicationAccessibleWrap.cpp create mode 100644 accessible/windows/msaa/ApplicationAccessibleWrap.h create mode 100644 accessible/windows/msaa/Compatibility.cpp create mode 100644 accessible/windows/msaa/Compatibility.h create mode 100644 accessible/windows/msaa/CompatibilityUIA.cpp create mode 100644 accessible/windows/msaa/DocAccessibleWrap.cpp create mode 100644 accessible/windows/msaa/DocAccessibleWrap.h create mode 100644 accessible/windows/msaa/EnumVariant.cpp create mode 100644 accessible/windows/msaa/EnumVariant.h create mode 100644 accessible/windows/msaa/HyperTextAccessibleWrap.cpp create mode 100644 accessible/windows/msaa/HyperTextAccessibleWrap.h create mode 100644 accessible/windows/msaa/IUnknownImpl.cpp create mode 100644 accessible/windows/msaa/IUnknownImpl.h create mode 100644 accessible/windows/msaa/LazyInstantiator.cpp create mode 100644 accessible/windows/msaa/LazyInstantiator.h create mode 100644 accessible/windows/msaa/MsaaAccessible.cpp create mode 100644 accessible/windows/msaa/MsaaAccessible.h create mode 100644 accessible/windows/msaa/MsaaDocAccessible.cpp create mode 100644 accessible/windows/msaa/MsaaDocAccessible.h create mode 100644 accessible/windows/msaa/MsaaIdGenerator.cpp create mode 100644 accessible/windows/msaa/MsaaIdGenerator.h create mode 100644 accessible/windows/msaa/MsaaRootAccessible.cpp create mode 100644 accessible/windows/msaa/MsaaRootAccessible.h create mode 100644 accessible/windows/msaa/MsaaXULMenuAccessible.cpp create mode 100644 accessible/windows/msaa/MsaaXULMenuAccessible.h create mode 100644 accessible/windows/msaa/NtUndoc.h create mode 100644 accessible/windows/msaa/Platform.cpp create mode 100644 accessible/windows/msaa/RootAccessibleWrap.cpp create mode 100644 accessible/windows/msaa/RootAccessibleWrap.h create mode 100644 accessible/windows/msaa/ServiceProvider.cpp create mode 100644 accessible/windows/msaa/ServiceProvider.h create mode 100644 accessible/windows/msaa/moz.build create mode 100644 accessible/windows/msaa/nsEventMap.h create mode 100644 accessible/windows/msaa/nsWinUtils.cpp create mode 100644 accessible/windows/msaa/nsWinUtils.h create mode 100644 accessible/windows/sdn/moz.build create mode 100644 accessible/windows/sdn/sdnAccessible-inl.h create mode 100644 accessible/windows/sdn/sdnAccessible.cpp create mode 100644 accessible/windows/sdn/sdnAccessible.h create mode 100644 accessible/windows/sdn/sdnDocAccessible.cpp create mode 100644 accessible/windows/sdn/sdnDocAccessible.h create mode 100644 accessible/windows/sdn/sdnTextAccessible.cpp create mode 100644 accessible/windows/sdn/sdnTextAccessible.h create mode 100644 accessible/windows/uia/moz.build create mode 100644 accessible/windows/uia/uiaRawElmProvider.cpp create mode 100644 accessible/windows/uia/uiaRawElmProvider.h create mode 100755 accessible/xpcom/AccEventGen.py create mode 100644 accessible/xpcom/AccEvents.conf create mode 100644 accessible/xpcom/moz.build create mode 100644 accessible/xpcom/nsAccessibleRelation.cpp create mode 100644 accessible/xpcom/nsAccessibleRelation.h create mode 100644 accessible/xpcom/xpcAccessibilityService.cpp create mode 100644 accessible/xpcom/xpcAccessibilityService.h create mode 100644 accessible/xpcom/xpcAccessible.cpp create mode 100644 accessible/xpcom/xpcAccessible.h create mode 100644 accessible/xpcom/xpcAccessibleApplication.cpp create mode 100644 accessible/xpcom/xpcAccessibleApplication.h create mode 100644 accessible/xpcom/xpcAccessibleDocument.cpp create mode 100644 accessible/xpcom/xpcAccessibleDocument.h create mode 100644 accessible/xpcom/xpcAccessibleGeneric.cpp create mode 100644 accessible/xpcom/xpcAccessibleGeneric.h create mode 100644 accessible/xpcom/xpcAccessibleHyperLink.cpp create mode 100644 accessible/xpcom/xpcAccessibleHyperLink.h create mode 100644 accessible/xpcom/xpcAccessibleHyperText.cpp create mode 100644 accessible/xpcom/xpcAccessibleHyperText.h create mode 100644 accessible/xpcom/xpcAccessibleImage.cpp create mode 100644 accessible/xpcom/xpcAccessibleImage.h create mode 100644 accessible/xpcom/xpcAccessibleMacInterface.h create mode 100644 accessible/xpcom/xpcAccessibleMacInterface.mm create mode 100644 accessible/xpcom/xpcAccessibleSelectable.cpp create mode 100644 accessible/xpcom/xpcAccessibleSelectable.h create mode 100644 accessible/xpcom/xpcAccessibleTable.cpp create mode 100644 accessible/xpcom/xpcAccessibleTable.h create mode 100644 accessible/xpcom/xpcAccessibleTableCell.cpp create mode 100644 accessible/xpcom/xpcAccessibleTableCell.h create mode 100644 accessible/xpcom/xpcAccessibleTextLeafRange.cpp create mode 100644 accessible/xpcom/xpcAccessibleTextLeafRange.h create mode 100644 accessible/xpcom/xpcAccessibleTextRange.cpp create mode 100644 accessible/xpcom/xpcAccessibleTextRange.h create mode 100644 accessible/xpcom/xpcAccessibleValue.cpp create mode 100644 accessible/xpcom/xpcAccessibleValue.h create mode 100644 accessible/xul/XULAlertAccessible.cpp create mode 100644 accessible/xul/XULAlertAccessible.h create mode 100644 accessible/xul/XULComboboxAccessible.cpp create mode 100644 accessible/xul/XULComboboxAccessible.h create mode 100644 accessible/xul/XULElementAccessibles.cpp create mode 100644 accessible/xul/XULElementAccessibles.h create mode 100644 accessible/xul/XULFormControlAccessible.cpp create mode 100644 accessible/xul/XULFormControlAccessible.h create mode 100644 accessible/xul/XULListboxAccessible.cpp create mode 100644 accessible/xul/XULListboxAccessible.h create mode 100644 accessible/xul/XULMenuAccessible.cpp create mode 100644 accessible/xul/XULMenuAccessible.h create mode 100644 accessible/xul/XULSelectControlAccessible.cpp create mode 100644 accessible/xul/XULSelectControlAccessible.h create mode 100644 accessible/xul/XULTabAccessible.cpp create mode 100644 accessible/xul/XULTabAccessible.h create mode 100644 accessible/xul/XULTreeAccessible.cpp create mode 100644 accessible/xul/XULTreeAccessible.h create mode 100644 accessible/xul/XULTreeGridAccessible.cpp create mode 100644 accessible/xul/XULTreeGridAccessible.h create mode 100644 accessible/xul/moz.build (limited to 'accessible') diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp new file mode 100644 index 0000000000..0b9fda291d --- /dev/null +++ b/accessible/android/AccessibleWrap.cpp @@ -0,0 +1,679 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccessibleWrap.h" + +#include "JavaBuiltins.h" +#include "LocalAccessible-inl.h" +#include "HyperTextAccessible-inl.h" +#include "AccAttributes.h" +#include "AccEvent.h" +#include "AndroidInputType.h" +#include "DocAccessibleWrap.h" +#include "SessionAccessibility.h" +#include "TextLeafAccessible.h" +#include "TraversalRule.h" +#include "Pivot.h" +#include "Platform.h" +#include "nsAccessibilityService.h" +#include "nsEventShell.h" +#include "nsIAccessibleAnnouncementEvent.h" +#include "nsAccUtils.h" +#include "nsTextEquivUtils.h" +#include "nsWhitespaceTokenizer.h" +#include "RootAccessible.h" + +#include "mozilla/a11y/PDocAccessibleChild.h" +#include "mozilla/jni/GeckoBundleUtils.h" +#include "mozilla/a11y/DocAccessibleParent.h" + +// icu TRUE conflicting with java::sdk::Boolean::TRUE() +// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265 +// https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78 +#ifdef TRUE +# undef TRUE +#endif + +using namespace mozilla::a11y; + +//----------------------------------------------------- +// construction +//----------------------------------------------------- +AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) + : LocalAccessible(aContent, aDoc), mID(SessionAccessibility::kUnsetID) { + if (!IPCAccessibilityActive()) { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + SessionAccessibility::RegisterAccessible(this); + } +} + +//----------------------------------------------------- +// destruction +//----------------------------------------------------- +AccessibleWrap::~AccessibleWrap() {} + +nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { + auto accessible = static_cast(aEvent->GetAccessible()); + NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE); + DocAccessibleWrap* doc = + static_cast(accessible->Document()); + if (doc) { + switch (aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) { + AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent); + HyperTextAccessible* ht = AsHyperText(); + // Pivot to the caret's position if it has an expanded selection. + // This is used mostly for find in page. + if ((ht && ht->SelectionCount())) { + DOMPoint point = + AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset()); + if (LocalAccessible* newPos = + doc->GetAccessibleOrContainer(point.node)) { + static_cast(newPos)->PivotTo( + java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, + true); + } + } + } + break; + } + case nsIAccessibleEvent::EVENT_SCROLLING_START: { + accessible->PivotTo( + java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, true); + break; + } + default: + break; + } + } + + nsresult rv = LocalAccessible::HandleAccEvent(aEvent); + NS_ENSURE_SUCCESS(rv, rv); + + accessible->HandleLiveRegionEvent(aEvent); + + if (IPCAccessibilityActive()) { + return NS_OK; + } + + // The accessible can become defunct if we have an xpcom event listener + // which decides it would be fun to change the DOM and flush layout. + if (accessible->IsDefunct() || !accessible->IsBoundToParent()) { + return NS_OK; + } + + if (doc) { + if (!doc->DocumentNode()->IsContentDocument()) { + return NS_OK; + } + } + + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(accessible); + if (!sessionAcc) { + return NS_OK; + } + + switch (aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_FOCUS: + sessionAcc->SendFocusEvent(accessible); + break; + case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: { + AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent); + if (!vcEvent->IsFromUserInput()) { + break; + } + + RefPtr newPosition = + static_cast(vcEvent->NewAccessible()); + if (sessionAcc && newPosition) { + if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) { + sessionAcc->SendHoverEnterEvent(newPosition); + } else if (vcEvent->BoundaryType() == nsIAccessiblePivot::NO_BOUNDARY) { + sessionAcc->SendAccessibilityFocusedEvent(newPosition); + } + + if (vcEvent->BoundaryType() != nsIAccessiblePivot::NO_BOUNDARY) { + sessionAcc->SendTextTraversedEvent( + newPosition, vcEvent->NewStartOffset(), vcEvent->NewEndOffset()); + } + } + break; + } + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + AccCaretMoveEvent* event = downcast_accEvent(aEvent); + sessionAcc->SendTextSelectionChangedEvent(accessible, + event->GetCaretOffset()); + break; + } + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { + AccTextChangeEvent* event = downcast_accEvent(aEvent); + sessionAcc->SendTextChangedEvent( + accessible, event->ModifiedText(), event->GetStartOffset(), + event->GetLength(), event->IsTextInserted(), + event->IsFromUserInput()); + break; + } + case nsIAccessibleEvent::EVENT_STATE_CHANGE: { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + auto state = event->GetState(); + if (state & states::CHECKED) { + sessionAcc->SendClickedEvent( + accessible, java::SessionAccessibility::FLAG_CHECKABLE | + (event->IsStateEnabled() + ? java::SessionAccessibility::FLAG_CHECKED + : 0)); + } + + if (state & states::EXPANDED) { + sessionAcc->SendClickedEvent( + accessible, java::SessionAccessibility::FLAG_EXPANDABLE | + (event->IsStateEnabled() + ? java::SessionAccessibility::FLAG_EXPANDED + : 0)); + } + + if (state & states::SELECTED) { + sessionAcc->SendSelectedEvent(accessible, event->IsStateEnabled()); + } + + if (state & states::BUSY) { + sessionAcc->SendWindowStateChangedEvent(accessible); + } + break; + } + case nsIAccessibleEvent::EVENT_SCROLLING: { + AccScrollingEvent* event = downcast_accEvent(aEvent); + sessionAcc->SendScrollingEvent(accessible, event->ScrollX(), + event->ScrollY(), event->MaxScrollX(), + event->MaxScrollY()); + break; + } + case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: { + AccAnnouncementEvent* event = downcast_accEvent(aEvent); + sessionAcc->SendAnnouncementEvent(accessible, event->Announcement(), + event->Priority()); + break; + } + case nsIAccessibleEvent::EVENT_REORDER: { + sessionAcc->SendWindowContentChangedEvent(); + break; + } + default: + break; + } + + return NS_OK; +} + +void AccessibleWrap::Shutdown() { + if (!IPCAccessibilityActive()) { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + SessionAccessibility::UnregisterAccessible(this); + } + LocalAccessible::Shutdown(); +} + +bool AccessibleWrap::DoAction(uint8_t aIndex) const { + if (ActionCount()) { + return LocalAccessible::DoAction(aIndex); + } + + if (mContent) { + // We still simulate a click on an accessible even if there is no + // known actions. For the sake of bad markup. + DoCommand(); + return true; + } + + return false; +} + +Accessible* AccessibleWrap::DoPivot(Accessible* aAccessible, + int32_t aGranularity, bool aForward, + bool aInclusive) { + Accessible* pivotRoot = nullptr; + if (aAccessible->IsRemote()) { + // If this is a remote accessible provide the top level + // remote doc as the pivot root for thread safety reasons. + DocAccessibleParent* doc = aAccessible->AsRemote()->Document(); + while (doc && !doc->IsTopLevel()) { + doc = doc->ParentDoc(); + } + MOZ_ASSERT(doc, "Failed to get top level DocAccessibleParent"); + pivotRoot = doc; + } + a11y::Pivot pivot(pivotRoot); + // Depending on the start accessible, the pivot rule will either traverse + // local or remote accessibles exclusively. + TraversalRule rule(aGranularity, aAccessible->IsLocal()); + Accessible* result = aForward ? pivot.Next(aAccessible, rule, aInclusive) + : pivot.Prev(aAccessible, rule, aInclusive); + + if (result && (result != aAccessible || aInclusive)) { + return result; + } + + return nullptr; +} + +bool AccessibleWrap::PivotTo(int32_t aGranularity, bool aForward, + bool aInclusive) { + Accessible* result = DoPivot(this, aGranularity, aForward, aInclusive); + if (result) { + MOZ_ASSERT(result->IsLocal()); + // Dispatch a virtual cursor change event that will be turned into an + // android accessibility focused changed event in the parent. + PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT + : nsIAccessiblePivot::REASON_PREV; + LocalAccessible* localResult = result->AsLocal(); + RefPtr event = new AccVCChangeEvent( + localResult->Document(), this, -1, -1, localResult, -1, -1, reason, + nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput); + nsEventShell::FireEvent(event); + + return true; + } + + return false; +} + +void AccessibleWrap::ExploreByTouch(float aX, float aY) { + a11y::Pivot pivot(RootAccessible()); + TraversalRule rule; + + Accessible* maybeResult = pivot.AtPoint(aX, aY, rule); + LocalAccessible* result = maybeResult ? maybeResult->AsLocal() : nullptr; + + if (result && result != this) { + RefPtr event = + new AccVCChangeEvent(result->Document(), this, -1, -1, result, -1, -1, + nsIAccessiblePivot::REASON_POINT, + nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput); + nsEventShell::FireEvent(event); + } +} + +void AccessibleWrap::NavigateText(int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, + bool aSelect) { + a11y::Pivot pivot(RootAccessible()); + + HyperTextAccessible* editable = + (State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr; + + int32_t start = aStartOffset, end = aEndOffset; + // If the accessible is an editable, set the virtual cursor position + // to its caret offset. Otherwise use the document's virtual cursor + // position as a starting offset. + if (editable) { + start = end = editable->CaretOffset(); + } + + uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY; + switch (aGranularity) { + case 1: // MOVEMENT_GRANULARITY_CHARACTER + pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY; + break; + case 2: // MOVEMENT_GRANULARITY_WORD + pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY; + break; + default: + break; + } + + int32_t newOffset; + Accessible* newAnchorBase = nullptr; + if (aForward) { + newAnchorBase = pivot.NextText(this, &start, &end, pivotGranularity); + newOffset = end; + } else { + newAnchorBase = pivot.PrevText(this, &start, &end, pivotGranularity); + newOffset = start; + } + LocalAccessible* newAnchor = + newAnchorBase ? newAnchorBase->AsLocal() : nullptr; + + if (newAnchor && (start != aStartOffset || end != aEndOffset)) { + if (IsTextLeaf() && newAnchor == LocalParent()) { + // For paragraphs, divs, spans, etc., we put a11y focus on the text leaf + // node instead of the HyperTextAccessible. However, Pivot will always + // return a HyperTextAccessible. Android doesn't support text navigation + // landing on an accessible which is different to the originating + // accessible. Therefore, if we're still within the same text leaf, + // translate the offsets to the text leaf. + int32_t thisChild = IndexInParent(); + HyperTextAccessible* newHyper = newAnchor->AsHyperText(); + MOZ_ASSERT(newHyper); + int32_t startChild = newHyper->GetChildIndexAtOffset(start); + // We use end - 1 because the end offset is exclusive, so end itself + // might be associated with the next child. + int32_t endChild = newHyper->GetChildIndexAtOffset(end - 1); + if (startChild == thisChild && endChild == thisChild) { + // We've landed within the same text leaf. + newAnchor = this; + int32_t thisOffset = newHyper->GetChildOffset(thisChild); + start -= thisOffset; + end -= thisOffset; + } + } + RefPtr event = new AccVCChangeEvent( + newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start, + end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput); + nsEventShell::FireEvent(event); + } + + // If we are in an editable, move the caret to the new virtual cursor + // offset. + if (editable) { + if (aSelect) { + int32_t anchor = editable->CaretOffset(); + if (editable->SelectionCount()) { + int32_t startSel, endSel; + GetSelectionOrCaret(&startSel, &endSel); + anchor = startSel == anchor ? endSel : startSel; + } + editable->SetSelectionBoundsAt(0, anchor, newOffset); + } else { + editable->SetCaretOffset(newOffset); + } + } +} + +void AccessibleWrap::SetSelection(int32_t aStart, int32_t aEnd) { + if (HyperTextAccessible* textAcc = AsHyperText()) { + if (aStart == aEnd) { + textAcc->SetCaretOffset(aStart); + } else { + textAcc->SetSelectionBoundsAt(0, aStart, aEnd); + } + } +} + +void AccessibleWrap::Cut() { + if ((State() & states::EDITABLE) == 0) { + return; + } + + if (HyperTextAccessible* textAcc = AsHyperText()) { + int32_t startSel, endSel; + GetSelectionOrCaret(&startSel, &endSel); + textAcc->CutText(startSel, endSel); + } +} + +void AccessibleWrap::Copy() { + if (HyperTextAccessible* textAcc = AsHyperText()) { + int32_t startSel, endSel; + GetSelectionOrCaret(&startSel, &endSel); + textAcc->CopyText(startSel, endSel); + } +} + +void AccessibleWrap::Paste() { + if ((State() & states::EDITABLE) == 0) { + return; + } + + if (IsHyperText()) { + RefPtr textAcc = AsHyperText(); + int32_t startSel, endSel; + GetSelectionOrCaret(&startSel, &endSel); + if (startSel != endSel) { + textAcc->DeleteText(startSel, endSel); + } + textAcc->PasteText(startSel); + } +} + +void AccessibleWrap::GetSelectionOrCaret(int32_t* aStartOffset, + int32_t* aEndOffset) { + *aStartOffset = *aEndOffset = -1; + if (HyperTextAccessible* textAcc = AsHyperText()) { + if (!textAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) { + *aStartOffset = *aEndOffset = textAcc->CaretOffset(); + } + } +} + +uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState, + uint8_t aActionCount) { + uint32_t flags = 0; + if (aState & states::CHECKABLE) { + flags |= java::SessionAccessibility::FLAG_CHECKABLE; + } + + if (aState & states::CHECKED) { + flags |= java::SessionAccessibility::FLAG_CHECKED; + } + + if (aState & states::INVALID) { + flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID; + } + + if (aState & states::EDITABLE) { + flags |= java::SessionAccessibility::FLAG_EDITABLE; + } + + if (aActionCount && aRole != roles::TEXT_LEAF) { + flags |= java::SessionAccessibility::FLAG_CLICKABLE; + } + + if (aState & states::ENABLED) { + flags |= java::SessionAccessibility::FLAG_ENABLED; + } + + if (aState & states::FOCUSABLE) { + flags |= java::SessionAccessibility::FLAG_FOCUSABLE; + } + + if (aState & states::FOCUSED) { + flags |= java::SessionAccessibility::FLAG_FOCUSED; + } + + if (aState & states::MULTI_LINE) { + flags |= java::SessionAccessibility::FLAG_MULTI_LINE; + } + + if (aState & states::SELECTABLE) { + flags |= java::SessionAccessibility::FLAG_SELECTABLE; + } + + if (aState & states::SELECTED) { + flags |= java::SessionAccessibility::FLAG_SELECTED; + } + + if (aState & states::EXPANDABLE) { + flags |= java::SessionAccessibility::FLAG_EXPANDABLE; + } + + if (aState & states::EXPANDED) { + flags |= java::SessionAccessibility::FLAG_EXPANDED; + } + + if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) { + flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER; + } + + if (aRole == roles::PASSWORD_TEXT) { + flags |= java::SessionAccessibility::FLAG_PASSWORD; + } + + return flags; +} + +void AccessibleWrap::GetRoleDescription(role aRole, AccAttributes* aAttributes, + nsAString& aGeckoRole, + nsAString& aRoleDescription) { + if (aRole == roles::HEADING && aAttributes) { + // The heading level is an attribute, so we need that. + nsAutoString headingLevel; + if (aAttributes->GetAttribute(nsGkAtoms::level, headingLevel)) { + nsAutoString token(u"heading-"); + token.Append(headingLevel); + if (LocalizeString(token, aRoleDescription)) { + return; + } + } + } + + if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) { + nsAutoString xmlRoles; + if (aAttributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles)) { + nsWhitespaceTokenizer tokenizer(xmlRoles); + while (tokenizer.hasMoreTokens()) { + if (LocalizeString(tokenizer.nextToken(), aRoleDescription)) { + return; + } + } + } + } + + GetAccService()->GetStringRole(aRole, aGeckoRole); + LocalizeString(aGeckoRole, aRoleDescription); +} + +int32_t AccessibleWrap::AndroidClass(Accessible* aAccessible) { + return GetVirtualViewID(aAccessible) == SessionAccessibility::kNoID + ? java::SessionAccessibility::CLASSNAME_WEBVIEW + : GetAndroidClass(aAccessible->Role()); +} + +int32_t AccessibleWrap::GetVirtualViewID(Accessible* aAccessible) { + if (aAccessible->IsLocal()) { + return static_cast(aAccessible)->mID; + } + + return static_cast(aAccessible->AsRemote()->GetWrapper()); +} + +void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible, + int32_t aVirtualViewID) { + if (aAccessible->IsLocal()) { + static_cast(aAccessible)->mID = aVirtualViewID; + } else { + aAccessible->AsRemote()->SetWrapper(static_cast(aVirtualViewID)); + } +} + +int32_t AccessibleWrap::GetAndroidClass(role aRole) { +#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ + msaaRole, ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + return androidClass; + + switch (aRole) { +#include "RoleMap.h" + default: + return java::SessionAccessibility::CLASSNAME_VIEW; + } + +#undef ROLE +} + +int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) { + if (aInputTypeAttr.EqualsIgnoreCase("email")) { + return java::sdk::InputType::TYPE_CLASS_TEXT | + java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; + } + + if (aInputTypeAttr.EqualsIgnoreCase("number")) { + return java::sdk::InputType::TYPE_CLASS_NUMBER; + } + + if (aInputTypeAttr.EqualsIgnoreCase("password")) { + return java::sdk::InputType::TYPE_CLASS_TEXT | + java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD; + } + + if (aInputTypeAttr.EqualsIgnoreCase("tel")) { + return java::sdk::InputType::TYPE_CLASS_PHONE; + } + + if (aInputTypeAttr.EqualsIgnoreCase("text")) { + return java::sdk::InputType::TYPE_CLASS_TEXT | + java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; + } + + if (aInputTypeAttr.EqualsIgnoreCase("url")) { + return java::sdk::InputType::TYPE_CLASS_TEXT | + java::sdk::InputType::TYPE_TEXT_VARIATION_URI; + } + + return 0; +} + +void AccessibleWrap::GetTextEquiv(nsString& aText) { + if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) { + // This is an accessible that normally doesn't get its name from its + // subtree, so we collect the text equivalent explicitly. + nsTextEquivUtils::GetTextEquivFromSubtree(this, aText); + } else { + Name(aText); + } +} + +bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) { + auto eventType = aEvent->GetEventType(); + if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED && + eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) { + // XXX: Right now only announce text inserted events. aria-relevant=removals + // is potentially on the chopping block[1]. We also don't support editable + // text because we currently can't descern the source of the change[2]. + // 1. https://github.com/w3c/aria/issues/712 + // 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189 + return false; + } + + if (aEvent->IsFromUserInput()) { + return false; + } + + RefPtr attributes = new AccAttributes(); + nsAccUtils::SetLiveContainerAttributes(attributes, this); + nsString live; + if (!attributes->GetAttribute(nsGkAtoms::containerLive, live)) { + return false; + } + + uint16_t priority = live.EqualsIgnoreCase("assertive") + ? nsIAccessibleAnnouncementEvent::ASSERTIVE + : nsIAccessibleAnnouncementEvent::POLITE; + + Maybe atomic = + attributes->GetAttribute(nsGkAtoms::containerAtomic); + LocalAccessible* announcementTarget = this; + nsAutoString announcement; + if (atomic && *atomic) { + LocalAccessible* atomicAncestor = nullptr; + for (LocalAccessible* parent = announcementTarget; parent; + parent = parent->LocalParent()) { + dom::Element* element = parent->Elm(); + if (element && + nsAccUtils::ARIAAttrValueIs(element, nsGkAtoms::aria_atomic, + nsGkAtoms::_true, eCaseMatters)) { + atomicAncestor = parent; + break; + } + } + + if (atomicAncestor) { + announcementTarget = atomicAncestor; + static_cast(atomicAncestor)->GetTextEquiv(announcement); + } + } else { + GetTextEquiv(announcement); + } + + announcement.CompressWhitespace(); + if (announcement.IsEmpty()) { + return false; + } + + announcementTarget->Announce(announcement, priority); + return true; +} diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h new file mode 100644 index 0000000000..3b5ef43f13 --- /dev/null +++ b/accessible/android/AccessibleWrap.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_AccessibleWrap_h_ +#define mozilla_a11y_AccessibleWrap_h_ + +#include "LocalAccessible.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/java/GeckoBundleWrappers.h" +#include "nsCOMPtr.h" + +namespace mozilla { +namespace a11y { + +class AccessibleWrap : public LocalAccessible { + public: + AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc); + virtual ~AccessibleWrap(); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY // TODO: Mark this as MOZ_CAN_RUN_SCRIPT + virtual nsresult + HandleAccEvent(AccEvent* aEvent) override; + + virtual void Shutdown() override; + + virtual bool DoAction(uint8_t aIndex) const override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual bool PivotTo(int32_t aGranularity, bool aForward, bool aInclusive); + + virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, bool aSelect); + + virtual void SetSelection(int32_t aStart, int32_t aEnd); + + virtual void Cut(); + + virtual void Copy(); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual void Paste(); + + void ExploreByTouch(float aX, float aY); + + static uint32_t GetFlags(role aRole, uint64_t aState, uint8_t aActionCount); + + static int32_t GetInputType(const nsString& aInputTypeAttr); + + static int32_t GetAndroidClass(role aRole); + + static void GetRoleDescription(role aRole, AccAttributes* aAttributes, + nsAString& aGeckoRole, + nsAString& aRoleDescription); + + static int32_t AndroidClass(Accessible* aAccessible); + + static int32_t GetVirtualViewID(Accessible* aAccessible); + + static void SetVirtualViewID(Accessible* aAccessible, int32_t aVirtualViewID); + + static Accessible* DoPivot(Accessible* aAccessible, int32_t aGranularity, + bool aForward, bool aInclusive); + + protected: + int32_t mID; + + private: + void GetTextEquiv(nsString& aText); + + bool HandleLiveRegionEvent(AccEvent* aEvent); + + void GetSelectionOrCaret(int32_t* aStartOffset, int32_t* aEndOffset); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/ApplicationAccessibleWrap.h b/accessible/android/ApplicationAccessibleWrap.h new file mode 100644 index 0000000000..89b07916c9 --- /dev/null +++ b/accessible/android/ApplicationAccessibleWrap.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__ +#define mozilla_a11y_ApplicationAccessibleWrap_h__ + +#include "ApplicationAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef ApplicationAccessible ApplicationAccessibleWrap; +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/DocAccessibleWrap.cpp b/accessible/android/DocAccessibleWrap.cpp new file mode 100644 index 0000000000..9f661dd226 --- /dev/null +++ b/accessible/android/DocAccessibleWrap.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "LocalAccessible-inl.h" +#include "AccAttributes.h" +#include "DocAccessibleChild.h" +#include "DocAccessibleWrap.h" +#include "nsIDocShell.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "Pivot.h" +#include "SessionAccessibility.h" +#include "TraversalRule.h" +#include "mozilla/PresShell.h" +#include "mozilla/a11y/DocAccessiblePlatformExtChild.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +#define UNIQUE_ID(acc) \ + !acc || (acc->IsDoc() && acc->AsDoc()->IPCDoc()) \ + ? 0 \ + : reinterpret_cast(acc->UniqueID()) + +//////////////////////////////////////////////////////////////////////////////// +// DocAccessibleWrap +//////////////////////////////////////////////////////////////////////////////// + +DocAccessibleWrap::DocAccessibleWrap(Document* aDocument, PresShell* aPresShell) + : DocAccessible(aDocument, aPresShell) { + // We need an nsINode associated with this accessible to register it with the + // right SessionAccessibility instance. When the base AccessibleWrap + // constructor is called we don't have one yet because null is passed as the + // content node. So we do it here after a Document is associated with the + // accessible. + if (!IPCAccessibilityActive()) { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + SessionAccessibility::RegisterAccessible(this); + } +} + +DocAccessibleWrap::~DocAccessibleWrap() {} + +void DocAccessibleWrap::Shutdown() { + // Unregister here before disconnecting from PresShell. + if (!IPCAccessibilityActive()) { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + if (IsRoot()) { + SessionAccessibility::UnregisterAll(PresShellPtr()); + } else { + SessionAccessibility::UnregisterAccessible(this); + } + } + DocAccessible::Shutdown(); +} + +DocAccessibleWrap* DocAccessibleWrap::GetTopLevelContentDoc( + AccessibleWrap* aAccessible) { + DocAccessibleWrap* doc = + static_cast(aAccessible->Document()); + while (doc && !doc->IsTopLevelContentDoc()) { + doc = static_cast(doc->ParentDocument()); + } + + return doc; +} + +bool DocAccessibleWrap::IsTopLevelContentDoc() { + DocAccessible* parentDoc = ParentDocument(); + return DocumentNode()->IsContentDocument() && + (!parentDoc || !parentDoc->DocumentNode()->IsContentDocument()); +} + +#undef UNIQUE_ID diff --git a/accessible/android/DocAccessibleWrap.h b/accessible/android/DocAccessibleWrap.h new file mode 100644 index 0000000000..c4408cdf41 --- /dev/null +++ b/accessible/android/DocAccessibleWrap.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_DocAccessibleWrap_h__ +#define mozilla_a11y_DocAccessibleWrap_h__ + +#include "DocAccessible.h" +#include "nsITimer.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class DocAccessibleWrap : public DocAccessible { + public: + DocAccessibleWrap(Document* aDocument, PresShell* aPresShell); + virtual ~DocAccessibleWrap(); + + virtual void Shutdown() override; + + DocAccessibleWrap* GetTopLevelContentDoc(AccessibleWrap* aAccessible); + + bool IsTopLevelContentDoc(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/HyperTextAccessibleWrap.h b/accessible/android/HyperTextAccessibleWrap.h new file mode 100644 index 0000000000..da569c8216 --- /dev/null +++ b/accessible/android/HyperTextAccessibleWrap.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_HyperTextAccessibleWrap_h__ +#define mozilla_a11y_HyperTextAccessibleWrap_h__ + +#include "HyperTextAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class HyperTextAccessible HyperTextAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/Platform.cpp b/accessible/android/Platform.cpp new file mode 100644 index 0000000000..9bde1cbc70 --- /dev/null +++ b/accessible/android/Platform.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "Platform.h" +#include "DocAccessibleWrap.h" +#include "SessionAccessibility.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/Components.h" +#include "nsIAccessibleEvent.h" +#include "nsIAccessiblePivot.h" +#include "nsIStringBundle.h" + +#define ROLE_STRINGS_URL "chrome://global/locale/AccessFu.properties" + +using namespace mozilla; +using namespace mozilla::a11y; + +static nsTHashMap sLocalizedStrings; + +void a11y::PlatformInit() { + nsresult rv = NS_OK; + nsCOMPtr stringBundleService = + components::StringBundle::Service(); + if (!stringBundleService) return; + + nsCOMPtr stringBundle; + nsCOMPtr sbs = components::StringBundle::Service(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to get string bundle service"); + return; + } + + rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(stringBundle)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to get string bundle"); + return; + } + + nsString localizedStr; + // Preload the state required localized string. + rv = stringBundle->GetStringFromName("stateRequired", localizedStr); + if (NS_SUCCEEDED(rv)) { + sLocalizedStrings.InsertOrUpdate(u"stateRequired"_ns, localizedStr); + } + + // Preload heading level localized descriptions 1 thru 6. + for (int32_t level = 1; level <= 6; level++) { + nsAutoString token; + token.AppendPrintf("heading-%d", level); + + nsAutoString formatString; + formatString.AppendInt(level); + AutoTArray formatParams; + formatParams.AppendElement(formatString); + rv = stringBundle->FormatStringFromName("headingLevel", formatParams, + localizedStr); + if (NS_SUCCEEDED(rv)) { + sLocalizedStrings.InsertOrUpdate(token, localizedStr); + } + } + + // Preload any roles that have localized versions +#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ + msaaRole, ia2Role, androidClass, nameRule) \ + rv = stringBundle->GetStringFromName(stringRole, localizedStr); \ + if (NS_SUCCEEDED(rv)) { \ + sLocalizedStrings.InsertOrUpdate(u##stringRole##_ns, localizedStr); \ + } + +#include "RoleMap.h" +#undef ROLE +} + +void a11y::PlatformShutdown() { sLocalizedStrings.Clear(); } + +void a11y::ProxyCreated(RemoteAccessible* aProxy) { + SessionAccessibility::RegisterAccessible(aProxy); +} + +void a11y::ProxyDestroyed(RemoteAccessible* aProxy) { + SessionAccessibility::UnregisterAccessible(aProxy); +} + +void a11y::ProxyEvent(RemoteAccessible* aTarget, uint32_t aEventType) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + if (!sessionAcc) { + return; + } + + switch (aEventType) { + case nsIAccessibleEvent::EVENT_FOCUS: + sessionAcc->SendFocusEvent(aTarget); + break; + case nsIAccessibleEvent::EVENT_REORDER: + sessionAcc->SendWindowContentChangedEvent(); + break; + default: + break; + } +} + +void a11y::ProxyStateChangeEvent(RemoteAccessible* aTarget, uint64_t aState, + bool aEnabled) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (!sessionAcc) { + return; + } + + if (aState & states::CHECKED) { + sessionAcc->SendClickedEvent( + aTarget, java::SessionAccessibility::FLAG_CHECKABLE | + (aEnabled ? java::SessionAccessibility::FLAG_CHECKED : 0)); + } + + if (aState & states::EXPANDED) { + sessionAcc->SendClickedEvent( + aTarget, + java::SessionAccessibility::FLAG_EXPANDABLE | + (aEnabled ? java::SessionAccessibility::FLAG_EXPANDED : 0)); + } + + if (aState & states::SELECTED) { + sessionAcc->SendSelectedEvent(aTarget, aEnabled); + } + + if (aState & states::BUSY) { + sessionAcc->SendWindowStateChangedEvent(aTarget); + } +} + +void a11y::ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, + int32_t aGranularity) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (sessionAcc) { + sessionAcc->SendTextSelectionChangedEvent(aTarget, aOffset); + } +} + +void a11y::ProxyTextChangeEvent(RemoteAccessible* aTarget, + const nsAString& aStr, int32_t aStart, + uint32_t aLen, bool aIsInsert, bool aFromUser) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (sessionAcc) { + sessionAcc->SendTextChangedEvent(aTarget, aStr, aStart, aLen, aIsInsert, + aFromUser); + } +} + +void a11y::ProxyShowHideEvent(RemoteAccessible* aTarget, + RemoteAccessible* aParent, bool aInsert, + bool aFromUser) { + // We rely on the window content changed events to be dispatched + // after the viewport cache is refreshed. +} + +void a11y::ProxySelectionEvent(RemoteAccessible*, RemoteAccessible*, uint32_t) { +} + +void a11y::ProxyVirtualCursorChangeEvent( + RemoteAccessible* aTarget, RemoteAccessible* aOldPosition, + int32_t aOldStartOffset, int32_t aOldEndOffset, + RemoteAccessible* aNewPosition, int32_t aNewStartOffset, + int32_t aNewEndOffset, int16_t aReason, int16_t aBoundaryType, + bool aFromUser) { + if (!aNewPosition || !aFromUser) { + return; + } + + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (!sessionAcc) { + return; + } + + if (aReason == nsIAccessiblePivot::REASON_POINT) { + sessionAcc->SendHoverEnterEvent(aNewPosition); + } else if (aBoundaryType == nsIAccessiblePivot::NO_BOUNDARY) { + sessionAcc->SendAccessibilityFocusedEvent(aNewPosition); + } + + if (aBoundaryType != nsIAccessiblePivot::NO_BOUNDARY) { + sessionAcc->SendTextTraversedEvent(aNewPosition, aNewStartOffset, + aNewEndOffset); + } +} + +void a11y::ProxyScrollingEvent(RemoteAccessible* aTarget, uint32_t aEventType, + uint32_t aScrollX, uint32_t aScrollY, + uint32_t aMaxScrollX, uint32_t aMaxScrollY) { + if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (sessionAcc) { + sessionAcc->SendScrollingEvent(aTarget, aScrollX, aScrollY, aMaxScrollX, + aMaxScrollY); + } + } +} + +void a11y::ProxyAnnouncementEvent(RemoteAccessible* aTarget, + const nsAString& aAnnouncement, + uint16_t aPriority) { + RefPtr sessionAcc = + SessionAccessibility::GetInstanceFor(aTarget); + + if (sessionAcc) { + sessionAcc->SendAnnouncementEvent(aTarget, aAnnouncement, aPriority); + } +} + +bool a11y::LocalizeString(const nsAString& aToken, nsAString& aLocalized) { + MOZ_ASSERT(XRE_IsParentProcess()); + + auto str = sLocalizedStrings.Lookup(aToken); + if (str) { + aLocalized.Assign(*str); + } else { + } + + return !!str; +} diff --git a/accessible/android/RootAccessibleWrap.cpp b/accessible/android/RootAccessibleWrap.cpp new file mode 100644 index 0000000000..d21aed39ba --- /dev/null +++ b/accessible/android/RootAccessibleWrap.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RootAccessibleWrap.h" + +#include "LocalAccessible-inl.h" + +#include "DocAccessibleParent.h" +#include "DocAccessible-inl.h" +#include "SessionAccessibility.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MouseEvent.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDoc, + PresShell* aPresShell) + : RootAccessible(aDoc, aPresShell) {} + +RootAccessibleWrap::~RootAccessibleWrap() {} + +nsresult RootAccessibleWrap::AddEventListeners() { + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr nstarget = window ? window->GetParentTarget() : nullptr; + + if (nstarget) { + nstarget->AddEventListener(u"MozMouseExploreByTouch"_ns, this, false, true); + } + + return RootAccessible::AddEventListeners(); +} + +nsresult RootAccessibleWrap::RemoveEventListeners() { + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr nstarget = window ? window->GetParentTarget() : nullptr; + if (nstarget) { + nstarget->RemoveEventListener(u"MozMouseExploreByTouch"_ns, this, true); + } + + return RootAccessible::RemoveEventListeners(); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +RootAccessibleWrap::HandleEvent(Event* aDOMEvent) { + WidgetMouseEvent* widgetEvent = aDOMEvent->WidgetEventPtr()->AsMouseEvent(); + if (widgetEvent && widgetEvent->mMessage == eMouseExploreByTouch) { + if (HasShutdown()) { + return NS_OK; + } + + if (MouseEvent* mouseEvent = aDOMEvent->AsMouseEvent()) { + LayoutDeviceIntPoint point = mouseEvent->ScreenPointLayoutDevicePix(); + ExploreByTouch(point.x, point.y); + } + + return NS_OK; + } + + return RootAccessible::HandleEvent(aDOMEvent); +} diff --git a/accessible/android/RootAccessibleWrap.h b/accessible/android/RootAccessibleWrap.h new file mode 100644 index 0000000000..5c5ba95f61 --- /dev/null +++ b/accessible/android/RootAccessibleWrap.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_RootAccessibleWrap_h__ +#define mozilla_a11y_RootAccessibleWrap_h__ + +#include "RootAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +/** + * Android specific functionality for the node at a root of the accessibility + * tree: see the RootAccessible superclass for further details. + */ +class RootAccessibleWrap : public RootAccessible { + public: + RootAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~RootAccessibleWrap(); + + // nsIDOMEventListener + NS_DECL_NSIDOMEVENTLISTENER + + protected: + virtual nsresult AddEventListeners() override; + virtual nsresult RemoveEventListeners() override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp new file mode 100644 index 0000000000..a5898a6cea --- /dev/null +++ b/accessible/android/SessionAccessibility.cpp @@ -0,0 +1,807 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SessionAccessibility.h" +#include "LocalAccessible-inl.h" +#include "AndroidUiThread.h" +#include "AndroidBridge.h" +#include "DocAccessibleParent.h" +#include "IDSet.h" +#include "nsThreadUtils.h" +#include "AccAttributes.h" +#include "AccessibilityEvent.h" +#include "HyperTextAccessible.h" +#include "HyperTextAccessible-inl.h" +#include "JavaBuiltins.h" +#include "RootAccessibleWrap.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsViewManager.h" + +#include "mozilla/PresShell.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/DocAccessiblePlatformExtParent.h" +#include "mozilla/a11y/DocManager.h" +#include "mozilla/jni/GeckoBundleUtils.h" +#include "mozilla/jni/NativesInlines.h" +#include "mozilla/widget/GeckoViewSupport.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/dom/MouseEventBinding.h" + +#ifdef DEBUG +# include +# define AALOG(args...) \ + __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args) +#else +# define AALOG(args...) \ + do { \ + } while (0) +#endif + +#define FORWARD_ACTION_TO_ACCESSIBLE(funcname, ...) \ + MOZ_ASSERT(NS_IsMainThread()); \ + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \ + if (Accessible* acc = GetAccessibleByID(aID)) { \ + if (acc->IsRemote()) { \ + acc->AsRemote()->funcname(__VA_ARGS__); \ + } else { \ + static_cast(acc->AsLocal())->funcname(__VA_ARGS__); \ + } \ + } + +#define FORWARD_EXT_ACTION_TO_ACCESSIBLE(funcname, ...) \ + MOZ_ASSERT(NS_IsMainThread()); \ + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); \ + if (Accessible* acc = GetAccessibleByID(aID)) { \ + if (RemoteAccessible* remote = acc->AsRemote()) { \ + Unused << remote->Document()->GetPlatformExtension()->Send##funcname( \ + remote->ID(), ##__VA_ARGS__); \ + } else { \ + static_cast(acc->AsLocal())->funcname(__VA_ARGS__); \ + } \ + } + +using namespace mozilla::a11y; + +// IDs should be a positive 32bit integer. +IDSet sIDSet(31UL); + +class Settings final + : public mozilla::java::SessionAccessibility::Settings::Natives { + public: + static void ToggleNativeAccessibility(bool aEnable) { + if (aEnable) { + GetOrCreateAccService(); + } else { + MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI); + } + } +}; + +SessionAccessibility::SessionAccessibility( + jni::NativeWeakPtr aWindow, + java::SessionAccessibility::NativeProvider::Param aSessionAccessibility) + : mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) { + SetAttached(true, nullptr); +} + +void SessionAccessibility::SetAttached(bool aAttached, + already_AddRefed aRunnable) { + if (RefPtr uiThread = GetAndroidUiThread()) { + uiThread->Dispatch(NS_NewRunnableFunction( + "SessionAccessibility::Attach", + [aAttached, + sa = java::SessionAccessibility::NativeProvider::GlobalRef( + mSessionAccessibility), + runnable = RefPtr(aRunnable)] { + sa->SetAttached(aAttached); + if (runnable) { + runnable->Run(); + } + })); + } +} + +void SessionAccessibility::Init() { + java::SessionAccessibility::NativeProvider::Natives< + SessionAccessibility>::Init(); + Settings::Init(); +} + +void SessionAccessibility::GetNodeInfo(int32_t aID, + mozilla::jni::Object::Param aNodeInfo) { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + java::GeckoBundle::GlobalRef ret = nullptr; + RefPtr self(this); + if (Accessible* acc = GetAccessibleByID(aID)) { + if (acc->IsLocal()) { + mal.Unlock(); + nsAppShell::SyncRunEvent( + [this, self, aID, aNodeInfo = jni::Object::GlobalRef(aNodeInfo)] { + if (Accessible* acc = GetAccessibleByID(aID)) { + PopulateNodeInfo(acc, aNodeInfo); + } else { + AALOG("oops, nothing for %d", aID); + } + }); + } else { + PopulateNodeInfo(acc, aNodeInfo); + } + } else { + AALOG("oops, nothing for %d", aID); + } +} + +int SessionAccessibility::GetNodeClassName(int32_t aID) { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + ReleasableMonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW; + RefPtr self(this); + if (Accessible* acc = GetAccessibleByID(aID)) { + if (acc->IsLocal()) { + mal.Unlock(); + nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] { + if (Accessible* acc = GetAccessibleByID(aID)) { + classNameEnum = AccessibleWrap::AndroidClass(acc); + } + }); + } else { + classNameEnum = AccessibleWrap::AndroidClass(acc); + } + } + + return classNameEnum; +} + +void SessionAccessibility::SetText(int32_t aID, jni::String::Param aText) { + if (Accessible* acc = GetAccessibleByID(aID)) { + if (acc->IsRemote()) { + acc->AsRemote()->ReplaceText(PromiseFlatString(aText->ToString())); + } else if (acc->AsLocal()->IsHyperText()) { + acc->AsLocal()->AsHyperText()->ReplaceText(aText->ToString()); + } + } +} + +void SessionAccessibility::Click(int32_t aID) { + FORWARD_ACTION_TO_ACCESSIBLE(DoAction, 0); +} + +bool SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity, + bool aForward, bool aInclusive) { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + RefPtr self(this); + if (Accessible* acc = GetAccessibleByID(aID)) { + if (acc->IsLocal()) { + nsAppShell::PostEvent( + [this, self, aID, aGranularity, aForward, aInclusive] { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + if (Accessible* _acc = GetAccessibleByID(aID)) { + MOZ_ASSERT(_acc && _acc->IsLocal()); + if (LocalAccessible* localAcc = _acc->AsLocal()) { + static_cast(localAcc)->PivotTo( + aGranularity, aForward, aInclusive); + } + } + }); + return true; + } + Accessible* result = + AccessibleWrap::DoPivot(acc, aGranularity, aForward, aInclusive); + if (result) { + int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(result); + nsAppShell::PostEvent([this, self, virtualViewID] { + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); + if (Accessible* acc = GetAccessibleByID(virtualViewID)) { + SendAccessibilityFocusedEvent(acc); + } + }); + return true; + } + } + + return false; +} + +void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { + auto gvAccessor(mWindow.Access()); + if (gvAccessor) { + if (nsWindow* gkWindow = gvAccessor->GetNsWindow()) { + WidgetMouseEvent hittest(true, eMouseExploreByTouch, gkWindow, + WidgetMouseEvent::eReal); + hittest.mRefPoint = LayoutDeviceIntPoint::Floor(aX, aY); + hittest.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH; + hittest.mFlags.mOnlyChromeDispatch = true; + gkWindow->DispatchInputEvent(&hittest); + } + } +} + +void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, + int32_t aStartOffset, + int32_t aEndOffset, bool aForward, + bool aSelect) { + FORWARD_EXT_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset, + aEndOffset, aForward, aSelect); +} + +void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart, + int32_t aEnd) { + FORWARD_EXT_ACTION_TO_ACCESSIBLE(SetSelection, aStart, aEnd); +} + +void SessionAccessibility::Cut(int32_t aID) { + FORWARD_EXT_ACTION_TO_ACCESSIBLE(Cut); +} + +void SessionAccessibility::Copy(int32_t aID) { + FORWARD_EXT_ACTION_TO_ACCESSIBLE(Copy); +} + +void SessionAccessibility::Paste(int32_t aID) { + FORWARD_EXT_ACTION_TO_ACCESSIBLE(Paste); +} + +#undef FORWARD_ACTION_TO_ACCESSIBLE +#undef FORWARD_EXT_ACTION_TO_ACCESSIBLE + +RefPtr SessionAccessibility::GetInstanceFor( + Accessible* aAccessible) { + MOZ_ASSERT(NS_IsMainThread()); + if (LocalAccessible* localAcc = aAccessible->AsLocal()) { + DocAccessible* docAcc = localAcc->Document(); + // If the accessible is being shutdown from the doc's shutdown + // the doc accessible won't have a ref to a presshell anymore, + // but we should have a ref to the DOM document node, and the DOM doc + // has a ref to the presshell. + dom::Document* doc = docAcc ? docAcc->DocumentNode() : nullptr; + if (doc && doc->IsContentDocument()) { + // Only content accessibles should have an associated SessionAccessible. + return GetInstanceFor(doc->GetPresShell()); + } + } else { + DocAccessibleParent* remoteDoc = aAccessible->AsRemote()->Document(); + if (remoteDoc->mSessionAccessibility) { + return remoteDoc->mSessionAccessibility; + } + dom::CanonicalBrowsingContext* cbc = + static_cast(remoteDoc->Manager()) + ->GetBrowsingContext() + ->Top(); + dom::BrowserParent* bp = cbc->GetBrowserParent(); + if (!bp) { + bp = static_cast( + aAccessible->AsRemote()->Document()->Manager()); + } + if (auto element = bp->GetOwnerElement()) { + if (auto doc = element->OwnerDoc()) { + if (nsPresContext* presContext = doc->GetPresContext()) { + RefPtr sessionAcc = + GetInstanceFor(presContext->PresShell()); + remoteDoc->mSessionAccessibility = sessionAcc; + return sessionAcc; + } + } else { + MOZ_ASSERT_UNREACHABLE( + "Browser parent's element does not have owner doc."); + } + } + } + + return nullptr; +} + +RefPtr SessionAccessibility::GetInstanceFor( + PresShell* aPresShell) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aPresShell) { + return nullptr; + } + + nsViewManager* vm = aPresShell->GetViewManager(); + if (!vm) { + return nullptr; + } + + nsCOMPtr rootWidget = vm->GetRootWidget(); + // `rootWidget` can be one of several types. Here we make sure it is an + // android nsWindow. + if (RefPtr window = nsWindow::From(rootWidget)) { + return window->GetSessionAccessibility(); + } + + return nullptr; +} + +void SessionAccessibility::SendAccessibilityFocusedEvent( + Accessible* aAccessible) { + MOZ_ASSERT(NS_IsMainThread()); + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), nullptr); + aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); +} + +void SessionAccessibility::SendHoverEnterEvent(Accessible* aAccessible) { + MOZ_ASSERT(NS_IsMainThread()); + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), nullptr); +} + +void SessionAccessibility::SendFocusEvent(Accessible* aAccessible) { + MOZ_ASSERT(NS_IsMainThread()); + // Suppress focus events from about:blank pages. + // This is important for tests. + if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { + return; + } + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_FOCUSED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), nullptr); +} + +void SessionAccessibility::SendScrollingEvent(Accessible* aAccessible, + int32_t aScrollX, + int32_t aScrollY, + int32_t aMaxScrollX, + int32_t aMaxScrollY) { + MOZ_ASSERT(NS_IsMainThread()); + int32_t virtualViewId = AccessibleWrap::GetVirtualViewID(aAccessible); + + if (virtualViewId != kNoID) { + // XXX: Support scrolling in subframes + return; + } + + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "scrollX", java::sdk::Integer::ValueOf(aScrollX)); + GECKOBUNDLE_PUT(eventInfo, "scrollY", java::sdk::Integer::ValueOf(aScrollY)); + GECKOBUNDLE_PUT(eventInfo, "maxScrollX", + java::sdk::Integer::ValueOf(aMaxScrollX)); + GECKOBUNDLE_PUT(eventInfo, "maxScrollY", + java::sdk::Integer::ValueOf(aMaxScrollY)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_SCROLLED, virtualViewId, + AccessibleWrap::AndroidClass(aAccessible), eventInfo); + SendWindowContentChangedEvent(); +} + +void SessionAccessibility::SendWindowContentChangedEvent() { + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED, kNoID, + java::SessionAccessibility::CLASSNAME_WEBVIEW, nullptr); +} + +void SessionAccessibility::SendWindowStateChangedEvent( + Accessible* aAccessible) { + MOZ_ASSERT(NS_IsMainThread()); + // Suppress window state changed events from about:blank pages. + // This is important for tests. + if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) { + return; + } + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_WINDOW_STATE_CHANGED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), nullptr); + + SendWindowContentChangedEvent(); +} + +void SessionAccessibility::SendTextSelectionChangedEvent( + Accessible* aAccessible, int32_t aCaretOffset) { + MOZ_ASSERT(NS_IsMainThread()); + int32_t fromIndex = aCaretOffset; + int32_t startSel = -1; + int32_t endSel = -1; + bool hasSelection = + aAccessible->AsHyperTextBase()->SelectionBoundsAt(0, &startSel, &endSel); + + if (hasSelection) { + fromIndex = startSel == aCaretOffset ? endSel : startSel; + } + + nsAutoString text; + if (aAccessible->IsHyperText()) { + aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); + } else if (aAccessible->IsText()) { + aAccessible->AppendTextTo(text, 0, -1); + } + + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); + GECKOBUNDLE_PUT(eventInfo, "fromIndex", + java::sdk::Integer::ValueOf(fromIndex)); + GECKOBUNDLE_PUT(eventInfo, "toIndex", + java::sdk::Integer::ValueOf(aCaretOffset)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_SELECTION_CHANGED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), eventInfo); +} + +void SessionAccessibility::SendTextChangedEvent(Accessible* aAccessible, + const nsAString& aStr, + int32_t aStart, uint32_t aLen, + bool aIsInsert, + bool aFromUser) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aFromUser) { + // Only dispatch text change events from users, for now. + return; + } + + nsAutoString text; + if (aAccessible->IsHyperText()) { + aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); + } else if (aAccessible->IsText()) { + aAccessible->AppendTextTo(text, 0, -1); + } + nsAutoString beforeText(text); + if (aIsInsert) { + beforeText.Cut(aStart, aLen); + } else { + beforeText.Insert(aStr, aStart); + } + + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); + GECKOBUNDLE_PUT(eventInfo, "beforeText", jni::StringParam(beforeText)); + GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(aStart)); + GECKOBUNDLE_PUT(eventInfo, "addedCount", + java::sdk::Integer::ValueOf(aIsInsert ? aLen : 0)); + GECKOBUNDLE_PUT(eventInfo, "removedCount", + java::sdk::Integer::ValueOf(aIsInsert ? 0 : aLen)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_CHANGED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), eventInfo); +} + +void SessionAccessibility::SendTextTraversedEvent(Accessible* aAccessible, + int32_t aStartOffset, + int32_t aEndOffset) { + MOZ_ASSERT(NS_IsMainThread()); + nsAutoString text; + if (aAccessible->IsHyperText()) { + aAccessible->AsHyperTextBase()->TextSubstring(0, -1, text); + } else if (aAccessible->IsText()) { + aAccessible->AppendTextTo(text, 0, -1); + } + + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text)); + GECKOBUNDLE_PUT(eventInfo, "fromIndex", + java::sdk::Integer::ValueOf(aStartOffset)); + GECKOBUNDLE_PUT(eventInfo, "toIndex", + java::sdk::Integer::ValueOf(aEndOffset)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent:: + TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), eventInfo); +} + +void SessionAccessibility::SendClickedEvent(Accessible* aAccessible, + uint32_t aFlags) { + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "flags", java::sdk::Integer::ValueOf(aFlags)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_CLICKED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), eventInfo); +} + +void SessionAccessibility::SendSelectedEvent(Accessible* aAccessible, + bool aSelected) { + MOZ_ASSERT(NS_IsMainThread()); + GECKOBUNDLE_START(eventInfo); + // Boolean::FALSE/TRUE gets clobbered by a macro, so ugh. + GECKOBUNDLE_PUT(eventInfo, "selected", + java::sdk::Integer::ValueOf(aSelected ? 1 : 0)); + GECKOBUNDLE_FINISH(eventInfo); + + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_VIEW_SELECTED, + AccessibleWrap::GetVirtualViewID(aAccessible), + AccessibleWrap::AndroidClass(aAccessible), eventInfo); +} + +void SessionAccessibility::SendAnnouncementEvent(Accessible* aAccessible, + const nsAString& aAnnouncement, + uint16_t aPriority) { + MOZ_ASSERT(NS_IsMainThread()); + GECKOBUNDLE_START(eventInfo); + GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(aAnnouncement)); + GECKOBUNDLE_FINISH(eventInfo); + + // Announcements should have the root as their source, so we ignore the + // accessible of the event. + mSessionAccessibility->SendEvent( + java::sdk::AccessibilityEvent::TYPE_ANNOUNCEMENT, kNoID, + java::SessionAccessibility::CLASSNAME_WEBVIEW, eventInfo); +} + +void SessionAccessibility::PopulateNodeInfo( + Accessible* aAccessible, mozilla::jni::Object::Param aNodeInfo) { + nsAutoString name; + aAccessible->Name(name); + nsAutoString textValue; + aAccessible->Value(textValue); + nsAutoString nodeID; + aAccessible->DOMNodeID(nodeID); + nsAutoString accDesc; + aAccessible->Description(accDesc); + uint64_t state = aAccessible->State(); + LayoutDeviceIntRect bounds = aAccessible->Bounds(); + uint8_t actionCount = aAccessible->ActionCount(); + int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); + Accessible* parent = virtualViewID != kNoID ? aAccessible->Parent() : nullptr; + int32_t parentID = parent ? AccessibleWrap::GetVirtualViewID(parent) : 0; + role role = aAccessible->Role(); + if (role == roles::LINK && !(state & states::LINKED)) { + // A link without the linked state ( with no href) shouldn't be presented + // as a link. + role = roles::TEXT; + } + + uint32_t flags = AccessibleWrap::GetFlags(role, state, actionCount); + int32_t className = AccessibleWrap::AndroidClass(aAccessible); + + nsAutoString hint; + nsAutoString text; + nsAutoString description; + if (state & states::EDITABLE) { + // An editable field's name is populated in the hint. + hint.Assign(name); + text.Assign(textValue); + } else { + if (role == roles::LINK || role == roles::HEADING) { + description.Assign(name); + } else { + text.Assign(name); + } + } + + if (!accDesc.IsEmpty()) { + if (!hint.IsEmpty()) { + // If this is an editable, the description is concatenated with a + // whitespace directly after the name. + hint.AppendLiteral(" "); + } + hint.Append(accDesc); + } + + if ((state & states::REQUIRED) != 0) { + nsAutoString requiredString; + if (LocalizeString(u"stateRequired"_ns, requiredString)) { + if (!hint.IsEmpty()) { + // If the hint is non-empty, concatenate with a comma for a brief pause. + hint.AppendLiteral(", "); + } + hint.Append(requiredString); + } + } + + RefPtr attributes = aAccessible->Attributes(); + + nsAutoString geckoRole; + nsAutoString roleDescription; + if (virtualViewID != kNoID) { + AccessibleWrap::GetRoleDescription(role, attributes, geckoRole, + roleDescription); + } + + int32_t inputType = 0; + if (attributes) { + nsString inputTypeAttr; + attributes->GetAttribute(nsGkAtoms::textInputType, inputTypeAttr); + inputType = AccessibleWrap::GetInputType(inputTypeAttr); + } + + auto childCount = aAccessible->ChildCount(); + nsTArray children(childCount); + if (!nsAccUtils::MustPrune(aAccessible)) { + for (uint32_t i = 0; i < childCount; i++) { + auto child = aAccessible->ChildAt(i); + children.AppendElement(AccessibleWrap::GetVirtualViewID(child)); + } + } + + const int32_t boundsArray[4] = {bounds.x, bounds.y, bounds.x + bounds.width, + bounds.y + bounds.height}; + + mSessionAccessibility->PopulateNodeInfo( + aNodeInfo, virtualViewID, parentID, jni::IntArray::From(children), flags, + className, jni::IntArray::New(boundsArray, 4), jni::StringParam(text), + jni::StringParam(description), jni::StringParam(hint), + jni::StringParam(geckoRole), jni::StringParam(roleDescription), + jni::StringParam(nodeID), inputType); + + if (aAccessible->HasNumericValue()) { + double curValue = aAccessible->CurValue(); + double minValue = aAccessible->MinValue(); + double maxValue = aAccessible->MaxValue(); + double step = aAccessible->Step(); + + int32_t rangeType = 0; // integer + if (maxValue == 1 && minValue == 0) { + rangeType = 2; // percent + } else if (std::round(step) != step) { + rangeType = 1; // float; + } + + mSessionAccessibility->PopulateNodeRangeInfo( + aNodeInfo, rangeType, static_cast(minValue), + static_cast(maxValue), static_cast(curValue)); + } + + if (attributes) { + Maybe rowIndex = + attributes->GetAttribute(nsGkAtoms::posinset); + if (rowIndex) { + mSessionAccessibility->PopulateNodeCollectionItemInfo( + aNodeInfo, *rowIndex - 1, 1, 0, 1); + } + + Maybe rowCount = + attributes->GetAttribute(nsGkAtoms::child_item_count); + if (rowCount) { + int32_t selectionMode = 0; + if (aAccessible->IsSelect()) { + selectionMode = (state & states::MULTISELECTABLE) ? 2 : 1; + } + mSessionAccessibility->PopulateNodeCollectionInfo( + aNodeInfo, *rowCount, 1, selectionMode, + attributes->HasAttribute(nsGkAtoms::tree)); + } + } +} + +Accessible* SessionAccessibility::GetAccessibleByID(int32_t aID) const { + Accessible* accessible = mIDToAccessibleMap.Get(aID); + if (accessible && accessible->IsLocal() && + accessible->AsLocal()->IsDefunct()) { + MOZ_ASSERT_UNREACHABLE("Registered accessible is defunct!"); + return nullptr; + } + + return accessible; +} + +#ifdef DEBUG +static bool IsDetachedDoc(Accessible* aAccessible) { + if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) { + return false; + } + + return !aAccessible->Parent() || + aAccessible->Parent()->FirstChild() != aAccessible; +} +#endif + +void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) { + if (IPCAccessibilityActive()) { + // Don't register accessible in content process. + return; + } + + nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); + RefPtr sessionAcc = GetInstanceFor(aAccessible); + if (!sessionAcc) { + return; + } + + bool isTopLevel = false; + if (aAccessible->IsLocal() && aAccessible->IsDoc()) { + DocAccessibleWrap* doc = + static_cast(aAccessible->AsLocal()->AsDoc()); + isTopLevel = doc->IsTopLevelContentDoc(); + } else if (aAccessible->IsRemote() && aAccessible->IsDoc()) { + isTopLevel = aAccessible->AsRemote()->AsDoc()->IsTopLevel(); + } + + int32_t virtualViewID = kNoID; + if (!isTopLevel) { + if (sessionAcc->mIDToAccessibleMap.IsEmpty()) { + // We expect there to already be at least one accessible + // registered (the top-level one). If it isn't we are + // probably in a shutdown process where it was already + // unregistered. So we don't register this accessible. + return; + } + // Don't use the special "unset" value (0). + while ((virtualViewID = sIDSet.GetID()) == kUnsetID) { + } + } + AccessibleWrap::SetVirtualViewID(aAccessible, virtualViewID); + + Accessible* oldAcc = sessionAcc->mIDToAccessibleMap.Get(virtualViewID); + if (oldAcc) { + // About to overwrite mapping of registered accessible. This should + // only happen when the registered accessible is a detached document. + MOZ_ASSERT(IsDetachedDoc(oldAcc), + "ID already registered to non-detached document"); + AccessibleWrap::SetVirtualViewID(oldAcc, kUnsetID); + } + + sessionAcc->mIDToAccessibleMap.InsertOrUpdate(virtualViewID, aAccessible); +} + +void SessionAccessibility::UnregisterAccessible(Accessible* aAccessible) { + if (IPCAccessibilityActive()) { + // Don't unregister accessible in content process. + return; + } + + nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); + int32_t virtualViewID = AccessibleWrap::GetVirtualViewID(aAccessible); + if (virtualViewID == kUnsetID) { + return; + } + + RefPtr sessionAcc = GetInstanceFor(aAccessible); + MOZ_ASSERT(sessionAcc, "Need SessionAccessibility to unregister Accessible!"); + if (sessionAcc) { + Accessible* registeredAcc = + sessionAcc->mIDToAccessibleMap.Get(virtualViewID); + if (registeredAcc != aAccessible) { + // Attempting to unregister an accessible that is not mapped to + // its virtual view ID. This probably means it is a detached document + // and a more recent document overwrote its '-1' mapping. + // We set its own virtual view ID to `kUnsetID` and return early. + MOZ_ASSERT(!registeredAcc || IsDetachedDoc(aAccessible), + "Accessible is detached document"); + AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); + return; + } + + MOZ_ASSERT(registeredAcc, "Unregistering unregistered accessible"); + MOZ_ASSERT(registeredAcc == aAccessible, "Unregistering wrong accessible"); + sessionAcc->mIDToAccessibleMap.Remove(virtualViewID); + } + + if (virtualViewID > kNoID) { + sIDSet.ReleaseID(virtualViewID); + } + + AccessibleWrap::SetVirtualViewID(aAccessible, kUnsetID); +} + +void SessionAccessibility::UnregisterAll(PresShell* aPresShell) { + if (IPCAccessibilityActive()) { + // Don't unregister accessible in content process. + return; + } + + nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); + RefPtr sessionAcc = GetInstanceFor(aPresShell); + if (sessionAcc) { + sessionAcc->mIDToAccessibleMap.Clear(); + } +} diff --git a/accessible/android/SessionAccessibility.h b/accessible/android/SessionAccessibility.h new file mode 100644 index 0000000000..5ec0293d3d --- /dev/null +++ b/accessible/android/SessionAccessibility.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_SessionAccessibility_h_ +#define mozilla_a11y_SessionAccessibility_h_ + +#include "mozilla/java/SessionAccessibilityNatives.h" +#include "mozilla/widget/GeckoViewSupport.h" +#include "nsAppShell.h" +#include "nsThreadUtils.h" +#include "nsWindow.h" +#include "AccessibleWrap.h" + +namespace mozilla { +namespace a11y { + +class AccessibleWrap; +class AccAttributes; +class Accessible; +class RemoteAccessible; +class RootAccessibleWrap; +class BatchData; + +class SessionAccessibility final + : public java::SessionAccessibility::NativeProvider::Natives< + SessionAccessibility> { + public: + typedef java::SessionAccessibility::NativeProvider::Natives< + SessionAccessibility> + Base; + + SessionAccessibility( + jni::NativeWeakPtr aWindow, + java::SessionAccessibility::NativeProvider::Param aSessionAccessibility); + + void OnWeakNonIntrusiveDetach(already_AddRefed aDisposer) { + SetAttached(false, std::move(aDisposer)); + } + + const java::SessionAccessibility::NativeProvider::Ref& + GetJavaAccessibility() { + return mSessionAccessibility; + } + + static void Init(); + static RefPtr GetInstanceFor(Accessible* aAccessible); + static RefPtr GetInstanceFor(PresShell* aPresShell); + + // Native implementations + using Base::AttachNative; + using Base::DisposeNative; + void GetNodeInfo(int32_t aID, mozilla::jni::Object::Param aNodeInfo); + int GetNodeClassName(int32_t aID); + void SetText(int32_t aID, jni::String::Param aText); + void Click(int32_t aID); + bool Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive); + void ExploreByTouch(int32_t aID, float aX, float aY); + void NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, bool aSelect); + void SetSelection(int32_t aID, int32_t aStart, int32_t aEnd); + void Cut(int32_t aID); + void Copy(int32_t aID); + void Paste(int32_t aID); + void StartNativeAccessibility(); + + // Event methods + void SendFocusEvent(Accessible* aAccessible); + void SendScrollingEvent(Accessible* aAccessible, int32_t aScrollX, + int32_t aScrollY, int32_t aMaxScrollX, + int32_t aMaxScrollY); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SendAccessibilityFocusedEvent(Accessible* aAccessible); + void SendHoverEnterEvent(Accessible* aAccessible); + void SendTextSelectionChangedEvent(Accessible* aAccessible, + int32_t aCaretOffset); + void SendTextTraversedEvent(Accessible* aAccessible, int32_t aStartOffset, + int32_t aEndOffset); + void SendTextChangedEvent(Accessible* aAccessible, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser); + void SendSelectedEvent(Accessible* aAccessible, bool aSelected); + void SendClickedEvent(Accessible* aAccessible, uint32_t aFlags); + void SendWindowContentChangedEvent(); + void SendWindowStateChangedEvent(Accessible* aAccessible); + void SendAnnouncementEvent(Accessible* aAccessible, + const nsAString& aAnnouncement, + uint16_t aPriority); + + Accessible* GetAccessibleByID(int32_t aID) const; + + static const int32_t kNoID = -1; + static const int32_t kUnsetID = 0; + + static void RegisterAccessible(Accessible* aAccessible); + static void UnregisterAccessible(Accessible* aAccessible); + static void UnregisterAll(PresShell* aPresShell); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SessionAccessibility) + + private: + ~SessionAccessibility() {} + + void PopulateNodeInfo(Accessible* aAccessible, + mozilla::jni::Object::Param aNodeInfo); + + void SetAttached(bool aAttached, already_AddRefed aRunnable); + + jni::NativeWeakPtr mWindow; // Parent only + java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility; + + /* + * This provides a mapping from 32 bit id to accessible objects. + */ + nsTHashMap mIDToAccessibleMap; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/TraversalRule.cpp b/accessible/android/TraversalRule.cpp new file mode 100644 index 0000000000..00e97cc164 --- /dev/null +++ b/accessible/android/TraversalRule.cpp @@ -0,0 +1,290 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "TraversalRule.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/a11y/Accessible.h" + +#include "Role.h" +#include "HTMLListAccessible.h" +#include "SessionAccessibility.h" +#include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +TraversalRule::TraversalRule() + : TraversalRule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, + true) {} + +TraversalRule::TraversalRule(int32_t aGranularity, bool aIsLocal) + : mGranularity(aGranularity), mIsLocal(aIsLocal) {} + +uint16_t TraversalRule::Match(Accessible* aAcc) { + MOZ_ASSERT(aAcc); + + if (mIsLocal && aAcc->IsRemote()) { + // If we encounter a remote accessible in a local rule, we should + // ignore the subtree because we won't encounter anymore local accessibles + // in it. + return nsIAccessibleTraversalRule::FILTER_IGNORE | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } else if (!mIsLocal && aAcc->IsLocal()) { + // If we encounter a local accessible in a remote rule we are likely + // traversing backwards/upwards, we don't ignore its subtree because it is + // likely the outer doc root of the remote tree. + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + uint64_t state = aAcc->State(); + + if ((state & states::INVISIBLE) != 0) { + return result; + } + + if (aAcc->Opacity() == 0.0f) { + return result | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + switch (mGranularity) { + case java::SessionAccessibility::HTML_GRANULARITY_LINK: + result |= LinkMatch(aAcc); + break; + case java::SessionAccessibility::HTML_GRANULARITY_CONTROL: + result |= ControlMatch(aAcc); + break; + case java::SessionAccessibility::HTML_GRANULARITY_SECTION: + result |= SectionMatch(aAcc); + break; + case java::SessionAccessibility::HTML_GRANULARITY_HEADING: + result |= HeadingMatch(aAcc); + break; + case java::SessionAccessibility::HTML_GRANULARITY_LANDMARK: + result |= LandmarkMatch(aAcc); + break; + default: + result |= DefaultMatch(aAcc); + break; + } + + return result; +} + +bool TraversalRule::IsSingleLineage(Accessible* aAccessible) { + Accessible* child = aAccessible; + while (child) { + switch (child->ChildCount()) { + case 0: + return true; + case 1: + child = child->FirstChild(); + break; + case 2: + if (IsListItemBullet(child->FirstChild())) { + child = child->LastChild(); + } else { + return false; + } + break; + default: + return false; + } + } + + return true; +} + +bool TraversalRule::IsListItemBullet(const Accessible* aAccessible) { + return aAccessible->Role() == roles::LISTITEM_MARKER; +} + +bool TraversalRule::IsFlatSubtree(const Accessible* aAccessible) { + for (auto child = aAccessible->FirstChild(); child; + child = child->NextSibling()) { + roles::Role role = child->Role(); + if (role == roles::TEXT_LEAF || role == roles::STATICTEXT) { + continue; + } + + if (child->ChildCount() > 0 || child->ActionCount() > 0) { + return false; + } + } + + return true; +} + +bool TraversalRule::HasName(const Accessible* aAccessible) { + nsAutoString name; + aAccessible->Name(name); + name.CompressWhitespace(); + return !name.IsEmpty(); +} + +uint16_t TraversalRule::LinkMatch(Accessible* aAccessible) { + if (aAccessible->Role() == roles::LINK && + (aAccessible->State() & states::LINKED) != 0) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} + +uint16_t TraversalRule::HeadingMatch(Accessible* aAccessible) { + if (aAccessible->Role() == roles::HEADING && aAccessible->ChildCount()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} + +uint16_t TraversalRule::SectionMatch(Accessible* aAccessible) { + roles::Role role = aAccessible->Role(); + if (role == roles::HEADING || role == roles::LANDMARK || + aAccessible->LandmarkRole()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} + +uint16_t TraversalRule::LandmarkMatch(Accessible* aAccessible) { + if (aAccessible->LandmarkRole()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} + +uint16_t TraversalRule::ControlMatch(Accessible* aAccessible) { + switch (aAccessible->Role()) { + case roles::PUSHBUTTON: + case roles::SPINBUTTON: + case roles::TOGGLE_BUTTON: + case roles::BUTTONDROPDOWN: + case roles::BUTTONDROPDOWNGRID: + case roles::COMBOBOX: + case roles::LISTBOX: + case roles::ENTRY: + case roles::PASSWORD_TEXT: + case roles::PAGETAB: + case roles::RADIOBUTTON: + case roles::RADIO_MENU_ITEM: + case roles::SLIDER: + case roles::CHECKBUTTON: + case roles::CHECK_MENU_ITEM: + case roles::SWITCH: + case roles::MENUITEM: + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + case roles::LINK: + return LinkMatch(aAccessible); + case roles::EDITCOMBOBOX: + if (aAccessible->State() & states::EDITABLE) { + // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is + // editable. If it's a 1.1 combobox, the combobox is just a container; + // we want to stop on the textbox inside it, not the container. + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + break; + default: + break; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} + +uint16_t TraversalRule::DefaultMatch(Accessible* aAccessible) { + switch (aAccessible->Role()) { + case roles::COMBOBOX: + // We don't want to ignore the subtree because this is often + // where the list box hangs out. + return nsIAccessibleTraversalRule::FILTER_MATCH; + case roles::EDITCOMBOBOX: + if (aAccessible->State() & states::EDITABLE) { + // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is + // editable. If it's a 1.1 combobox, the combobox is just a container; + // we want to stop on the textbox inside it. + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + break; + case roles::TEXT_LEAF: + case roles::GRAPHIC: + // Nameless text leaves are boring, skip them. + if (HasName(aAccessible)) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + break; + case roles::STATICTEXT: + // Ignore list bullets + if (!IsListItemBullet(aAccessible)) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + break; + case roles::HEADER: + case roles::HEADING: + case roles::COLUMNHEADER: + case roles::ROWHEADER: + case roles::STATUSBAR: + if ((aAccessible->ChildCount() > 0 || HasName(aAccessible)) && + (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible))) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + break; + case roles::GRID_CELL: + if (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible)) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + break; + case roles::LABEL: + if (IsFlatSubtree(aAccessible)) { + // Match if this is a label with text but no nested controls. + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + break; + case roles::MENUITEM: + case roles::LINK: + case roles::PAGETAB: + case roles::PUSHBUTTON: + case roles::CHECKBUTTON: + case roles::RADIOBUTTON: + case roles::PROGRESSBAR: + case roles::BUTTONDROPDOWN: + case roles::BUTTONMENU: + case roles::CHECK_MENU_ITEM: + case roles::PASSWORD_TEXT: + case roles::RADIO_MENU_ITEM: + case roles::TOGGLE_BUTTON: + case roles::ENTRY: + case roles::KEY: + case roles::SLIDER: + case roles::SPINBUTTON: + case roles::OPTION: + case roles::SWITCH: + case roles::MATHML_MATH: + // Ignore the subtree, if there is one. So that we don't land on + // the same content that was already presented by its parent. + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + default: + break; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE; +} diff --git a/accessible/android/TraversalRule.h b/accessible/android/TraversalRule.h new file mode 100644 index 0000000000..27b2b2f5fb --- /dev/null +++ b/accessible/android/TraversalRule.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef _TraversalRule_H_ +#define _TraversalRule_H_ + +#include "Pivot.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Class represents a simple traversal rule. + */ +class TraversalRule : public PivotRule { + public: + TraversalRule(); + explicit TraversalRule(int32_t aGranularity, bool aIsLocal); + + ~TraversalRule() = default; + + virtual uint16_t Match(Accessible* aAcc) override; + + private: + bool IsSingleLineage(Accessible* aAccessible); + + bool IsFlatSubtree(const Accessible* aAccessible); + + bool IsListItemBullet(const Accessible* aAccessible); + + bool HasName(const Accessible* aAccessible); + + uint16_t DefaultMatch(Accessible* aAccessible); + + uint16_t LinkMatch(Accessible* aAccessible); + + uint16_t HeadingMatch(Accessible* aAccessible); + + uint16_t ControlMatch(Accessible* aAccessible); + + uint16_t SectionMatch(Accessible* aAccessible); + + uint16_t LandmarkMatch(Accessible* aAccessible); + + int32_t mGranularity; + + bool mIsLocal; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/android/moz.build b/accessible/android/moz.build new file mode 100644 index 0000000000..2370d9a6f5 --- /dev/null +++ b/accessible/android/moz.build @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.a11y += [ + "AccessibleWrap.h", + "HyperTextAccessibleWrap.h", + "SessionAccessibility.h", + "TraversalRule.h", +] + +SOURCES += [ + "AccessibleWrap.cpp", + "DocAccessibleWrap.cpp", + "Platform.cpp", + "RootAccessibleWrap.cpp", + "SessionAccessibility.cpp", + "TraversalRule.cpp", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", + "/accessible/html", + "/accessible/ipc", + "/accessible/ipc/other", + "/accessible/xpcom", + "/accessible/xul", + "/dom/base", + "/widget", + "/widget/android", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/accessible/aom/AccessibleNode.cpp b/accessible/aom/AccessibleNode.cpp new file mode 100644 index 0000000000..43cfcf8a47 --- /dev/null +++ b/accessible/aom/AccessibleNode.cpp @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccessibleNode.h" +#include "mozilla/dom/AccessibleNodeBinding.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "nsContentUtils.h" +#include "nsISimpleEnumerator.h" + +#include "AccAttributes.h" +#include "LocalAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "DocAccessible.h" + +#include "mozilla/dom/Document.h" // for inline nsINode::GetParentObject +#include "mozilla/dom/ToJSValue.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +bool AccessibleNode::IsAOMEnabled(JSContext* aCx, JSObject* /*unused*/) { + return nsContentUtils::IsSystemCaller(aCx) || + StaticPrefs::accessibility_AOM_enabled(); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AccessibleNode, mRelationProperties, + mIntl, mDOMNode, mStates) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AccessibleNode) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AccessibleNode) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AccessibleNode) + +AccessibleNode::AccessibleNode(nsINode* aNode) + : mDoubleProperties(3), + mIntProperties(3), + mUIntProperties(6), + mBooleanProperties(0), + mRelationProperties(3), + mStringProperties(16), + mDOMNode(aNode) { + nsAccessibilityService* accService = GetOrCreateAccService(); + if (!accService) { + return; + } + + DocAccessible* doc = accService->GetDocAccessible(mDOMNode->OwnerDoc()); + if (doc) { + mIntl = doc->GetAccessible(mDOMNode); + } +} + +AccessibleNode::~AccessibleNode() {} + +/* virtual */ +JSObject* AccessibleNode::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return AccessibleNode_Binding::Wrap(aCx, this, aGivenProto); +} + +/* virtual */ +ParentObject AccessibleNode::GetParentObject() const { + return mDOMNode->GetParentObject(); +} + +void AccessibleNode::GetComputedRole(nsAString& aRole) { + if (mIntl) { + nsAccessibilityService* accService = GetOrCreateAccService(); + if (accService) { + accService->GetStringRole(mIntl->Role(), aRole); + return; + } + } + + aRole.AssignLiteral("unknown"); +} + +void AccessibleNode::GetStates(nsTArray& aStates) { + nsAccessibilityService* accService = GetOrCreateAccService(); + if (!mIntl || !accService) { + aStates.AppendElement(u"defunct"_ns); + return; + } + + if (mStates) { + aStates = mStates->StringArray().Clone(); + return; + } + + mStates = accService->GetStringStates(mIntl->State()); + aStates = mStates->StringArray().Clone(); +} + +void AccessibleNode::GetAttributes(nsTArray& aAttributes) { + if (!mIntl) { + return; + } + + RefPtr attrs = mIntl->Attributes(); + + for (const auto& iter : *attrs) { + aAttributes.AppendElement(nsAtomString(iter.Name())); + } +} + +bool AccessibleNode::Is(const Sequence& aFlavors) { + nsAccessibilityService* accService = GetOrCreateAccService(); + if (!mIntl || !accService) { + for (const auto& flavor : aFlavors) { + if (!flavor.EqualsLiteral("unknown") && + !flavor.EqualsLiteral("defunct")) { + return false; + } + } + return true; + } + + nsAutoString role; + accService->GetStringRole(mIntl->Role(), role); + + if (!mStates) { + mStates = accService->GetStringStates(mIntl->State()); + } + + for (const auto& flavor : aFlavors) { + if (!flavor.Equals(role) && !mStates->Contains(flavor)) { + return false; + } + } + return true; +} + +bool AccessibleNode::Has(const Sequence& aAttributes) { + if (!mIntl) { + return false; + } + RefPtr attrs = mIntl->Attributes(); + for (const auto& attr : aAttributes) { + RefPtr attrAtom = NS_Atomize(attr); + if (!attrs->HasAttribute(attrAtom)) { + return false; + } + } + return true; +} + +void AccessibleNode::Get(JSContext* aCX, const nsAString& aAttribute, + JS::MutableHandle aValue, + ErrorResult& aRv) { + if (!mIntl) { + aRv.ThrowInvalidStateError("No attributes available"); + return; + } + + RefPtr attrAtom = NS_Atomize(aAttribute); + RefPtr attrs = mIntl->Attributes(); + nsAutoString valueStr; + attrs->GetAttribute(attrAtom, valueStr); + if (!ToJSValue(aCX, valueStr, aValue)) { + aRv.NoteJSContextException(aCX); + return; + } +} + +nsINode* AccessibleNode::GetDOMNode() { return mDOMNode; } diff --git a/accessible/aom/AccessibleNode.h b/accessible/aom/AccessibleNode.h new file mode 100644 index 0000000000..e9b328b13d --- /dev/null +++ b/accessible/aom/AccessibleNode.h @@ -0,0 +1,212 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef A11Y_AOM_ACCESSIBLENODE_H +#define A11Y_AOM_ACCESSIBLENODE_H + +#include "nsTHashMap.h" +#include "nsRefPtrHashtable.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/Nullable.h" + +class nsINode; + +namespace mozilla { + +class ErrorResult; + +namespace a11y { +class LocalAccessible; +} + +namespace dom { + +class DOMStringList; +struct ParentObject; + +#define ANODE_ENUM(name) e##name, + +#define ANODE_FUNC(typeName, type, name) \ + dom::Nullable Get##name() { \ + return GetProperty(AOM##typeName##Property::e##name); \ + } \ + \ + void Set##name(const dom::Nullable& a##name) { \ + SetProperty(AOM##typeName##Property::e##name, a##name); \ + } + +#define ANODE_STRING_FUNC(name) \ + void Get##name(nsAString& a##name) { \ + return GetProperty(AOMStringProperty::e##name, a##name); \ + } \ + \ + void Set##name(const nsAString& a##name) { \ + SetProperty(AOMStringProperty::e##name, a##name); \ + } + +#define ANODE_RELATION_FUNC(name) \ + already_AddRefed Get##name() { \ + return GetProperty(AOMRelationProperty::e##name); \ + } \ + \ + void Set##name(AccessibleNode* a##name) { \ + SetProperty(AOMRelationProperty::e##name, a##name); \ + } + +#define ANODE_PROPS(typeName, type, ...) \ + enum class AOM##typeName##Property{ \ + MOZ_FOR_EACH(ANODE_ENUM, (), (__VA_ARGS__))}; \ + MOZ_FOR_EACH(ANODE_FUNC, (typeName, type, ), (__VA_ARGS__)) + +#define ANODE_STRING_PROPS(...) \ + enum class AOMStringProperty { \ + MOZ_FOR_EACH(ANODE_ENUM, (), (__VA_ARGS__)) \ + }; \ + MOZ_FOR_EACH(ANODE_STRING_FUNC, (), (__VA_ARGS__)) + +#define ANODE_RELATION_PROPS(...) \ + enum class AOMRelationProperty { \ + MOZ_FOR_EACH(ANODE_ENUM, (), (__VA_ARGS__)) \ + }; \ + MOZ_FOR_EACH(ANODE_RELATION_FUNC, (), (__VA_ARGS__)) + +#define ANODE_ACCESSOR_MUTATOR(typeName, type, defVal) \ + nsTHashMap m##typeName##Properties; \ + \ + dom::Nullable GetProperty(AOM##typeName##Property aProperty) { \ + type value = defVal; \ + if (m##typeName##Properties.Get(static_cast(aProperty), &value)) { \ + return dom::Nullable(value); \ + } \ + return dom::Nullable(); \ + } \ + \ + void SetProperty(AOM##typeName##Property aProperty, \ + const dom::Nullable& aValue) { \ + if (aValue.IsNull()) { \ + m##typeName##Properties.Remove(static_cast(aProperty)); \ + } else { \ + m##typeName##Properties.InsertOrUpdate(static_cast(aProperty), \ + aValue.Value()); \ + } \ + } + +class AccessibleNode : public nsISupports, public nsWrapperCache { + public: + explicit AccessibleNode(nsINode* aNode); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS; + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AccessibleNode); + + JSObject* WrapObject(JSContext* aCx, JS::Handle aGivenProto) final; + dom::ParentObject GetParentObject() const; + + void GetComputedRole(nsAString& aRole); + void GetStates(nsTArray& aStates); + void GetAttributes(nsTArray& aAttributes); + nsINode* GetDOMNode(); + + bool Is(const Sequence& aFlavors); + bool Has(const Sequence& aAttributes); + void Get(JSContext* cx, const nsAString& aAttribute, + JS::MutableHandle aValue, ErrorResult& aRv); + + static bool IsAOMEnabled(JSContext*, JSObject*); + + ANODE_STRING_PROPS(Autocomplete, Checked, Current, HasPopUp, Invalid, + KeyShortcuts, Label, Live, Orientation, Placeholder, + Pressed, Relevant, Role, RoleDescription, Sort, ValueText) + + ANODE_PROPS(Boolean, bool, Atomic, Busy, Disabled, Expanded, Hidden, Modal, + Multiline, Multiselectable, ReadOnly, Required, Selected) + + ANODE_PROPS(UInt, uint32_t, ColIndex, ColSpan, Level, PosInSet, RowIndex, + RowSpan) + + ANODE_PROPS(Int, int32_t, ColCount, RowCount, SetSize) + + ANODE_PROPS(Double, double, ValueMax, ValueMin, ValueNow) + + ANODE_RELATION_PROPS(ActiveDescendant, Details, ErrorMessage) + + protected: + AccessibleNode(const AccessibleNode& aCopy) = delete; + AccessibleNode& operator=(const AccessibleNode& aCopy) = delete; + virtual ~AccessibleNode(); + + void GetProperty(AOMStringProperty aProperty, nsAString& aRetval) { + nsString data; + if (!mStringProperties.Get(static_cast(aProperty), &data)) { + SetDOMStringToNull(data); + } + aRetval = data; + } + + void SetProperty(AOMStringProperty aProperty, const nsAString& aValue) { + if (DOMStringIsNull(aValue)) { + mStringProperties.Remove(static_cast(aProperty)); + } else { + nsString value(aValue); + mStringProperties.InsertOrUpdate(static_cast(aProperty), value); + } + } + + dom::Nullable GetProperty(AOMBooleanProperty aProperty) { + int num = static_cast(aProperty); + if (mBooleanProperties & (1U << (2 * num))) { + bool data = static_cast(mBooleanProperties & (1U << (2 * num + 1))); + return dom::Nullable(data); + } + return dom::Nullable(); + } + + void SetProperty(AOMBooleanProperty aProperty, + const dom::Nullable& aValue) { + int num = static_cast(aProperty); + if (aValue.IsNull()) { + mBooleanProperties &= ~(1U << (2 * num)); + } else { + mBooleanProperties |= (1U << (2 * num)); + mBooleanProperties = + (aValue.Value() ? mBooleanProperties | (1U << (2 * num + 1)) + : mBooleanProperties & ~(1U << (2 * num + 1))); + } + } + + ANODE_ACCESSOR_MUTATOR(Double, double, 0.0) + ANODE_ACCESSOR_MUTATOR(Int, int32_t, 0) + ANODE_ACCESSOR_MUTATOR(UInt, uint32_t, 0) + + already_AddRefed GetProperty(AOMRelationProperty aProperty) { + return mRelationProperties.Get(static_cast(aProperty)); + } + + void SetProperty(AOMRelationProperty aProperty, AccessibleNode* aValue) { + if (!aValue) { + mRelationProperties.Remove(static_cast(aProperty)); + } else { + mRelationProperties.InsertOrUpdate(static_cast(aProperty), + RefPtr{aValue}); + } + } + + // The 2k'th bit indicates whether the k'th boolean property is used(1) or + // not(0) and 2k+1'th bit contains the property's value(1:true, 0:false) + uint32_t mBooleanProperties; + nsRefPtrHashtable mRelationProperties; + nsTHashMap mStringProperties; + + RefPtr mIntl; + RefPtr mDOMNode; + RefPtr mStates; +}; + +} // namespace dom +} // namespace mozilla + +#endif // A11Y_JSAPI_ACCESSIBLENODE diff --git a/accessible/aom/moz.build b/accessible/aom/moz.build new file mode 100644 index 0000000000..88b941435e --- /dev/null +++ b/accessible/aom/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "AccessibleNode.h", +] + +UNIFIED_SOURCES += [ + "AccessibleNode.cpp", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + LOCAL_INCLUDES += [ + "/accessible/atk", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + LOCAL_INCLUDES += [ + "/accessible/windows/ia2", + "/accessible/windows/msaa", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LOCAL_INCLUDES += [ + "/accessible/mac", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + LOCAL_INCLUDES += [ + "/accessible/android", + ] +else: + LOCAL_INCLUDES += [ + "/accessible/other", + ] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/accessible/atk/AccessibleWrap.cpp b/accessible/atk/AccessibleWrap.cpp new file mode 100644 index 0000000000..050bec7fcd --- /dev/null +++ b/accessible/atk/AccessibleWrap.cpp @@ -0,0 +1,1510 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "AccessibleWrap.h" + +#include "LocalAccessible-inl.h" +#include "AccAttributes.h" +#include "ApplicationAccessibleWrap.h" +#include "InterfaceInitFuncs.h" +#include "nsAccUtils.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "OuterDocAccessible.h" +#include "RemoteAccessible.h" +#include "DocAccessibleParent.h" +#include "RootAccessible.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "nsMai.h" +#include "nsMaiHyperlink.h" +#include "nsString.h" +#include "nsStateMap.h" +#include "mozilla/a11y/Platform.h" +#include "Relation.h" +#include "RootAccessible.h" +#include "States.h" +#include "nsISimpleEnumerator.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Sprintf.h" +#include "nsAccessibilityService.h" +#include "nsComponentManagerUtils.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +MaiAtkObject::EAvailableAtkSignals MaiAtkObject::gAvailableAtkSignals = + eUnknown; + +// defined in ApplicationAccessibleWrap.cpp +extern "C" GType g_atk_hyperlink_impl_type; + +/* MaiAtkObject */ + +enum { + ACTIVATE, + CREATE, + DEACTIVATE, + DESTROY, + MAXIMIZE, + MINIMIZE, + RESIZE, + RESTORE, + LAST_SIGNAL +}; + +enum MaiInterfaceType { + MAI_INTERFACE_COMPONENT, /* 0 */ + MAI_INTERFACE_ACTION, + MAI_INTERFACE_VALUE, + MAI_INTERFACE_EDITABLE_TEXT, + MAI_INTERFACE_HYPERTEXT, + MAI_INTERFACE_HYPERLINK_IMPL, + MAI_INTERFACE_SELECTION, + MAI_INTERFACE_TABLE, + MAI_INTERFACE_TEXT, + MAI_INTERFACE_DOCUMENT, + MAI_INTERFACE_IMAGE, /* 10 */ + MAI_INTERFACE_TABLE_CELL +}; + +static GType GetAtkTypeForMai(MaiInterfaceType type) { + switch (type) { + case MAI_INTERFACE_COMPONENT: + return ATK_TYPE_COMPONENT; + case MAI_INTERFACE_ACTION: + return ATK_TYPE_ACTION; + case MAI_INTERFACE_VALUE: + return ATK_TYPE_VALUE; + case MAI_INTERFACE_EDITABLE_TEXT: + return ATK_TYPE_EDITABLE_TEXT; + case MAI_INTERFACE_HYPERTEXT: + return ATK_TYPE_HYPERTEXT; + case MAI_INTERFACE_HYPERLINK_IMPL: + return g_atk_hyperlink_impl_type; + case MAI_INTERFACE_SELECTION: + return ATK_TYPE_SELECTION; + case MAI_INTERFACE_TABLE: + return ATK_TYPE_TABLE; + case MAI_INTERFACE_TEXT: + return ATK_TYPE_TEXT; + case MAI_INTERFACE_DOCUMENT: + return ATK_TYPE_DOCUMENT; + case MAI_INTERFACE_IMAGE: + return ATK_TYPE_IMAGE; + case MAI_INTERFACE_TABLE_CELL: + MOZ_ASSERT(false); + } + return G_TYPE_INVALID; +} + +#define NON_USER_EVENT ":system" + +// The atk interfaces we can expose without checking what version of ATK we are +// dealing with. At the moment AtkTableCell is the only interface we can't +// always expose. +static const GInterfaceInfo atk_if_infos[] = { + {(GInterfaceInitFunc)componentInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)actionInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)valueInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, + nullptr}, + {(GInterfaceInitFunc)editableTextInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)hypertextInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)hyperlinkImplInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)selectionInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)tableInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, + nullptr}, + {(GInterfaceInitFunc)textInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, + nullptr}, + {(GInterfaceInitFunc)documentInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}, + {(GInterfaceInitFunc)imageInterfaceInitCB, (GInterfaceFinalizeFunc) nullptr, + nullptr}}; + +static GQuark quark_mai_hyperlink = 0; + +AtkHyperlink* MaiAtkObject::GetAtkHyperlink() { + NS_ASSERTION(quark_mai_hyperlink, "quark_mai_hyperlink not initialized"); + MaiHyperlink* maiHyperlink = + (MaiHyperlink*)g_object_get_qdata(G_OBJECT(this), quark_mai_hyperlink); + if (!maiHyperlink) { + maiHyperlink = new MaiHyperlink(acc); + g_object_set_qdata(G_OBJECT(this), quark_mai_hyperlink, maiHyperlink); + } + + return maiHyperlink->GetAtkHyperlink(); +} + +void MaiAtkObject::Shutdown() { + acc = nullptr; + MaiHyperlink* maiHyperlink = + (MaiHyperlink*)g_object_get_qdata(G_OBJECT(this), quark_mai_hyperlink); + if (maiHyperlink) { + delete maiHyperlink; + g_object_set_qdata(G_OBJECT(this), quark_mai_hyperlink, nullptr); + } +} + +struct MaiAtkObjectClass { + AtkObjectClass parent_class; +}; + +static guint mai_atk_object_signals[LAST_SIGNAL] = { + 0, +}; + +static void MaybeFireNameChange(AtkObject* aAtkObj, const nsString& aNewName); + +G_BEGIN_DECLS +/* callbacks for MaiAtkObject */ +static void classInitCB(AtkObjectClass* aClass); +static void initializeCB(AtkObject* aAtkObj, gpointer aData); +static void finalizeCB(GObject* aObj); + +/* callbacks for AtkObject virtual functions */ +static const gchar* getNameCB(AtkObject* aAtkObj); +/* getDescriptionCB is also used by image interface */ +const gchar* getDescriptionCB(AtkObject* aAtkObj); +static AtkRole getRoleCB(AtkObject* aAtkObj); +static AtkAttributeSet* getAttributesCB(AtkObject* aAtkObj); +static const gchar* GetLocaleCB(AtkObject*); +static AtkObject* getParentCB(AtkObject* aAtkObj); +static gint getChildCountCB(AtkObject* aAtkObj); +static AtkObject* refChildCB(AtkObject* aAtkObj, gint aChildIndex); +static gint getIndexInParentCB(AtkObject* aAtkObj); +static AtkStateSet* refStateSetCB(AtkObject* aAtkObj); +static AtkRelationSet* refRelationSetCB(AtkObject* aAtkObj); + +/* the missing atkobject virtual functions */ +/* + static AtkLayer getLayerCB(AtkObject *aAtkObj); + static gint getMdiZorderCB(AtkObject *aAtkObj); + static void SetNameCB(AtkObject *aAtkObj, + const gchar *name); + static void SetDescriptionCB(AtkObject *aAtkObj, + const gchar *description); + static void SetParentCB(AtkObject *aAtkObj, + AtkObject *parent); + static void SetRoleCB(AtkObject *aAtkObj, + AtkRole role); + static guint ConnectPropertyChangeHandlerCB( + AtkObject *aObj, + AtkPropertyChangeHandler *handler); + static void RemovePropertyChangeHandlerCB( + AtkObject *aAtkObj, + guint handler_id); + static void InitializeCB(AtkObject *aAtkObj, + gpointer data); + static void ChildrenChangedCB(AtkObject *aAtkObj, + guint change_index, + gpointer changed_child); + static void FocusEventCB(AtkObject *aAtkObj, + gboolean focus_in); + static void PropertyChangeCB(AtkObject *aAtkObj, + AtkPropertyValues *values); + static void StateChangeCB(AtkObject *aAtkObj, + const gchar *name, + gboolean state_set); + static void VisibleDataChangedCB(AtkObject *aAtkObj); +*/ +G_END_DECLS + +static GType GetMaiAtkType(uint16_t interfacesBits); +static const char* GetUniqueMaiAtkTypeName(uint16_t interfacesBits); + +static gpointer parent_class = nullptr; + +GType mai_atk_object_get_type(void) { + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof(MaiAtkObjectClass), + (GBaseInitFunc) nullptr, + (GBaseFinalizeFunc) nullptr, + (GClassInitFunc)classInitCB, + (GClassFinalizeFunc) nullptr, + nullptr, /* class data */ + sizeof(MaiAtkObject), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) nullptr, + nullptr /* value table */ + }; + + type = g_type_register_static(ATK_TYPE_OBJECT, "MaiAtkObject", &tinfo, + GTypeFlags(0)); + quark_mai_hyperlink = g_quark_from_static_string("MaiHyperlink"); + } + return type; +} + +AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) + : LocalAccessible(aContent, aDoc), mAtkObject(nullptr) {} + +AccessibleWrap::~AccessibleWrap() { + NS_ASSERTION(!mAtkObject, "ShutdownAtkObject() is not called"); +} + +void AccessibleWrap::ShutdownAtkObject() { + if (!mAtkObject) return; + + NS_ASSERTION(IS_MAI_OBJECT(mAtkObject), "wrong type of atk object"); + if (IS_MAI_OBJECT(mAtkObject)) MAI_ATK_OBJECT(mAtkObject)->Shutdown(); + + g_object_unref(mAtkObject); + mAtkObject = nullptr; +} + +void AccessibleWrap::Shutdown() { + ShutdownAtkObject(); + LocalAccessible::Shutdown(); +} + +void AccessibleWrap::GetNativeInterface(void** aOutAccessible) { + *aOutAccessible = nullptr; + + if (!mAtkObject) { + if (IsDefunct() || IsText()) { + // We don't create ATK objects for node which has been shutdown or + // plain text leaves + return; + } + + GType type = GetMaiAtkType(CreateMaiInterfaces()); + if (!type) return; + + mAtkObject = reinterpret_cast(g_object_new(type, nullptr)); + if (!mAtkObject) return; + + atk_object_initialize(mAtkObject, static_cast(this)); + mAtkObject->role = ATK_ROLE_INVALID; + mAtkObject->layer = ATK_LAYER_INVALID; + } + + *aOutAccessible = mAtkObject; +} + +AtkObject* AccessibleWrap::GetAtkObject(void) { + void* atkObj = nullptr; + GetNativeInterface(&atkObj); + return static_cast(atkObj); +} + +// Get AtkObject from LocalAccessible interface +/* static */ +AtkObject* AccessibleWrap::GetAtkObject(LocalAccessible* acc) { + void* atkObjPtr = nullptr; + acc->GetNativeInterface(&atkObjPtr); + return atkObjPtr ? ATK_OBJECT(atkObjPtr) : nullptr; +} + +/* private */ +uint16_t AccessibleWrap::CreateMaiInterfaces(void) { + uint16_t interfacesBits = 0; + + // The Component interface is supported by all accessibles. + interfacesBits |= 1 << MAI_INTERFACE_COMPONENT; + + // Add Action interface if the action count is more than zero. + if (ActionCount() > 0) interfacesBits |= 1 << MAI_INTERFACE_ACTION; + + // Text, Editabletext, and Hypertext interface. + HyperTextAccessible* hyperText = AsHyperText(); + if (hyperText && hyperText->IsTextRole()) { + interfacesBits |= 1 << MAI_INTERFACE_TEXT; + interfacesBits |= 1 << MAI_INTERFACE_EDITABLE_TEXT; + if (!nsAccUtils::MustPrune(this)) { + interfacesBits |= 1 << MAI_INTERFACE_HYPERTEXT; + } + } + + // Value interface. + if (HasNumericValue()) interfacesBits |= 1 << MAI_INTERFACE_VALUE; + + // Document interface. + if (IsDoc()) interfacesBits |= 1 << MAI_INTERFACE_DOCUMENT; + + if (IsImage()) interfacesBits |= 1 << MAI_INTERFACE_IMAGE; + + // HyperLink interface. + if (IsLink()) interfacesBits |= 1 << MAI_INTERFACE_HYPERLINK_IMPL; + + if (!nsAccUtils::MustPrune(this)) { // These interfaces require children + // Table interface. + if (AsTable()) interfacesBits |= 1 << MAI_INTERFACE_TABLE; + + if (AsTableCell()) interfacesBits |= 1 << MAI_INTERFACE_TABLE_CELL; + + // Selection interface. + if (IsSelect()) { + interfacesBits |= 1 << MAI_INTERFACE_SELECTION; + } + } + + return interfacesBits; +} + +static GType GetMaiAtkType(uint16_t interfacesBits) { + GType type; + static const GTypeInfo tinfo = { + sizeof(MaiAtkObjectClass), + (GBaseInitFunc) nullptr, + (GBaseFinalizeFunc) nullptr, + (GClassInitFunc) nullptr, + (GClassFinalizeFunc) nullptr, + nullptr, /* class data */ + sizeof(MaiAtkObject), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) nullptr, + nullptr /* value table */ + }; + + /* + * The members we use to register GTypes are GetAtkTypeForMai + * and atk_if_infos, which are constant values to each MaiInterface + * So we can reuse the registered GType when having + * the same MaiInterface types. + */ + const char* atkTypeName = GetUniqueMaiAtkTypeName(interfacesBits); + type = g_type_from_name(atkTypeName); + if (type) { + return type; + } + + /* + * gobject limits the number of types that can directly derive from any + * given object type to 4095. + */ + static uint16_t typeRegCount = 0; + if (typeRegCount++ >= 4095) { + return G_TYPE_INVALID; + } + type = g_type_register_static(MAI_TYPE_ATK_OBJECT, atkTypeName, &tinfo, + GTypeFlags(0)); + + for (uint32_t index = 0; index < ArrayLength(atk_if_infos); index++) { + if (interfacesBits & (1 << index)) { + g_type_add_interface_static(type, + GetAtkTypeForMai((MaiInterfaceType)index), + &atk_if_infos[index]); + } + } + + // Special case AtkTableCell so we can check what version of Atk we are + // dealing with. + if (IsAtkVersionAtLeast(2, 12) && + (interfacesBits & (1 << MAI_INTERFACE_TABLE_CELL))) { + const GInterfaceInfo cellInfo = { + (GInterfaceInitFunc)tableCellInterfaceInitCB, + (GInterfaceFinalizeFunc) nullptr, nullptr}; + g_type_add_interface_static(type, gAtkTableCellGetTypeFunc(), &cellInfo); + } + + return type; +} + +static const char* GetUniqueMaiAtkTypeName(uint16_t interfacesBits) { +#define MAI_ATK_TYPE_NAME_LEN (30) /* 10+sizeof(uint16_t)*8/4+1 < 30 */ + + static gchar namePrefix[] = "MaiAtkType"; /* size = 10 */ + static gchar name[MAI_ATK_TYPE_NAME_LEN + 1]; + + SprintfLiteral(name, "%s%x", namePrefix, interfacesBits); + name[MAI_ATK_TYPE_NAME_LEN] = '\0'; + + return name; +} + +bool AccessibleWrap::IsValidObject() { + // to ensure we are not shut down + return !IsDefunct(); +} + +/* static functions for ATK callbacks */ +void classInitCB(AtkObjectClass* aClass) { + GObjectClass* gobject_class = G_OBJECT_CLASS(aClass); + + parent_class = g_type_class_peek_parent(aClass); + + aClass->get_name = getNameCB; + aClass->get_description = getDescriptionCB; + aClass->get_parent = getParentCB; + aClass->get_n_children = getChildCountCB; + aClass->ref_child = refChildCB; + aClass->get_index_in_parent = getIndexInParentCB; + aClass->get_role = getRoleCB; + aClass->get_attributes = getAttributesCB; + aClass->get_object_locale = GetLocaleCB; + aClass->ref_state_set = refStateSetCB; + aClass->ref_relation_set = refRelationSetCB; + + aClass->initialize = initializeCB; + + gobject_class->finalize = finalizeCB; + + mai_atk_object_signals[ACTIVATE] = g_signal_new( + "activate", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[CREATE] = g_signal_new( + "create", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[DEACTIVATE] = g_signal_new( + "deactivate", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[DESTROY] = g_signal_new( + "destroy", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[MAXIMIZE] = g_signal_new( + "maximize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[MINIMIZE] = g_signal_new( + "minimize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[RESIZE] = g_signal_new( + "resize", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + mai_atk_object_signals[RESTORE] = g_signal_new( + "restore", MAI_TYPE_ATK_OBJECT, G_SIGNAL_RUN_LAST, + 0, /* default signal handler */ + nullptr, nullptr, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); +} + +void initializeCB(AtkObject* aAtkObj, gpointer aData) { + NS_ASSERTION((IS_MAI_OBJECT(aAtkObj)), "Invalid AtkObject"); + NS_ASSERTION(aData, "Invalid Data to init AtkObject"); + if (!aAtkObj || !aData) return; + + /* call parent init function */ + /* AtkObjectClass has not a "initialize" function now, + * maybe it has later + */ + + if (ATK_OBJECT_CLASS(parent_class)->initialize) { + ATK_OBJECT_CLASS(parent_class)->initialize(aAtkObj, aData); + } + + /* initialize object */ + MAI_ATK_OBJECT(aAtkObj)->acc = static_cast(aData); +} + +void finalizeCB(GObject* aObj) { + if (!IS_MAI_OBJECT(aObj)) return; + NS_ASSERTION(!MAI_ATK_OBJECT(aObj)->acc, "acc NOT null"); + + // call parent finalize function + // finalize of GObjectClass will unref the accessible parent if has + if (G_OBJECT_CLASS(parent_class)->finalize) { + G_OBJECT_CLASS(parent_class)->finalize(aObj); + } +} + +const gchar* getNameCB(AtkObject* aAtkObj) { + nsAutoString name; + if (Accessible* acc = GetInternalObj(aAtkObj)) { + acc->Name(name); + } else { + return nullptr; + } + + // XXX Firing an event from here does not seem right + MaybeFireNameChange(aAtkObj, name); + + return aAtkObj->name; +} + +static void MaybeFireNameChange(AtkObject* aAtkObj, const nsString& aNewName) { + NS_ConvertUTF16toUTF8 newNameUTF8(aNewName); + if (aAtkObj->name && !strcmp(aAtkObj->name, newNameUTF8.get())) return; + + // Below we duplicate the functionality of atk_object_set_name(), + // but without calling atk_object_get_name(). Instead of + // atk_object_get_name() we directly access aAtkObj->name. This is because + // atk_object_get_name() would call getNameCB() which would call + // MaybeFireNameChange() (or atk_object_set_name() before this problem was + // fixed) and we would get an infinite recursion. + // See http://bugzilla.mozilla.org/733712 + + // Do not notify for initial name setting. + // See bug http://bugzilla.gnome.org/665870 + bool notify = !!aAtkObj->name; + + free(aAtkObj->name); + aAtkObj->name = strdup(newNameUTF8.get()); + + if (notify) g_object_notify(G_OBJECT(aAtkObj), "accessible-name"); +} + +const gchar* getDescriptionCB(AtkObject* aAtkObj) { + nsAutoString uniDesc; + if (Accessible* acc = GetInternalObj(aAtkObj)) { + acc->Description(uniDesc); + } else { + return nullptr; + } + + NS_ConvertUTF8toUTF16 objDesc(aAtkObj->description); + if (!uniDesc.Equals(objDesc)) { + atk_object_set_description(aAtkObj, NS_ConvertUTF16toUTF8(uniDesc).get()); + } + + return aAtkObj->description; +} + +AtkRole getRoleCB(AtkObject* aAtkObj) { + if (aAtkObj->role != ATK_ROLE_INVALID) return aAtkObj->role; + + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return ATK_ROLE_INVALID; + } + +#ifdef DEBUG + if (AccessibleWrap* accWrap = GetAccessibleWrap(aAtkObj)) { + NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(accWrap), + "Does not support Text interface when it should"); + } +#endif + +#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ + msaaRole, ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + aAtkObj->role = atkRole; \ + break; + + switch (acc->Role()) { +#include "RoleMap.h" + default: + MOZ_CRASH("Unknown role."); + } + +#undef ROLE + + if (aAtkObj->role == ATK_ROLE_LIST_BOX && !IsAtkVersionAtLeast(2, 1)) { + aAtkObj->role = ATK_ROLE_LIST; + } else if (aAtkObj->role == ATK_ROLE_TABLE_ROW && + !IsAtkVersionAtLeast(2, 1)) { + aAtkObj->role = ATK_ROLE_LIST_ITEM; + } else if (aAtkObj->role == ATK_ROLE_MATH && !IsAtkVersionAtLeast(2, 12)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_COMMENT && !IsAtkVersionAtLeast(2, 12)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_LANDMARK && + !IsAtkVersionAtLeast(2, 12)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_FOOTNOTE && + !IsAtkVersionAtLeast(2, 25, 2)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_STATIC && !IsAtkVersionAtLeast(2, 16)) { + aAtkObj->role = ATK_ROLE_TEXT; + } else if ((aAtkObj->role == ATK_ROLE_MATH_FRACTION || + aAtkObj->role == ATK_ROLE_MATH_ROOT) && + !IsAtkVersionAtLeast(2, 16)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_MARK && !IsAtkVersionAtLeast(2, 36)) { + aAtkObj->role = ATK_ROLE_TEXT; + } else if (aAtkObj->role == ATK_ROLE_SUGGESTION && + !IsAtkVersionAtLeast(2, 36)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if (aAtkObj->role == ATK_ROLE_COMMENT && !IsAtkVersionAtLeast(2, 36)) { + aAtkObj->role = ATK_ROLE_SECTION; + } else if ((aAtkObj->role == ATK_ROLE_CONTENT_DELETION || + aAtkObj->role == ATK_ROLE_CONTENT_INSERTION) && + !IsAtkVersionAtLeast(2, 34)) { + aAtkObj->role = ATK_ROLE_SECTION; + } + + return aAtkObj->role; +} + +static AtkAttributeSet* ConvertToAtkAttributeSet(AccAttributes* aAttributes) { + if (!aAttributes) { + return nullptr; + } + + AtkAttributeSet* objAttributeSet = nullptr; + + for (auto iter : *aAttributes) { + nsAutoString name; + iter.NameAsString(name); + if (name.Equals(u"placeholder")) { + name.AssignLiteral(u"placeholder-text"); + } + + nsAutoString value; + iter.ValueAsString(value); + + AtkAttribute* objAttr = (AtkAttribute*)g_malloc(sizeof(AtkAttribute)); + objAttr->name = g_strdup(NS_ConvertUTF16toUTF8(name).get()); + objAttr->value = g_strdup(NS_ConvertUTF16toUTF8(value).get()); + objAttributeSet = g_slist_prepend(objAttributeSet, objAttr); + } + + // libspi will free it + return objAttributeSet; +} + +AtkAttributeSet* getAttributesCB(AtkObject* aAtkObj) { + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return nullptr; + } + RefPtr attributes = acc->Attributes(); + return ConvertToAtkAttributeSet(attributes); +} + +const gchar* GetLocaleCB(AtkObject* aAtkObj) { + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return nullptr; + } + + nsAutoString locale; + acc->Language(locale); + return AccessibleWrap::ReturnString(locale); +} + +AtkObject* getParentCB(AtkObject* aAtkObj) { + if (aAtkObj->accessible_parent) return aAtkObj->accessible_parent; + + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return nullptr; + } + + Accessible* parent = acc->Parent(); + AtkObject* atkParent = parent ? GetWrapperFor(parent) : nullptr; + if (atkParent) atk_object_set_parent(aAtkObj, atkParent); + + return aAtkObj->accessible_parent; +} + +gint getChildCountCB(AtkObject* aAtkObj) { + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc || nsAccUtils::MustPrune(acc)) { + return 0; + } + return static_cast(acc->EmbeddedChildCount()); +} + +AtkObject* refChildCB(AtkObject* aAtkObj, gint aChildIndex) { + // aChildIndex should not be less than zero + if (aChildIndex < 0) { + return nullptr; + } + + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc || nsAccUtils::MustPrune(acc)) { + return nullptr; + } + Accessible* accChild = acc->EmbeddedChildAt(aChildIndex); + if (!accChild) { + return nullptr; + } + + AtkObject* childAtkObj = GetWrapperFor(accChild); + NS_ASSERTION(childAtkObj, "Fail to get AtkObj"); + if (!childAtkObj) { + return nullptr; + } + + g_object_ref(childAtkObj); + + if (aAtkObj != childAtkObj->accessible_parent) { + atk_object_set_parent(childAtkObj, aAtkObj); + } + + return childAtkObj; +} + +gint getIndexInParentCB(AtkObject* aAtkObj) { + // We don't use LocalAccessible::IndexInParent() because we don't include text + // leaf nodes as children in ATK. + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return -1; + } + if (acc->IsDoc()) { + return 0; + } + Accessible* parent = acc->Parent(); + if (!parent) { + return -1; + } + return parent->IndexOfEmbeddedChild(acc); +} + +static void TranslateStates(uint64_t aState, roles::Role aRole, + AtkStateSet* aStateSet) { + // atk doesn't have a read only state so read only things shouldn't be + // editable. However, we don't do this for list items because Gecko always + // exposes those as read only. + if ((aState & states::READONLY) && aRole != roles::LISTITEM) { + aState &= ~states::EDITABLE; + } + + // Convert every state to an entry in AtkStateMap + uint64_t bitMask = 1; + for (auto stateIndex = 0U; stateIndex < gAtkStateMapLen; stateIndex++) { + if (gAtkStateMap[stateIndex] + .atkState) { // There's potentially an ATK state for this + bool isStateOn = (aState & bitMask) != 0; + if (gAtkStateMap[stateIndex].stateMapEntryType == kMapOpposite) { + isStateOn = !isStateOn; + } + if (isStateOn) { + atk_state_set_add_state(aStateSet, gAtkStateMap[stateIndex].atkState); + } + } + bitMask <<= 1; + } +} + +AtkStateSet* refStateSetCB(AtkObject* aAtkObj) { + AtkStateSet* state_set = nullptr; + state_set = ATK_OBJECT_CLASS(parent_class)->ref_state_set(aAtkObj); + + if (Accessible* acc = GetInternalObj(aAtkObj)) { + TranslateStates(acc->State(), acc->Role(), state_set); + } else { + TranslateStates(states::DEFUNCT, roles::NOTHING, state_set); + } + + return state_set; +} + +static void UpdateAtkRelation(RelationType aType, Accessible* aAcc, + AtkRelationType aAtkType, + AtkRelationSet* aAtkSet) { + if (aAtkType == ATK_RELATION_NULL) return; + + AtkRelation* atkRelation = + atk_relation_set_get_relation_by_type(aAtkSet, aAtkType); + if (atkRelation) atk_relation_set_remove(aAtkSet, atkRelation); + + Relation rel(aAcc->RelationByType(aType)); + nsTArray targets; + Accessible* tempAcc = nullptr; + while ((tempAcc = rel.Next())) { + targets.AppendElement(GetWrapperFor(tempAcc)); + } + + if (targets.Length()) { + atkRelation = + atk_relation_new(targets.Elements(), targets.Length(), aAtkType); + atk_relation_set_add(aAtkSet, atkRelation); + g_object_unref(atkRelation); + } +} + +AtkRelationSet* refRelationSetCB(AtkObject* aAtkObj) { + AtkRelationSet* relation_set = + ATK_OBJECT_CLASS(parent_class)->ref_relation_set(aAtkObj); + + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return relation_set; + } + +#define RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) \ + UpdateAtkRelation(RelationType::geckoType, acc, atkType, relation_set); + +#include "RelationTypeMap.h" + +#undef RELATIONTYPE + + return relation_set; +} + +// Check if aAtkObj is a valid MaiAtkObject, and return the AccessibleWrap +// for it. +AccessibleWrap* GetAccessibleWrap(AtkObject* aAtkObj) { + NS_ENSURE_TRUE(IS_MAI_OBJECT(aAtkObj), nullptr); + + // If we're working with an ATK object, we need to convert the Accessible + // back to an AccessibleWrap: + Accessible* storedAcc = MAI_ATK_OBJECT(aAtkObj)->acc; + if (!storedAcc) { + return nullptr; + } + auto* accWrap = static_cast(storedAcc->AsLocal()); + + // Check if the accessible was deconstructed. + if (!accWrap) return nullptr; + + NS_ENSURE_TRUE(accWrap->GetAtkObject() == aAtkObj, nullptr); + + AccessibleWrap* appAccWrap = ApplicationAcc(); + if (appAccWrap != accWrap && !accWrap->IsValidObject()) { + return nullptr; + } + + return accWrap; +} + +RemoteAccessible* GetProxy(AtkObject* aObj) { + Accessible* acc = GetInternalObj(aObj); + if (!acc) { + return nullptr; + } + + return acc->AsRemote(); +} + +Accessible* GetInternalObj(AtkObject* aObj) { + if (!aObj || !IS_MAI_OBJECT(aObj)) return nullptr; + + return MAI_ATK_OBJECT(aObj)->acc; +} + +AtkObject* GetWrapperFor(Accessible* aAcc) { + if (!aAcc) { + return nullptr; + } + + if (aAcc->IsRemote()) { + return reinterpret_cast(aAcc->AsRemote()->GetWrapper()); + } + + return AccessibleWrap::GetAtkObject(aAcc->AsLocal()); +} + +static uint16_t GetInterfacesForProxy(RemoteAccessible* aProxy) { + uint16_t interfaces = 1 << MAI_INTERFACE_COMPONENT; + if (aProxy->IsHyperText()) { + interfaces |= (1 << MAI_INTERFACE_HYPERTEXT) | (1 << MAI_INTERFACE_TEXT) | + (1 << MAI_INTERFACE_EDITABLE_TEXT); + } + + if (aProxy->IsLink()) { + interfaces |= 1 << MAI_INTERFACE_HYPERLINK_IMPL; + } + + if (aProxy->HasNumericValue()) { + interfaces |= 1 << MAI_INTERFACE_VALUE; + } + + if (aProxy->IsTable()) { + interfaces |= 1 << MAI_INTERFACE_TABLE; + } + + if (aProxy->IsTableCell()) { + interfaces |= 1 << MAI_INTERFACE_TABLE_CELL; + } + + if (aProxy->IsImage()) { + interfaces |= 1 << MAI_INTERFACE_IMAGE; + } + + if (aProxy->IsDoc()) { + interfaces |= 1 << MAI_INTERFACE_DOCUMENT; + } + + if (aProxy->IsSelect()) { + interfaces |= 1 << MAI_INTERFACE_SELECTION; + } + + if (aProxy->IsActionable()) { + interfaces |= 1 << MAI_INTERFACE_ACTION; + } + + return interfaces; +} + +void a11y::ProxyCreated(RemoteAccessible* aProxy) { + MOZ_ASSERT(aProxy->RemoteParent() || aProxy->IsDoc(), + "Need parent to check for HyperLink interface"); + GType type = GetMaiAtkType(GetInterfacesForProxy(aProxy)); + NS_ASSERTION(type, "why don't we have a type!"); + + AtkObject* obj = reinterpret_cast(g_object_new(type, nullptr)); + if (!obj) return; + + atk_object_initialize(obj, static_cast(aProxy)); + obj->role = ATK_ROLE_INVALID; + obj->layer = ATK_LAYER_INVALID; + aProxy->SetWrapper(reinterpret_cast(obj)); +} + +void a11y::ProxyDestroyed(RemoteAccessible* aProxy) { + auto obj = reinterpret_cast(aProxy->GetWrapper()); + if (!obj) { + return; + } + + obj->Shutdown(); + g_object_unref(obj); + aProxy->SetWrapper(0); +} + +nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { + nsresult rv = LocalAccessible::HandleAccEvent(aEvent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IPCAccessibilityActive()) { + return NS_OK; + } + + LocalAccessible* accessible = aEvent->GetAccessible(); + NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE); + + // The accessible can become defunct if we have an xpcom event listener + // which decides it would be fun to change the DOM and flush layout. + if (accessible->IsDefunct()) return NS_OK; + + uint32_t type = aEvent->GetEventType(); + + AtkObject* atkObj = AccessibleWrap::GetAtkObject(accessible); + + // We don't create ATK objects for plain text leaves, just return NS_OK in + // such case. + if (!atkObj) { + NS_ASSERTION(type == nsIAccessibleEvent::EVENT_SHOW || + type == nsIAccessibleEvent::EVENT_HIDE, + "Event other than SHOW and HIDE fired for plain text leaves"); + return NS_OK; + } + + AccessibleWrap* accWrap = GetAccessibleWrap(atkObj); + if (!accWrap) { + return NS_OK; // Node is shut down + } + + switch (type) { + case nsIAccessibleEvent::EVENT_STATE_CHANGE: { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + MAI_ATK_OBJECT(atkObj)->FireStateChangeEvent(event->GetState(), + event->IsStateEnabled()); + break; + } + + case nsIAccessibleEvent::EVENT_TEXT_REMOVED: + case nsIAccessibleEvent::EVENT_TEXT_INSERTED: { + AccTextChangeEvent* event = downcast_accEvent(aEvent); + NS_ENSURE_TRUE(event, NS_ERROR_FAILURE); + + MAI_ATK_OBJECT(atkObj)->FireTextChangeEvent( + event->ModifiedText(), event->GetStartOffset(), event->GetLength(), + event->IsTextInserted(), event->IsFromUserInput()); + + return NS_OK; + } + + case nsIAccessibleEvent::EVENT_FOCUS: { + a11y::RootAccessible* rootAccWrap = accWrap->RootAccessible(); + if (rootAccWrap && rootAccWrap->IsActivated()) { + atk_focus_tracker_notify(atkObj); + // Fire state change event for focus + atk_object_notify_state_change(atkObj, ATK_STATE_FOCUSED, true); + return NS_OK; + } + } break; + + case nsIAccessibleEvent::EVENT_NAME_CHANGE: { + nsAutoString newName; + accessible->Name(newName); + + MaybeFireNameChange(atkObj, newName); + + break; + } + + case nsIAccessibleEvent::EVENT_VALUE_CHANGE: + case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: + if (accessible->HasNumericValue()) { + // Make sure this is a numeric value. Don't fire for string value + // changes (e.g. text editing) ATK values are always numeric. + g_object_notify((GObject*)atkObj, "accessible-value"); + } + break; + + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { + // XXX: dupe events may be fired + AccSelChangeEvent* selChangeEvent = downcast_accEvent(aEvent); + g_signal_emit_by_name( + AccessibleWrap::GetAtkObject(selChangeEvent->Widget()), + "selection_changed"); + break; + } + + case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: { + g_signal_emit_by_name(atkObj, "selection_changed"); + break; + } + + case nsIAccessibleEvent::EVENT_ALERT: + // A hack using state change showing events as alert events. + atk_object_notify_state_change(atkObj, ATK_STATE_SHOWING, true); + break; + + case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: + g_signal_emit_by_name(atkObj, "text_selection_changed"); + break; + + case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { + AccCaretMoveEvent* caretMoveEvent = downcast_accEvent(aEvent); + NS_ASSERTION(caretMoveEvent, "Event needs event data"); + if (!caretMoveEvent) break; + + int32_t caretOffset = caretMoveEvent->GetCaretOffset(); + g_signal_emit_by_name(atkObj, "text_caret_moved", caretOffset); + } break; + + case nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED: + g_signal_emit_by_name(atkObj, "text-attributes-changed"); + break; + + case nsIAccessibleEvent::EVENT_TABLE_MODEL_CHANGED: + g_signal_emit_by_name(atkObj, "model_changed"); + break; + + case nsIAccessibleEvent::EVENT_TABLE_ROW_INSERT: { + AccTableChangeEvent* tableEvent = downcast_accEvent(aEvent); + NS_ENSURE_TRUE(tableEvent, NS_ERROR_FAILURE); + + int32_t rowIndex = tableEvent->GetIndex(); + int32_t numRows = tableEvent->GetCount(); + + g_signal_emit_by_name(atkObj, "row_inserted", rowIndex, numRows); + } break; + + case nsIAccessibleEvent::EVENT_TABLE_ROW_DELETE: { + AccTableChangeEvent* tableEvent = downcast_accEvent(aEvent); + NS_ENSURE_TRUE(tableEvent, NS_ERROR_FAILURE); + + int32_t rowIndex = tableEvent->GetIndex(); + int32_t numRows = tableEvent->GetCount(); + + g_signal_emit_by_name(atkObj, "row_deleted", rowIndex, numRows); + } break; + + case nsIAccessibleEvent::EVENT_TABLE_ROW_REORDER: { + g_signal_emit_by_name(atkObj, "row_reordered"); + break; + } + + case nsIAccessibleEvent::EVENT_TABLE_COLUMN_INSERT: { + AccTableChangeEvent* tableEvent = downcast_accEvent(aEvent); + NS_ENSURE_TRUE(tableEvent, NS_ERROR_FAILURE); + + int32_t colIndex = tableEvent->GetIndex(); + int32_t numCols = tableEvent->GetCount(); + g_signal_emit_by_name(atkObj, "column_inserted", colIndex, numCols); + } break; + + case nsIAccessibleEvent::EVENT_TABLE_COLUMN_DELETE: { + AccTableChangeEvent* tableEvent = downcast_accEvent(aEvent); + NS_ENSURE_TRUE(tableEvent, NS_ERROR_FAILURE); + + int32_t colIndex = tableEvent->GetIndex(); + int32_t numCols = tableEvent->GetCount(); + g_signal_emit_by_name(atkObj, "column_deleted", colIndex, numCols); + } break; + + case nsIAccessibleEvent::EVENT_TABLE_COLUMN_REORDER: + g_signal_emit_by_name(atkObj, "column_reordered"); + break; + + case nsIAccessibleEvent::EVENT_SECTION_CHANGED: + g_signal_emit_by_name(atkObj, "visible_data_changed"); + break; + + case nsIAccessibleEvent::EVENT_SHOW: { + AccMutationEvent* event = downcast_accEvent(aEvent); + LocalAccessible* parentAcc = + event ? event->LocalParent() : accessible->LocalParent(); + AtkObject* parent = AccessibleWrap::GetAtkObject(parentAcc); + NS_ENSURE_STATE(parent); + auto obj = reinterpret_cast(atkObj); + obj->FireAtkShowHideEvent(parent, true, aEvent->IsFromUserInput()); + return NS_OK; + } + + case nsIAccessibleEvent::EVENT_HIDE: { + // XXX - Handle native dialog accessibles. + if (!accessible->IsRoot() && accessible->HasARIARole() && + accessible->ARIARole() == roles::DIALOG) { + guint id = g_signal_lookup("deactivate", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } + + AccMutationEvent* event = downcast_accEvent(aEvent); + LocalAccessible* parentAcc = + event ? event->LocalParent() : accessible->LocalParent(); + AtkObject* parent = AccessibleWrap::GetAtkObject(parentAcc); + NS_ENSURE_STATE(parent); + auto obj = reinterpret_cast(atkObj); + obj->FireAtkShowHideEvent(parent, false, aEvent->IsFromUserInput()); + return NS_OK; + } + + /* + * Because dealing with menu is very different between nsIAccessible + * and ATK, and the menu activity is important, specially transfer the + * following two event. + * Need more verification by AT test. + */ + case nsIAccessibleEvent::EVENT_MENU_START: + case nsIAccessibleEvent::EVENT_MENU_END: + break; + + case nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE: { + guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + + // Always fire a current focus event after activation. + FocusMgr()->ForceFocusEvent(); + } break; + + case nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE: { + guint id = g_signal_lookup("deactivate", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } break; + + case nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE: { + guint id = g_signal_lookup("maximize", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } break; + + case nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE: { + guint id = g_signal_lookup("minimize", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } break; + + case nsIAccessibleEvent::EVENT_WINDOW_RESTORE: { + guint id = g_signal_lookup("restore", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } break; + + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: + if (accessible->IsDoc()) { + g_signal_emit_by_name(atkObj, "load_complete"); + } + // XXX - Handle native dialog accessibles. + if (!accessible->IsRoot() && accessible->HasARIARole() && + accessible->ARIARole() == roles::DIALOG) { + guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT); + g_signal_emit(atkObj, id, 0); + } + break; + + case nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD: + if (accessible->IsDoc()) { + g_signal_emit_by_name(atkObj, "reload"); + } + break; + + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED: + if (accessible->IsDoc()) { + g_signal_emit_by_name(atkObj, "load_stopped"); + } + break; + + case nsIAccessibleEvent::EVENT_MENUPOPUP_START: + atk_focus_tracker_notify(atkObj); // fire extra focus event + atk_object_notify_state_change(atkObj, ATK_STATE_VISIBLE, true); + atk_object_notify_state_change(atkObj, ATK_STATE_SHOWING, true); + break; + + case nsIAccessibleEvent::EVENT_MENUPOPUP_END: + atk_object_notify_state_change(atkObj, ATK_STATE_VISIBLE, false); + atk_object_notify_state_change(atkObj, ATK_STATE_SHOWING, false); + break; + } + + return NS_OK; +} + +void a11y::ProxyEvent(RemoteAccessible* aTarget, uint32_t aEventType) { + AtkObject* wrapper = GetWrapperFor(aTarget); + + switch (aEventType) { + case nsIAccessibleEvent::EVENT_FOCUS: + atk_focus_tracker_notify(wrapper); + atk_object_notify_state_change(wrapper, ATK_STATE_FOCUSED, true); + break; + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: + if (aTarget->IsDoc()) { + g_signal_emit_by_name(wrapper, "load_complete"); + } + break; + case nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD: + if (aTarget->IsDoc()) { + g_signal_emit_by_name(wrapper, "reload"); + } + break; + case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED: + if (aTarget->IsDoc()) { + g_signal_emit_by_name(wrapper, "load_stopped"); + } + break; + case nsIAccessibleEvent::EVENT_MENUPOPUP_START: + atk_focus_tracker_notify(wrapper); // fire extra focus event + atk_object_notify_state_change(wrapper, ATK_STATE_VISIBLE, true); + atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, true); + break; + case nsIAccessibleEvent::EVENT_MENUPOPUP_END: + atk_object_notify_state_change(wrapper, ATK_STATE_VISIBLE, false); + atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, false); + break; + case nsIAccessibleEvent::EVENT_ALERT: + // A hack using state change showing events as alert events. + atk_object_notify_state_change(wrapper, ATK_STATE_SHOWING, true); + break; + case nsIAccessibleEvent::EVENT_VALUE_CHANGE: + g_object_notify((GObject*)wrapper, "accessible-value"); + break; + case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: + g_signal_emit_by_name(wrapper, "text_selection_changed"); + break; + case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: + g_signal_emit_by_name(wrapper, "selection_changed"); + break; + case nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED: + g_signal_emit_by_name(wrapper, "text-attributes-changed"); + break; + } +} + +void a11y::ProxyStateChangeEvent(RemoteAccessible* aTarget, uint64_t aState, + bool aEnabled) { + MaiAtkObject* atkObj = MAI_ATK_OBJECT(GetWrapperFor(aTarget)); + atkObj->FireStateChangeEvent(aState, aEnabled); +} + +void a11y::ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, + int32_t aGranularity) { + AtkObject* wrapper = GetWrapperFor(aTarget); + g_signal_emit_by_name(wrapper, "text_caret_moved", aOffset); +} + +void MaiAtkObject::FireStateChangeEvent(uint64_t aState, bool aEnabled) { + auto state = aState; + int32_t stateIndex = -1; + while (state > 0) { + ++stateIndex; + state >>= 1; + } + + MOZ_ASSERT( + stateIndex >= 0 && stateIndex < static_cast(gAtkStateMapLen), + "No ATK state for internal state was found"); + if (stateIndex < 0 || stateIndex >= static_cast(gAtkStateMapLen)) { + return; + } + + if (gAtkStateMap[stateIndex].atkState != kNone) { + MOZ_ASSERT(gAtkStateMap[stateIndex].stateMapEntryType != kNoStateChange, + "State changes should not fired for this state"); + + if (gAtkStateMap[stateIndex].stateMapEntryType == kMapOpposite) { + aEnabled = !aEnabled; + } + + // Fire state change for first state if there is one to map + atk_object_notify_state_change(&parent, gAtkStateMap[stateIndex].atkState, + aEnabled); + } +} + +void a11y::ProxyTextChangeEvent(RemoteAccessible* aTarget, + const nsAString& aStr, int32_t aStart, + uint32_t aLen, bool aIsInsert, bool aFromUser) { + MaiAtkObject* atkObj = MAI_ATK_OBJECT(GetWrapperFor(aTarget)); + atkObj->FireTextChangeEvent(aStr, aStart, aLen, aIsInsert, aFromUser); +} + +#define OLD_TEXT_INSERTED "text_changed::insert" +#define OLD_TEXT_REMOVED "text_changed::delete" +static const char* oldTextChangeStrings[2][2] = { + {OLD_TEXT_REMOVED NON_USER_EVENT, OLD_TEXT_INSERTED NON_USER_EVENT}, + {OLD_TEXT_REMOVED, OLD_TEXT_INSERTED}}; + +#define TEXT_INSERTED "text-insert" +#define TEXT_REMOVED "text-remove" +#define NON_USER_DETAIL "::system" +static const char* textChangedStrings[2][2] = { + {TEXT_REMOVED NON_USER_DETAIL, TEXT_INSERTED NON_USER_DETAIL}, + {TEXT_REMOVED, TEXT_INSERTED}}; + +void MaiAtkObject::FireTextChangeEvent(const nsAString& aStr, int32_t aStart, + uint32_t aLen, bool aIsInsert, + bool aFromUser) { + if (gAvailableAtkSignals == eUnknown) { + gAvailableAtkSignals = g_signal_lookup("text-insert", G_OBJECT_TYPE(this)) + ? eHaveNewAtkTextSignals + : eNoNewAtkSignals; + } + + if (gAvailableAtkSignals == eNoNewAtkSignals) { + // XXX remove this code and the gHaveNewTextSignals check when we can + // stop supporting old atk since it doesn't really work anyway + // see bug 619002 + const char* signal_name = oldTextChangeStrings[aFromUser][aIsInsert]; + g_signal_emit_by_name(this, signal_name, aStart, aLen); + } else { + const char* signal_name = textChangedStrings[aFromUser][aIsInsert]; + g_signal_emit_by_name(this, signal_name, aStart, aLen, + NS_ConvertUTF16toUTF8(aStr).get()); + } +} + +void a11y::ProxyShowHideEvent(RemoteAccessible* aTarget, + RemoteAccessible* aParent, bool aInsert, + bool aFromUser) { + MaiAtkObject* obj = MAI_ATK_OBJECT(GetWrapperFor(aTarget)); + obj->FireAtkShowHideEvent(GetWrapperFor(aParent), aInsert, aFromUser); +} + +#define ADD_EVENT "children_changed::add" +#define HIDE_EVENT "children_changed::remove" + +static const char* kMutationStrings[2][2] = { + {HIDE_EVENT NON_USER_EVENT, ADD_EVENT NON_USER_EVENT}, + {HIDE_EVENT, ADD_EVENT}, +}; + +void MaiAtkObject::FireAtkShowHideEvent(AtkObject* aParent, bool aIsAdded, + bool aFromUser) { + int32_t indexInParent = getIndexInParentCB(&this->parent); + const char* signal_name = kMutationStrings[aFromUser][aIsAdded]; + g_signal_emit_by_name(aParent, signal_name, indexInParent, this, nullptr); +} + +void a11y::ProxySelectionEvent(RemoteAccessible*, RemoteAccessible* aWidget, + uint32_t) { + MaiAtkObject* obj = MAI_ATK_OBJECT(GetWrapperFor(aWidget)); + g_signal_emit_by_name(obj, "selection_changed"); +} + +// static +void AccessibleWrap::GetKeyBinding(Accessible* aAccessible, + nsAString& aResult) { + // Return all key bindings including access key and keyboard shortcut. + + // Get access key. + nsAutoString keyBindingsStr; + KeyBinding keyBinding = aAccessible->AccessKey(); + if (!keyBinding.IsEmpty()) { + keyBinding.AppendToString(keyBindingsStr, KeyBinding::eAtkFormat); + + Accessible* parent = aAccessible->Parent(); + roles::Role role = parent ? parent->Role() : roles::NOTHING; + if (role == roles::PARENT_MENUITEM || role == roles::MENUITEM || + role == roles::RADIO_MENU_ITEM || role == roles::CHECK_MENU_ITEM) { + // It is submenu, expose keyboard shortcuts from menu hierarchy like + // "s;f:s" + nsAutoString keysInHierarchyStr = keyBindingsStr; + do { + KeyBinding parentKeyBinding = parent->AccessKey(); + if (!parentKeyBinding.IsEmpty()) { + nsAutoString str; + parentKeyBinding.ToString(str, KeyBinding::eAtkFormat); + str.Append(':'); + + keysInHierarchyStr.Insert(str, 0); + } + } while ((parent = parent->Parent()) && parent->Role() != roles::MENUBAR); + + keyBindingsStr.Append(';'); + keyBindingsStr.Append(keysInHierarchyStr); + } + } else { + // No access key, add ';' to point this. + keyBindingsStr.Append(';'); + } + + // Get keyboard shortcut. + keyBindingsStr.Append(';'); + if (LocalAccessible* localAcc = aAccessible->AsLocal()) { + keyBinding = localAcc->KeyboardShortcut(); + if (!keyBinding.IsEmpty()) { + keyBinding.AppendToString(keyBindingsStr, KeyBinding::eAtkFormat); + } + } + aResult = keyBindingsStr; +} + +// static +Accessible* AccessibleWrap::GetColumnHeader(TableAccessible* aAccessible, + int32_t aColIdx) { + if (!aAccessible) { + return nullptr; + } + + Accessible* cell = aAccessible->CellAt(0, aColIdx); + if (!cell) { + return nullptr; + } + + // If the cell at the first row is column header then assume it is column + // header for all rows, + if (cell->Role() == roles::COLUMNHEADER) { + return cell; + } + + // otherwise get column header for the data cell at the first row. + TableCellAccessible* tableCell = cell->AsTableCell(); + if (!tableCell) { + return nullptr; + } + + AutoTArray headerCells; + tableCell->ColHeaderCells(&headerCells); + if (headerCells.IsEmpty()) { + return nullptr; + } + + return headerCells[0]; +} + +// static +Accessible* AccessibleWrap::GetRowHeader(TableAccessible* aAccessible, + int32_t aRowIdx) { + if (!aAccessible) { + return nullptr; + } + + Accessible* cell = aAccessible->CellAt(aRowIdx, 0); + if (!cell) { + return nullptr; + } + + // If the cell at the first column is row header then assume it is row + // header for all columns, + if (cell->Role() == roles::ROWHEADER) { + return cell; + } + + // otherwise get row header for the data cell at the first column. + TableCellAccessible* tableCell = cell->AsTableCell(); + if (!tableCell) { + return nullptr; + } + + AutoTArray headerCells; + tableCell->RowHeaderCells(&headerCells); + if (headerCells.IsEmpty()) { + return nullptr; + } + + return headerCells[0]; +} diff --git a/accessible/atk/AccessibleWrap.h b/accessible/atk/AccessibleWrap.h new file mode 100644 index 0000000000..3ef404534f --- /dev/null +++ b/accessible/atk/AccessibleWrap.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef __NS_ACCESSIBLE_WRAP_H__ +#define __NS_ACCESSIBLE_WRAP_H__ + +#include "nsCOMPtr.h" +#include "LocalAccessible.h" + +struct _AtkObject; +typedef struct _AtkObject AtkObject; + +enum AtkProperty { + PROP_0, // gobject convention + PROP_NAME, + PROP_DESCRIPTION, + PROP_PARENT, // ancestry has changed + PROP_ROLE, + PROP_LAYER, + PROP_MDI_ZORDER, + PROP_TABLE_CAPTION, + PROP_TABLE_COLUMN_DESCRIPTION, + PROP_TABLE_COLUMN_HEADER, + PROP_TABLE_ROW_DESCRIPTION, + PROP_TABLE_ROW_HEADER, + PROP_TABLE_SUMMARY, + PROP_LAST // gobject convention +}; + +struct AtkPropertyChange { + int32_t type; // property type as listed above + void* oldvalue; + void* newvalue; +}; + +namespace mozilla { +namespace a11y { + +class MaiHyperlink; + +/** + * Atk specific functionality for an accessibility tree node that originated in + * mDoc's content process. + * + * AccessibleWrap, and its descendents in atk directory provide the + * implementation of AtkObject. + */ +class AccessibleWrap : public LocalAccessible { + public: + AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc); + virtual ~AccessibleWrap(); + void ShutdownAtkObject(); + + virtual void Shutdown() override; + + // return the atk object for this AccessibleWrap + virtual void GetNativeInterface(void** aOutAccessible) override; + virtual nsresult HandleAccEvent(AccEvent* aEvent) override; + + AtkObject* GetAtkObject(void); + static AtkObject* GetAtkObject(LocalAccessible* aAccessible); + + bool IsValidObject(); + + static const char* ReturnString(nsAString& aString) { + static nsCString returnedString; + CopyUTF16toUTF8(aString, returnedString); + return returnedString.get(); + } + + static void GetKeyBinding(Accessible* aAccessible, nsAString& aResult); + + static Accessible* GetColumnHeader(TableAccessible* aAccessible, + int32_t aColIdx); + static Accessible* GetRowHeader(TableAccessible* aAccessible, + int32_t aRowIdx); + + protected: + nsresult FireAtkStateChangeEvent(AccEvent* aEvent, AtkObject* aObject); + nsresult FireAtkTextChangedEvent(AccEvent* aEvent, AtkObject* aObject); + + AtkObject* mAtkObject; + + private: + uint16_t CreateMaiInterfaces(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif /* __NS_ACCESSIBLE_WRAP_H__ */ diff --git a/accessible/atk/ApplicationAccessibleWrap.cpp b/accessible/atk/ApplicationAccessibleWrap.cpp new file mode 100644 index 0000000000..78c7f6047d --- /dev/null +++ b/accessible/atk/ApplicationAccessibleWrap.cpp @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "ApplicationAccessibleWrap.h" + +#include "nsMai.h" +#include "nsAccessibilityService.h" + +#include +#include "atk/atkobject.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +// ApplicationAccessibleWrap + +ApplicationAccessibleWrap::ApplicationAccessibleWrap() + : ApplicationAccessible() {} + +ApplicationAccessibleWrap::~ApplicationAccessibleWrap() { + AccessibleWrap::ShutdownAtkObject(); +} + +gboolean toplevel_event_watcher(GSignalInvocationHint* ihint, + guint n_param_values, + const GValue* param_values, gpointer data) { + static GQuark sQuark_gecko_acc_obj = 0; + + if (!sQuark_gecko_acc_obj) { + sQuark_gecko_acc_obj = g_quark_from_static_string("GeckoAccObj"); + } + + if (nsAccessibilityService::IsShutdown()) return TRUE; + + GObject* object = + reinterpret_cast(g_value_get_object(param_values)); + if (!GTK_IS_WINDOW(object)) return TRUE; + + AtkObject* child = gtk_widget_get_accessible(GTK_WIDGET(object)); + AtkRole role = atk_object_get_role(child); + + // GTK native dialog + if (!IS_MAI_OBJECT(child) && + (role == ATK_ROLE_DIALOG || role == ATK_ROLE_FILE_CHOOSER || + role == ATK_ROLE_COLOR_CHOOSER || role == ATK_ROLE_FONT_CHOOSER)) { + if (data == reinterpret_cast(nsIAccessibleEvent::EVENT_SHOW)) { + // Attach the dialog accessible to app accessible tree + LocalAccessible* windowAcc = + GetAccService()->AddNativeRootAccessible(child); + g_object_set_qdata(G_OBJECT(child), sQuark_gecko_acc_obj, + reinterpret_cast(windowAcc)); + + } else { + // Deattach the dialog accessible + LocalAccessible* windowAcc = reinterpret_cast( + g_object_get_qdata(G_OBJECT(child), sQuark_gecko_acc_obj)); + if (windowAcc) { + GetAccService()->RemoveNativeRootAccessible(windowAcc); + g_object_set_qdata(G_OBJECT(child), sQuark_gecko_acc_obj, nullptr); + } + } + } + + return TRUE; +} + +ENameValueFlag ApplicationAccessibleWrap::Name(nsString& aName) const { + // ATK doesn't provide a way to obtain an application name (for example, + // Firefox or Thunderbird) like IA2 does. Thus let's return an application + // name as accessible name that was used to get a branding name (for example, + // Minefield aka nightly Firefox or Daily aka nightly Thunderbird). + AppName(aName); + return eNameOK; +} + +void ApplicationAccessibleWrap::GetNativeInterface(void** aOutAccessible) { + *aOutAccessible = nullptr; + + if (!mAtkObject) { + mAtkObject = reinterpret_cast( + g_object_new(MAI_TYPE_ATK_OBJECT, nullptr)); + if (!mAtkObject) return; + + atk_object_initialize(mAtkObject, static_cast(this)); + mAtkObject->role = ATK_ROLE_INVALID; + mAtkObject->layer = ATK_LAYER_INVALID; + } + + *aOutAccessible = mAtkObject; +} + +struct AtkRootAccessibleAddedEvent { + AtkObject* app_accessible; + AtkObject* root_accessible; + uint32_t index; +}; + +gboolean fireRootAccessibleAddedCB(gpointer data) { + AtkRootAccessibleAddedEvent* eventData = (AtkRootAccessibleAddedEvent*)data; + g_signal_emit_by_name(eventData->app_accessible, "children_changed::add", + eventData->index, eventData->root_accessible, nullptr); + g_object_unref(eventData->app_accessible); + g_object_unref(eventData->root_accessible); + free(data); + + return FALSE; +} + +bool ApplicationAccessibleWrap::InsertChildAt(uint32_t aIdx, + LocalAccessible* aChild) { + if (!ApplicationAccessible::InsertChildAt(aIdx, aChild)) return false; + + AtkObject* atkAccessible = AccessibleWrap::GetAtkObject(aChild); + atk_object_set_parent(atkAccessible, mAtkObject); + + uint32_t count = mChildren.Length(); + + // Emit children_changed::add in a timeout + // to make sure aRootAccWrap is fully initialized. + AtkRootAccessibleAddedEvent* eventData = + (AtkRootAccessibleAddedEvent*)malloc(sizeof(AtkRootAccessibleAddedEvent)); + if (eventData) { + eventData->app_accessible = mAtkObject; + eventData->root_accessible = atkAccessible; + eventData->index = count - 1; + g_object_ref(mAtkObject); + g_object_ref(atkAccessible); + g_timeout_add(0, fireRootAccessibleAddedCB, eventData); + } + + return true; +} + +bool ApplicationAccessibleWrap::RemoveChild(LocalAccessible* aChild) { + int32_t index = aChild->IndexInParent(); + + AtkObject* atkAccessible = AccessibleWrap::GetAtkObject(aChild); + atk_object_set_parent(atkAccessible, nullptr); + g_signal_emit_by_name(mAtkObject, "children_changed::remove", index, + atkAccessible, nullptr); + + return ApplicationAccessible::RemoveChild(aChild); +} diff --git a/accessible/atk/ApplicationAccessibleWrap.h b/accessible/atk/ApplicationAccessibleWrap.h new file mode 100644 index 0000000000..fe14dc045a --- /dev/null +++ b/accessible/atk/ApplicationAccessibleWrap.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__ +#define mozilla_a11y_ApplicationAccessibleWrap_h__ + +#include "ApplicationAccessible.h" + +namespace mozilla { +namespace a11y { + +class ApplicationAccessibleWrap : public ApplicationAccessible { + public: + ApplicationAccessibleWrap(); + virtual ~ApplicationAccessibleWrap(); + + // LocalAccessible + virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override; + virtual bool InsertChildAt(uint32_t aIdx, LocalAccessible* aChild) override; + virtual bool RemoveChild(LocalAccessible* aChild) override; + + /** + * Return the atk object for app root accessible. + */ + virtual void GetNativeInterface(void** aOutAccessible) override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif /* __NS_APP_ROOT_ACCESSIBLE_H__ */ diff --git a/accessible/atk/DOMtoATK.cpp b/accessible/atk/DOMtoATK.cpp new file mode 100644 index 0000000000..2c23731bba --- /dev/null +++ b/accessible/atk/DOMtoATK.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "DOMtoATK.h" +#include "nsUTF8Utils.h" + +namespace mozilla { +namespace a11y { + +namespace DOMtoATK { + +void AddBOMs(nsACString& aDest, const nsACString& aSource) { + uint32_t destlength = 0; + + // First compute how much room we will need. + for (uint32_t srci = 0; srci < aSource.Length();) { + int bytes = UTF8traits::bytes(aSource[srci]); + if (bytes >= 4) { + // Non-BMP character, will add a BOM after it. + destlength += 3; + } + // Skip whole character encoding. + srci += bytes; + destlength += bytes; + } + + uint32_t desti = 0; // Index within aDest. + + // Add BOMs after non-BMP characters. + aDest.SetLength(destlength); + for (uint32_t srci = 0; srci < aSource.Length();) { + uint32_t bytes = UTF8traits::bytes(aSource[srci]); + + MOZ_ASSERT(bytes <= aSource.Length() - srci, + "We should have the whole sequence"); + + // Copy whole sequence. + aDest.Replace(desti, bytes, Substring(aSource, srci, bytes)); + desti += bytes; + srci += bytes; + + if (bytes >= 4) { + // More than 4 bytes in UTF-8 encoding exactly means more than 16 encoded + // bits. This is thus a non-BMP character which needed a surrogate + // pair to get encoded in UTF-16, add a BOM after it. + + // And add a BOM after it. + aDest.Replace(desti, 3, "\xEF\xBB\xBF"); + desti += 3; + } + } + MOZ_ASSERT(desti == destlength, + "Incoherency between computed length" + "and actually translated length"); +} + +void ATKStringConverterHelper::AdjustOffsets(gint* aStartOffset, + gint* aEndOffset, gint count) { + MOZ_ASSERT(!mAdjusted, + "DOMtoATK::ATKStringConverterHelper::AdjustOffsets needs to be " + "called only once"); + + if (*aStartOffset > 0) { + (*aStartOffset)--; + mStartShifted = true; + } + + if (*aEndOffset >= 0 && *aEndOffset < count) { + (*aEndOffset)++; + mEndShifted = true; + } + +#ifdef DEBUG + mAdjusted = true; +#endif +} + +gchar* ATKStringConverterHelper::FinishUTF16toUTF8(nsCString& aStr) { + int skip = 0; + + if (mStartShifted) { + // AdjustOffsets added a leading character. + + MOZ_ASSERT(aStr.Length() > 0, "There should be a leading character"); + MOZ_ASSERT( + static_cast(aStr.Length()) >= UTF8traits::bytes(aStr.CharAt(0)), + "The leading character should be complete"); + + // drop first character + skip = UTF8traits::bytes(aStr.CharAt(0)); + } + + if (mEndShifted) { + // AdjustOffsets added a trailing character. + + MOZ_ASSERT(aStr.Length() > 0, "There should be a trailing character"); + + int trail = -1; + // Find beginning of last character. + for (trail = aStr.Length() - 1; trail >= 0; trail--) { + if (!UTF8traits::isInSeq(aStr.CharAt(trail))) { + break; + } + } + MOZ_ASSERT(trail >= 0, + "There should be at least a whole trailing character"); + MOZ_ASSERT(trail + UTF8traits::bytes(aStr.CharAt(trail)) == + static_cast(aStr.Length()), + "The trailing character should be complete"); + + // Drop the last character. + aStr.Truncate(trail); + } + + // copy and return, libspi will free it + return g_strdup(aStr.get() + skip); +} + +gchar* ATKStringConverterHelper::ConvertAdjusted(const nsAString& aStr) { + MOZ_ASSERT(mAdjusted, + "DOMtoATK::ATKStringConverterHelper::AdjustOffsets needs to be " + "called before ATKStringConverterHelper::ConvertAdjusted"); + + NS_ConvertUTF16toUTF8 cautoStr(aStr); + if (!cautoStr.get()) { + return nullptr; + } + + nsAutoCString cautoStrBOMs; + AddBOMs(cautoStrBOMs, cautoStr); + return FinishUTF16toUTF8(cautoStrBOMs); +} + +gchar* Convert(const nsAString& aStr) { + NS_ConvertUTF16toUTF8 cautoStr(aStr); + if (!cautoStr.get()) { + return nullptr; + } + + nsAutoCString cautoStrBOMs; + AddBOMs(cautoStrBOMs, cautoStr); + return g_strdup(cautoStrBOMs.get()); +} + +} // namespace DOMtoATK + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/atk/DOMtoATK.h b/accessible/atk/DOMtoATK.h new file mode 100644 index 0000000000..322358bc6e --- /dev/null +++ b/accessible/atk/DOMtoATK.h @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include +#include +#include "mozilla/a11y/HyperTextAccessibleBase.h" +#include "nsCharTraits.h" +#include "nsString.h" + +/** + * ATK offsets are counted in unicode codepoints, while DOM offsets are counted + * in UTF-16 code units. That makes a difference for non-BMP characters, + * which need two UTF-16 code units to be represented (a pair of surrogates), + * while they are just one unicode character. + * + * To keep synchronization between ATK offsets (unicode codepoints) and DOM + * offsets (UTF-16 code units), after translation from UTF-16 to UTF-8 we add a + * BOM after each non-BMP character (which would otherwise use 2 UTF-16 + * code units for only 1 unicode codepoint). + * + * BOMs (Byte Order Marks, U+FEFF, also known as ZERO WIDTH NO-BREAK SPACE, but + * that usage is deprecated) normally only appear at the beginning of unicode + * files, but their occurrence within text (notably after cut&paste) is not + * uncommon, and are thus considered as non-text. + * + * Since the selection requested through ATK may not contain both surrogates + * at the ends of the selection, we need to fetch one UTF-16 code point more + * on both side, and get rid of it before returning the string to ATK. The + * ATKStringConverterHelper class maintains this, NewATKString should be used + * to call it properly. + * + * In the end, + * - if the start is between the high and low surrogates, the UTF-8 result + * includes a BOM from it but not the character + * - if the end is between the high and low surrogates, the UTF-8 result + * includes the character but *not* the BOM + * - all non-BMP characters that are fully in the string are in the UTF-8 result + * as character followed by BOM + */ +namespace mozilla { +namespace a11y { + +namespace DOMtoATK { + +/** + * Converts a string of accessible text into ATK gchar* string (by adding + * BOMs). This can be used when offsets do not need to be adjusted because + * ends of the string can not fall between surrogates. + */ +gchar* Convert(const nsAString& aStr); + +/** + * Add a BOM after each non-BMP character. + */ +void AddBOMs(nsACString& aDest, const nsACString& aSource); + +class ATKStringConverterHelper { + public: + ATKStringConverterHelper(void) + : +#ifdef DEBUG + mAdjusted(false), +#endif + mStartShifted(false), + mEndShifted(false) { + } + + /** + * In order to properly get non-BMP values, offsets need to be changed + * to get one character more on each end, so that ConvertUTF16toUTF8 can + * convert surrogates even if the originally requested offsets fall between + * them. + */ + void AdjustOffsets(gint* aStartOffset, gint* aEndOffset, gint count); + + /** + * Converts a string of accessible text with adjusted offsets into ATK + * gchar* string (by adding BOMs). Note, AdjustOffsets has to be called + * before getting the text passed to this. + */ + gchar* ConvertAdjusted(const nsAString& aStr); + + private: + /** + * Remove the additional characters requested by PrepareUTF16toUTF8. + */ + gchar* FinishUTF16toUTF8(nsCString& aStr); + +#ifdef DEBUG + bool mAdjusted; +#endif + bool mStartShifted; + bool mEndShifted; +}; + +/** + * Get text from aAccessible, using ATKStringConverterHelper to properly + * introduce appropriate BOMs. + */ +inline gchar* NewATKString(HyperTextAccessibleBase* aAccessible, + gint aStartOffset, gint aEndOffset) { + gint startOffset = aStartOffset, endOffset = aEndOffset; + ATKStringConverterHelper converter; + converter.AdjustOffsets(&startOffset, &endOffset, + gint(aAccessible->CharacterCount())); + nsAutoString str; + aAccessible->TextSubstring(startOffset, endOffset, str); + + if (str.Length() == 0) { + // Bogus offsets, or empty string, either way we do not need conversion. + return g_strdup(""); + } + + return converter.ConvertAdjusted(str); +} + +/** + * Get a character from aAccessible, fetching more data as appropriate to + * properly get non-BMP characters or a BOM as appropriate. + */ +inline gunichar ATKCharacter(HyperTextAccessibleBase* aAccessible, + gint aOffset) { + // char16_t is unsigned short in Mozilla, gnuichar is guint32 in glib. + gunichar character = static_cast(aAccessible->CharAt(aOffset)); + + if (NS_IS_LOW_SURROGATE(character)) { + // Trailing surrogate, return BOM instead. + return 0xFEFF; + } + + if (NS_IS_HIGH_SURROGATE(character)) { + // Heading surrogate, get the trailing surrogate and combine them. + gunichar characterLow = + static_cast(aAccessible->CharAt(aOffset + 1)); + + if (!NS_IS_LOW_SURROGATE(characterLow)) { + // It should have been a trailing surrogate... Flag the error. + return 0xFFFD; + } + return SURROGATE_TO_UCS4(character, characterLow); + } + + return character; +} + +} // namespace DOMtoATK + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/atk/DocAccessibleWrap.cpp b/accessible/atk/DocAccessibleWrap.cpp new file mode 100644 index 0000000000..f3dfba71ac --- /dev/null +++ b/accessible/atk/DocAccessibleWrap.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "DocAccessibleWrap.h" +#include "mozilla/PresShell.h" +#include "nsIWidgetListener.h" +#include "nsTArray.h" +#include "nsWindow.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// DocAccessibleWrap +//////////////////////////////////////////////////////////////////////////////// + +DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument, + PresShell* aPresShell) + : DocAccessible(aDocument, aPresShell) {} + +DocAccessibleWrap::~DocAccessibleWrap() {} + +bool DocAccessibleWrap::IsActivated() { + if (nsWindow* window = nsWindow::GetFocusedWindow()) { + if (nsIWidgetListener* listener = window->GetWidgetListener()) { + if (PresShell* presShell = listener->GetPresShell()) { + return presShell == PresShellPtr(); + } + } + } + + return false; +} diff --git a/accessible/atk/DocAccessibleWrap.h b/accessible/atk/DocAccessibleWrap.h new file mode 100644 index 0000000000..883a4b8f0a --- /dev/null +++ b/accessible/atk/DocAccessibleWrap.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +/* For documentation of the accessibility architecture, + * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html + */ + +#ifndef mozilla_a11y_DocAccessibleWrap_h__ +#define mozilla_a11y_DocAccessibleWrap_h__ + +#include "DocAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class DocAccessibleWrap : public DocAccessible { + public: + DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~DocAccessibleWrap(); + + bool IsActivated(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/atk/HyperTextAccessibleWrap.h b/accessible/atk/HyperTextAccessibleWrap.h new file mode 100644 index 0000000000..2c5745a2b3 --- /dev/null +++ b/accessible/atk/HyperTextAccessibleWrap.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_HyperTextAccessibleWrap_h__ +#define mozilla_a11y_HyperTextAccessibleWrap_h__ + +#include "HyperTextAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef class HyperTextAccessible HyperTextAccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/atk/InterfaceInitFuncs.h b/accessible/atk/InterfaceInitFuncs.h new file mode 100644 index 0000000000..43ed8ff4ee --- /dev/null +++ b/accessible/atk/InterfaceInitFuncs.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef ATK_INTERFACE_INIT_FUNCS_H_ +#define ATK_INTERFACE_INIT_FUNCS_H_ + +#include + +namespace mozilla { +namespace a11y { + +class AccessibleWrap; + +} // namespace a11y +} // namespace mozilla + +extern "C" { +void actionInterfaceInitCB(AtkActionIface* aIface); +void componentInterfaceInitCB(AtkComponentIface* aIface); +void documentInterfaceInitCB(AtkDocumentIface* aIface); +void editableTextInterfaceInitCB(AtkEditableTextIface* aIface); +void hyperlinkImplInterfaceInitCB(AtkHyperlinkImplIface* aIface); +void hypertextInterfaceInitCB(AtkHypertextIface* aIface); +void imageInterfaceInitCB(AtkImageIface* aIface); +void selectionInterfaceInitCB(AtkSelectionIface* aIface); +void tableInterfaceInitCB(AtkTableIface* aIface); +void tableCellInterfaceInitCB(AtkTableCellIface* aIface); +void textInterfaceInitCB(AtkTextIface* aIface); +void valueInterfaceInitCB(AtkValueIface* aIface); +} + +/** + * XXX these should live in a file of utils for atk. + */ +AtkObject* refAccessibleAtPointHelper(AtkObject* aAtkObj, gint aX, gint aY, + AtkCoordType aCoordType); +void getExtentsHelper(AtkObject* aAtkObj, gint* aX, gint* aY, gint* aWidth, + gint* aHeight, AtkCoordType aCoordType); + +#endif // ATK_INTERFACE_INIT_FUNCS_H_ diff --git a/accessible/atk/Platform.cpp b/accessible/atk/Platform.cpp new file mode 100644 index 0000000000..e166fcfc32 --- /dev/null +++ b/accessible/atk/Platform.cpp @@ -0,0 +1,271 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "Platform.h" + +#include "nsIAccessibleEvent.h" +#include "nsIGSettingsService.h" +#include "nsMai.h" +#include "nsServiceManagerUtils.h" +#include "prenv.h" +#include "prlink.h" + +#ifdef MOZ_ENABLE_DBUS +# include +#endif +#include + +using namespace mozilla; +using namespace mozilla::a11y; + +int atkMajorVersion = 1, atkMinorVersion = 12, atkMicroVersion = 0; + +GType (*gAtkTableCellGetTypeFunc)(); + +extern "C" { +typedef GType (*AtkGetTypeType)(void); +typedef void (*AtkBridgeAdaptorInit)(int*, char**[]); +} + +static PRLibrary* sATKLib = nullptr; +static const char sATKLibName[] = "libatk-1.0.so.0"; +static const char sATKHyperlinkImplGetTypeSymbol[] = + "atk_hyperlink_impl_get_type"; + +gboolean toplevel_event_watcher(GSignalInvocationHint*, guint, const GValue*, + gpointer); +static bool sToplevel_event_hook_added = false; +static gulong sToplevel_show_hook = 0; +static gulong sToplevel_hide_hook = 0; + +GType g_atk_hyperlink_impl_type = G_TYPE_INVALID; + +struct AtkBridgeModule { + const char* libName; + PRLibrary* lib; + const char* initName; + AtkBridgeAdaptorInit init; +}; + +static AtkBridgeModule sAtkBridge = {"libatk-bridge-2.0.so.0", nullptr, + "atk_bridge_adaptor_init", nullptr}; + +static nsresult LoadGtkModule(AtkBridgeModule& aModule) { + NS_ENSURE_ARG(aModule.libName); + + if (!(aModule.lib = PR_LoadLibrary(aModule.libName))) { + return NS_ERROR_FAILURE; + } + + // we have loaded the library, try to get the function ptrs + if (!(aModule.init = (AtkBridgeAdaptorInit)PR_FindFunctionSymbol( + aModule.lib, aModule.initName))) { + // fail, :( + PR_UnloadLibrary(aModule.lib); + aModule.lib = nullptr; + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +void a11y::PlatformInit() { + if (!ShouldA11yBeEnabled()) return; + + sATKLib = PR_LoadLibrary(sATKLibName); + if (!sATKLib) return; + + AtkGetTypeType pfn_atk_hyperlink_impl_get_type = + (AtkGetTypeType)PR_FindFunctionSymbol(sATKLib, + sATKHyperlinkImplGetTypeSymbol); + if (pfn_atk_hyperlink_impl_get_type) { + g_atk_hyperlink_impl_type = pfn_atk_hyperlink_impl_get_type(); + } + + gAtkTableCellGetTypeFunc = + (GType(*)())PR_FindFunctionSymbol(sATKLib, "atk_table_cell_get_type"); + + const char* (*atkGetVersion)() = + (const char* (*)())PR_FindFunctionSymbol(sATKLib, "atk_get_version"); + if (atkGetVersion) { + const char* version = atkGetVersion(); + if (version) { + char* endPtr = nullptr; + atkMajorVersion = strtol(version, &endPtr, 10); + if (atkMajorVersion != 0L) { + atkMinorVersion = strtol(endPtr + 1, &endPtr, 10); + if (atkMinorVersion != 0L) { + atkMicroVersion = strtol(endPtr + 1, &endPtr, 10); + } + } + } + } + + // Initialize the MAI Utility class, it will overwrite gail_util. + g_type_class_unref(g_type_class_ref(mai_util_get_type())); + + // Init atk-bridge now + PR_SetEnv("NO_AT_BRIDGE=0"); + nsresult rv = LoadGtkModule(sAtkBridge); + if (NS_SUCCEEDED(rv)) { + (*sAtkBridge.init)(nullptr, nullptr); + } + + if (!sToplevel_event_hook_added) { + sToplevel_event_hook_added = true; + sToplevel_show_hook = g_signal_add_emission_hook( + g_signal_lookup("show", GTK_TYPE_WINDOW), 0, toplevel_event_watcher, + reinterpret_cast(nsIAccessibleEvent::EVENT_SHOW), nullptr); + sToplevel_hide_hook = g_signal_add_emission_hook( + g_signal_lookup("hide", GTK_TYPE_WINDOW), 0, toplevel_event_watcher, + reinterpret_cast(nsIAccessibleEvent::EVENT_HIDE), nullptr); + } +} + +void a11y::PlatformShutdown() { + if (sToplevel_event_hook_added) { + sToplevel_event_hook_added = false; + g_signal_remove_emission_hook(g_signal_lookup("show", GTK_TYPE_WINDOW), + sToplevel_show_hook); + g_signal_remove_emission_hook(g_signal_lookup("hide", GTK_TYPE_WINDOW), + sToplevel_hide_hook); + } + + if (sAtkBridge.lib) { + // Do not shutdown/unload atk-bridge, + // an exit function registered will take care of it + // PR_UnloadLibrary(sAtkBridge.lib); + sAtkBridge.lib = nullptr; + sAtkBridge.init = nullptr; + } + // if (sATKLib) { + // PR_UnloadLibrary(sATKLib); + // sATKLib = nullptr; + // } +} + +static const char sAccEnv[] = "GNOME_ACCESSIBILITY"; +#ifdef MOZ_ENABLE_DBUS +static DBusPendingCall* sPendingCall = nullptr; +#endif + +void a11y::PreInit() { +#ifdef MOZ_ENABLE_DBUS + static bool sChecked = FALSE; + if (sChecked) return; + + sChecked = TRUE; + + // dbus is only checked if GNOME_ACCESSIBILITY is unset + // also make sure that a session bus address is available to prevent dbus from + // starting a new one. Dbus confuses the test harness when it creates a new + // process (see bug 693343) + if (PR_GetEnv(sAccEnv) || !PR_GetEnv("DBUS_SESSION_BUS_ADDRESS")) return; + + DBusConnection* bus = dbus_bus_get(DBUS_BUS_SESSION, nullptr); + if (!bus) return; + + dbus_connection_set_exit_on_disconnect(bus, FALSE); + + static const char* iface = "org.a11y.Status"; + static const char* member = "IsEnabled"; + DBusMessage* message; + message = + dbus_message_new_method_call("org.a11y.Bus", "/org/a11y/bus", + "org.freedesktop.DBus.Properties", "Get"); + if (!message) goto dbus_done; + + dbus_message_append_args(message, DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, + &member, DBUS_TYPE_INVALID); + dbus_connection_send_with_reply(bus, message, &sPendingCall, 1000); + dbus_message_unref(message); + +dbus_done: + dbus_connection_unref(bus); +#endif +} + +bool a11y::ShouldA11yBeEnabled() { + static bool sChecked = false, sShouldEnable = false; + if (sChecked) return sShouldEnable; + + sChecked = true; + + EPlatformDisabledState disabledState = PlatformDisabledState(); + if (disabledState == ePlatformIsDisabled) { + return sShouldEnable = false; + } + if (disabledState == ePlatformIsForceEnabled) { + return sShouldEnable = true; + } + + // check if accessibility enabled/disabled by environment variable + const char* envValue = PR_GetEnv(sAccEnv); + if (envValue) return sShouldEnable = !!atoi(envValue); + +#ifdef MOZ_ENABLE_DBUS + PreInit(); + bool dbusSuccess = false; + DBusMessage* reply = nullptr; + if (!sPendingCall) goto dbus_done; + + dbus_pending_call_block(sPendingCall); + reply = dbus_pending_call_steal_reply(sPendingCall); + dbus_pending_call_unref(sPendingCall); + sPendingCall = nullptr; + if (!reply || + dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_METHOD_RETURN || + strcmp(dbus_message_get_signature(reply), DBUS_TYPE_VARIANT_AS_STRING)) { + goto dbus_done; + } + + DBusMessageIter iter, iter_variant, iter_struct; + dbus_bool_t dResult; + dbus_message_iter_init(reply, &iter); + dbus_message_iter_recurse(&iter, &iter_variant); + switch (dbus_message_iter_get_arg_type(&iter_variant)) { + case DBUS_TYPE_STRUCT: + // at-spi2-core 2.2.0-2.2.1 had a bug where it returned a struct + dbus_message_iter_recurse(&iter_variant, &iter_struct); + if (dbus_message_iter_get_arg_type(&iter_struct) == DBUS_TYPE_BOOLEAN) { + dbus_message_iter_get_basic(&iter_struct, &dResult); + sShouldEnable = dResult; + dbusSuccess = true; + } + + break; + case DBUS_TYPE_BOOLEAN: + dbus_message_iter_get_basic(&iter_variant, &dResult); + sShouldEnable = dResult; + dbusSuccess = true; + break; + default: + break; + } + +dbus_done: + if (reply) dbus_message_unref(reply); + + if (dbusSuccess) return sShouldEnable; +#endif + +// check GSettings +#define GSETINGS_A11Y_INTERFACE "org.gnome.desktop.interface" +#define GSETINGS_A11Y_KEY "toolkit-accessibility" + nsCOMPtr gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + nsCOMPtr a11y_settings; + + if (gsettings) { + gsettings->GetCollectionForSchema(nsLiteralCString(GSETINGS_A11Y_INTERFACE), + getter_AddRefs(a11y_settings)); + if (a11y_settings) { + a11y_settings->GetBoolean(nsLiteralCString(GSETINGS_A11Y_KEY), + &sShouldEnable); + } + } + + return sShouldEnable; +} diff --git a/accessible/atk/RootAccessibleWrap.cpp b/accessible/atk/RootAccessibleWrap.cpp new file mode 100644 index 0000000000..7d5534c564 --- /dev/null +++ b/accessible/atk/RootAccessibleWrap.cpp @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "RootAccessibleWrap.h" + +#include "atk/atkobject.h" +#include "nsTArray.h" + +#include + +using namespace mozilla::a11y; + +GtkWindowAccessible::GtkWindowAccessible(AtkObject* aAccessible) + : DummyAccessible() { + g_object_ref(aAccessible); + mAtkObject = aAccessible; +} + +GtkWindowAccessible::~GtkWindowAccessible() { + g_object_unref(mAtkObject); + mAtkObject = nullptr; +} diff --git a/accessible/atk/RootAccessibleWrap.h b/accessible/atk/RootAccessibleWrap.h new file mode 100644 index 0000000000..75038f698d --- /dev/null +++ b/accessible/atk/RootAccessibleWrap.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_RootAccessibleWrap_h__ +#define mozilla_a11y_RootAccessibleWrap_h__ + +#include "BaseAccessibles.h" +#include "RootAccessible.h" + +namespace mozilla { +namespace a11y { + +typedef RootAccessible RootAccessibleWrap; + +/* GtkWindowAccessible is the accessible class for gtk+ native window. + * The instance of GtkWindowAccessible is a child of MaiAppRoot instance. + * It is added into root when the toplevel window is created, and removed + * from root when the toplevel window is destroyed. + */ +class GtkWindowAccessible final : public DummyAccessible { + public: + explicit GtkWindowAccessible(AtkObject* aAccessible); + virtual ~GtkWindowAccessible(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif /* mozilla_a11y_Root_Accessible_Wrap_h__ */ diff --git a/accessible/atk/UtilInterface.cpp b/accessible/atk/UtilInterface.cpp new file mode 100644 index 0000000000..8389e09f80 --- /dev/null +++ b/accessible/atk/UtilInterface.cpp @@ -0,0 +1,347 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "ApplicationAccessible.h" +#include "mozilla/Likely.h" +#include "nsAccessibilityService.h" +#include "nsMai.h" + +#include +#include +#include +#include + +using namespace mozilla; +using namespace mozilla::a11y; + +typedef AtkUtil MaiUtil; +typedef AtkUtilClass MaiUtilClass; + +#define MAI_VERSION MOZILLA_VERSION +#define MAI_NAME "Gecko" + +extern "C" { +static guint (*gail_add_global_event_listener)(GSignalEmissionHook listener, + const gchar* event_type); +static void (*gail_remove_global_event_listener)(guint remove_listener); +static void (*gail_remove_key_event_listener)(guint remove_listener); +static AtkObject* (*gail_get_root)(); +} + +struct MaiUtilListenerInfo { + gint key; + guint signal_id; + gulong hook_id; + // For window create/destory/minimize/maximize/restore/activate/deactivate + // events, we'll chain gail_util's add/remove_global_event_listener. + // So we store the listenerid returned by gail's add_global_event_listener + // in this structure to call gail's remove_global_event_listener later. + guint gail_listenerid; +}; + +static GHashTable* sListener_list = nullptr; +static gint sListener_idx = 1; + +extern "C" { +static guint add_listener(GSignalEmissionHook listener, + const gchar* object_type, const gchar* signal, + const gchar* hook_data, guint gail_listenerid = 0) { + GType type; + guint signal_id; + gint rc = 0; + + type = g_type_from_name(object_type); + if (type) { + signal_id = g_signal_lookup(signal, type); + if (signal_id > 0) { + MaiUtilListenerInfo* listener_info; + + rc = sListener_idx; + + listener_info = + (MaiUtilListenerInfo*)g_malloc(sizeof(MaiUtilListenerInfo)); + listener_info->key = sListener_idx; + listener_info->hook_id = g_signal_add_emission_hook( + signal_id, 0, listener, g_strdup(hook_data), (GDestroyNotify)g_free); + listener_info->signal_id = signal_id; + listener_info->gail_listenerid = gail_listenerid; + + g_hash_table_insert(sListener_list, &(listener_info->key), listener_info); + sListener_idx++; + } else { + g_warning("Invalid signal type %s\n", signal); + } + } else { + g_warning("Invalid object type %s\n", object_type); + } + return rc; +} + +static guint mai_util_add_global_event_listener(GSignalEmissionHook listener, + const gchar* event_type) { + guint rc = 0; + gchar** split_string; + + split_string = g_strsplit(event_type, ":", 3); + + if (split_string) { + if (!strcmp("window", split_string[0])) { + guint gail_listenerid = 0; + if (gail_add_global_event_listener) { + // call gail's function to track gtk native window events + gail_listenerid = gail_add_global_event_listener(listener, event_type); + } + + rc = add_listener(listener, "MaiAtkObject", split_string[1], event_type, + gail_listenerid); + } else { + rc = add_listener(listener, split_string[1], split_string[2], event_type); + } + g_strfreev(split_string); + } + return rc; +} + +static void mai_util_remove_global_event_listener(guint remove_listener) { + if (remove_listener > 0) { + MaiUtilListenerInfo* listener_info; + gint tmp_idx = remove_listener; + + listener_info = + (MaiUtilListenerInfo*)g_hash_table_lookup(sListener_list, &tmp_idx); + + if (listener_info != nullptr) { + if (gail_remove_global_event_listener && listener_info->gail_listenerid) { + gail_remove_global_event_listener(listener_info->gail_listenerid); + } + + /* Hook id of 0 and signal id of 0 are invalid */ + if (listener_info->hook_id != 0 && listener_info->signal_id != 0) { + /* Remove the emission hook */ + g_signal_remove_emission_hook(listener_info->signal_id, + listener_info->hook_id); + + /* Remove the element from the hash */ + g_hash_table_remove(sListener_list, &tmp_idx); + } else { + g_warning("Invalid listener hook_id %ld or signal_id %d\n", + listener_info->hook_id, listener_info->signal_id); + } + } else { + // atk-bridge is initialized with gail (e.g. yelp) + // try gail_remove_global_event_listener + if (gail_remove_global_event_listener) { + return gail_remove_global_event_listener(remove_listener); + } + + g_warning("No listener with the specified listener id %d", + remove_listener); + } + } else { + g_warning("Invalid listener_id %d", remove_listener); + } +} + +static AtkKeyEventStruct* atk_key_event_from_gdk_event_key(GdkEventKey* key) { + AtkKeyEventStruct* event = g_new0(AtkKeyEventStruct, 1); + switch (key->type) { + case GDK_KEY_PRESS: + event->type = ATK_KEY_EVENT_PRESS; + break; + case GDK_KEY_RELEASE: + event->type = ATK_KEY_EVENT_RELEASE; + break; + default: + g_assert_not_reached(); + return nullptr; + } + event->state = key->state; + event->keyval = key->keyval; + event->length = key->length; + if (key->string && key->string[0] && + g_unichar_isgraph(g_utf8_get_char(key->string))) { + event->string = key->string; + } else if (key->type == GDK_KEY_PRESS || key->type == GDK_KEY_RELEASE) { + event->string = gdk_keyval_name(key->keyval); + } + event->keycode = key->hardware_keycode; + event->timestamp = key->time; + + return event; +} + +struct MaiKeyEventInfo { + AtkKeyEventStruct* key_event; + gpointer func_data; +}; + +union AtkKeySnoopFuncPointer { + AtkKeySnoopFunc func_ptr; + gpointer data; +}; + +static gboolean notify_hf(gpointer key, gpointer value, gpointer data) { + MaiKeyEventInfo* info = (MaiKeyEventInfo*)data; + AtkKeySnoopFuncPointer atkKeySnoop; + atkKeySnoop.data = value; + return (atkKeySnoop.func_ptr)(info->key_event, info->func_data) ? TRUE + : FALSE; +} + +static void insert_hf(gpointer key, gpointer value, gpointer data) { + GHashTable* new_table = (GHashTable*)data; + g_hash_table_insert(new_table, key, value); +} + +static GHashTable* sKey_listener_list = nullptr; + +static gint mai_key_snooper(GtkWidget* the_widget, GdkEventKey* event, + gpointer func_data) { + /* notify each AtkKeySnoopFunc in turn... */ + + MaiKeyEventInfo* info = g_new0(MaiKeyEventInfo, 1); + gint consumed = 0; + if (sKey_listener_list) { + GHashTable* new_hash = g_hash_table_new(nullptr, nullptr); + g_hash_table_foreach(sKey_listener_list, insert_hf, new_hash); + info->key_event = atk_key_event_from_gdk_event_key(event); + info->func_data = func_data; + consumed = g_hash_table_foreach_steal(new_hash, notify_hf, info); + g_hash_table_destroy(new_hash); + g_free(info->key_event); + } + g_free(info); + return (consumed ? 1 : 0); +} + +static guint sKey_snooper_id = 0; + +static guint mai_util_add_key_event_listener(AtkKeySnoopFunc listener, + gpointer data) { + if (MOZ_UNLIKELY(!listener)) { + return 0; + } + + static guint key = 0; + + if (!sKey_listener_list) { + sKey_listener_list = g_hash_table_new(nullptr, nullptr); + } + + // If we have no registered event listeners then we need to (re)install the + // key event snooper. + if (g_hash_table_size(sKey_listener_list) == 0) { + sKey_snooper_id = gtk_key_snooper_install(mai_key_snooper, data); + } + + AtkKeySnoopFuncPointer atkKeySnoop; + atkKeySnoop.func_ptr = listener; + key++; + g_hash_table_insert(sKey_listener_list, GUINT_TO_POINTER(key), + atkKeySnoop.data); + return key; +} + +static void mai_util_remove_key_event_listener(guint remove_listener) { + if (!sKey_listener_list) { + // atk-bridge is initialized with gail (e.g. yelp) + // try gail_remove_key_event_listener + return gail_remove_key_event_listener(remove_listener); + } + + g_hash_table_remove(sKey_listener_list, GUINT_TO_POINTER(remove_listener)); + if (g_hash_table_size(sKey_listener_list) == 0) { + gtk_key_snooper_remove(sKey_snooper_id); + } +} + +static AtkObject* mai_util_get_root() { + ApplicationAccessible* app = ApplicationAcc(); + if (app) return app->GetAtkObject(); + + // We've shutdown, try to use gail instead + // (to avoid assert in spi_atk_tidy_windows()) + // XXX tbsaunde then why didn't we replace the gail atk_util impl? + if (gail_get_root) return gail_get_root(); + + return nullptr; +} + +static const gchar* mai_util_get_toolkit_name() { return MAI_NAME; } + +static const gchar* mai_util_get_toolkit_version() { return MAI_VERSION; } + +static void _listener_info_destroy(gpointer data) { g_free(data); } + +static void window_added(AtkObject* atk_obj, guint index, AtkObject* child) { + if (!IS_MAI_OBJECT(child)) return; + + static guint id = g_signal_lookup("create", MAI_TYPE_ATK_OBJECT); + g_signal_emit(child, id, 0); +} + +static void window_removed(AtkObject* atk_obj, guint index, AtkObject* child) { + if (!IS_MAI_OBJECT(child)) return; + + static guint id = g_signal_lookup("destroy", MAI_TYPE_ATK_OBJECT); + g_signal_emit(child, id, 0); +} + +static void UtilInterfaceInit(MaiUtilClass* klass) { + AtkUtilClass* atk_class; + gpointer data; + + data = g_type_class_peek(ATK_TYPE_UTIL); + atk_class = ATK_UTIL_CLASS(data); + + // save gail function pointer + gail_add_global_event_listener = atk_class->add_global_event_listener; + gail_remove_global_event_listener = atk_class->remove_global_event_listener; + gail_remove_key_event_listener = atk_class->remove_key_event_listener; + gail_get_root = atk_class->get_root; + + atk_class->add_global_event_listener = mai_util_add_global_event_listener; + atk_class->remove_global_event_listener = + mai_util_remove_global_event_listener; + atk_class->add_key_event_listener = mai_util_add_key_event_listener; + atk_class->remove_key_event_listener = mai_util_remove_key_event_listener; + atk_class->get_root = mai_util_get_root; + atk_class->get_toolkit_name = mai_util_get_toolkit_name; + atk_class->get_toolkit_version = mai_util_get_toolkit_version; + + sListener_list = g_hash_table_new_full(g_int_hash, g_int_equal, nullptr, + _listener_info_destroy); + // Keep track of added/removed windows. + AtkObject* root = atk_get_root(); + g_signal_connect(root, "children-changed::add", (GCallback)window_added, + nullptr); + g_signal_connect(root, "children-changed::remove", (GCallback)window_removed, + nullptr); +} +} + +GType mai_util_get_type() { + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof(MaiUtilClass), + (GBaseInitFunc) nullptr, /* base init */ + (GBaseFinalizeFunc) nullptr, /* base finalize */ + (GClassInitFunc)UtilInterfaceInit, /* class init */ + (GClassFinalizeFunc) nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof(MaiUtil), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) nullptr, /* instance init */ + nullptr /* value table */ + }; + + type = + g_type_register_static(ATK_TYPE_UTIL, "MaiUtil", &tinfo, GTypeFlags(0)); + } + return type; +} diff --git a/accessible/atk/moz.build b/accessible/atk/moz.build new file mode 100644 index 0000000000..57b86622b6 --- /dev/null +++ b/accessible/atk/moz.build @@ -0,0 +1,66 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.a11y += [ + "AccessibleWrap.h", + "HyperTextAccessibleWrap.h", +] + +SOURCES += [ + "AccessibleWrap.cpp", + "ApplicationAccessibleWrap.cpp", + "DocAccessibleWrap.cpp", + "DOMtoATK.cpp", + "nsMaiHyperlink.cpp", + "nsMaiInterfaceAction.cpp", + "nsMaiInterfaceComponent.cpp", + "nsMaiInterfaceDocument.cpp", + "nsMaiInterfaceEditableText.cpp", + "nsMaiInterfaceHyperlinkImpl.cpp", + "nsMaiInterfaceHypertext.cpp", + "nsMaiInterfaceImage.cpp", + "nsMaiInterfaceSelection.cpp", + "nsMaiInterfaceTable.cpp", + "nsMaiInterfaceTableCell.cpp", + "nsMaiInterfaceText.cpp", + "nsMaiInterfaceValue.cpp", + "Platform.cpp", + "RootAccessibleWrap.cpp", + "UtilInterface.cpp", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", + "/accessible/html", + "/accessible/ipc", + "/accessible/ipc/other", + "/accessible/xpcom", + "/accessible/xul", + "/layout/generic", + "/other-licenses/atk-1.0", + "/widget", + "/widget/gtk", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +if CONFIG["MOZ_ENABLE_DBUS"]: + CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"] + +include("/ipc/chromium/chromium-config.mozbuild") + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + # Used in G_DEFINE_TYPE_EXTENDED macro, probably fixed in newer glib / + # gobject headers. See bug 1243331 comment 3. + CXXFLAGS += [ + "-Wno-error=unused-function", + "-Wno-unused-local-typedefs", + ] diff --git a/accessible/atk/nsMai.h b/accessible/atk/nsMai.h new file mode 100644 index 0000000000..175a3c64ff --- /dev/null +++ b/accessible/atk/nsMai.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef __NS_MAI_H__ +#define __NS_MAI_H__ + +#include +#include +#include + +#include "AccessibleWrap.h" + +namespace mozilla { +namespace a11y { +class RemoteAccessible; +class Accessible; +} // namespace a11y +} // namespace mozilla + +#define MAI_TYPE_ATK_OBJECT (mai_atk_object_get_type()) +#define MAI_ATK_OBJECT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), MAI_TYPE_ATK_OBJECT, MaiAtkObject)) +#define MAI_ATK_OBJECT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), MAI_TYPE_ATK_OBJECT, MaiAtkObjectClass)) +#define IS_MAI_OBJECT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), MAI_TYPE_ATK_OBJECT)) +#define IS_MAI_OBJECT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), MAI_TYPE_ATK_OBJECT)) +#define MAI_ATK_OBJECT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), MAI_TYPE_ATK_OBJECT, MaiAtkObjectClass)) +GType mai_atk_object_get_type(void); +GType mai_util_get_type(); + +// This is a pointer to the atk_table_cell_get_type function if we are using +// a version of atk that defines that. +extern "C" GType (*gAtkTableCellGetTypeFunc)(); + +mozilla::a11y::AccessibleWrap* GetAccessibleWrap(AtkObject* aAtkObj); +mozilla::a11y::RemoteAccessible* GetProxy(AtkObject* aAtkObj); +mozilla::a11y::Accessible* GetInternalObj(AtkObject* aObj); +AtkObject* GetWrapperFor(mozilla::a11y::Accessible* acc); + +extern int atkMajorVersion, atkMinorVersion, atkMicroVersion; + +/** + * Return true if the loaded version of libatk-1.0.so is at least + * aMajor.aMinor.aMicro. + */ +static inline bool IsAtkVersionAtLeast(int aMajor, int aMinor, int aMicro = 0) { + return aMajor < atkMajorVersion || + (aMajor == atkMajorVersion && + (aMinor < atkMinorVersion || + (aMinor == atkMinorVersion && aMicro <= atkMicroVersion))); +} + +/** + * This MaiAtkObject is a thin wrapper, in the MAI namespace, for AtkObject + */ +struct MaiAtkObject { + AtkObject parent; + /* + * The AccessibleWrap whose properties and features are exported + * via this object instance. + */ + mozilla::a11y::Accessible* acc; + + /* + * Get the AtkHyperlink for this atk object. + */ + AtkHyperlink* GetAtkHyperlink(); + + /* + * Shutdown this AtkObject. + */ + void Shutdown(); + + /* + * Notify atk of a state change on this AtkObject. + */ + void FireStateChangeEvent(uint64_t aState, bool aEnabled); + + /* + * Notify ATK of a text change within this ATK object. + */ + void FireTextChangeEvent(const nsAString& aStr, int32_t aStart, uint32_t aLen, + bool aIsInsert, bool aIsFromUser); + + /** + * Notify ATK of a shown or hidden subtree rooted at aObject whose parent is + * aParent + */ + void FireAtkShowHideEvent(AtkObject* aParent, bool aIsAdded, bool aFromUser); + + private: + /* + * do we have text-remove and text-insert signals if not we need to use + * text-changed see AccessibleWrap::FireAtkTextChangedEvent() and + * bug 619002 + */ + enum EAvailableAtkSignals { + eUnknown, + eHaveNewAtkTextSignals, + eNoNewAtkSignals + }; + + static EAvailableAtkSignals gAvailableAtkSignals; +}; + +#endif /* __NS_MAI_H__ */ diff --git a/accessible/atk/nsMaiHyperlink.cpp b/accessible/atk/nsMaiHyperlink.cpp new file mode 100644 index 0000000000..65f223e528 --- /dev/null +++ b/accessible/atk/nsMaiHyperlink.cpp @@ -0,0 +1,216 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "nsIURI.h" +#include "nsMaiHyperlink.h" +#include "mozilla/a11y/RemoteAccessible.h" + +using namespace mozilla::a11y; + +/* MaiAtkHyperlink */ + +#define MAI_TYPE_ATK_HYPERLINK (mai_atk_hyperlink_get_type()) +#define MAI_ATK_HYPERLINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), MAI_TYPE_ATK_HYPERLINK, MaiAtkHyperlink)) +#define MAI_ATK_HYPERLINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), MAI_TYPE_ATK_HYPERLINK, \ + MaiAtkHyperlinkClass)) +#define MAI_IS_ATK_HYPERLINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), MAI_TYPE_ATK_HYPERLINK)) +#define MAI_IS_ATK_HYPERLINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), MAI_TYPE_ATK_HYPERLINK)) +#define MAI_ATK_HYPERLINK_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), MAI_TYPE_ATK_HYPERLINK, \ + MaiAtkHyperlinkClass)) + +/** + * This MaiAtkHyperlink is a thin wrapper, in the MAI namespace, + * for AtkHyperlink + */ + +struct MaiAtkHyperlink { + AtkHyperlink parent; + + /* + * The MaiHyperlink whose properties and features are exported via this + * hyperlink instance. + */ + MaiHyperlink* maiHyperlink; +}; + +struct MaiAtkHyperlinkClass { + AtkHyperlinkClass parent_class; +}; + +GType mai_atk_hyperlink_get_type(void); + +G_BEGIN_DECLS +/* callbacks for AtkHyperlink */ +static void classInitCB(AtkHyperlinkClass* aClass); +static void finalizeCB(GObject* aObj); + +/* callbacks for AtkHyperlink virtual functions */ +static gchar* getUriCB(AtkHyperlink* aLink, gint aLinkIndex); +static AtkObject* getObjectCB(AtkHyperlink* aLink, gint aLinkIndex); +static gint getEndIndexCB(AtkHyperlink* aLink); +static gint getStartIndexCB(AtkHyperlink* aLink); +static gboolean isValidCB(AtkHyperlink* aLink); +static gint getAnchorCountCB(AtkHyperlink* aLink); +G_END_DECLS + +static gpointer parent_class = nullptr; + +static MaiHyperlink* GetMaiHyperlink(AtkHyperlink* aHyperlink) { + NS_ENSURE_TRUE(MAI_IS_ATK_HYPERLINK(aHyperlink), nullptr); + MaiHyperlink* maiHyperlink = MAI_ATK_HYPERLINK(aHyperlink)->maiHyperlink; + NS_ENSURE_TRUE(maiHyperlink != nullptr, nullptr); + NS_ENSURE_TRUE(maiHyperlink->GetAtkHyperlink() == aHyperlink, nullptr); + return maiHyperlink; +} + +GType mai_atk_hyperlink_get_type(void) { + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof(MaiAtkHyperlinkClass), + (GBaseInitFunc) nullptr, + (GBaseFinalizeFunc) nullptr, + (GClassInitFunc)classInitCB, + (GClassFinalizeFunc) nullptr, + nullptr, /* class data */ + sizeof(MaiAtkHyperlink), /* instance size */ + 0, /* nb preallocs */ + (GInstanceInitFunc) nullptr, + nullptr /* value table */ + }; + + type = g_type_register_static(ATK_TYPE_HYPERLINK, "MaiAtkHyperlink", &tinfo, + GTypeFlags(0)); + } + return type; +} + +MaiHyperlink::MaiHyperlink(Accessible* aHyperLink) + : mHyperlink(aHyperLink), mMaiAtkHyperlink(nullptr) { + mMaiAtkHyperlink = reinterpret_cast( + g_object_new(mai_atk_hyperlink_get_type(), nullptr)); + NS_ASSERTION(mMaiAtkHyperlink, "OUT OF MEMORY"); + if (!mMaiAtkHyperlink) return; + + MAI_ATK_HYPERLINK(mMaiAtkHyperlink)->maiHyperlink = this; +} + +MaiHyperlink::~MaiHyperlink() { + if (mMaiAtkHyperlink) { + MAI_ATK_HYPERLINK(mMaiAtkHyperlink)->maiHyperlink = nullptr; + g_object_unref(mMaiAtkHyperlink); + } +} + +/* static functions for ATK callbacks */ + +void classInitCB(AtkHyperlinkClass* aClass) { + GObjectClass* gobject_class = G_OBJECT_CLASS(aClass); + + parent_class = g_type_class_peek_parent(aClass); + + aClass->get_uri = getUriCB; + aClass->get_object = getObjectCB; + aClass->get_end_index = getEndIndexCB; + aClass->get_start_index = getStartIndexCB; + aClass->is_valid = isValidCB; + aClass->get_n_anchors = getAnchorCountCB; + + gobject_class->finalize = finalizeCB; +} + +void finalizeCB(GObject* aObj) { + NS_ASSERTION(MAI_IS_ATK_HYPERLINK(aObj), "Invalid MaiAtkHyperlink"); + if (!MAI_IS_ATK_HYPERLINK(aObj)) return; + + MaiAtkHyperlink* maiAtkHyperlink = MAI_ATK_HYPERLINK(aObj); + maiAtkHyperlink->maiHyperlink = nullptr; + + /* call parent finalize function */ + if (G_OBJECT_CLASS(parent_class)->finalize) { + G_OBJECT_CLASS(parent_class)->finalize(aObj); + } +} + +gchar* getUriCB(AtkHyperlink* aLink, gint aLinkIndex) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) { + return nullptr; + } + + Accessible* acc = maiLink->Acc(); + if (!acc) { + return nullptr; + } + + nsAutoCString cautoStr; + nsCOMPtr uri = acc->AnchorURIAt(aLinkIndex); + if (!uri) return nullptr; + + nsresult rv = uri->GetSpec(cautoStr); + NS_ENSURE_SUCCESS(rv, nullptr); + + return g_strdup(cautoStr.get()); +} + +AtkObject* getObjectCB(AtkHyperlink* aLink, gint aLinkIndex) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) { + return nullptr; + } + + Accessible* acc = maiLink->Acc(); + if (!acc) { + return nullptr; + } + + Accessible* anchor = acc->AnchorAt(aLinkIndex); + return anchor ? GetWrapperFor(anchor) : nullptr; +} + +gint getEndIndexCB(AtkHyperlink* aLink) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) return false; + + return static_cast(maiLink->Acc()->EndOffset()); +} + +gint getStartIndexCB(AtkHyperlink* aLink) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) return -1; + + return static_cast(maiLink->Acc()->StartOffset()); +} + +gboolean isValidCB(AtkHyperlink* aLink) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) return false; + + Accessible* acc = maiLink->Acc(); + if (!acc) { + return false; + } + + return static_cast(acc->IsLinkValid()); +} + +gint getAnchorCountCB(AtkHyperlink* aLink) { + MaiHyperlink* maiLink = GetMaiHyperlink(aLink); + if (!maiLink) return -1; + + Accessible* acc = maiLink->Acc(); + if (!acc) { + return -1; + } + + return static_cast(acc->AnchorCount()); +} diff --git a/accessible/atk/nsMaiHyperlink.h b/accessible/atk/nsMaiHyperlink.h new file mode 100644 index 0000000000..34f517cc7a --- /dev/null +++ b/accessible/atk/nsMaiHyperlink.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef __MAI_HYPERLINK_H__ +#define __MAI_HYPERLINK_H__ + +#include "nsMai.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/LocalAccessible.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "nsDebug.h" + +struct _AtkHyperlink; +typedef struct _AtkHyperlink AtkHyperlink; + +namespace mozilla { +namespace a11y { + +/* + * MaiHyperlink is a auxiliary class for MaiInterfaceHyperText. + */ + +class MaiHyperlink { + public: + explicit MaiHyperlink(Accessible* aHyperLink); + ~MaiHyperlink(); + + public: + AtkHyperlink* GetAtkHyperlink() const { return mMaiAtkHyperlink; } + Accessible* Acc() { + if (!mHyperlink) { + return nullptr; + } + NS_ASSERTION(mHyperlink->IsLink(), "Why isn't it a link!"); + return mHyperlink; + } + + protected: + Accessible* mHyperlink; + AtkHyperlink* mMaiAtkHyperlink; +}; + +} // namespace a11y +} // namespace mozilla + +#endif /* __MAI_HYPERLINK_H__ */ diff --git a/accessible/atk/nsMaiInterfaceAction.cpp b/accessible/atk/nsMaiInterfaceAction.cpp new file mode 100644 index 0000000000..8149e0aff5 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceAction.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "nsMai.h" +#include "mozilla/Likely.h" +#include "nsAccessibilityService.h" +#include "RemoteAccessible.h" +#include "nsString.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { + +static gboolean doActionCB(AtkAction* aAction, gint aActionIndex) { + AtkObject* atkObject = ATK_OBJECT(aAction); + if (Accessible* acc = GetInternalObj(atkObject)) { + return acc->DoAction(aActionIndex); + } + + return false; +} + +static gint getActionCountCB(AtkAction* aAction) { + AtkObject* atkObject = ATK_OBJECT(aAction); + if (Accessible* acc = GetInternalObj(atkObject)) { + return acc->ActionCount(); + } + + return 0; +} + +static const gchar* getActionDescriptionCB(AtkAction* aAction, + gint aActionIndex) { + AtkObject* atkObject = ATK_OBJECT(aAction); + nsAutoString description; + if (Accessible* acc = GetInternalObj(atkObject)) { + acc->ActionDescriptionAt(aActionIndex, description); + return AccessibleWrap::ReturnString(description); + } + + return nullptr; +} + +static const gchar* getActionNameCB(AtkAction* aAction, gint aActionIndex) { + AtkObject* atkObject = ATK_OBJECT(aAction); + nsAutoString autoStr; + if (Accessible* acc = GetInternalObj(atkObject)) { + acc->ActionNameAt(aActionIndex, autoStr); + return AccessibleWrap::ReturnString(autoStr); + } + + return nullptr; +} + +static const gchar* getKeyBindingCB(AtkAction* aAction, gint aActionIndex) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aAction)); + if (!acc) { + return nullptr; + } + nsAutoString keyBindingsStr; + AccessibleWrap::GetKeyBinding(acc, keyBindingsStr); + + return AccessibleWrap::ReturnString(keyBindingsStr); +} +} + +void actionInterfaceInitCB(AtkActionIface* aIface) { + NS_ASSERTION(aIface, "Invalid aIface"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->do_action = doActionCB; + aIface->get_n_actions = getActionCountCB; + aIface->get_description = getActionDescriptionCB; + aIface->get_keybinding = getKeyBindingCB; + aIface->get_name = getActionNameCB; +} diff --git a/accessible/atk/nsMaiInterfaceComponent.cpp b/accessible/atk/nsMaiInterfaceComponent.cpp new file mode 100644 index 0000000000..c2da38fa22 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceComponent.cpp @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "AccessibleWrap.h" +#include "nsAccUtils.h" +#include "nsMai.h" +#include "mozilla/Likely.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsAccessibilityService.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { + +static AtkObject* refAccessibleAtPointCB(AtkComponent* aComponent, gint aAccX, + gint aAccY, AtkCoordType aCoordType) { + return refAccessibleAtPointHelper(ATK_OBJECT(aComponent), aAccX, aAccY, + aCoordType); +} + +static void getExtentsCB(AtkComponent* aComponent, gint* aX, gint* aY, + gint* aWidth, gint* aHeight, AtkCoordType aCoordType) { + getExtentsHelper(ATK_OBJECT(aComponent), aX, aY, aWidth, aHeight, aCoordType); +} + +static gboolean grabFocusCB(AtkComponent* aComponent) { + AtkObject* atkObject = ATK_OBJECT(aComponent); + Accessible* acc = GetInternalObj(atkObject); + if (acc) { + acc->TakeFocus(); + return TRUE; + } + return FALSE; +} + +// ScrollType is compatible +MOZ_CAN_RUN_SCRIPT_BOUNDARY +static gboolean scrollToCB(AtkComponent* aComponent, AtkScrollType type) { + AtkObject* atkObject = ATK_OBJECT(aComponent); + if (Accessible* acc = GetInternalObj(atkObject)) { + acc->ScrollTo(type); + return TRUE; + } + + return FALSE; +} + +// CoordType is compatible +static gboolean scrollToPointCB(AtkComponent* aComponent, AtkCoordType coords, + gint x, gint y) { + AtkObject* atkObject = ATK_OBJECT(aComponent); + AccessibleWrap* accWrap = GetAccessibleWrap(atkObject); + if (accWrap) { + accWrap->ScrollToPoint(coords, x, y); + return TRUE; + } + + RemoteAccessible* proxy = GetProxy(atkObject); + if (proxy) { + proxy->ScrollToPoint(coords, x, y); + return TRUE; + } + + return FALSE; +} +} + +AtkObject* refAccessibleAtPointHelper(AtkObject* aAtkObj, gint aX, gint aY, + AtkCoordType aCoordType) { + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return nullptr; + } + + // Accessible::ChildAtPoint(x,y) is in screen pixels. + if (aCoordType == ATK_XY_WINDOW) { + mozilla::LayoutDeviceIntPoint winCoords = + nsAccUtils::GetScreenCoordsForWindow(acc); + aX += winCoords.x; + aY += winCoords.y; + } + + Accessible* accAtPoint = + acc->ChildAtPoint(aX, aY, Accessible::EWhichChildAtPoint::DeepestChild); + if (!accAtPoint) { + return nullptr; + } + roles::Role role = accAtPoint->Role(); + if (role == roles::TEXT_LEAF || role == roles::STATICTEXT) { + // We don't include text leaf nodes in the ATK tree, so return the parent. + accAtPoint = accAtPoint->Parent(); + MOZ_ASSERT(accAtPoint, "Text leaf should always have a parent"); + } + AtkObject* atkObj = GetWrapperFor(accAtPoint); + if (atkObj) { + g_object_ref(atkObj); + } + return atkObj; +} + +void getExtentsHelper(AtkObject* aAtkObj, gint* aX, gint* aY, gint* aWidth, + gint* aHeight, AtkCoordType aCoordType) { + *aX = *aY = *aWidth = *aHeight = -1; + + Accessible* acc = GetInternalObj(aAtkObj); + if (!acc) { + return; + } + + mozilla::LayoutDeviceIntRect screenRect = acc->Bounds(); + if (screenRect.IsEmpty()) { + return; + } + + if (aCoordType == ATK_XY_WINDOW) { + mozilla::LayoutDeviceIntPoint winCoords = + nsAccUtils::GetScreenCoordsForWindow(acc); + screenRect.x -= winCoords.x; + screenRect.y -= winCoords.y; + } + + *aX = screenRect.x; + *aY = screenRect.y; + *aWidth = screenRect.width; + *aHeight = screenRect.height; +} + +void componentInterfaceInitCB(AtkComponentIface* aIface) { + NS_ASSERTION(aIface, "Invalid Interface"); + if (MOZ_UNLIKELY(!aIface)) return; + + /* + * Use default implementation in atk for contains, get_position, + * and get_size + */ + aIface->ref_accessible_at_point = refAccessibleAtPointCB; + aIface->get_extents = getExtentsCB; + aIface->grab_focus = grabFocusCB; + if (IsAtkVersionAtLeast(2, 30)) { + aIface->scroll_to = scrollToCB; + aIface->scroll_to_point = scrollToPointCB; + } +} diff --git a/accessible/atk/nsMaiInterfaceDocument.cpp b/accessible/atk/nsMaiInterfaceDocument.cpp new file mode 100644 index 0000000000..da1bffce37 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceDocument.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "AccessibleWrap.h" +#include "DocAccessible.h" +#include "nsAccUtils.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/Likely.h" + +using namespace mozilla::a11y; + +static const char* const kDocUrlName = "DocURL"; +static const char* const kMimeTypeName = "MimeType"; + +// below functions are vfuncs on an ATK interface so they need to be C call +extern "C" { + +static const gchar* getDocumentLocaleCB(AtkDocument* aDocument); +static AtkAttributeSet* getDocumentAttributesCB(AtkDocument* aDocument); +static const gchar* getDocumentAttributeValueCB(AtkDocument* aDocument, + const gchar* aAttrName); + +void documentInterfaceInitCB(AtkDocumentIface* aIface) { + NS_ASSERTION(aIface, "Invalid Interface"); + if (MOZ_UNLIKELY(!aIface)) return; + + /* + * We don't support get_document or set_attribute right now. + */ + aIface->get_document_attributes = getDocumentAttributesCB; + aIface->get_document_attribute_value = getDocumentAttributeValueCB; + aIface->get_document_locale = getDocumentLocaleCB; +} + +const gchar* getDocumentLocaleCB(AtkDocument* aDocument) { + nsAutoString locale; + Accessible* acc = GetInternalObj(ATK_OBJECT(aDocument)); + if (acc) { + acc->Language(locale); + } + + return locale.IsEmpty() ? nullptr : AccessibleWrap::ReturnString(locale); +} + +static inline GSList* prependToList(GSList* aList, const char* const aName, + const nsAutoString& aValue) { + if (aValue.IsEmpty()) { + return aList; + } + + // libspi will free these + AtkAttribute* atkAttr = (AtkAttribute*)g_malloc(sizeof(AtkAttribute)); + atkAttr->name = g_strdup(aName); + atkAttr->value = g_strdup(NS_ConvertUTF16toUTF8(aValue).get()); + return g_slist_prepend(aList, atkAttr); +} + +AtkAttributeSet* getDocumentAttributesCB(AtkDocument* aDocument) { + nsAutoString url; + nsAutoString mimeType; + Accessible* acc = GetInternalObj(ATK_OBJECT(aDocument)); + + if (!acc || !acc->IsDoc()) { + return nullptr; + } + + nsAccUtils::DocumentURL(acc, url); + nsAccUtils::DocumentMimeType(acc, mimeType); + + // according to atkobject.h, AtkAttributeSet is a GSList + GSList* attributes = nullptr; + attributes = prependToList(attributes, kDocUrlName, url); + attributes = prependToList(attributes, kMimeTypeName, mimeType); + + return attributes; +} + +const gchar* getDocumentAttributeValueCB(AtkDocument* aDocument, + const gchar* aAttrName) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aDocument)); + + if (!acc || !acc->IsDoc()) { + return nullptr; + } + + nsAutoString attrValue; + if (!strcasecmp(aAttrName, kDocUrlName)) { + nsAccUtils::DocumentURL(acc, attrValue); + } else if (!strcasecmp(aAttrName, kMimeTypeName)) { + nsAccUtils::DocumentMimeType(acc, attrValue); + } else { + return nullptr; + } + + return attrValue.IsEmpty() ? nullptr + : AccessibleWrap::ReturnString(attrValue); +} +} diff --git a/accessible/atk/nsMaiInterfaceEditableText.cpp b/accessible/atk/nsMaiInterfaceEditableText.cpp new file mode 100644 index 0000000000..2dc692362e --- /dev/null +++ b/accessible/atk/nsMaiInterfaceEditableText.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "HyperTextAccessible-inl.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "nsString.h" +#include "mozilla/Likely.h" + +using namespace mozilla::a11y; + +extern "C" { +static void setTextContentsCB(AtkEditableText* aText, const gchar* aString) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + NS_ConvertUTF8toUTF16 strContent(aString); + text->ReplaceText(strContent); + } + } +} + +static void insertTextCB(AtkEditableText* aText, const gchar* aString, + gint aLength, gint* aPosition) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + NS_ConvertUTF8toUTF16 strContent(aString); + text->InsertText(strContent, *aPosition); + } + } +} + +static void copyTextCB(AtkEditableText* aText, gint aStartPos, gint aEndPos) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + text->CopyText(aStartPos, aEndPos); + } + } +} + +static void cutTextCB(AtkEditableText* aText, gint aStartPos, gint aEndPos) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + text->CutText(aStartPos, aEndPos); + } + } +} + +static void deleteTextCB(AtkEditableText* aText, gint aStartPos, gint aEndPos) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + text->DeleteText(aStartPos, aEndPos); + } + } +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY +static void pasteTextCB(AtkEditableText* aText, gint aPosition) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (acc->IsTextRole()) { + return; + } + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + text->PasteText(aPosition); + } + } +} +} + +void editableTextInterfaceInitCB(AtkEditableTextIface* aIface) { + NS_ASSERTION(aIface, "Invalid aIface"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->set_text_contents = setTextContentsCB; + aIface->insert_text = insertTextCB; + aIface->copy_text = copyTextCB; + aIface->cut_text = cutTextCB; + aIface->delete_text = deleteTextCB; + aIface->paste_text = pasteTextCB; +} diff --git a/accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp b/accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp new file mode 100644 index 0000000000..ed8c4f4fce --- /dev/null +++ b/accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "nsMaiHyperlink.h" +#include "mozilla/Likely.h" + +using namespace mozilla::a11y; + +extern "C" { +static AtkHyperlink* getHyperlinkCB(AtkHyperlinkImpl* aImpl) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aImpl)); + if (!acc) { + return nullptr; + } + + NS_ASSERTION(acc->IsLink(), "why isn't it a link!"); + + return MAI_ATK_OBJECT(aImpl)->GetAtkHyperlink(); +} +} + +void hyperlinkImplInterfaceInitCB(AtkHyperlinkImplIface* aIface) { + NS_ASSERTION(aIface, "no interface!"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_hyperlink = getHyperlinkCB; +} diff --git a/accessible/atk/nsMaiInterfaceHypertext.cpp b/accessible/atk/nsMaiInterfaceHypertext.cpp new file mode 100644 index 0000000000..1e073c87d2 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceHypertext.cpp @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "HyperTextAccessible.h" +#include "nsMai.h" +#include "nsMaiHyperlink.h" +#include "RemoteAccessible.h" +#include "mozilla/Likely.h" + +using namespace mozilla::a11y; + +extern "C" { + +static AtkHyperlink* getLinkCB(AtkHypertext* aText, gint aLinkIndex) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (HyperTextAccessibleBase* hyperText = acc->AsHyperTextBase()) { + Accessible* linkAcc = hyperText->LinkAt(aLinkIndex); + AtkObject* atkHyperLink = GetWrapperFor(linkAcc); + NS_ENSURE_TRUE(IS_MAI_OBJECT(atkHyperLink), nullptr); + return MAI_ATK_OBJECT(atkHyperLink)->GetAtkHyperlink(); + } + } + + return nullptr; +} + +static gint getLinkCountCB(AtkHypertext* aText) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (HyperTextAccessibleBase* hyperText = acc->AsHyperTextBase()) { + return static_cast(hyperText->LinkCount()); + } + } + return -1; +} + +static gint getLinkIndexCB(AtkHypertext* aText, gint aCharIndex) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return -1; + } + HyperTextAccessibleBase* hyperText = acc->AsHyperTextBase(); + if (!hyperText) { + return -1; + } + return hyperText->LinkIndexAtOffset(aCharIndex); +} + +} // extern "C" + +void hypertextInterfaceInitCB(AtkHypertextIface* aIface) { + NS_ASSERTION(aIface, "no interface!"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_link = getLinkCB; + aIface->get_n_links = getLinkCountCB; + aIface->get_link_index = getLinkIndexCB; +} diff --git a/accessible/atk/nsMaiInterfaceImage.cpp b/accessible/atk/nsMaiInterfaceImage.cpp new file mode 100644 index 0000000000..dee28f109f --- /dev/null +++ b/accessible/atk/nsMaiInterfaceImage.cpp @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "AccessibleWrap.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/Likely.h" +#include "nsMai.h" +#include "nsIAccessibleTypes.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { +const gchar* getDescriptionCB(AtkObject* aAtkObj); + +static void getImagePositionCB(AtkImage* aImage, gint* aAccX, gint* aAccY, + AtkCoordType aCoordType) { + LayoutDeviceIntPoint pos(-1, -1); + uint32_t geckoCoordType = + (aCoordType == ATK_XY_WINDOW) + ? nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE + : nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE; + + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aImage))) { + pos = acc->Position(geckoCoordType); + } + + *aAccX = pos.x; + *aAccY = pos.y; +} + +static const gchar* getImageDescriptionCB(AtkImage* aImage) { + return getDescriptionCB(ATK_OBJECT(aImage)); +} + +static void getImageSizeCB(AtkImage* aImage, gint* aAccWidth, + gint* aAccHeight) { + LayoutDeviceIntSize size(-1, -1); + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aImage))) { + size = acc->Size(); + } + + *aAccWidth = size.width; + *aAccHeight = size.height; +} + +} // extern "C" + +void imageInterfaceInitCB(AtkImageIface* aIface) { + NS_ASSERTION(aIface, "no interface!"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_image_position = getImagePositionCB; + aIface->get_image_description = getImageDescriptionCB; + aIface->get_image_size = getImageSizeCB; +} diff --git a/accessible/atk/nsMaiInterfaceSelection.cpp b/accessible/atk/nsMaiInterfaceSelection.cpp new file mode 100644 index 0000000000..80b4d260f1 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceSelection.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "LocalAccessible-inl.h" +#include "AccessibleWrap.h" +#include "nsMai.h" +#include "mozilla/Likely.h" + +#include +#include + +using namespace mozilla::a11y; + +extern "C" { + +static gboolean addSelectionCB(AtkSelection* aSelection, gint i) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->AddItemToSelection(i); + } + + return FALSE; +} + +static gboolean clearSelectionCB(AtkSelection* aSelection) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->UnselectAll(); + } + + return FALSE; +} + +static AtkObject* refSelectionCB(AtkSelection* aSelection, gint i) { + AtkObject* atkObj = nullptr; + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + Accessible* selectedItem = acc->GetSelectedItem(i); + if (selectedItem) { + atkObj = GetWrapperFor(selectedItem); + } + + if (atkObj) { + g_object_ref(atkObj); + } + + return atkObj; +} + +static gint getSelectionCountCB(AtkSelection* aSelection) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->SelectedItemCount(); + } + + return -1; +} + +static gboolean isChildSelectedCB(AtkSelection* aSelection, gint i) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->IsItemSelected(i); + } + + return FALSE; +} + +static gboolean removeSelectionCB(AtkSelection* aSelection, gint i) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->RemoveItemFromSelection(i); + } + + return FALSE; +} + +static gboolean selectAllSelectionCB(AtkSelection* aSelection) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aSelection)); + if (acc && acc->IsSelect()) { + return acc->SelectAll(); + } + + return FALSE; +} +} + +void selectionInterfaceInitCB(AtkSelectionIface* aIface) { + NS_ASSERTION(aIface, "Invalid aIface"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->add_selection = addSelectionCB; + aIface->clear_selection = clearSelectionCB; + aIface->ref_selection = refSelectionCB; + aIface->get_selection_count = getSelectionCountCB; + aIface->is_child_selected = isChildSelectedCB; + aIface->remove_selection = removeSelectionCB; + aIface->select_all_selection = selectAllSelectionCB; +} diff --git a/accessible/atk/nsMaiInterfaceTable.cpp b/accessible/atk/nsMaiInterfaceTable.cpp new file mode 100644 index 0000000000..cfba9e78d1 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceTable.cpp @@ -0,0 +1,264 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "AccessibleWrap.h" +#include "mozilla/a11y/TableAccessible.h" +#include "nsAccessibilityService.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "nsTArray.h" + +#include "mozilla/Likely.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { +static AtkObject* refAtCB(AtkTable* aTable, gint aRowIdx, gint aColIdx) { + if (aRowIdx < 0 || aColIdx < 0) { + return nullptr; + } + + AtkObject* cellAtkObj = nullptr; + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + Accessible* cell = acc->AsTable()->CellAt(aRowIdx, aColIdx); + if (!cell) { + return nullptr; + } + + cellAtkObj = GetWrapperFor(cell); + + if (cellAtkObj) { + g_object_ref(cellAtkObj); + } + + return cellAtkObj; +} + +static gint getIndexAtCB(AtkTable* aTable, gint aRowIdx, gint aColIdx) { + if (aRowIdx < 0 || aColIdx < 0) { + return -1; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->CellIndexAt(aRowIdx, aColIdx)); +} + +static gint getColumnAtIndexCB(AtkTable* aTable, gint aIdx) { + if (aIdx < 0) { + return -1; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->ColIndexAt(aIdx)); +} + +static gint getRowAtIndexCB(AtkTable* aTable, gint aIdx) { + if (aIdx < 0) { + return -1; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->RowIndexAt(aIdx)); +} + +static gint getColumnCountCB(AtkTable* aTable) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->ColCount()); +} + +static gint getRowCountCB(AtkTable* aTable) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->RowCount()); +} + +static gint getColumnExtentAtCB(AtkTable* aTable, gint aRowIdx, gint aColIdx) { + if (aRowIdx < 0 || aColIdx < 0) { + return -1; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->ColExtentAt(aRowIdx, aColIdx)); +} + +static gint getRowExtentAtCB(AtkTable* aTable, gint aRowIdx, gint aColIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return -1; + } + return static_cast(acc->AsTable()->RowExtentAt(aRowIdx, aColIdx)); +} + +static AtkObject* getCaptionCB(AtkTable* aTable) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + Accessible* caption = acc->AsTable()->Caption(); + return caption ? GetWrapperFor(caption) : nullptr; +} + +static const gchar* getColumnDescriptionCB(AtkTable* aTable, gint aColumn) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + nsAutoString autoStr; + acc->AsTable()->ColDescription(aColumn, autoStr); + return AccessibleWrap::ReturnString(autoStr); +} + +static AtkObject* getColumnHeaderCB(AtkTable* aTable, gint aColIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + Accessible* header = AccessibleWrap::GetColumnHeader(acc->AsTable(), aColIdx); + return header ? GetWrapperFor(header) : nullptr; +} + +static const gchar* getRowDescriptionCB(AtkTable* aTable, gint aRow) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + nsAutoString autoStr; + acc->AsTable()->RowDescription(aRow, autoStr); + return AccessibleWrap::ReturnString(autoStr); +} + +static AtkObject* getRowHeaderCB(AtkTable* aTable, gint aRowIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return nullptr; + } + Accessible* header = AccessibleWrap::GetRowHeader(acc->AsTable(), aRowIdx); + return header ? GetWrapperFor(header) : nullptr; +} + +static AtkObject* getSummaryCB(AtkTable* aTable) { + // Neither html:table nor xul:tree nor ARIA grid/tree have an ability to + // link an accessible object to specify a summary. There is closes method + // in TableAccessible::summary to get a summary as a string which is not + // mapped directly to ATK. + return nullptr; +} + +static gint getSelectedColumnsCB(AtkTable* aTable, gint** aSelected) { + *aSelected = nullptr; + + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return 0; + } + AutoTArray cols; + acc->AsTable()->SelectedColIndices(&cols); + + if (cols.IsEmpty()) return 0; + + gint* atkColumns = g_new(gint, cols.Length()); + if (!atkColumns) { + NS_WARNING("OUT OF MEMORY"); + return 0; + } + + memcpy(atkColumns, cols.Elements(), cols.Length() * sizeof(uint32_t)); + *aSelected = atkColumns; + return cols.Length(); +} + +static gint getSelectedRowsCB(AtkTable* aTable, gint** aSelected) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return 0; + } + AutoTArray rows; + acc->AsTable()->SelectedRowIndices(&rows); + + gint* atkRows = g_new(gint, rows.Length()); + if (!atkRows) { + NS_WARNING("OUT OF MEMORY"); + return 0; + } + + memcpy(atkRows, rows.Elements(), rows.Length() * sizeof(uint32_t)); + *aSelected = atkRows; + return rows.Length(); +} + +static gboolean isColumnSelectedCB(AtkTable* aTable, gint aColIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return FALSE; + } + return static_cast(acc->AsTable()->IsColSelected(aColIdx)); +} + +static gboolean isRowSelectedCB(AtkTable* aTable, gint aRowIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return FALSE; + } + return static_cast(acc->AsTable()->IsRowSelected(aRowIdx)); +} + +static gboolean isCellSelectedCB(AtkTable* aTable, gint aRowIdx, gint aColIdx) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTable)); + if (!acc) { + return FALSE; + } + return static_cast( + acc->AsTable()->IsCellSelected(aRowIdx, aColIdx)); +} +} + +void tableInterfaceInitCB(AtkTableIface* aIface) { + NS_ASSERTION(aIface, "no interface!"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->ref_at = refAtCB; + aIface->get_index_at = getIndexAtCB; + aIface->get_column_at_index = getColumnAtIndexCB; + aIface->get_row_at_index = getRowAtIndexCB; + aIface->get_n_columns = getColumnCountCB; + aIface->get_n_rows = getRowCountCB; + aIface->get_column_extent_at = getColumnExtentAtCB; + aIface->get_row_extent_at = getRowExtentAtCB; + aIface->get_caption = getCaptionCB; + aIface->get_column_description = getColumnDescriptionCB; + aIface->get_column_header = getColumnHeaderCB; + aIface->get_row_description = getRowDescriptionCB; + aIface->get_row_header = getRowHeaderCB; + aIface->get_summary = getSummaryCB; + aIface->get_selected_columns = getSelectedColumnsCB; + aIface->get_selected_rows = getSelectedRowsCB; + aIface->is_column_selected = isColumnSelectedCB; + aIface->is_row_selected = isRowSelectedCB; + aIface->is_selected = isCellSelectedCB; +} diff --git a/accessible/atk/nsMaiInterfaceTableCell.cpp b/accessible/atk/nsMaiInterfaceTableCell.cpp new file mode 100644 index 0000000000..06a684a87d --- /dev/null +++ b/accessible/atk/nsMaiInterfaceTableCell.cpp @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "nsAccessibilityService.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "nsTArray.h" + +#include "mozilla/Likely.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { +static gint GetColumnSpanCB(AtkTableCell* aCell) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return 0; + } + return static_cast(acc->AsTableCell()->ColExtent()); +} + +static gint GetRowSpanCB(AtkTableCell* aCell) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return 0; + } + return static_cast(acc->AsTableCell()->RowExtent()); +} + +static gboolean GetPositionCB(AtkTableCell* aCell, gint* aRow, gint* aCol) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return false; + } + TableCellAccessible* cell = acc->AsTableCell(); + if (!cell) { + return false; + } + *aRow = static_cast(cell->RowIdx()); + *aCol = static_cast(cell->ColIdx()); + return true; +} + +static gboolean GetColumnRowSpanCB(AtkTableCell* aCell, gint* aCol, gint* aRow, + gint* aColExtent, gint* aRowExtent) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return false; + } + TableCellAccessible* cellAcc = acc->AsTableCell(); + if (!cellAcc) { + return false; + } + *aCol = static_cast(cellAcc->ColIdx()); + *aRow = static_cast(cellAcc->RowIdx()); + *aColExtent = static_cast(cellAcc->ColExtent()); + *aRowExtent = static_cast(cellAcc->ColExtent()); + return true; +} + +static AtkObject* GetTableCB(AtkTableCell* aTableCell) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aTableCell)); + if (!acc) { + return nullptr; + } + TableCellAccessible* cell = acc->AsTableCell(); + if (!cell) { + return nullptr; + } + TableAccessible* table = cell->Table(); + if (!table) { + return nullptr; + } + Accessible* tableAcc = table->AsAccessible(); + return tableAcc ? GetWrapperFor(tableAcc) : nullptr; +} + +static GPtrArray* GetColumnHeaderCellsCB(AtkTableCell* aCell) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return nullptr; + } + TableCellAccessible* cell = acc->AsTableCell(); + if (!cell) { + return nullptr; + } + AutoTArray headers; + cell->ColHeaderCells(&headers); + if (headers.IsEmpty()) { + return nullptr; + } + + GPtrArray* atkHeaders = g_ptr_array_sized_new(headers.Length()); + for (Accessible* header : headers) { + AtkObject* atkHeader = GetWrapperFor(header); + g_object_ref(atkHeader); + g_ptr_array_add(atkHeaders, atkHeader); + } + + return atkHeaders; +} + +static GPtrArray* GetRowHeaderCellsCB(AtkTableCell* aCell) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aCell)); + if (!acc) { + return nullptr; + } + TableCellAccessible* cell = acc->AsTableCell(); + if (!cell) { + return nullptr; + } + AutoTArray headers; + cell->RowHeaderCells(&headers); + if (headers.IsEmpty()) { + return nullptr; + } + + GPtrArray* atkHeaders = g_ptr_array_sized_new(headers.Length()); + for (Accessible* header : headers) { + AtkObject* atkHeader = GetWrapperFor(header); + g_object_ref(atkHeader); + g_ptr_array_add(atkHeaders, atkHeader); + } + + return atkHeaders; +} +} + +void tableCellInterfaceInitCB(AtkTableCellIface* aIface) { + NS_ASSERTION(aIface, "no interface!"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_column_span = GetColumnSpanCB; + aIface->get_column_header_cells = GetColumnHeaderCellsCB; + aIface->get_position = GetPositionCB; + aIface->get_row_span = GetRowSpanCB; + aIface->get_row_header_cells = GetRowHeaderCellsCB; + aIface->get_row_column_span = GetColumnRowSpanCB; + aIface->get_table = GetTableCB; +} diff --git a/accessible/atk/nsMaiInterfaceText.cpp b/accessible/atk/nsMaiInterfaceText.cpp new file mode 100644 index 0000000000..6a5d431bc8 --- /dev/null +++ b/accessible/atk/nsMaiInterfaceText.cpp @@ -0,0 +1,564 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" +#include "mozilla/a11y/PDocAccessible.h" +#include "nsAccessibilityService.h" +#include "LocalAccessible-inl.h" +#include "HyperTextAccessible-inl.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "AccAttributes.h" + +#include "nsIAccessibleTypes.h" +#include "nsISimpleEnumerator.h" +#include "nsUTF8Utils.h" + +#include "mozilla/Likely.h" + +#include "DOMtoATK.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +static const char* sAtkTextAttrNames[ATK_TEXT_ATTR_LAST_DEFINED]; + +static AtkAttributeSet* ConvertToAtkTextAttributeSet( + AccAttributes* aAttributes) { + if (!aAttributes) { + // This can happen if an Accessible dies in the content process, but the + // parent hasn't been udpated yet. + return nullptr; + } + + AtkAttributeSet* atkAttributeSet = nullptr; + + for (auto iter : *aAttributes) { + AtkAttribute* atkAttr = (AtkAttribute*)g_malloc(sizeof(AtkAttribute)); + nsAutoString value; + // We set atkAttr->name directly for each case. For the value, we set the + // value string for each case. atkAttr->value is set at the end based on the + // value string. + + // Set atkAttr->name to a specific ATK attribute name. + auto atkName = [&atkAttr](AtkTextAttribute aAttrNum) { + atkAttr->name = g_strdup(sAtkTextAttrNames[aAttrNum]); + }; + // Set value to a formatted ATK color value. + auto colorValue = [&iter, &value] { + // The format of the atk attribute is r,g,b and the gecko one is + // rgb(r, g, b). + auto color = iter.Value(); + MOZ_ASSERT(color); + value.AppendInt(NS_GET_R(color->mValue)); + value.Append(','); + value.AppendInt(NS_GET_G(color->mValue)); + value.Append(','); + value.AppendInt(NS_GET_B(color->mValue)); + }; + + nsAtom* name = iter.Name(); + if (name == nsGkAtoms::color) { + atkName(ATK_TEXT_ATTR_FG_COLOR); + colorValue(); + } else if (name == nsGkAtoms::backgroundColor) { + atkName(ATK_TEXT_ATTR_BG_COLOR); + colorValue(); + } else if (name == nsGkAtoms::font_family) { + atkName(ATK_TEXT_ATTR_FAMILY_NAME); + iter.ValueAsString(value); + } else if (name == nsGkAtoms::font_size) { + atkName(ATK_TEXT_ATTR_SIZE); + // ATK wants the number of points without pt at the end. + auto fontSize = iter.Value(); + MOZ_ASSERT(fontSize); + value.AppendInt(fontSize->mValue); + } else if (name == nsGkAtoms::fontWeight) { + atkName(ATK_TEXT_ATTR_WEIGHT); + iter.ValueAsString(value); + } else if (name == nsGkAtoms::invalid) { + atkName(ATK_TEXT_ATTR_INVALID); + iter.ValueAsString(value); + } else { + nsAutoString nameStr; + iter.NameAsString(nameStr); + atkAttr->name = g_strdup(NS_ConvertUTF16toUTF8(nameStr).get()); + iter.ValueAsString(value); + } + + atkAttr->value = g_strdup(NS_ConvertUTF16toUTF8(value).get()); + atkAttributeSet = g_slist_prepend(atkAttributeSet, atkAttr); + } + + // libatk-adaptor will free it + return atkAttributeSet; +} + +extern "C" { + +static gchar* getTextCB(AtkText* aText, gint aStartOffset, gint aEndOffset) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc || !acc->IsTextRole()) { + return nullptr; + } + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text) { + return nullptr; + } + return DOMtoATK::NewATKString(text, aStartOffset, aEndOffset); +} + +static gint getCharacterCountCB(AtkText* aText); + +// Note: this does not support magic offsets, which is fine for its callers +// which do not implement any. +static gchar* getCharTextAtOffset(AtkText* aText, gint aOffset, + gint* aStartOffset, gint* aEndOffset) { + gint end = aOffset + 1; + gint count = getCharacterCountCB(aText); + + if (aOffset > count) { + aOffset = count; + } + if (end > count) { + end = count; + } + if (aOffset < 0) { + aOffset = 0; + } + if (end < 0) { + end = 0; + } + *aStartOffset = aOffset; + *aEndOffset = end; + + return getTextCB(aText, aOffset, end); +} + +static gchar* getTextAfterOffsetCB(AtkText* aText, gint aOffset, + AtkTextBoundary aBoundaryType, + gint* aStartOffset, gint* aEndOffset) { + if (aBoundaryType == ATK_TEXT_BOUNDARY_CHAR) { + return getCharTextAtOffset(aText, aOffset + 1, aStartOffset, aEndOffset); + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + nsAutoString autoStr; + int32_t startOffset = 0, endOffset = 0; + text->TextAfterOffset(aOffset, aBoundaryType, &startOffset, &endOffset, + autoStr); + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + + // libspi will free it. + return DOMtoATK::Convert(autoStr); +} + +static gchar* getTextAtOffsetCB(AtkText* aText, gint aOffset, + AtkTextBoundary aBoundaryType, + gint* aStartOffset, gint* aEndOffset) { + if (aBoundaryType == ATK_TEXT_BOUNDARY_CHAR) { + return getCharTextAtOffset(aText, aOffset, aStartOffset, aEndOffset); + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + nsAutoString autoStr; + int32_t startOffset = 0, endOffset = 0; + text->TextAtOffset(aOffset, aBoundaryType, &startOffset, &endOffset, autoStr); + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + + // libspi will free it. + return DOMtoATK::Convert(autoStr); +} + +static gunichar getCharacterAtOffsetCB(AtkText* aText, gint aOffset) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return 0; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (text) { + return DOMtoATK::ATKCharacter(text, aOffset); + } + + return 0; +} + +static gchar* getTextBeforeOffsetCB(AtkText* aText, gint aOffset, + AtkTextBoundary aBoundaryType, + gint* aStartOffset, gint* aEndOffset) { + if (aBoundaryType == ATK_TEXT_BOUNDARY_CHAR) { + return getCharTextAtOffset(aText, aOffset - 1, aStartOffset, aEndOffset); + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + nsAutoString autoStr; + int32_t startOffset = 0, endOffset = 0; + text->TextBeforeOffset(aOffset, aBoundaryType, &startOffset, &endOffset, + autoStr); + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + + // libspi will free it. + return DOMtoATK::Convert(autoStr); +} + +static gint getCaretOffsetCB(AtkText* aText) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return -1; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return -1; + } + + return static_cast(text->CaretOffset()); +} + +static AtkAttributeSet* getRunAttributesCB(AtkText* aText, gint aOffset, + gint* aStartOffset, + gint* aEndOffset) { + *aStartOffset = -1; + *aEndOffset = -1; + int32_t startOffset = 0, endOffset = 0; + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + RefPtr attributes = + text->TextAttributes(false, aOffset, &startOffset, &endOffset); + + *aStartOffset = startOffset; + *aEndOffset = endOffset; + + return ConvertToAtkTextAttributeSet(attributes); +} + +static AtkAttributeSet* getDefaultAttributesCB(AtkText* aText) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + RefPtr attributes = text->DefaultTextAttributes(); + return ConvertToAtkTextAttributeSet(attributes); +} + +static void getCharacterExtentsCB(AtkText* aText, gint aOffset, gint* aX, + gint* aY, gint* aWidth, gint* aHeight, + AtkCoordType aCoords) { + if (!aX || !aY || !aWidth || !aHeight) { + return; + } + *aX = *aY = *aWidth = *aHeight = -1; + + uint32_t geckoCoordType; + if (aCoords == ATK_XY_SCREEN) { + geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE; + } else { + geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return; + } + + LayoutDeviceIntRect rect = text->CharBounds(aOffset, geckoCoordType); + + *aX = rect.x; + *aY = rect.y; + *aWidth = rect.width; + *aHeight = rect.height; +} + +static void getRangeExtentsCB(AtkText* aText, gint aStartOffset, + gint aEndOffset, AtkCoordType aCoords, + AtkTextRectangle* aRect) { + if (!aRect) { + return; + } + aRect->x = aRect->y = aRect->width = aRect->height = -1; + + uint32_t geckoCoordType; + if (aCoords == ATK_XY_SCREEN) { + geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE; + } else { + geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE; + } + + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return; + } + + LayoutDeviceIntRect rect = + text->TextBounds(aStartOffset, aEndOffset, geckoCoordType); + + aRect->x = rect.x; + aRect->y = rect.y; + aRect->width = rect.width; + aRect->height = rect.height; +} + +static gint getCharacterCountCB(AtkText* aText) { + if (Accessible* acc = GetInternalObj(ATK_OBJECT(aText))) { + if (HyperTextAccessibleBase* text = acc->AsHyperTextBase()) { + return static_cast(text->CharacterCount()); + } + } + return 0; +} + +static gint getOffsetAtPointCB(AtkText* aText, gint aX, gint aY, + AtkCoordType aCoords) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return -1; + } + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return -1; + } + return static_cast(text->OffsetAtPoint( + aX, aY, + (aCoords == ATK_XY_SCREEN + ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE + : nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE))); +} + +static gint getTextSelectionCountCB(AtkText* aText) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return 0; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return 0; + } + + return text->SelectionCount(); +} + +static gchar* getTextSelectionCB(AtkText* aText, gint aSelectionNum, + gint* aStartOffset, gint* aEndOffset) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return nullptr; + } + + int32_t startOffset = 0, endOffset = 0; + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return nullptr; + } + + text->SelectionBoundsAt(aSelectionNum, &startOffset, &endOffset); + *aStartOffset = startOffset; + *aEndOffset = endOffset; + + return getTextCB(aText, *aStartOffset, *aEndOffset); +} + +// set methods +static gboolean addTextSelectionCB(AtkText* aText, gint aStartOffset, + gint aEndOffset) { + AccessibleWrap* accWrap = GetAccessibleWrap(ATK_OBJECT(aText)); + if (accWrap) { + HyperTextAccessible* text = accWrap->AsHyperText(); + if (!text || !text->IsTextRole()) { + return FALSE; + } + + return text->AddToSelection(aStartOffset, aEndOffset); + } + if (RemoteAccessible* proxy = GetProxy(ATK_OBJECT(aText))) { + return proxy->AddToSelection(aStartOffset, aEndOffset); + } + + return FALSE; +} + +static gboolean removeTextSelectionCB(AtkText* aText, gint aSelectionNum) { + AccessibleWrap* accWrap = GetAccessibleWrap(ATK_OBJECT(aText)); + if (accWrap) { + HyperTextAccessible* text = accWrap->AsHyperText(); + if (!text || !text->IsTextRole()) { + return FALSE; + } + + return text->RemoveFromSelection(aSelectionNum); + } + if (RemoteAccessible* proxy = GetProxy(ATK_OBJECT(aText))) { + return proxy->RemoveFromSelection(aSelectionNum); + } + + return FALSE; +} + +static gboolean setTextSelectionCB(AtkText* aText, gint aSelectionNum, + gint aStartOffset, gint aEndOffset) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc || !acc->IsTextRole()) { + return FALSE; + } + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text) { + return FALSE; + } + return text->SetSelectionBoundsAt(aSelectionNum, aStartOffset, aEndOffset); +} + +static gboolean setCaretOffsetCB(AtkText* aText, gint aOffset) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return FALSE; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text || !acc->IsTextRole()) { + return FALSE; + } + + text->SetCaretOffset(aOffset); + return TRUE; +} + +static gboolean scrollSubstringToCB(AtkText* aText, gint aStartOffset, + gint aEndOffset, AtkScrollType aType) { + Accessible* acc = GetInternalObj(ATK_OBJECT(aText)); + if (!acc) { + return FALSE; + } + + HyperTextAccessibleBase* text = acc->AsHyperTextBase(); + if (!text) { + return FALSE; + } + + text->ScrollSubstringTo(aStartOffset, aEndOffset, aType); + + return TRUE; +} + +static gboolean scrollSubstringToPointCB(AtkText* aText, gint aStartOffset, + gint aEndOffset, AtkCoordType aCoords, + gint aX, gint aY) { + AtkObject* atkObject = ATK_OBJECT(aText); + AccessibleWrap* accWrap = GetAccessibleWrap(atkObject); + if (accWrap) { + HyperTextAccessible* text = accWrap->AsHyperText(); + if (!text || !text->IsTextRole() || + !text->IsValidRange(aStartOffset, aEndOffset)) { + return FALSE; + } + text->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoords, aX, aY); + return TRUE; + } + + RemoteAccessible* proxy = GetProxy(atkObject); + if (proxy) { + proxy->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoords, aX, aY); + return TRUE; + } + + return FALSE; +} +} + +void textInterfaceInitCB(AtkTextIface* aIface) { + NS_ASSERTION(aIface, "Invalid aIface"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_text = getTextCB; + aIface->get_text_after_offset = getTextAfterOffsetCB; + aIface->get_text_at_offset = getTextAtOffsetCB; + aIface->get_character_at_offset = getCharacterAtOffsetCB; + aIface->get_text_before_offset = getTextBeforeOffsetCB; + aIface->get_caret_offset = getCaretOffsetCB; + aIface->get_run_attributes = getRunAttributesCB; + aIface->get_default_attributes = getDefaultAttributesCB; + aIface->get_character_extents = getCharacterExtentsCB; + aIface->get_range_extents = getRangeExtentsCB; + aIface->get_character_count = getCharacterCountCB; + aIface->get_offset_at_point = getOffsetAtPointCB; + aIface->get_n_selections = getTextSelectionCountCB; + aIface->get_selection = getTextSelectionCB; + + // set methods + aIface->add_selection = addTextSelectionCB; + aIface->remove_selection = removeTextSelectionCB; + aIface->set_selection = setTextSelectionCB; + aIface->set_caret_offset = setCaretOffsetCB; + + if (IsAtkVersionAtLeast(2, 32)) { + aIface->scroll_substring_to = scrollSubstringToCB; + aIface->scroll_substring_to_point = scrollSubstringToPointCB; + } + + // Cache the string values of the atk text attribute names. + for (uint32_t i = 0; i < ArrayLength(sAtkTextAttrNames); i++) { + sAtkTextAttrNames[i] = + atk_text_attribute_get_name(static_cast(i)); + } +} diff --git a/accessible/atk/nsMaiInterfaceValue.cpp b/accessible/atk/nsMaiInterfaceValue.cpp new file mode 100644 index 0000000000..05a7da171e --- /dev/null +++ b/accessible/atk/nsMaiInterfaceValue.cpp @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "InterfaceInitFuncs.h" + +#include "AccessibleWrap.h" +#include "nsMai.h" +#include "RemoteAccessible.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Likely.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +extern "C" { + +static void getCurrentValueCB(AtkValue* obj, GValue* value) { + Accessible* acc = GetInternalObj(ATK_OBJECT(obj)); + if (!acc) { + return; + } + + memset(value, 0, sizeof(GValue)); + double accValue = acc->CurValue(); + if (std::isnan(accValue)) return; + + g_value_init(value, G_TYPE_DOUBLE); + g_value_set_double(value, accValue); +} + +static void getMaximumValueCB(AtkValue* obj, GValue* value) { + Accessible* acc = GetInternalObj(ATK_OBJECT(obj)); + if (!acc) { + return; + } + + memset(value, 0, sizeof(GValue)); + double accValue = acc->MaxValue(); + if (std::isnan(accValue)) return; + + g_value_init(value, G_TYPE_DOUBLE); + g_value_set_double(value, accValue); +} + +static void getMinimumValueCB(AtkValue* obj, GValue* value) { + Accessible* acc = GetInternalObj(ATK_OBJECT(obj)); + if (!acc) { + return; + } + + memset(value, 0, sizeof(GValue)); + double accValue = acc->MinValue(); + if (std::isnan(accValue)) return; + + g_value_init(value, G_TYPE_DOUBLE); + g_value_set_double(value, accValue); +} + +static void getMinimumIncrementCB(AtkValue* obj, GValue* minimumIncrement) { + Accessible* acc = GetInternalObj(ATK_OBJECT(obj)); + if (!acc) { + return; + } + + memset(minimumIncrement, 0, sizeof(GValue)); + double accValue = acc->Step(); + if (std::isnan(accValue)) { + accValue = 0; // zero if the minimum increment is undefined + } + + g_value_init(minimumIncrement, G_TYPE_DOUBLE); + g_value_set_double(minimumIncrement, accValue); +} + +static gboolean setCurrentValueCB(AtkValue* obj, const GValue* value) { + Accessible* acc = GetInternalObj(ATK_OBJECT(obj)); + if (!acc) { + return false; + } + + double accValue = g_value_get_double(value); + return acc->SetCurValue(accValue); +} + +void valueInterfaceInitCB(AtkValueIface* aIface) { + NS_ASSERTION(aIface, "Invalid aIface"); + if (MOZ_UNLIKELY(!aIface)) return; + + aIface->get_current_value = getCurrentValueCB; + aIface->get_maximum_value = getMaximumValueCB; + aIface->get_minimum_value = getMinimumValueCB; + aIface->get_minimum_increment = getMinimumIncrementCB; + aIface->set_current_value = setCurrentValueCB; +} +} diff --git a/accessible/atk/nsStateMap.h b/accessible/atk/nsStateMap.h new file mode 100644 index 0000000000..3587ccd6cf --- /dev/null +++ b/accessible/atk/nsStateMap.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include +#include "AccessibleWrap.h" + +#include + +// clang-format off +/****************************************************************************** +The following accessible states aren't translated, just ignored: + STATE_READONLY: Supported indirectly via EXT_STATE_EDITABLE + STATE_HOTTRACKED: No ATK equivalent. No known use case. + The nsIAccessible state is not currently supported. + STATE_FLOATING: No ATK equivalent. No known use case. + The nsIAccessible state is not currently supported. + STATE_MOVEABLE: No ATK equivalent. No known use case. + The nsIAccessible state is not currently supported. + STATE_SELFVOICING: No ATK equivalent -- the object has self-TTS. + The nsIAccessible state is not currently supported. + STATE_LINKED: The object is formatted as a hyperlink. Supported via ATK_ROLE_LINK. + STATE_EXTSELECTABLE: Indicates that an object extends its selection. + This is supported via STATE_MULTISELECTABLE. + STATE_PROTECTED: The object is a password-protected edit control. + Supported via ATK_ROLE_PASSWORD_TEXT + STATE_PINNED: The object is pinned, usually indicating it is fixed in + place and has permanence. No ATK equivalent. The + accessible state is not currently supported. + +The following ATK states are not supported: + ATK_STATE_ARMED: No clear use case, used briefly when button is activated + ATK_STATE_HAS_TOOLTIP: No clear use case, no IA2 equivalent + ATK_STATE_ICONIFIED: Mozilla does not have elements which are collapsable into icons + ATK_STATE_TRUNCATED: No clear use case. Indicates that an object's onscreen content is truncated, + e.g. a text value in a spreadsheet cell. No IA2 state. +******************************************************************************/ +// clang-format on + +enum EStateMapEntryType { + kMapDirectly, + kMapOpposite, // For example, UNAVAILABLE is the opposite of ENABLED + kNoStateChange, // Don't fire state change event +}; + +const AtkStateType kNone = ATK_STATE_INVALID; + +struct AtkStateMap { + AtkStateType atkState; + EStateMapEntryType stateMapEntryType; +}; + +// Map array from cross platform states to ATK states +static const AtkStateMap gAtkStateMap[] = + { + // Cross Platform States + // clang-format off + { kNone, kMapOpposite }, // states::UNAVAILABLE = 1 << 0 + { ATK_STATE_SELECTED, kMapDirectly }, // states::SELECTED = 1 << 1 + { ATK_STATE_FOCUSED, kMapDirectly }, // states::FOCUSED = 1 << 2 + { ATK_STATE_PRESSED, kMapDirectly }, // states::PRESSED = 1 << 3 + { ATK_STATE_CHECKED, kMapDirectly }, // states::CHECKED = 1 << 4 + { ATK_STATE_INDETERMINATE, kMapDirectly }, // states::MIXED = 1 << 5 + { kNone, kMapDirectly }, // states::READONLY = 1 << 6 + { kNone, kMapDirectly }, // states::HOTTRACKED = 1 << 7 + { ATK_STATE_DEFAULT, kMapDirectly }, // states::DEFAULT = 1 << 8 + { ATK_STATE_EXPANDED, kMapDirectly }, // states::EXPANDED = 1 << 9 + { kNone, kNoStateChange }, // states::COLLAPSED = 1 << 10 + { ATK_STATE_BUSY, kMapDirectly }, // states::BUSY = 1 << 11 + { kNone, kMapDirectly }, // states::FLOATING = 1 << 12 + { ATK_STATE_CHECKABLE, kMapDirectly }, // states::CHECKABLE = 1 << 13 + { ATK_STATE_ANIMATED, kMapDirectly }, // states::ANIMATED = 1 << 14 + { ATK_STATE_VISIBLE, kMapOpposite }, // states::INVISIBLE = 1 << 15 + { ATK_STATE_SHOWING, kMapOpposite }, // states::OFFSCREEN = 1 << 16 + { ATK_STATE_RESIZABLE, kMapDirectly }, // states::SIZEABLE = 1 << 17 + { kNone, kMapDirectly }, // states::MOVEABLE = 1 << 18 + { kNone, kMapDirectly }, // states::SELFVOICING = 1 << 19 + { ATK_STATE_FOCUSABLE, kMapDirectly }, // states::FOCUSABLE = 1 << 20 + { ATK_STATE_SELECTABLE, kMapDirectly }, // states::SELECTABLE = 1 << 21 + { kNone, kMapDirectly }, // states::LINKED = 1 << 22 + { ATK_STATE_VISITED, kMapDirectly }, // states::TRAVERSED = 1 << 23 + { ATK_STATE_MULTISELECTABLE, kMapDirectly }, // states::MULTISELECTABLE = 1 << 24 + { kNone, kMapDirectly }, // states::EXTSELECTABLE = 1 << 25 + { ATK_STATE_REQUIRED, kMapDirectly }, // states::STATE_REQUIRED = 1 << 26 + { kNone, kMapDirectly }, // states::ALERT_MEDIUM = 1 << 27 + { ATK_STATE_INVALID_ENTRY, kMapDirectly }, // states::INVALID = 1 << 28 + { kNone, kMapDirectly }, // states::PROTECTED = 1 << 29 + { ATK_STATE_HAS_POPUP, kMapDirectly }, // states::HASPOPUP = 1 << 30 + { ATK_STATE_SUPPORTS_AUTOCOMPLETION, kMapDirectly }, // states::SUPPORTS_AUTOCOMPLETION = 1 << 31 + { ATK_STATE_DEFUNCT, kMapDirectly }, // states::DEFUNCT = 1 << 32 + { ATK_STATE_SELECTABLE_TEXT, kMapDirectly }, // states::SELECTABLE_TEXT = 1 << 33 + { ATK_STATE_EDITABLE, kMapDirectly }, // states::EDITABLE = 1 << 34 + { ATK_STATE_ACTIVE, kMapDirectly }, // states::ACTIVE = 1 << 35 + { ATK_STATE_MODAL, kMapDirectly }, // states::MODAL = 1 << 36 + { ATK_STATE_MULTI_LINE, kMapDirectly }, // states::MULTI_LINE = 1 << 37 + { ATK_STATE_HORIZONTAL, kMapDirectly }, // states::HORIZONTAL = 1 << 38 + { ATK_STATE_OPAQUE, kMapDirectly }, // states::OPAQUE = 1 << 39 + { ATK_STATE_SINGLE_LINE, kMapDirectly }, // states::SINGLE_LINE = 1 << 40 + { ATK_STATE_TRANSIENT, kMapDirectly }, // states::TRANSIENT = 1 << 41 + { ATK_STATE_VERTICAL, kMapDirectly }, // states::VERTICAL = 1 << 42 + { ATK_STATE_STALE, kMapDirectly }, // states::STALE = 1 << 43 + { ATK_STATE_ENABLED, kMapDirectly }, // states::ENABLED = 1 << 44 + { ATK_STATE_SENSITIVE, kMapDirectly }, // states::SENSITIVE = 1 << 45 + { ATK_STATE_EXPANDABLE, kMapDirectly }, // states::EXPANDABLE = 1 << 46 + { kNone, kMapDirectly }, // states::PINNED = 1 << 47 + { ATK_STATE_ACTIVE, kMapDirectly } // states::CURRENT = 1 << 48 + // clang-format on +}; + +static const auto gAtkStateMapLen = std::extent::value; + +static_assert(((uint64_t)0x1) << (gAtkStateMapLen - 1) == + mozilla::a11y::states::LAST_ENTRY, + "ATK states map is out of sync with internal states"); diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp new file mode 100644 index 0000000000..ea0dee60b9 --- /dev/null +++ b/accessible/base/ARIAMap.cpp @@ -0,0 +1,1628 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ARIAMap.h" + +#include "AccAttributes.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "Role.h" +#include "States.h" + +#include "nsAttrName.h" +#include "nsGenericHTMLElement.h" +#include "nsWhitespaceTokenizer.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/dom/Element.h" + +#include "nsUnicharUtils.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::a11y::aria; + +static const uint32_t kGenericAccType = 0; + +/** + * This list of WAI-defined roles are currently hardcoded. + * Eventually we will most likely be loading an RDF resource that contains this + * information Using RDF will also allow for role extensibility. See bug 280138. + * + * Definition of nsRoleMapEntry contains comments explaining this table. + * + * When no Role enum mapping exists for an ARIA role, the role will be exposed + * via the object attribute "xml-roles". + */ + +static const nsRoleMapEntry sWAIRoleMaps[] = { + // clang-format off + { // alert + nsGkAtoms::alert, + roles::ALERT, + kUseMapRole, + eNoValue, + eNoAction, +#if defined(XP_MACOSX) + eAssertiveLiveAttr, +#else + eNoLiveAttr, +#endif + eAlert, + kNoReqStates + }, + { // alertdialog + nsGkAtoms::alertdialog, + roles::DIALOG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // application + nsGkAtoms::application, + roles::APPLICATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // article + nsGkAtoms::article, + roles::ARTICLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // banner + nsGkAtoms::banner, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // blockquote + nsGkAtoms::blockquote, + roles::BLOCKQUOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // button + nsGkAtoms::button, + roles::PUSHBUTTON, + kUseMapRole, + eNoValue, + ePressAction, + eNoLiveAttr, + eButton, + kNoReqStates + // eARIAPressed is auto applied on any button + }, + { // caption + nsGkAtoms::caption, + roles::CAPTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // cell + nsGkAtoms::cell, + roles::CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates + }, + { // checkbox + nsGkAtoms::checkbox, + roles::CHECKBUTTON, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed, + eARIAReadonly + }, + { // code + nsGkAtoms::code, + roles::CODE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // columnheader + nsGkAtoms::columnheader, + roles::COLUMNHEADER, + kUseMapRole, + eNoValue, + eSortAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectableIfDefined, + eARIAReadonly + }, + { // combobox, which consists of text input and popup + nsGkAtoms::combobox, + roles::EDITCOMBOBOX, + kUseMapRole, + eNoValue, + eOpenCloseAction, + eNoLiveAttr, + eCombobox, + states::COLLAPSED | states::HASPOPUP, + eARIAAutoComplete, + eARIAReadonly, + eARIAOrientation + }, + { // comment + nsGkAtoms::comment, + roles::COMMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // complementary + nsGkAtoms::complementary, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // contentinfo + nsGkAtoms::contentinfo, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // deletion + nsGkAtoms::deletion, + roles::CONTENT_DELETION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // dialog + nsGkAtoms::dialog, + roles::DIALOG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // directory + nsGkAtoms::directory, + roles::LIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eList, + states::READONLY + }, + { // doc-abstract + nsGkAtoms::docAbstract, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-acknowledgments + nsGkAtoms::docAcknowledgments, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-afterword + nsGkAtoms::docAfterword, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-appendix + nsGkAtoms::docAppendix, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-backlink + nsGkAtoms::docBacklink, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-biblioentry + nsGkAtoms::docBiblioentry, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // doc-bibliography + nsGkAtoms::docBibliography, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-biblioref + nsGkAtoms::docBiblioref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-chapter + nsGkAtoms::docChapter, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-colophon + nsGkAtoms::docColophon, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-conclusion + nsGkAtoms::docConclusion, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-cover + nsGkAtoms::docCover, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-credit + nsGkAtoms::docCredit, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-credits + nsGkAtoms::docCredits, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-dedication + nsGkAtoms::docDedication, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-endnote + nsGkAtoms::docEndnote, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // doc-endnotes + nsGkAtoms::docEndnotes, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-epigraph + nsGkAtoms::docEpigraph, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-epilogue + nsGkAtoms::docEpilogue, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-errata + nsGkAtoms::docErrata, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-example + nsGkAtoms::docExample, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-footnote + nsGkAtoms::docFootnote, + roles::FOOTNOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-foreword + nsGkAtoms::docForeword, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-glossary + nsGkAtoms::docGlossary, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-glossref + nsGkAtoms::docGlossref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-index + nsGkAtoms::docIndex, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-introduction + nsGkAtoms::docIntroduction, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-noteref + nsGkAtoms::docNoteref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-notice + nsGkAtoms::docNotice, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-pagebreak + nsGkAtoms::docPagebreak, + roles::SEPARATOR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-pagelist + nsGkAtoms::docPagelist, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-part + nsGkAtoms::docPart, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-preface + nsGkAtoms::docPreface, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-prologue + nsGkAtoms::docPrologue, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-pullquote + nsGkAtoms::docPullquote, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-qna + nsGkAtoms::docQna, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-subtitle + nsGkAtoms::docSubtitle, + roles::HEADING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-tip + nsGkAtoms::docTip, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-toc + nsGkAtoms::docToc, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // document + nsGkAtoms::document, + roles::NON_NATIVE_DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // feed + nsGkAtoms::feed, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // figure + nsGkAtoms::figure, + roles::FIGURE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // form + nsGkAtoms::form, + roles::FORM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // graphics-document + nsGkAtoms::graphicsDocument, + roles::NON_NATIVE_DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // graphics-object + nsGkAtoms::graphicsObject, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // graphics-symbol + nsGkAtoms::graphicsSymbol, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // grid + nsGkAtoms::grid, + roles::TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect | eTable, + kNoReqStates, + eARIAMultiSelectable, + eARIAReadonly, + eFocusableUntilDisabled + }, + { // gridcell + nsGkAtoms::gridcell, + roles::GRID_CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectable, + eARIAReadonly + }, + { // group + nsGkAtoms::group, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // heading + nsGkAtoms::heading, + roles::HEADING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // img + nsGkAtoms::img, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // insertion + nsGkAtoms::insertion, + roles::CONTENT_INSERTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // key + nsGkAtoms::key, + roles::KEY, + kUseMapRole, + eNoValue, + ePressAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAPressed + }, + { // link + nsGkAtoms::link, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // list + nsGkAtoms::list_, + roles::LIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eList, + states::READONLY + }, + { // listbox + nsGkAtoms::listbox, + roles::LISTBOX, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eListControl | eSelect, + states::VERTICAL, + eARIAMultiSelectable, + eARIAReadonly, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // listitem + nsGkAtoms::listitem, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, // XXX: should depend on state, parent accessible + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // log + nsGkAtoms::log_, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + ePoliteLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // main + nsGkAtoms::main, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // mark + nsGkAtoms::mark, + roles::MARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // marquee + nsGkAtoms::marquee, + roles::ANIMATION, + kUseMapRole, + eNoValue, + eNoAction, + eOffLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // math + nsGkAtoms::math, + roles::FLAT_EQUATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // menu + nsGkAtoms::menu, + roles::MENUPOPUP, + kUseMapRole, + eNoValue, + eNoAction, // XXX: technically accessibles of menupopup role haven't + // any action, but menu can be open or close. + eNoLiveAttr, + kGenericAccType, + states::VERTICAL, + eARIAOrientation + }, + { // menubar + nsGkAtoms::menubar, + roles::MENUBAR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // menuitem + nsGkAtoms::menuitem, + roles::MENUITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // menuitemcheckbox + nsGkAtoms::menuitemcheckbox, + roles::CHECK_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed, + eARIAReadonly + }, + { // menuitemradio + nsGkAtoms::menuitemradio, + roles::RADIO_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool, + eARIAReadonly + }, + { // meter + nsGkAtoms::meter, + roles::METER, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // navigation + nsGkAtoms::navigation, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // none + nsGkAtoms::none, + roles::NOTHING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // note + nsGkAtoms::note_, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // option + nsGkAtoms::option, + roles::OPTION, + kUseMapRole, + eNoValue, + eSelectAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable, + eARIACheckedMixed + }, + { // paragraph + nsGkAtoms::paragraph, + roles::PARAGRAPH, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // presentation + nsGkAtoms::presentation, + roles::NOTHING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // progressbar + nsGkAtoms::progressbar, + roles::PROGRESSBAR, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY, + eIndeterminateIfNoValue + }, + { // radio + nsGkAtoms::radio, + roles::RADIOBUTTON, + kUseMapRole, + eNoValue, + eSelectAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool + }, + { // radiogroup + nsGkAtoms::radiogroup, + roles::RADIO_GROUP, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAOrientation, + eARIAReadonly + }, + { // region + nsGkAtoms::region, + roles::REGION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // row + nsGkAtoms::row, + roles::ROW, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableRow, + kNoReqStates, + eARIASelectable + }, + { // rowgroup + nsGkAtoms::rowgroup, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // rowheader + nsGkAtoms::rowheader, + roles::ROWHEADER, + kUseMapRole, + eNoValue, + eSortAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectableIfDefined, + eARIAReadonly + }, + { // scrollbar + nsGkAtoms::scrollbar, + roles::SCROLLBAR, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::VERTICAL, + eARIAOrientation, + eARIAReadonly + }, + { // search + nsGkAtoms::search, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // searchbox + nsGkAtoms::searchbox, + roles::ENTRY, + kUseMapRole, + eNoValue, + eActivateAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAAutoComplete, + eARIAMultiline, + eARIAReadonlyOrEditable + }, + { // separator + nsGkAtoms::separator_, + roles::SEPARATOR, + kUseMapRole, + eHasValueMinMaxIfFocusable, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // slider + nsGkAtoms::slider, + roles::SLIDER, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation, + eARIAReadonly + }, + { // spinbutton + nsGkAtoms::spinbutton, + roles::SPINBUTTON, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAReadonly + }, + { // status + nsGkAtoms::status, + roles::STATUSBAR, + kUseMapRole, + eNoValue, + eNoAction, + ePoliteLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // subscript + nsGkAtoms::subscript, + roles::SUBSCRIPT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType + }, + { // suggestion + nsGkAtoms::suggestion, + roles::SUGGESTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // superscript + nsGkAtoms::superscript, + roles::SUPERSCRIPT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType + }, + { // switch + nsGkAtoms::svgSwitch, + roles::SWITCH, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool, + eARIAReadonly + }, + { // tab + nsGkAtoms::tab, + roles::PAGETAB, + kUseMapRole, + eNoValue, + eSwitchAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable + }, + { // table + nsGkAtoms::table, + roles::TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTable, + kNoReqStates, + eARIASelectable + }, + { // tablist + nsGkAtoms::tablist, + roles::PAGETABLIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect, + states::HORIZONTAL, + eARIAOrientation, + eARIAMultiSelectable + }, + { // tabpanel + nsGkAtoms::tabpanel, + roles::PROPERTYPAGE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // term + nsGkAtoms::term, + roles::TERM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // textbox + nsGkAtoms::textbox, + roles::ENTRY, + kUseMapRole, + eNoValue, + eActivateAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAAutoComplete, + eARIAMultiline, + eARIAReadonlyOrEditable + }, + { // timer + nsGkAtoms::timer, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eOffLiveAttr, + kNoReqStates + }, + { // toolbar + nsGkAtoms::toolbar, + roles::TOOLBAR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // tooltip + nsGkAtoms::tooltip, + roles::TOOLTIP, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // tree + nsGkAtoms::tree, + roles::OUTLINE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect, + states::VERTICAL, + eARIAReadonly, + eARIAMultiSelectable, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // treegrid + nsGkAtoms::treegrid, + roles::TREE_TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect | eTable, + kNoReqStates, + eARIAReadonly, + eARIAMultiSelectable, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // treeitem + nsGkAtoms::treeitem, + roles::OUTLINEITEM, + kUseMapRole, + eNoValue, + eActivateAction, // XXX: should expose second 'expand/collapse' action based + // on states + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable, + eARIACheckedMixed + } + // clang-format on +}; + +static const nsRoleMapEntry sLandmarkRoleMap = { + nsGkAtoms::_empty, roles::NOTHING, kUseNativeRole, eNoValue, + eNoAction, eNoLiveAttr, kGenericAccType, kNoReqStates}; + +nsRoleMapEntry aria::gEmptyRoleMap = { + nsGkAtoms::_empty, roles::TEXT_CONTAINER, kUseMapRole, eNoValue, + eNoAction, eNoLiveAttr, kGenericAccType, kNoReqStates}; + +/** + * Universal (Global) states: + * The following state rules are applied to any accessible element, + * whether there is an ARIA role or not: + */ +static const EStateRule sWAIUnivStateMap[] = { + eARIABusy, eARIACurrent, eARIADisabled, + eARIAExpanded, // Currently under spec review but precedent exists + eARIAHasPopup, // Note this is a tokenised attribute starting in ARIA 1.1 + eARIAInvalid, eARIAModal, + eARIARequired, // XXX not global, Bug 553117 + eARIANone}; + +/** + * ARIA attribute map for attribute characteristics. + * @note ARIA attributes that don't have any flags are not included here. + */ + +struct AttrCharacteristics { + const nsStaticAtom* const attributeName; + const uint8_t characteristics; +}; + +static const AttrCharacteristics gWAIUnivAttrMap[] = { + // clang-format off + {nsGkAtoms::aria_activedescendant, ATTR_BYPASSOBJ }, + {nsGkAtoms::aria_atomic, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_busy, ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_checked, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, /* exposes checkable obj attr */ + {nsGkAtoms::aria_colcount, ATTR_VALINT }, + {nsGkAtoms::aria_colindex, ATTR_VALINT }, + {nsGkAtoms::aria_controls, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_current, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + // XXX Ideally, aria-description shouldn't expose a description object + // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the + // description-from attribute is implemented (bug 1726087), clients such as + // NVDA depend on the description object attribute to work out whether the + // accDescription originated from aria-description. + {nsGkAtoms::aria_description, ATTR_GLOBAL }, + {nsGkAtoms::aria_details, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_disabled, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_dropeffect, ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_errormessage, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_expanded, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_grabbed, ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_haspopup, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_hidden, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, /* handled special way */ + {nsGkAtoms::aria_invalid, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_label, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_level, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {nsGkAtoms::aria_live, ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_modal, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {nsGkAtoms::aria_multiline, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_multiselectable, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_owns, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_orientation, ATTR_VALTOKEN }, + {nsGkAtoms::aria_posinset, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {nsGkAtoms::aria_pressed, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_readonly, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_relevant, ATTR_GLOBAL }, + {nsGkAtoms::aria_required, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_rowcount, ATTR_VALINT }, + {nsGkAtoms::aria_rowindex, ATTR_VALINT }, + {nsGkAtoms::aria_selected, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_setsize, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {nsGkAtoms::aria_sort, ATTR_VALTOKEN }, + {nsGkAtoms::aria_valuenow, ATTR_BYPASSOBJ }, + {nsGkAtoms::aria_valuemin, ATTR_BYPASSOBJ }, + {nsGkAtoms::aria_valuemax, ATTR_BYPASSOBJ }, + {nsGkAtoms::aria_valuetext, ATTR_BYPASSOBJ } + // clang-format on +}; + +const nsRoleMapEntry* aria::GetRoleMap(dom::Element* aEl) { + return GetRoleMapFromIndex(GetRoleMapIndex(aEl)); +} + +uint8_t aria::GetRoleMapIndex(dom::Element* aEl) { + nsAutoString roles; + if (!aEl || !nsAccUtils::GetARIAAttr(aEl, nsGkAtoms::role, roles) || + roles.IsEmpty()) { + // We treat role="" as if the role attribute is absent (per aria spec:8.1.1) + return NO_ROLE_MAP_ENTRY_INDEX; + } + + nsWhitespaceTokenizer tokenizer(roles); + while (tokenizer.hasMoreTokens()) { + // Do a binary search through table for the next role in role list + const nsDependentSubstring role = tokenizer.nextToken(); + size_t idx; + auto comparator = [&role](const nsRoleMapEntry& aEntry) { + return Compare(role, aEntry.ARIARoleString(), + nsCaseInsensitiveStringComparator); + }; + if (BinarySearchIf(sWAIRoleMaps, 0, ArrayLength(sWAIRoleMaps), comparator, + &idx)) { + return idx; + } + } + + // Always use some entry index if there is a non-empty role string + // To ensure an accessible object is created + return LANDMARK_ROLE_MAP_ENTRY_INDEX; +} + +const nsRoleMapEntry* aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex) { + switch (aRoleMapIndex) { + case NO_ROLE_MAP_ENTRY_INDEX: + return nullptr; + case EMPTY_ROLE_MAP_ENTRY_INDEX: + return &gEmptyRoleMap; + case LANDMARK_ROLE_MAP_ENTRY_INDEX: + return &sLandmarkRoleMap; + default: + return sWAIRoleMaps + aRoleMapIndex; + } +} + +uint8_t aria::GetIndexFromRoleMap(const nsRoleMapEntry* aRoleMapEntry) { + if (aRoleMapEntry == nullptr) { + return NO_ROLE_MAP_ENTRY_INDEX; + } else if (aRoleMapEntry == &gEmptyRoleMap) { + return EMPTY_ROLE_MAP_ENTRY_INDEX; + } else if (aRoleMapEntry == &sLandmarkRoleMap) { + return LANDMARK_ROLE_MAP_ENTRY_INDEX; + } else { + uint8_t index = aRoleMapEntry - sWAIRoleMaps; + MOZ_ASSERT(aria::IsRoleMapIndexValid(index)); + return index; + } +} + +bool aria::IsRoleMapIndexValid(uint8_t aRoleMapIndex) { + switch (aRoleMapIndex) { + case NO_ROLE_MAP_ENTRY_INDEX: + case EMPTY_ROLE_MAP_ENTRY_INDEX: + case LANDMARK_ROLE_MAP_ENTRY_INDEX: + return true; + } + return aRoleMapIndex < ArrayLength(sWAIRoleMaps); +} + +uint64_t aria::UniversalStatesFor(mozilla::dom::Element* aElement) { + uint64_t state = 0; + uint32_t index = 0; + while (MapToState(sWAIUnivStateMap[index], aElement, &state)) index++; + + return state; +} + +uint8_t aria::AttrCharacteristicsFor(nsAtom* aAtom) { + for (uint32_t i = 0; i < ArrayLength(gWAIUnivAttrMap); i++) { + if (gWAIUnivAttrMap[i].attributeName == aAtom) { + return gWAIUnivAttrMap[i].characteristics; + } + } + + return 0; +} + +bool aria::HasDefinedARIAHidden(nsIContent* aContent) { + return aContent && aContent->IsElement() && + nsAccUtils::ARIAAttrValueIs(aContent->AsElement(), + nsGkAtoms::aria_hidden, nsGkAtoms::_true, + eCaseMatters); +} + +//////////////////////////////////////////////////////////////////////////////// +// AttrIterator class + +AttrIterator::AttrIterator(nsIContent* aContent) + : mElement(dom::Element::FromNode(aContent)), + mIteratingDefaults(false), + mAttrIdx(0), + mAttrCharacteristics(0) { + mAttrs = mElement ? &mElement->GetAttrs() : nullptr; + mAttrCount = mAttrs ? mAttrs->AttrCount() : 0; +} + +bool AttrIterator::Next() { + while (mAttrIdx < mAttrCount) { + const nsAttrName* attr = mAttrs->GetSafeAttrNameAt(mAttrIdx); + mAttrIdx++; + if (attr->NamespaceEquals(kNameSpaceID_None)) { + mAttrAtom = attr->Atom(); + nsDependentAtomString attrStr(mAttrAtom); + if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // Not ARIA + + if (mIteratingDefaults) { + if (mOverriddenAttrs.Contains(mAttrAtom)) { + continue; + } + } else { + mOverriddenAttrs.Insert(mAttrAtom); + } + + // AttrCharacteristicsFor has to search for the entry, so cache it here + // rather than having to search again later. + mAttrCharacteristics = aria::AttrCharacteristicsFor(mAttrAtom); + if (mAttrCharacteristics & ATTR_BYPASSOBJ) { + continue; // No need to handle exposing as obj attribute here + } + + if ((mAttrCharacteristics & ATTR_VALTOKEN) && + !nsAccUtils::HasDefinedARIAToken(mAttrs, mAttrAtom)) { + continue; // only expose token based attributes if they are defined + } + + if ((mAttrCharacteristics & ATTR_BYPASSOBJ_IF_FALSE) && + mAttrs->AttrValueIs(kNameSpaceID_None, mAttrAtom, nsGkAtoms::_false, + eCaseMatters)) { + continue; // only expose token based attribute if value is not 'false'. + } + + return true; + } + } + + mAttrCharacteristics = 0; + mAttrAtom = nullptr; + + if (const auto* defaults = nsAccUtils::GetARIADefaults(mElement); + !mIteratingDefaults && defaults) { + mIteratingDefaults = true; + mAttrs = defaults; + mAttrCount = mAttrs->AttrCount(); + mAttrIdx = 0; + return Next(); + } + + return false; +} + +nsAtom* AttrIterator::AttrName() const { return mAttrAtom; } + +void AttrIterator::AttrValue(nsAString& aAttrValue) const { + nsAutoString value; + if (mAttrs->GetAttr(kNameSpaceID_None, mAttrAtom, value)) { + if (mAttrCharacteristics & ATTR_VALTOKEN) { + nsAtom* normalizedValue = + nsAccUtils::NormalizeARIAToken(mAttrs, mAttrAtom); + if (normalizedValue) { + nsDependentAtomString normalizedValueStr(normalizedValue); + aAttrValue.Assign(normalizedValueStr); + return; + } + } + aAttrValue.Assign(value); + } +} + +bool AttrIterator::ExposeAttr(AccAttributes* aTargetAttrs) const { + if (mAttrCharacteristics & ATTR_VALTOKEN) { + nsAtom* normalizedValue = nsAccUtils::NormalizeARIAToken(mAttrs, mAttrAtom); + if (normalizedValue) { + aTargetAttrs->SetAttribute(mAttrAtom, normalizedValue); + return true; + } + } else if (mAttrCharacteristics & ATTR_VALINT) { + int32_t intVal; + if (nsCoreUtils::GetUIntAttrValue(mAttrs->GetAttr(mAttrAtom), &intVal)) { + aTargetAttrs->SetAttribute(mAttrAtom, intVal); + return true; + } + if (mAttrAtom == nsGkAtoms::aria_colcount || + mAttrAtom == nsGkAtoms::aria_rowcount) { + // These attributes allow a value of -1. + if (mAttrs->AttrValueIs(kNameSpaceID_None, mAttrAtom, u"-1"_ns, + eCaseMatters)) { + aTargetAttrs->SetAttribute(mAttrAtom, -1); + return true; + } + } + return false; // Invalid value. + } + nsAutoString value; + if (mAttrs->GetAttr(kNameSpaceID_None, mAttrAtom, value)) { + aTargetAttrs->SetAttribute(mAttrAtom, std::move(value)); + return true; + } + return false; +} diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h new file mode 100644 index 0000000000..7e0d9469b2 --- /dev/null +++ b/accessible/base/ARIAMap.h @@ -0,0 +1,335 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_aria_ARIAMap_h_ +#define mozilla_a11y_aria_ARIAMap_h_ + +#include "ARIAStateMap.h" +#include "mozilla/a11y/AccTypes.h" +#include "mozilla/a11y/Role.h" + +#include "nsAtom.h" +#include "nsIContent.h" +#include "nsTHashSet.h" + +class nsINode; + +namespace mozilla::dom { +class Element; +} + +//////////////////////////////////////////////////////////////////////////////// +// Value constants + +/** + * Used to define if role requires to expose Value interface. + */ +enum EValueRule { + /** + * Value interface isn't exposed. + */ + eNoValue, + + /** + * Value interface is implemented, supports value, min and max from + * aria-valuenow, aria-valuemin and aria-valuemax. + */ + eHasValueMinMax, + + /** + * Value interface is implemented, but only if the element is focusable. + * For instance, in ARIA 1.1 the ability for authors to create adjustable + * splitters was provided by supporting the value interface on separators + * that are focusable. Non-focusable separators expose no value information. + */ + eHasValueMinMaxIfFocusable +}; + +//////////////////////////////////////////////////////////////////////////////// +// Action constants + +/** + * Used to define if the role requires to expose action. + */ +enum EActionRule { + eNoAction, + eActivateAction, + eClickAction, + ePressAction, + eCheckUncheckAction, + eExpandAction, + eJumpAction, + eOpenCloseAction, + eSelectAction, + eSortAction, + eSwitchAction +}; + +//////////////////////////////////////////////////////////////////////////////// +// Live region constants + +/** + * Used to define if role exposes default value of aria-live attribute. + */ +enum ELiveAttrRule { + eNoLiveAttr, + eOffLiveAttr, + ePoliteLiveAttr, + eAssertiveLiveAttr +}; + +//////////////////////////////////////////////////////////////////////////////// +// Role constants + +/** + * ARIA role overrides role from native markup. + */ +const bool kUseMapRole = true; + +/** + * ARIA role doesn't override the role from native markup. + */ +const bool kUseNativeRole = false; + +//////////////////////////////////////////////////////////////////////////////// +// ARIA attribute characteristic masks + +/** + * This mask indicates the attribute should not be exposed as an object + * attribute via the catch-all logic in Accessible::Attributes(). + * This means it either isn't mean't to be exposed as an object attribute, or + * that it should, but is already handled in other code. + */ +const uint8_t ATTR_BYPASSOBJ = 0x1 << 0; +const uint8_t ATTR_BYPASSOBJ_IF_FALSE = 0x1 << 1; + +/** + * This mask indicates the attribute is expected to have an NMTOKEN or bool + * value. (See for example usage in Accessible::Attributes()) + */ +const uint8_t ATTR_VALTOKEN = 0x1 << 2; + +/** + * Indicate the attribute is global state or property (refer to + * http://www.w3.org/TR/wai-aria/states_and_properties#global_states). + */ +const uint8_t ATTR_GLOBAL = 0x1 << 3; + +/** + * Indicates that the attribute should have an integer value. + */ +const uint8_t ATTR_VALINT = 0x1 << 4; + +//////////////////////////////////////////////////////////////////////////////// +// State map entry + +/** + * Used in nsRoleMapEntry.state if no nsIAccessibleStates are automatic for + * a given role. + */ +#define kNoReqStates 0 + +//////////////////////////////////////////////////////////////////////////////// +// Role map entry + +/** + * For each ARIA role, this maps the nsIAccessible information. + */ +struct nsRoleMapEntry { + /** + * Return true if matches to the given ARIA role. + */ + bool Is(nsAtom* aARIARole) const { return roleAtom == aARIARole; } + + /** + * Return true if ARIA role has the given accessible type. + */ + bool IsOfType(mozilla::a11y::AccGenericType aType) const { + return accTypes & aType; + } + + /** + * Return ARIA role. + */ + const nsDependentAtomString ARIARoleString() const { + return nsDependentAtomString(roleAtom); + } + + // ARIA role: string representation such as "button" + nsStaticAtom* const roleAtom; + + // Role mapping rule: maps to enum Role + mozilla::a11y::role role; + + // Role rule: whether to use mapped role or native semantics + bool roleRule; + + // Value mapping rule: how to compute accessible value + EValueRule valueRule; + + // Action mapping rule, how to expose accessible action + EActionRule actionRule; + + // 'live' and 'container-live' object attributes mapping rule: how to expose + // these object attributes if ARIA 'live' attribute is missed. + ELiveAttrRule liveAttRule; + + // LocalAccessible types this role belongs to. + uint32_t accTypes; + + // Automatic state mapping rule: always include in states + uint64_t state; // or kNoReqStates if no default state for this role + + // ARIA properties supported for this role (in other words, the aria-foo + // attribute to accessible states mapping rules). + // Currently you cannot have unlimited mappings, because + // a variable sized array would not allow the use of + // C++'s struct initialization feature. + mozilla::a11y::aria::EStateRule attributeMap1; + mozilla::a11y::aria::EStateRule attributeMap2; + mozilla::a11y::aria::EStateRule attributeMap3; + mozilla::a11y::aria::EStateRule attributeMap4; +}; + +//////////////////////////////////////////////////////////////////////////////// +// ARIA map + +/** + * These provide the mappings for WAI-ARIA roles, states and properties using + * the structs defined in this file and ARIAStateMap files. + */ +namespace mozilla { +namespace a11y { +class AccAttributes; + +namespace aria { + +/** + * Empty role map entry. Used by accessibility service to create an accessible + * if the accessible can't use role of used accessible class. For example, + * it is used for table cells that aren't contained by table. + */ +extern nsRoleMapEntry gEmptyRoleMap; + +/** + * Constants for the role map entry index to indicate that the role map entry + * isn't in sWAIRoleMaps, but rather is a special entry: nullptr, + * gEmptyRoleMap, and sLandmarkRoleMap + */ +const uint8_t NO_ROLE_MAP_ENTRY_INDEX = UINT8_MAX - 2; +const uint8_t EMPTY_ROLE_MAP_ENTRY_INDEX = UINT8_MAX - 1; +const uint8_t LANDMARK_ROLE_MAP_ENTRY_INDEX = UINT8_MAX; + +/** + * Get the role map entry for a given DOM node. This will use the first + * ARIA role if the role attribute provides a space delimited list of roles. + * + * @param aEl [in] the DOM node to get the role map entry for + * @return a pointer to the role map entry for the ARIA role, or nullptr + * if none + */ +const nsRoleMapEntry* GetRoleMap(dom::Element* aEl); + +/** + * Get the role map entry pointer's index for a given DOM node. This will use + * the first ARIA role if the role attribute provides a space delimited list of + * roles. + * + * @param aEl [in] the DOM node to get the role map entry for + * @return the index of the pointer to the role map entry for the ARIA + * role, or NO_ROLE_MAP_ENTRY_INDEX if none + */ +uint8_t GetRoleMapIndex(dom::Element* aEl); + +/** + * Get the role map entry pointer for a given role map entry index. + * + * @param aRoleMapIndex [in] the role map index to get the role map entry + * pointer for + * @return a pointer to the role map entry for the ARIA role, + * or nullptr, if none + */ +const nsRoleMapEntry* GetRoleMapFromIndex(uint8_t aRoleMapIndex); + +/** + * Get the role map entry index for a given role map entry pointer. If the role + * map entry is within sWAIRoleMaps, return the index within that array, + * otherwise return one of the special index constants listed above. + * + * @param aRoleMap [in] the role map entry pointer to get the index for + * @return the index of the pointer to the role map entry, or + * NO_ROLE_MAP_ENTRY_INDEX if none + */ +uint8_t GetIndexFromRoleMap(const nsRoleMapEntry* aRoleMap); + +/** + * Determine whether a role map entry index is valid. + */ +bool IsRoleMapIndexValid(uint8_t aRoleMapIndex); + +/** + * Return accessible state from ARIA universal states applied to the given + * element. + */ +uint64_t UniversalStatesFor(dom::Element* aElement); + +/** + * Get the ARIA attribute characteristics for a given ARIA attribute. + * + * @param aAtom ARIA attribute + * @return A bitflag representing the attribute characteristics + * (see above for possible bit masks, prefixed "ATTR_") + */ +uint8_t AttrCharacteristicsFor(nsAtom* aAtom); + +/** + * Return true if the element has defined aria-hidden. + */ +bool HasDefinedARIAHidden(nsIContent* aContent); + +/** + * Represents a simple enumerator for iterating through ARIA attributes + * exposed as object attributes on a given accessible. + */ +class AttrIterator { + public: + explicit AttrIterator(nsIContent* aContent); + + bool Next(); + + nsAtom* AttrName() const; + + void AttrValue(nsAString& aAttrValue) const; + + /** + * Expose this ARIA attribute in a specified AccAttributes. The appropriate + * type will be used for the attribute; e.g. an atom for a token value. + */ + bool ExposeAttr(AccAttributes* aTargetAttrs) const; + + private: + AttrIterator() = delete; + AttrIterator(const AttrIterator&) = delete; + AttrIterator& operator=(const AttrIterator&) = delete; + + dom::Element* mElement; + + bool mIteratingDefaults; + nsTHashSet> mOverriddenAttrs; + + const AttrArray* mAttrs; + uint32_t mAttrIdx; + uint32_t mAttrCount; + RefPtr mAttrAtom; + uint8_t mAttrCharacteristics; +}; + +} // namespace aria +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/ARIAStateMap.cpp b/accessible/base/ARIAStateMap.cpp new file mode 100644 index 0000000000..6bf20cf1cc --- /dev/null +++ b/accessible/base/ARIAStateMap.cpp @@ -0,0 +1,334 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "ARIAMap.h" +#include "nsAccUtils.h" +#include "States.h" + +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::a11y::aria; + +/** + * Used to store state map rule data for ARIA attribute of enum type. + */ +struct EnumTypeData { + // ARIA attribute name. + nsStaticAtom* const mAttrName; + + // States if the attribute value is matched to the enum value. Used as + // Element::AttrValuesArray, last item must be nullptr. + nsStaticAtom* const mValues[4]; + + // States applied if corresponding enum values are matched. + const uint64_t mStates[3]; + + // States to clear in case of match. + const uint64_t mClearState; +}; + +enum ETokenType { + eBoolType = 0, + eMixedType = 1, // can take 'mixed' value + eDefinedIfAbsent = 2 // permanent and false state are applied if absent +}; + +/** + * Used to store state map rule data for ARIA attribute of token type (including + * mixed value). + */ +struct TokenTypeData { + TokenTypeData(nsAtom* aAttrName, uint32_t aType, uint64_t aPermanentState, + uint64_t aTrueState, uint64_t aFalseState = 0) + : mAttrName(aAttrName), + mType(aType), + mPermanentState(aPermanentState), + mTrueState(aTrueState), + mFalseState(aFalseState) {} + + // ARIA attribute name. + nsAtom* const mAttrName; + + // Type. + const uint32_t mType; + + // State applied if the attribute is defined or mType doesn't have + // eDefinedIfAbsent flag set. + const uint64_t mPermanentState; + + // States applied if the attribute value is true/false. + const uint64_t mTrueState; + const uint64_t mFalseState; +}; + +/** + * Map enum type attribute value to accessible state. + */ +static void MapEnumType(dom::Element* aElement, uint64_t* aState, + const EnumTypeData& aData); + +/** + * Map token type attribute value to states. + */ +static void MapTokenType(dom::Element* aContent, uint64_t* aState, + const TokenTypeData& aData); + +bool aria::MapToState(EStateRule aRule, dom::Element* aElement, + uint64_t* aState) { + switch (aRule) { + case eARIAAutoComplete: { + static const EnumTypeData data = { + nsGkAtoms::aria_autocomplete, + {nsGkAtoms::inlinevalue, nsGkAtoms::list_, nsGkAtoms::both, nullptr}, + {states::SUPPORTS_AUTOCOMPLETION, + states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION, + states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION}, + 0}; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIABusy: { + static const EnumTypeData data = { + nsGkAtoms::aria_busy, + {nsGkAtoms::_true, nsGkAtoms::error, nullptr}, + {states::BUSY, states::INVALID}, + 0}; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIACheckableBool: { + static const TokenTypeData data(nsGkAtoms::aria_checked, + eBoolType | eDefinedIfAbsent, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIACheckableMixed: { + static const TokenTypeData data(nsGkAtoms::aria_checked, + eMixedType | eDefinedIfAbsent, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIACheckedMixed: { + static const TokenTypeData data(nsGkAtoms::aria_checked, eMixedType, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIACurrent: { + static const TokenTypeData data(nsGkAtoms::aria_current, eBoolType, 0, + states::CURRENT); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIADisabled: { + static const TokenTypeData data(nsGkAtoms::aria_disabled, eBoolType, 0, + states::UNAVAILABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAExpanded: { + static const TokenTypeData data(nsGkAtoms::aria_expanded, eBoolType, 0, + states::EXPANDED, states::COLLAPSED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAHasPopup: { + static const TokenTypeData data(nsGkAtoms::aria_haspopup, eBoolType, 0, + states::HASPOPUP); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAInvalid: { + static const TokenTypeData data(nsGkAtoms::aria_invalid, eBoolType, 0, + states::INVALID); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAModal: { + static const TokenTypeData data(nsGkAtoms::aria_modal, eBoolType, 0, + states::MODAL); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAMultiline: { + static const TokenTypeData data(nsGkAtoms::aria_multiline, + eBoolType | eDefinedIfAbsent, 0, + states::MULTI_LINE, states::SINGLE_LINE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAMultiSelectable: { + static const TokenTypeData data( + nsGkAtoms::aria_multiselectable, eBoolType, 0, + states::MULTISELECTABLE | states::EXTSELECTABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAOrientation: { + static const EnumTypeData data = { + nsGkAtoms::aria_orientation, + {nsGkAtoms::horizontal, nsGkAtoms::vertical, nullptr}, + {states::HORIZONTAL, states::VERTICAL}, + states::HORIZONTAL | states::VERTICAL}; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIAPressed: { + static const TokenTypeData data(nsGkAtoms::aria_pressed, eMixedType, 0, + states::PRESSED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAReadonly: { + static const TokenTypeData data(nsGkAtoms::aria_readonly, eBoolType, 0, + states::READONLY); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAReadonlyOrEditable: { + static const TokenTypeData data(nsGkAtoms::aria_readonly, + eBoolType | eDefinedIfAbsent, 0, + states::READONLY, states::EDITABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIARequired: { + static const TokenTypeData data(nsGkAtoms::aria_required, eBoolType, 0, + states::REQUIRED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIASelectable: { + static const TokenTypeData data(nsGkAtoms::aria_selected, + eBoolType | eDefinedIfAbsent, + states::SELECTABLE, states::SELECTED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIASelectableIfDefined: { + static const TokenTypeData data(nsGkAtoms::aria_selected, eBoolType, + states::SELECTABLE, states::SELECTED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eReadonlyUntilEditable: { + if (!(*aState & states::EDITABLE)) *aState |= states::READONLY; + + return true; + } + + case eIndeterminateIfNoValue: { + if (!nsAccUtils::HasARIAAttr(aElement, nsGkAtoms::aria_valuenow) && + !nsAccUtils::HasARIAAttr(aElement, nsGkAtoms::aria_valuetext)) { + *aState |= states::MIXED; + } + + return true; + } + + case eFocusableUntilDisabled: { + if (!nsAccUtils::HasDefinedARIAToken(aElement, + nsGkAtoms::aria_disabled) || + nsAccUtils::ARIAAttrValueIs(aElement, nsGkAtoms::aria_disabled, + nsGkAtoms::_false, eCaseMatters)) { + *aState |= states::FOCUSABLE; + } + + return true; + } + + default: + return false; + } +} + +static void MapEnumType(dom::Element* aElement, uint64_t* aState, + const EnumTypeData& aData) { + switch (nsAccUtils::FindARIAAttrValueIn(aElement, aData.mAttrName, + aData.mValues, eCaseMatters)) { + case 0: + *aState = (*aState & ~aData.mClearState) | aData.mStates[0]; + return; + case 1: + *aState = (*aState & ~aData.mClearState) | aData.mStates[1]; + return; + case 2: + *aState = (*aState & ~aData.mClearState) | aData.mStates[2]; + return; + } +} + +static void MapTokenType(dom::Element* aElement, uint64_t* aState, + const TokenTypeData& aData) { + if (nsAccUtils::HasDefinedARIAToken(aElement, aData.mAttrName)) { + if (nsAccUtils::ARIAAttrValueIs(aElement, aData.mAttrName, nsGkAtoms::mixed, + eCaseMatters)) { + if (aData.mType & eMixedType) { + *aState |= aData.mPermanentState | states::MIXED; + } else { // unsupported use of 'mixed' is an authoring error + *aState |= aData.mPermanentState | aData.mFalseState; + } + return; + } + + if (nsAccUtils::ARIAAttrValueIs(aElement, aData.mAttrName, + nsGkAtoms::_false, eCaseMatters)) { + *aState |= aData.mPermanentState | aData.mFalseState; + return; + } + + *aState |= aData.mPermanentState | aData.mTrueState; + return; + } + + if (aData.mType & eDefinedIfAbsent) { + *aState |= aData.mPermanentState | aData.mFalseState; + } +} diff --git a/accessible/base/ARIAStateMap.h b/accessible/base/ARIAStateMap.h new file mode 100644 index 0000000000..20490aa901 --- /dev/null +++ b/accessible/base/ARIAStateMap.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef _mozilla_a11y_aria_ARIAStateMap_h_ +#define _mozilla_a11y_aria_ARIAStateMap_h_ + +#include + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { +namespace aria { + +/** + * List of the ARIA state mapping rules. + */ +enum EStateRule { + eARIANone, + eARIAAutoComplete, + eARIABusy, + eARIACheckableBool, + eARIACheckableMixed, + eARIACheckedMixed, + eARIACurrent, + eARIADisabled, + eARIAExpanded, + eARIAHasPopup, + eARIAInvalid, + eARIAModal, + eARIAMultiline, + eARIAMultiSelectable, + eARIAOrientation, + eARIAPressed, + eARIAReadonly, + eARIAReadonlyOrEditable, + eARIARequired, + eARIASelectable, + eARIASelectableIfDefined, + eReadonlyUntilEditable, + eIndeterminateIfNoValue, + eFocusableUntilDisabled +}; + +/** + * Expose the accessible states for the given element accordingly to state + * mapping rule. + * + * @param aRule [in] state mapping rule ID + * @param aElement [in] node of the accessible + * @param aState [in/out] accessible states + * @return true if state map rule ID is valid + */ +bool MapToState(EStateRule aRule, dom::Element* aElement, uint64_t* aState); + +} // namespace aria +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccAttributes.cpp b/accessible/base/AccAttributes.cpp new file mode 100644 index 0000000000..2c557fe66b --- /dev/null +++ b/accessible/base/AccAttributes.cpp @@ -0,0 +1,271 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccAttributes.h" +#include "StyleInfo.h" +#include "mozilla/Maybe.h" +#include "mozilla/ToString.h" +#include "nsAtom.h" + +using namespace mozilla::a11y; + +bool AccAttributes::GetAttribute(nsAtom* aAttrName, + nsAString& aAttrValue) const { + if (auto value = mData.Lookup(aAttrName)) { + StringFromValueAndName(aAttrName, *value, aAttrValue); + return true; + } + + return false; +} + +void AccAttributes::StringFromValueAndName(nsAtom* aAttrName, + const AttrValueType& aValue, + nsAString& aValueString) { + aValueString.Truncate(); + + aValue.match( + [&aValueString](const bool& val) { + aValueString.Assign(val ? u"true" : u"false"); + }, + [&aValueString](const float& val) { + aValueString.AppendFloat(val * 100); + aValueString.Append(u"%"); + }, + [&aValueString](const double& val) { aValueString.AppendFloat(val); }, + [&aValueString](const int32_t& val) { aValueString.AppendInt(val); }, + [&aValueString](const RefPtr& val) { + val->ToString(aValueString); + }, + [&aValueString](const nsTArray& val) { + if (const size_t len = val.Length()) { + for (size_t i = 0; i < len - 1; i++) { + aValueString.AppendInt(val[i]); + aValueString.Append(u", "); + } + aValueString.AppendInt(val[len - 1]); + } else { + // The array is empty + NS_WARNING( + "Hmm, should we have used a DeleteEntry() for this instead?"); + aValueString.Append(u"[ ]"); + } + }, + [&aValueString](const CSSCoord& val) { + aValueString.AppendFloat(val); + aValueString.Append(u"px"); + }, + [&aValueString](const FontSize& val) { + aValueString.AppendInt(val.mValue); + aValueString.Append(u"pt"); + }, + [&aValueString](const Color& val) { + StyleInfo::FormatColor(val.mValue, aValueString); + }, + [&aValueString](const DeleteEntry& val) { + aValueString.Append(u"-delete-entry-"); + }, + [&aValueString](const UniquePtr& val) { + aValueString.Assign(*val); + }, + [&aValueString](const RefPtr& val) { + aValueString.Assign(u"AccAttributes{...}"); + }, + [&aValueString](const uint64_t& val) { aValueString.AppendInt(val); }, + [&aValueString](const UniquePtr& val) { + aValueString.Assign(u"AccGroupInfo{...}"); + }, + [&aValueString](const UniquePtr& val) { + aValueString.AppendPrintf("Matrix4x4=%s", ToString(*val).c_str()); + }, + [&aValueString](const nsTArray& val) { + if (const size_t len = val.Length()) { + for (size_t i = 0; i < len - 1; i++) { + aValueString.AppendInt(val[i]); + aValueString.Append(u", "); + } + aValueString.AppendInt(val[len - 1]); + } else { + // The array is empty + NS_WARNING( + "Hmm, should we have used a DeleteEntry() for this instead?"); + aValueString.Append(u"[ ]"); + } + }); +} + +void AccAttributes::Update(AccAttributes* aOther) { + for (auto iter = aOther->mData.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data().is()) { + mData.Remove(iter.Key()); + } else { + mData.InsertOrUpdate(iter.Key(), std::move(iter.Data())); + } + iter.Remove(); + } +} + +bool AccAttributes::Equal(const AccAttributes* aOther) const { + if (Count() != aOther->Count()) { + return false; + } + for (auto iter = mData.ConstIter(); !iter.Done(); iter.Next()) { + const auto otherEntry = aOther->mData.Lookup(iter.Key()); + if (!otherEntry) { + return false; + } + if (iter.Data().is>()) { + // Because we store nsString in a UniquePtr, we must handle it specially + // so we compare the string and not the pointer. + if (!otherEntry->is>()) { + return false; + } + const auto& thisStr = iter.Data().as>(); + const auto& otherStr = otherEntry->as>(); + if (*thisStr != *otherStr) { + return false; + } + } else if (iter.Data() != otherEntry.Data()) { + return false; + } + } + return true; +} + +void AccAttributes::CopyTo(AccAttributes* aDest) const { + for (auto iter = mData.ConstIter(); !iter.Done(); iter.Next()) { + iter.Data().match( + [&iter, &aDest](const bool& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const float& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const double& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const int32_t& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const RefPtr& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const nsTArray& val) { + // We don't copy arrays. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an array"); + }, + [&iter, &aDest](const CSSCoord& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const FontSize& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const Color& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const DeleteEntry& val) { + // We don't copy DeleteEntry. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing a DeleteEntry"); + }, + [&iter, &aDest](const UniquePtr& val) { + aDest->SetAttributeStringCopy(iter.Key(), *val); + }, + [](const RefPtr& val) { + // We don't copy nested AccAttributes. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an AccAttributes"); + }, + [&iter, &aDest](const uint64_t& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const UniquePtr& val) { + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an AccGroupInfo"); + }, + [](const UniquePtr& val) { + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing a matrix"); + }, + [](const nsTArray& val) { + // We don't copy arrays. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an array"); + }); + } +} + +#ifdef A11Y_LOG +void AccAttributes::DebugPrint(const char* aPrefix, + const AccAttributes& aAttributes) { + nsAutoString prettyString; + prettyString.AssignLiteral("{\n"); + for (const auto& iter : aAttributes) { + nsAutoString name; + iter.NameAsString(name); + + nsAutoString value; + iter.ValueAsString(value); + prettyString.AppendLiteral(" "); + prettyString.Append(name); + prettyString.AppendLiteral(": "); + prettyString.Append(value); + prettyString.AppendLiteral("\n"); + } + + prettyString.AppendLiteral("}"); + printf("%s %s\n", aPrefix, NS_ConvertUTF16toUTF8(prettyString).get()); +} +#endif + +size_t AccAttributes::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = + aMallocSizeOf(this) + mData.ShallowSizeOfExcludingThis(aMallocSizeOf); + + for (auto iter : *this) { + size += iter.SizeOfExcludingThis(aMallocSizeOf); + } + + return size; +} + +size_t AccAttributes::Entry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = 0; + + // We don't count the size of Name() since it's counted by the atoms table + // memory reporter. + + if (mValue->is>()) { + size += mValue->as>().ShallowSizeOfExcludingThis( + aMallocSizeOf); + } else if (mValue->is>()) { + // String data will never be shared. + size += mValue->as>()->SizeOfIncludingThisIfUnshared( + aMallocSizeOf); + } else if (mValue->is>()) { + size += + mValue->as>()->SizeOfIncludingThis(aMallocSizeOf); + } else if (mValue->is>()) { + size += mValue->as>()->SizeOfIncludingThis( + aMallocSizeOf); + } else if (mValue->is>()) { + size += aMallocSizeOf(mValue->as>().get()); + } else if (mValue->is>()) { + size += mValue->as>().ShallowSizeOfExcludingThis( + aMallocSizeOf); + } else { + // This type is stored directly and already counted or is an atom and + // stored and counted in the atoms table. + // Assert that we have exhausted all the remaining variant types. + MOZ_ASSERT(mValue->is>() || mValue->is() || + mValue->is() || mValue->is() || + mValue->is() || mValue->is() || + mValue->is() || mValue->is() || + mValue->is() || mValue->is()); + } + + return size; +} diff --git a/accessible/base/AccAttributes.h b/accessible/base/AccAttributes.h new file mode 100644 index 0000000000..6ee78aaaa0 --- /dev/null +++ b/accessible/base/AccAttributes.h @@ -0,0 +1,294 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef AccAttributes_h_ +#define AccAttributes_h_ + +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/a11y/AccGroupInfo.h" +#include "mozilla/Variant.h" +#include "nsTHashMap.h" +#include "nsAtom.h" +#include "nsStringFwd.h" +#include "mozilla/gfx/Matrix.h" + +class nsVariant; + +namespace IPC { +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { + +struct FontSize { + int32_t mValue; + + bool operator==(const FontSize& aOther) const { + return mValue == aOther.mValue; + } + + bool operator!=(const FontSize& aOther) const { + return mValue != aOther.mValue; + } +}; + +struct Color { + nscolor mValue; + + bool operator==(const Color& aOther) const { return mValue == aOther.mValue; } + + bool operator!=(const Color& aOther) const { return mValue != aOther.mValue; } +}; + +// A special type. If an entry has a value of this type, it instructs the +// target instance of an Update to remove the entry with the same key value. +struct DeleteEntry { + DeleteEntry() : mValue(true) {} + bool mValue; + + bool operator==(const DeleteEntry& aOther) const { return true; } + + bool operator!=(const DeleteEntry& aOther) const { return false; } +}; + +class AccAttributes { + // Warning! An AccAttributes can contain another AccAttributes. This is + // intended for object and text attributes. However, the nested + // AccAttributes should never itself contain another AccAttributes, nor + // should it create a cycle. We don't do cycle collection here for + // performance reasons, so violating this rule will cause leaks! + using AttrValueType = + Variant, nsTArray, + CSSCoord, FontSize, Color, DeleteEntry, UniquePtr, + RefPtr, uint64_t, UniquePtr, + UniquePtr, nsTArray>; + static_assert(sizeof(AttrValueType) <= 16); + using AtomVariantMap = nsTHashMap, AttrValueType>; + + protected: + ~AccAttributes() = default; + + public: + AccAttributes() = default; + AccAttributes(const AccAttributes&) = delete; + AccAttributes& operator=(const AccAttributes&) = delete; + + NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::AccAttributes) + + template + void SetAttribute(nsAtom* aAttrName, T&& aAttrValue) { + using ValType = + std::remove_const_t>; + if constexpr (std::is_convertible_v) { + static_assert(std::is_rvalue_reference_v, + "Please only move strings into this function. To make a " + "copy, use SetAttributeStringCopy."); + UniquePtr value = MakeUnique(std::move(aAttrValue)); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_same_v) { + UniquePtr value = MakeUnique(aAttrValue); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_same_v) { + UniquePtr value(aAttrValue); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_convertible_v) { + mData.InsertOrUpdate(aAttrName, AsVariant(RefPtr(aAttrValue))); + } else { + mData.InsertOrUpdate(aAttrName, AsVariant(std::forward(aAttrValue))); + } + } + + void SetAttributeStringCopy(nsAtom* aAttrName, nsString aAttrValue) { + SetAttribute(aAttrName, std::move(aAttrValue)); + } + + template + Maybe GetAttribute(nsAtom* aAttrName) const { + if (auto value = mData.Lookup(aAttrName)) { + if constexpr (std::is_same_v) { + if (value->is>()) { + const T& val = *(value->as>().get()); + return SomeRef(val); + } + } else if constexpr (std::is_same_v) { + if (value->is>()) { + const T& val = *(value->as>()); + return SomeRef(val); + } + } else { + if (value->is()) { + const T& val = value->as(); + return SomeRef(val); + } + } + } + return Nothing(); + } + + template + RefPtr GetAttributeRefPtr(nsAtom* aAttrName) const { + if (auto value = mData.Lookup(aAttrName)) { + if (value->is>()) { + RefPtr ref = value->as>(); + return ref; + } + } + return nullptr; + } + + template + Maybe GetMutableAttribute(nsAtom* aAttrName) const { + static_assert(std::is_same_v, T> || + std::is_same_v, T>, + "Only arrays should be mutable attributes"); + if (auto value = mData.Lookup(aAttrName)) { + if (value->is()) { + T& val = value->as(); + return SomeRef(val); + } + } + return Nothing(); + } + + // Get stringified value + bool GetAttribute(nsAtom* aAttrName, nsAString& aAttrValue) const; + + bool HasAttribute(nsAtom* aAttrName) const { + return mData.Contains(aAttrName); + } + + bool Remove(nsAtom* aAttrName) { return mData.Remove(aAttrName); } + + uint32_t Count() const { return mData.Count(); } + + // Update one instance with the entries in another. The supplied AccAttributes + // will be emptied. + void Update(AccAttributes* aOther); + + /** + * Return true if all the attributes in this instance are equal to all the + * attributes in another instance. + */ + bool Equal(const AccAttributes* aOther) const; + + /** + * Copy attributes from this instance to another instance. + * This should only be used in very specific cases; e.g. merging two sets of + * cached attributes without modifying the cache. It can only copy simple + * value types; e.g. it can't copy array values. Attempting to copy an + * AccAttributes with uncopyable values will cause an assertion. + */ + void CopyTo(AccAttributes* aDest) const; + + // An entry class for our iterator. + class Entry { + public: + Entry(nsAtom* aAttrName, const AttrValueType* aAttrValue) + : mName(aAttrName), mValue(aAttrValue) {} + + nsAtom* Name() const { return mName; } + + template + Maybe Value() const { + if constexpr (std::is_same_v) { + if (mValue->is>()) { + const T& val = *(mValue->as>().get()); + return SomeRef(val); + } + } else if constexpr (std::is_same_v) { + if (mValue->is>()) { + const T& val = *(mValue->as>()); + return SomeRef(val); + } + } else { + if (mValue->is()) { + const T& val = mValue->as(); + return SomeRef(val); + } + } + return Nothing(); + } + + void NameAsString(nsString& aName) const { + mName->ToString(aName); + if (StringBeginsWith(aName, u"aria-"_ns)) { + // Found 'aria-' + aName.ReplaceLiteral(0, 5, u""); + } + } + + void ValueAsString(nsAString& aValueString) const { + StringFromValueAndName(mName, *mValue, aValueString); + } + + // Size of the pair in the hash table. + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf); + + private: + nsAtom* mName; + const AttrValueType* mValue; + + friend class AccAttributes; + }; + + class Iterator { + public: + explicit Iterator(AtomVariantMap::const_iterator aIter) + : mHashIterator(aIter) {} + + Iterator() = delete; + Iterator(const Iterator&) = delete; + Iterator& operator=(const Iterator&) = delete; + + bool operator!=(const Iterator& aOther) const { + return mHashIterator != aOther.mHashIterator; + } + + Iterator& operator++() { + mHashIterator++; + return *this; + } + + Entry operator*() const { + auto& entry = *mHashIterator; + return Entry(entry.GetKey(), &entry.GetData()); + } + + private: + AtomVariantMap::const_iterator mHashIterator; + }; + + friend class Iterator; + + Iterator begin() const { return Iterator(mData.begin()); } + Iterator end() const { return Iterator(mData.end()); } + +#ifdef A11Y_LOG + static void DebugPrint(const char* aPrefix, const AccAttributes& aAttributes); +#endif + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf); + + private: + static void StringFromValueAndName(nsAtom* aAttrName, + const AttrValueType& aValue, + nsAString& aValueString); + + AtomVariantMap mData; + + friend struct IPC::ParamTraits; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccEvent.cpp b/accessible/base/AccEvent.cpp new file mode 100644 index 0000000000..b32d55ca06 --- /dev/null +++ b/accessible/base/AccEvent.cpp @@ -0,0 +1,301 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "AccEvent.h" + +#include "nsAccUtils.h" +#include "DocAccessible.h" +#include "xpcAccEvents.h" +#include "States.h" +#include "TextRange.h" +#include "xpcAccessibleDocument.h" +#include "xpcAccessibleTextRange.h" + +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/UserActivation.h" + +#include "nsComponentManagerUtils.h" +#include "nsIMutableArray.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +static_assert(static_cast(eNoUserInput) == false && + static_cast(eFromUserInput) == true, + "EIsFromUserInput cannot be casted to bool"); + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent constructors + +AccEvent::AccEvent(uint32_t aEventType, LocalAccessible* aAccessible, + EIsFromUserInput aIsFromUserInput, EEventRule aEventRule) + : mEventType(aEventType), mEventRule(aEventRule), mAccessible(aAccessible) { + if (aIsFromUserInput == eAutoDetect) { + mIsFromUserInput = dom::UserActivation::IsHandlingUserInput(); + } else { + mIsFromUserInput = aIsFromUserInput == eFromUserInput ? true : false; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent cycle collection + +NS_IMPL_CYCLE_COLLECTION_CLASS(AccEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AccEvent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessible) + if (AccTreeMutationEvent* tmEvent = downcast_accEvent(tmp)) { + tmEvent->SetNextEvent(nullptr); + tmEvent->SetPrevEvent(nullptr); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AccEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessible) + if (AccTreeMutationEvent* tmEvent = downcast_accEvent(tmp)) { + CycleCollectionNoteChild(cb, tmEvent->NextEvent(), "mNext"); + CycleCollectionNoteChild(cb, tmEvent->PrevEvent(), "mPrevEvent"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// AccTextChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +// Note: we pass in eAllowDupes to the base class because we don't support text +// events coalescence. We fire delayed text change events in DocAccessible but +// we continue to base the event off the accessible object rather than just the +// node. This means we won't try to create an accessible based on the node when +// we are ready to fire the event and so we will no longer assert at that point +// if the node was removed from the document. Either way, the AT won't work with +// a defunct accessible so the behaviour should be equivalent. +AccTextChangeEvent::AccTextChangeEvent(LocalAccessible* aAccessible, + int32_t aStart, + const nsAString& aModifiedText, + bool aIsInserted, + EIsFromUserInput aIsFromUserInput) + : AccEvent( + aIsInserted + ? static_cast(nsIAccessibleEvent::EVENT_TEXT_INSERTED) + : static_cast(nsIAccessibleEvent::EVENT_TEXT_REMOVED), + aAccessible, aIsFromUserInput, eAllowDupes), + mStart(aStart), + mIsInserted(aIsInserted), + mModifiedText(aModifiedText) { + // XXX We should use IsFromUserInput here, but that isn't always correct + // when the text change isn't related to content insertion or removal. + mIsFromUserInput = + mAccessible->State() & (states::FOCUSED | states::EDITABLE); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccHideEvent +//////////////////////////////////////////////////////////////////////////////// + +AccHideEvent::AccHideEvent(LocalAccessible* aTarget, bool aNeedsShutdown) + : AccMutationEvent(::nsIAccessibleEvent::EVENT_HIDE, aTarget), + mNeedsShutdown(aNeedsShutdown) { + mNextSibling = mAccessible->LocalNextSibling(); + mPrevSibling = mAccessible->LocalPrevSibling(); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccShowEvent +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// AccTextSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccTextSelChangeEvent::AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, + int32_t aReason, + int32_t aGranularity) + : AccEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, aTarget, + eAutoDetect, eCoalesceTextSelChange), + mSel(aSelection), + mReason(aReason), + mGranularity(aGranularity) {} + +AccTextSelChangeEvent::~AccTextSelChangeEvent() {} + +bool AccTextSelChangeEvent::IsCaretMoveOnly() const { + return mSel->RangeCount() == 1 && mSel->IsCollapsed() && + ((mReason & (nsISelectionListener::COLLAPSETOSTART_REASON | + nsISelectionListener::COLLAPSETOEND_REASON)) == 0); +} + +void AccTextSelChangeEvent::SelectionRanges( + nsTArray* aRanges) const { + TextRange::TextRangesFromSelection(mSel, aRanges); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccSelChangeEvent::AccSelChangeEvent(LocalAccessible* aWidget, + LocalAccessible* aItem, + SelChangeType aSelChangeType) + : AccEvent(0, aItem, eAutoDetect, eCoalesceSelectionChange), + mWidget(aWidget), + mItem(aItem), + mSelChangeType(aSelChangeType), + mPreceedingCount(0), + mPackedEvent(nullptr) { + if (aSelChangeType == eSelectionAdd) { + if (mWidget->GetSelectedItem(1)) { + mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; + } else { + mEventType = nsIAccessibleEvent::EVENT_SELECTION; + } + } else { + mEventType = nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// AccTableChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccTableChangeEvent::AccTableChangeEvent(LocalAccessible* aAccessible, + uint32_t aEventType, + int32_t aRowOrColIndex, + int32_t aNumRowsOrCols) + : AccEvent(aEventType, aAccessible), + mRowOrColIndex(aRowOrColIndex), + mNumRowsOrCols(aNumRowsOrCols) {} + +//////////////////////////////////////////////////////////////////////////////// +// AccVCChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccVCChangeEvent::AccVCChangeEvent(LocalAccessible* aAccessible, + LocalAccessible* aOldAccessible, + int32_t aOldStart, int32_t aOldEnd, + LocalAccessible* aNewAccessible, + int32_t aNewStart, int32_t aNewEnd, + int16_t aReason, int16_t aBoundaryType, + EIsFromUserInput aIsFromUserInput) + : AccEvent(::nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED, aAccessible, + aIsFromUserInput), + mOldAccessible(aOldAccessible), + mNewAccessible(aNewAccessible), + mOldStart(aOldStart), + mNewStart(aNewStart), + mOldEnd(aOldEnd), + mNewEnd(aNewEnd), + mReason(aReason), + mBoundaryType(aBoundaryType) {} + +already_AddRefed a11y::MakeXPCEvent(AccEvent* aEvent) { + DocAccessible* doc = aEvent->Document(); + LocalAccessible* acc = aEvent->GetAccessible(); + nsINode* node = acc->GetNode(); + bool fromUser = aEvent->IsFromUserInput(); + uint32_t type = aEvent->GetEventType(); + uint32_t eventGroup = aEvent->GetEventGroups(); + nsCOMPtr xpEvent; + + if (eventGroup & (1 << AccEvent::eStateChangeEvent)) { + AccStateChangeEvent* sc = downcast_accEvent(aEvent); + bool extra = false; + uint32_t state = nsAccUtils::To32States(sc->GetState(), &extra); + xpEvent = new xpcAccStateChangeEvent(type, ToXPC(acc), ToXPCDocument(doc), + node, fromUser, state, extra, + sc->IsStateEnabled()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eTextChangeEvent)) { + AccTextChangeEvent* tc = downcast_accEvent(aEvent); + nsString text; + tc->GetModifiedText(text); + xpEvent = new xpcAccTextChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, + tc->GetStartOffset(), tc->GetLength(), tc->IsTextInserted(), text); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eHideEvent)) { + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + xpEvent = new xpcAccHideEvent(type, ToXPC(acc), ToXPCDocument(doc), node, + fromUser, ToXPC(hideEvent->TargetParent()), + ToXPC(hideEvent->TargetNextSibling()), + ToXPC(hideEvent->TargetPrevSibling())); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eCaretMoveEvent)) { + AccCaretMoveEvent* cm = downcast_accEvent(aEvent); + xpEvent = new xpcAccCaretMoveEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, + cm->GetCaretOffset(), cm->IsSelectionCollapsed(), cm->IsAtEndOfLine(), + cm->GetGranularity()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eTextSelChangeEvent)) { + AccTextSelChangeEvent* tsc = downcast_accEvent(aEvent); + AutoTArray ranges; + tsc->SelectionRanges(&ranges); + + nsCOMPtr xpcRanges = + do_CreateInstance(NS_ARRAY_CONTRACTID); + uint32_t len = ranges.Length(); + for (uint32_t idx = 0; idx < len; idx++) { + xpcRanges->AppendElement(new xpcAccessibleTextRange(ranges[idx])); + } + + xpEvent = new xpcAccTextSelectionChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, xpcRanges); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eVirtualCursorChangeEvent)) { + AccVCChangeEvent* vcc = downcast_accEvent(aEvent); + xpEvent = new xpcAccVirtualCursorChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, + ToXPC(vcc->OldAccessible()), vcc->OldStartOffset(), vcc->OldEndOffset(), + ToXPC(vcc->NewAccessible()), vcc->NewStartOffset(), vcc->NewEndOffset(), + vcc->Reason(), vcc->BoundaryType()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eObjectAttrChangedEvent)) { + AccObjectAttrChangedEvent* oac = downcast_accEvent(aEvent); + nsString attribute; + oac->GetAttribute()->ToString(attribute); + xpEvent = new xpcAccObjectAttributeChangedEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, attribute); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eScrollingEvent)) { + AccScrollingEvent* sa = downcast_accEvent(aEvent); + xpEvent = new xpcAccScrollingEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, sa->ScrollX(), + sa->ScrollY(), sa->MaxScrollX(), sa->MaxScrollY()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eAnnouncementEvent)) { + AccAnnouncementEvent* aa = downcast_accEvent(aEvent); + xpEvent = new xpcAccAnnouncementEvent(type, ToXPC(acc), ToXPCDocument(doc), + node, fromUser, aa->Announcement(), + aa->Priority()); + return xpEvent.forget(); + } + + xpEvent = + new xpcAccEvent(type, ToXPC(acc), ToXPCDocument(doc), node, fromUser); + return xpEvent.forget(); +} diff --git a/accessible/base/AccEvent.h b/accessible/base/AccEvent.h new file mode 100644 index 0000000000..e56506fdea --- /dev/null +++ b/accessible/base/AccEvent.h @@ -0,0 +1,629 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _AccEvent_H_ +#define _AccEvent_H_ + +#include "nsIAccessibleEvent.h" + +#include "mozilla/a11y/LocalAccessible.h" + +class nsEventShell; +namespace mozilla { + +namespace dom { +class Selection; +} + +namespace a11y { + +class DocAccessible; +class EventQueue; +class TextRange; + +// Constants used to point whether the event is from user input. +enum EIsFromUserInput { + // eNoUserInput: event is not from user input + eNoUserInput = 0, + // eFromUserInput: event is from user input + eFromUserInput = 1, + // eAutoDetect: the value should be obtained from event state manager + eAutoDetect = -1 +}; + +/** + * Generic accessible event. + */ +class AccEvent { + public: + // Rule for accessible events. + // The rule will be applied when flushing pending events. + enum EEventRule { + // eAllowDupes : More than one event of the same type is allowed. + // This event will always be emitted. This flag is used for events that + // don't support coalescence. + eAllowDupes, + + // eCoalesceReorder : For reorder events from the same subtree or the same + // node, only the umbrella event on the ancestor will be emitted. + eCoalesceReorder, + + // eCoalesceOfSameType : For events of the same type, only the newest event + // will be processed. + eCoalesceOfSameType, + + // eCoalesceSelectionChange: coalescence of selection change events. + eCoalesceSelectionChange, + + // eCoalesceStateChange: coalesce state change events. + eCoalesceStateChange, + + // eCoalesceTextSelChange: coalescence of text selection change events. + eCoalesceTextSelChange, + + // eRemoveDupes : For repeat events, only the newest event in queue + // will be emitted. + eRemoveDupes, + + // eDoNotEmit : This event is confirmed as a duplicate, do not emit it. + eDoNotEmit + }; + + // Initialize with an accessible. + AccEvent(uint32_t aEventType, LocalAccessible* aAccessible, + EIsFromUserInput aIsFromUserInput = eAutoDetect, + EEventRule aEventRule = eRemoveDupes); + + // AccEvent + uint32_t GetEventType() const { return mEventType; } + EEventRule GetEventRule() const { return mEventRule; } + bool IsFromUserInput() const { return mIsFromUserInput; } + EIsFromUserInput FromUserInput() const { + return static_cast(mIsFromUserInput); + } + + LocalAccessible* GetAccessible() const { return mAccessible; } + DocAccessible* Document() const { return mAccessible->Document(); } + + /** + * Down casting. + */ + enum EventGroup { + eGenericEvent, + eStateChangeEvent, + eTextChangeEvent, + eTreeMutationEvent, + eMutationEvent, + eReorderEvent, + eHideEvent, + eShowEvent, + eCaretMoveEvent, + eTextSelChangeEvent, + eSelectionChangeEvent, + eTableChangeEvent, + eVirtualCursorChangeEvent, + eObjectAttrChangedEvent, + eScrollingEvent, + eAnnouncementEvent, + }; + + static const EventGroup kEventGroup = eGenericEvent; + virtual unsigned int GetEventGroups() const { return 1U << eGenericEvent; } + + /** + * Reference counting and cycle collection. + */ + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AccEvent) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(AccEvent) + + protected: + virtual ~AccEvent() {} + + bool mIsFromUserInput; + uint32_t mEventType; + EEventRule mEventRule; + RefPtr mAccessible; + + friend class EventQueue; + friend class EventTree; + friend class ::nsEventShell; + friend class NotificationController; +}; + +/** + * Accessible state change event. + */ +class AccStateChangeEvent : public AccEvent { + public: + AccStateChangeEvent(LocalAccessible* aAccessible, uint64_t aState, + bool aIsEnabled, + EIsFromUserInput aIsFromUserInput = eAutoDetect) + : AccEvent(nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible, + aIsFromUserInput, eCoalesceStateChange), + mState(aState), + mIsEnabled(aIsEnabled) {} + + AccStateChangeEvent(LocalAccessible* aAccessible, uint64_t aState) + : AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible, + eAutoDetect, eCoalesceStateChange), + mState(aState) { + mIsEnabled = (mAccessible->State() & mState) != 0; + } + + // AccEvent + static const EventGroup kEventGroup = eStateChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eStateChangeEvent); + } + + // AccStateChangeEvent + uint64_t GetState() const { return mState; } + bool IsStateEnabled() const { return mIsEnabled; } + + private: + uint64_t mState; + bool mIsEnabled; + + friend class EventQueue; +}; + +/** + * Accessible text change event. + */ +class AccTextChangeEvent : public AccEvent { + public: + AccTextChangeEvent(LocalAccessible* aAccessible, int32_t aStart, + const nsAString& aModifiedText, bool aIsInserted, + EIsFromUserInput aIsFromUserInput = eAutoDetect); + + // AccEvent + static const EventGroup kEventGroup = eTextChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eTextChangeEvent); + } + + // AccTextChangeEvent + int32_t GetStartOffset() const { return mStart; } + uint32_t GetLength() const { return mModifiedText.Length(); } + bool IsTextInserted() const { return mIsInserted; } + void GetModifiedText(nsAString& aModifiedText) { + aModifiedText = mModifiedText; + } + const nsString& ModifiedText() const { return mModifiedText; } + + private: + int32_t mStart; + bool mIsInserted; + nsString mModifiedText; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * A base class for events related to tree mutation, either an AccMutation + * event, or an AccReorderEvent. + */ +class AccTreeMutationEvent : public AccEvent { + public: + AccTreeMutationEvent(uint32_t aEventType, LocalAccessible* aTarget) + : AccEvent(aEventType, aTarget, eAutoDetect, eCoalesceReorder), + mGeneration(0) {} + + // Event + static const EventGroup kEventGroup = eTreeMutationEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eTreeMutationEvent); + } + + void SetNextEvent(AccTreeMutationEvent* aNext) { mNextEvent = aNext; } + void SetPrevEvent(AccTreeMutationEvent* aPrev) { mPrevEvent = aPrev; } + AccTreeMutationEvent* NextEvent() const { return mNextEvent; } + AccTreeMutationEvent* PrevEvent() const { return mPrevEvent; } + + /** + * A sequence number to know when this event was fired. + */ + uint32_t EventGeneration() const { return mGeneration; } + void SetEventGeneration(uint32_t aGeneration) { mGeneration = aGeneration; } + + private: + RefPtr mNextEvent; + RefPtr mPrevEvent; + uint32_t mGeneration; +}; + +/** + * Base class for show and hide accessible events. + */ +class AccMutationEvent : public AccTreeMutationEvent { + public: + AccMutationEvent(uint32_t aEventType, LocalAccessible* aTarget) + : AccTreeMutationEvent(aEventType, aTarget) { + // Don't coalesce these since they are coalesced by reorder event. Coalesce + // contained text change events. + mParent = mAccessible->LocalParent(); + } + virtual ~AccMutationEvent() {} + + // Event + static const EventGroup kEventGroup = eMutationEvent; + virtual unsigned int GetEventGroups() const override { + return AccTreeMutationEvent::GetEventGroups() | (1U << eMutationEvent); + } + + // MutationEvent + bool IsShow() const { return mEventType == nsIAccessibleEvent::EVENT_SHOW; } + bool IsHide() const { return mEventType == nsIAccessibleEvent::EVENT_HIDE; } + + LocalAccessible* LocalParent() const { return mParent; } + + protected: + RefPtr mParent; + RefPtr mTextChangeEvent; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * Accessible hide event. + */ +class AccHideEvent : public AccMutationEvent { + public: + explicit AccHideEvent(LocalAccessible* aTarget, bool aNeedsShutdown = true); + + // Event + static const EventGroup kEventGroup = eHideEvent; + virtual unsigned int GetEventGroups() const override { + return AccMutationEvent::GetEventGroups() | (1U << eHideEvent); + } + + // AccHideEvent + LocalAccessible* TargetParent() const { return mParent; } + LocalAccessible* TargetNextSibling() const { return mNextSibling; } + LocalAccessible* TargetPrevSibling() const { return mPrevSibling; } + bool NeedsShutdown() const { return mNeedsShutdown; } + + protected: + bool mNeedsShutdown; + RefPtr mNextSibling; + RefPtr mPrevSibling; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * Accessible show event. + */ +class AccShowEvent : public AccMutationEvent { + public: + explicit AccShowEvent(LocalAccessible* aTarget) + : AccMutationEvent(::nsIAccessibleEvent::EVENT_SHOW, aTarget) {} + + // Event + static const EventGroup kEventGroup = eShowEvent; + virtual unsigned int GetEventGroups() const override { + return AccMutationEvent::GetEventGroups() | (1U << eShowEvent); + } +}; + +/** + * Class for reorder accessible event. Takes care about + */ +class AccReorderEvent : public AccTreeMutationEvent { + public: + explicit AccReorderEvent(LocalAccessible* aTarget) + : AccTreeMutationEvent(::nsIAccessibleEvent::EVENT_REORDER, aTarget) {} + virtual ~AccReorderEvent() {} + + // Event + static const EventGroup kEventGroup = eReorderEvent; + virtual unsigned int GetEventGroups() const override { + return AccTreeMutationEvent::GetEventGroups() | (1U << eReorderEvent); + } + + /* + * Make this an inner reorder event that is coalesced into + * a reorder event of an ancestor. + */ + void SetInner() { mEventType = ::nsIAccessibleEvent::EVENT_INNER_REORDER; } +}; + +/** + * Accessible caret move event. + */ +class AccCaretMoveEvent : public AccEvent { + public: + AccCaretMoveEvent(LocalAccessible* aAccessible, int32_t aCaretOffset, + bool aIsSelectionCollapsed, bool aIsAtEndOfLine, + int32_t aGranularity, + EIsFromUserInput aIsFromUserInput = eAutoDetect) + : AccEvent(::nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED, aAccessible, + aIsFromUserInput), + mCaretOffset(aCaretOffset), + mIsSelectionCollapsed(aIsSelectionCollapsed), + mIsAtEndOfLine(aIsAtEndOfLine), + mGranularity(aGranularity) {} + virtual ~AccCaretMoveEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eCaretMoveEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eCaretMoveEvent); + } + + // AccCaretMoveEvent + int32_t GetCaretOffset() const { return mCaretOffset; } + + bool IsSelectionCollapsed() const { return mIsSelectionCollapsed; } + bool IsAtEndOfLine() { return mIsAtEndOfLine; } + + int32_t GetGranularity() const { return mGranularity; } + + private: + int32_t mCaretOffset; + + bool mIsSelectionCollapsed; + bool mIsAtEndOfLine; + int32_t mGranularity; +}; + +/** + * Accessible text selection change event. + */ +class AccTextSelChangeEvent : public AccEvent { + public: + AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, int32_t aReason, + int32_t aGranularity); + virtual ~AccTextSelChangeEvent(); + + // AccEvent + static const EventGroup kEventGroup = eTextSelChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eTextSelChangeEvent); + } + + // AccTextSelChangeEvent + + /** + * Return true if the text selection change wasn't caused by pure caret move. + */ + bool IsCaretMoveOnly() const; + + int32_t GetGranularity() const { return mGranularity; } + + /** + * Return selection ranges in document/control. + */ + void SelectionRanges(nsTArray* aRanges) const; + + private: + RefPtr mSel; + int32_t mReason; + int32_t mGranularity; + + friend class EventQueue; + friend class SelectionManager; +}; + +/** + * Accessible widget selection change event. + */ +class AccSelChangeEvent : public AccEvent { + public: + enum SelChangeType { eSelectionAdd, eSelectionRemove }; + + AccSelChangeEvent(LocalAccessible* aWidget, LocalAccessible* aItem, + SelChangeType aSelChangeType); + + virtual ~AccSelChangeEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eSelectionChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eSelectionChangeEvent); + } + + // AccSelChangeEvent + LocalAccessible* Widget() const { return mWidget; } + + private: + RefPtr mWidget; + RefPtr mItem; + SelChangeType mSelChangeType; + uint32_t mPreceedingCount; + AccSelChangeEvent* mPackedEvent; + + friend class EventQueue; +}; + +/** + * Accessible table change event. + */ +class AccTableChangeEvent : public AccEvent { + public: + AccTableChangeEvent(LocalAccessible* aAccessible, uint32_t aEventType, + int32_t aRowOrColIndex, int32_t aNumRowsOrCols); + + // AccEvent + static const EventGroup kEventGroup = eTableChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eTableChangeEvent); + } + + // AccTableChangeEvent + uint32_t GetIndex() const { return mRowOrColIndex; } + uint32_t GetCount() const { return mNumRowsOrCols; } + + private: + uint32_t mRowOrColIndex; // the start row/column after which the rows are + // inserted/deleted. + uint32_t mNumRowsOrCols; // the number of inserted/deleted rows/columns +}; + +/** + * Accessible virtual cursor change event. + */ +class AccVCChangeEvent : public AccEvent { + public: + AccVCChangeEvent(LocalAccessible* aAccessible, + LocalAccessible* aOldAccessible, int32_t aOldStart, + int32_t aOldEnd, LocalAccessible* aNewAccessible, + int32_t aNewStart, int32_t aNewEnd, int16_t aReason, + int16_t aBoundaryType, + EIsFromUserInput aIsFromUserInput = eFromUserInput); + + virtual ~AccVCChangeEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eVirtualCursorChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eVirtualCursorChangeEvent); + } + + // AccVCChangeEvent + LocalAccessible* OldAccessible() const { return mOldAccessible; } + int32_t OldStartOffset() const { return mOldStart; } + int32_t OldEndOffset() const { return mOldEnd; } + LocalAccessible* NewAccessible() const { return mNewAccessible; } + int32_t NewStartOffset() const { return mNewStart; } + int32_t NewEndOffset() const { return mNewEnd; } + int32_t Reason() const { return mReason; } + int32_t BoundaryType() const { return mBoundaryType; } + + private: + RefPtr mOldAccessible; + RefPtr mNewAccessible; + int32_t mOldStart; + int32_t mNewStart; + int32_t mOldEnd; + int32_t mNewEnd; + int16_t mReason; + int16_t mBoundaryType; +}; + +/** + * Accessible object attribute changed event. + */ +class AccObjectAttrChangedEvent : public AccEvent { + public: + AccObjectAttrChangedEvent(LocalAccessible* aAccessible, nsAtom* aAttribute) + : AccEvent(::nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, + aAccessible), + mAttribute(aAttribute) {} + + // AccEvent + static const EventGroup kEventGroup = eObjectAttrChangedEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eObjectAttrChangedEvent); + } + + // AccObjectAttrChangedEvent + nsAtom* GetAttribute() const { return mAttribute; } + + private: + RefPtr mAttribute; + + virtual ~AccObjectAttrChangedEvent() {} +}; + +/** + * Accessible scroll event. + */ +class AccScrollingEvent : public AccEvent { + public: + AccScrollingEvent(uint32_t aEventType, LocalAccessible* aAccessible, + uint32_t aScrollX, uint32_t aScrollY, uint32_t aMaxScrollX, + uint32_t aMaxScrollY) + : AccEvent(aEventType, aAccessible), + mScrollX(aScrollX), + mScrollY(aScrollY), + mMaxScrollX(aMaxScrollX), + mMaxScrollY(aMaxScrollY) {} + + virtual ~AccScrollingEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eScrollingEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eScrollingEvent); + } + + // The X scrolling offset of the container when the event was fired. + uint32_t ScrollX() { return mScrollX; } + // The Y scrolling offset of the container when the event was fired. + uint32_t ScrollY() { return mScrollY; } + // The max X offset of the container. + uint32_t MaxScrollX() { return mMaxScrollX; } + // The max Y offset of the container. + uint32_t MaxScrollY() { return mMaxScrollY; } + + private: + uint32_t mScrollX; + uint32_t mScrollY; + uint32_t mMaxScrollX; + uint32_t mMaxScrollY; +}; + +/** + * Accessible announcement event. + */ +class AccAnnouncementEvent : public AccEvent { + public: + AccAnnouncementEvent(LocalAccessible* aAccessible, + const nsAString& aAnnouncement, uint16_t aPriority) + : AccEvent(nsIAccessibleEvent::EVENT_ANNOUNCEMENT, aAccessible), + mAnnouncement(aAnnouncement), + mPriority(aPriority) {} + + virtual ~AccAnnouncementEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eAnnouncementEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eAnnouncementEvent); + } + + const nsString& Announcement() const { return mAnnouncement; } + + uint16_t Priority() { return mPriority; } + + private: + nsString mAnnouncement; + uint16_t mPriority; +}; + +/** + * Downcast the generic accessible event object to derived type. + */ +class downcast_accEvent { + public: + explicit downcast_accEvent(AccEvent* e) : mRawPtr(e) {} + + template + operator Destination*() { + if (!mRawPtr) return nullptr; + + return mRawPtr->GetEventGroups() & (1U << Destination::kEventGroup) + ? static_cast(mRawPtr) + : nullptr; + } + + private: + AccEvent* mRawPtr; +}; + +/** + * Return a new xpcom accessible event for the given internal one. + */ +already_AddRefed MakeXPCEvent(AccEvent* aEvent); + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccGroupInfo.cpp b/accessible/base/AccGroupInfo.cpp new file mode 100644 index 0000000000..3b536b1aa4 --- /dev/null +++ b/accessible/base/AccGroupInfo.cpp @@ -0,0 +1,397 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccGroupInfo.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/TableAccessible.h" + +#include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" + +#include "Pivot.h" +#include "States.h" + +using namespace mozilla::a11y; + +static role BaseRole(role aRole); + +// This rule finds candidate siblings for compound widget children. +class CompoundWidgetSiblingRule : public PivotRule { + public: + CompoundWidgetSiblingRule() = delete; + explicit CompoundWidgetSiblingRule(role aRole) : mRole(aRole) {} + + uint16_t Match(Accessible* aAcc) override { + // If the acc has a matching role, that's a valid sibling. If the acc is + // separator then the group is ended. Return a match for separators with + // the assumption that the caller will check for the role of the returned + // accessible. + const role accRole = aAcc->Role(); + if (BaseRole(accRole) == mRole || accRole == role::SEPARATOR) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + // Ignore generic accessibles, but keep searching through the subtree for + // siblings. + if (aAcc->IsGeneric()) { + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + private: + role mRole = role::NOTHING; +}; + +AccGroupInfo::AccGroupInfo(const Accessible* aItem, role aRole) + : mPosInSet(0), mSetSize(0), mParentId(0), mItem(aItem), mRole(aRole) { + MOZ_COUNT_CTOR(AccGroupInfo); + Update(); +} + +void AccGroupInfo::Update() { + mParentId = 0; + + Accessible* parent = mItem->GetNonGenericParent(); + if (!parent) { + return; + } + + const int32_t level = GetARIAOrDefaultLevel(mItem); + + // Compute position in set. + mPosInSet = 1; + + // Search backwards through the tree for candidate siblings. + Accessible* candidateSibling = const_cast(mItem); + Pivot pivot{parent}; + CompoundWidgetSiblingRule widgetSiblingRule{mRole}; + while ((candidateSibling = pivot.Prev(candidateSibling, widgetSiblingRule)) && + candidateSibling != parent) { + // If the sibling is separator then the group is ended. + if (candidateSibling->Role() == roles::SEPARATOR) { + break; + } + + const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingGroupInfo && candidateSibling->State() & states::INVISIBLE) { + continue; + } + + // Check if it's hierarchical flatten structure, i.e. if the sibling + // level is lesser than this one then group is ended, if the sibling level + // is greater than this one then the group is split by some child elements + // (group will be continued). + const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); + if (siblingLevel < level) { + mParentId = candidateSibling->ID(); + break; + } + + // Skip subset. + if (siblingLevel > level) { + continue; + } + + // If the previous item in the group has calculated group information then + // build group information for this item based on found one. + if (siblingGroupInfo) { + mPosInSet += siblingGroupInfo->mPosInSet; + mParentId = siblingGroupInfo->mParentId; + mSetSize = siblingGroupInfo->mSetSize; + return; + } + + mPosInSet++; + } + + // Compute set size. + mSetSize = mPosInSet; + + candidateSibling = const_cast(mItem); + while ((candidateSibling = pivot.Next(candidateSibling, widgetSiblingRule)) && + candidateSibling != parent) { + // If the sibling is separator then the group is ended. + if (candidateSibling->Role() == roles::SEPARATOR) { + break; + } + + const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingGroupInfo && candidateSibling->State() & states::INVISIBLE) { + continue; + } + + // and check if it's hierarchical flatten structure. + const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); + if (siblingLevel < level) { + break; + } + + // Skip subset. + if (siblingLevel > level) { + continue; + } + + // If the next item in the group has calculated group information then + // build group information for this item based on found one. + if (siblingGroupInfo) { + mParentId = siblingGroupInfo->mParentId; + mSetSize = siblingGroupInfo->mSetSize; + return; + } + + mSetSize++; + } + + if (mParentId) { + return; + } + + roles::Role parentRole = parent->Role(); + if (ShouldReportRelations(mRole, parentRole)) { + mParentId = parent->ID(); + } + + // ARIA tree and list can be arranged by using ARIA groups to organize levels. + if (parentRole != roles::GROUPING) { + return; + } + + // Way #1 for ARIA tree (not ARIA treegrid): previous sibling of a group is a + // parent. In other words the parent of the tree item will be a group and + // the previous tree item of the group is a conceptual parent of the tree + // item. + if (mRole == roles::OUTLINEITEM) { + // Find the relevant grandparent of the item. Use that parent as the root + // and find the previous outline item sibling within that root. + Accessible* grandParent = parent->GetNonGenericParent(); + MOZ_ASSERT(grandParent); + Pivot pivot{grandParent}; + CompoundWidgetSiblingRule parentSiblingRule{mRole}; + Accessible* parentPrevSibling = pivot.Prev(parent, widgetSiblingRule); + if (parentPrevSibling && parentPrevSibling->Role() == mRole) { + mParentId = parentPrevSibling->ID(); + return; + } + } + + // Way #2 for ARIA list and tree: group is a child of an item. In other words + // the parent of the item will be a group and containing item of the group is + // a conceptual parent of the item. + if (mRole == roles::LISTITEM || mRole == roles::OUTLINEITEM) { + Accessible* grandParent = parent->GetNonGenericParent(); + if (grandParent && grandParent->Role() == mRole) { + mParentId = grandParent->ID(); + } + } +} + +AccGroupInfo* AccGroupInfo::CreateGroupInfo(const Accessible* aAccessible) { + mozilla::a11y::role role = aAccessible->Role(); + if (role != mozilla::a11y::roles::ROW && + role != mozilla::a11y::roles::OUTLINEITEM && + role != mozilla::a11y::roles::OPTION && + role != mozilla::a11y::roles::LISTITEM && + role != mozilla::a11y::roles::MENUITEM && + role != mozilla::a11y::roles::COMBOBOX_OPTION && + role != mozilla::a11y::roles::RICH_OPTION && + role != mozilla::a11y::roles::CHECK_RICH_OPTION && + role != mozilla::a11y::roles::PARENT_MENUITEM && + role != mozilla::a11y::roles::CHECK_MENU_ITEM && + role != mozilla::a11y::roles::RADIO_MENU_ITEM && + role != mozilla::a11y::roles::RADIOBUTTON && + role != mozilla::a11y::roles::PAGETAB && + role != mozilla::a11y::roles::COMMENT) { + return nullptr; + } + + AccGroupInfo* info = new AccGroupInfo(aAccessible, BaseRole(role)); + return info; +} + +Accessible* AccGroupInfo::FirstItemOf(const Accessible* aContainer) { + // ARIA tree can be arranged by ARIA groups case #1 (previous sibling of a + // group is a parent) or by aria-level. + a11y::role containerRole = aContainer->Role(); + Accessible* item = aContainer->NextSibling(); + if (item) { + if (containerRole == roles::OUTLINEITEM && + item->Role() == roles::GROUPING) { + item = item->FirstChild(); + } + + if (item) { + AccGroupInfo* itemGroupInfo = item->GetOrCreateGroupInfo(); + if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) { + return item; + } + } + } + + // ARIA list and tree can be arranged by ARIA groups case #2 (group is + // a child of an item). + item = aContainer->LastChild(); + if (!item) return nullptr; + + if (item->Role() == roles::GROUPING && + (containerRole == roles::LISTITEM || + containerRole == roles::OUTLINEITEM)) { + item = item->FirstChild(); + if (item) { + AccGroupInfo* itemGroupInfo = item->GetOrCreateGroupInfo(); + if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) { + return item; + } + } + } + + // Otherwise, it can be a direct child if the container is a list or tree. + item = aContainer->FirstChild(); + if (ShouldReportRelations(item->Role(), containerRole)) return item; + + return nullptr; +} + +uint32_t AccGroupInfo::TotalItemCount(Accessible* aContainer, + bool* aIsHierarchical) { + uint32_t itemCount = 0; + switch (aContainer->Role()) { + case roles::TABLE: + if (auto val = aContainer->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) { + if (*val >= 0) { + return *val; + } + } + if (TableAccessible* tableAcc = aContainer->AsTable()) { + return tableAcc->RowCount(); + } + break; + case roles::ROW: + if (Accessible* table = nsAccUtils::TableFor(aContainer)) { + if (auto val = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) { + if (*val >= 0) { + return *val; + } + } + if (TableAccessible* tableAcc = table->AsTable()) { + return tableAcc->ColCount(); + } + } + break; + case roles::OUTLINE: + case roles::LIST: + case roles::MENUBAR: + case roles::MENUPOPUP: + case roles::COMBOBOX: + case roles::GROUPING: + case roles::TREE_TABLE: + case roles::COMBOBOX_LIST: + case roles::LISTBOX: + case roles::DEFINITION_LIST: + case roles::EDITCOMBOBOX: + case roles::RADIO_GROUP: + case roles::PAGETABLIST: { + Accessible* childItem = AccGroupInfo::FirstItemOf(aContainer); + if (!childItem) { + childItem = aContainer->FirstChild(); + if (childItem && childItem->IsTextLeaf()) { + // First child can be a text leaf, check its sibling for an item. + childItem = childItem->NextSibling(); + } + } + + if (childItem) { + GroupPos groupPos = childItem->GroupPosition(); + itemCount = groupPos.setSize; + if (groupPos.level && aIsHierarchical) { + *aIsHierarchical = true; + } + } + break; + } + default: + break; + } + + return itemCount; +} + +Accessible* AccGroupInfo::NextItemTo(Accessible* aItem) { + AccGroupInfo* groupInfo = aItem->GetOrCreateGroupInfo(); + if (!groupInfo) return nullptr; + + // If the item in middle of the group then search next item in siblings. + if (groupInfo->PosInSet() >= groupInfo->SetSize()) return nullptr; + + Accessible* parent = aItem->Parent(); + uint32_t childCount = parent->ChildCount(); + for (uint32_t idx = aItem->IndexInParent() + 1; idx < childCount; idx++) { + Accessible* nextItem = parent->ChildAt(idx); + AccGroupInfo* nextGroupInfo = nextItem->GetOrCreateGroupInfo(); + if (nextGroupInfo && + nextGroupInfo->ConceptualParent() == groupInfo->ConceptualParent()) { + return nextItem; + } + } + + MOZ_ASSERT_UNREACHABLE( + "Item in the middle of the group but there's no next item!"); + return nullptr; +} + +size_t AccGroupInfo::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + // We don't count mParentId or mItem since they (should be) counted + // as part of the document. + return aMallocSizeOf(this); +} + +bool AccGroupInfo::ShouldReportRelations(role aRole, role aParentRole) { + // We only want to report hierarchy-based node relations for items in tree or + // list form. ARIA level/owns relations are always reported. + if (aParentRole == roles::OUTLINE && aRole == roles::OUTLINEITEM) return true; + if (aParentRole == roles::TREE_TABLE && aRole == roles::ROW) return true; + if (aParentRole == roles::LIST && aRole == roles::LISTITEM) return true; + + return false; +} + +int32_t AccGroupInfo::GetARIAOrDefaultLevel(const Accessible* aAccessible) { + int32_t level = 0; + aAccessible->ARIAGroupPosition(&level, nullptr, nullptr); + + if (level != 0) return level; + + return aAccessible->GetLevel(true); +} + +Accessible* AccGroupInfo::ConceptualParent() const { + if (!mParentId) { + // The conceptual parent can never be the document, so id 0 means none. + return nullptr; + } + if (Accessible* doc = + nsAccUtils::DocumentFor(const_cast(mItem))) { + return nsAccUtils::GetAccessibleByID(doc, mParentId); + } + return nullptr; +} + +static role BaseRole(role aRole) { + if (aRole == roles::CHECK_MENU_ITEM || aRole == roles::PARENT_MENUITEM || + aRole == roles::RADIO_MENU_ITEM) { + return roles::MENUITEM; + } + + if (aRole == roles::CHECK_RICH_OPTION) { + return roles::RICH_OPTION; + } + + return aRole; +} diff --git a/accessible/base/AccGroupInfo.h b/accessible/base/AccGroupInfo.h new file mode 100644 index 0000000000..0c62cc4b8e --- /dev/null +++ b/accessible/base/AccGroupInfo.h @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef AccGroupInfo_h_ +#define AccGroupInfo_h_ + +#include "nsISupportsImpl.h" +#include "mozilla/MemoryReporting.h" +#include "Role.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Calculate and store group information. + */ +class AccGroupInfo { + public: + MOZ_COUNTED_DTOR(AccGroupInfo) + + AccGroupInfo() = default; + AccGroupInfo(AccGroupInfo&&) = default; + AccGroupInfo& operator=(AccGroupInfo&&) = default; + + /** + * Return 1-based position in the group. + */ + uint32_t PosInSet() const { return mPosInSet; } + + /** + * Return a number of items in the group. + */ + uint32_t SetSize() const { return mSetSize; } + + /** + * Return a direct or logical parent of the accessible that this group info is + * created for. + */ + Accessible* ConceptualParent() const; + + /** + * Update group information. + */ + void Update(); + + /** + * Create group info. + */ + static AccGroupInfo* CreateGroupInfo(const Accessible* aAccessible); + + /** + * Return a first item for the given container. + */ + static Accessible* FirstItemOf(const Accessible* aContainer); + + /** + * Return total number of items in container, and if it is has nested + * collections. + */ + static uint32_t TotalItemCount(Accessible* aContainer, bool* aIsHierarchical); + + /** + * Return next item of the same group to the given item. + */ + static Accessible* NextItemTo(Accessible* aItem); + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf); + + protected: + AccGroupInfo(const Accessible* aItem, a11y::role aRole); + + private: + AccGroupInfo(const AccGroupInfo&) = delete; + AccGroupInfo& operator=(const AccGroupInfo&) = delete; + + /** + * Return true if the given parent and child roles should have their node + * relations reported. + */ + static bool ShouldReportRelations(a11y::role aRole, a11y::role aParentRole); + + /** + * Return ARIA level value or the default one if ARIA is missed for the + * given accessible. + */ + static int32_t GetARIAOrDefaultLevel(const Accessible* aAccessible); + + uint32_t mPosInSet; + uint32_t mSetSize; + uint64_t mParentId; + const Accessible* mItem; + a11y::role mRole; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccIterator.cpp b/accessible/base/AccIterator.cpp new file mode 100644 index 0000000000..41380d41a4 --- /dev/null +++ b/accessible/base/AccIterator.cpp @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccIterator.h" + +#include "AccGroupInfo.h" +#include "DocAccessible-inl.h" +#include "XULTreeAccessible.h" +#include "nsAccUtils.h" + +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/DocumentOrShadowRoot.h" +#include "mozilla/dom/HTMLLabelElement.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// AccIterator +//////////////////////////////////////////////////////////////////////////////// + +AccIterator::AccIterator(const LocalAccessible* aAccessible, + filters::FilterFuncPtr aFilterFunc) + : mFilterFunc(aFilterFunc) { + mState = new IteratorState(aAccessible); +} + +AccIterator::~AccIterator() { + while (mState) { + IteratorState* tmp = mState; + mState = tmp->mParentState; + delete tmp; + } +} + +LocalAccessible* AccIterator::Next() { + while (mState) { + LocalAccessible* child = mState->mParent->LocalChildAt(mState->mIndex++); + if (!child) { + IteratorState* tmp = mState; + mState = mState->mParentState; + delete tmp; + + continue; + } + + uint32_t result = mFilterFunc(child); + if (result & filters::eMatch) return child; + + if (!(result & filters::eSkipSubtree)) { + IteratorState* childState = new IteratorState(child, mState); + mState = childState; + } + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccIterator::IteratorState + +AccIterator::IteratorState::IteratorState(const LocalAccessible* aParent, + IteratorState* mParentState) + : mParent(aParent), mIndex(0), mParentState(mParentState) {} + +//////////////////////////////////////////////////////////////////////////////// +// RelatedAccIterator +//////////////////////////////////////////////////////////////////////////////// + +RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument, + nsIContent* aDependentContent, + nsAtom* aRelAttr) + : mDocument(aDocument), mRelAttr(aRelAttr), mProviders(nullptr), mIndex(0) { + nsAutoString id; + if (aDependentContent->IsElement() && + aDependentContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, + id)) { + mProviders = mDocument->GetRelProviders(aDependentContent->AsElement(), id); + } +} + +LocalAccessible* RelatedAccIterator::Next() { + if (!mProviders) return nullptr; + + while (mIndex < mProviders->Length()) { + const auto& provider = (*mProviders)[mIndex++]; + + // Return related accessible for the given attribute. + if (provider->mRelAttr == mRelAttr) { + LocalAccessible* related = mDocument->GetAccessible(provider->mContent); + if (related) { + return related; + } + + // If the document content is pointed by relation then return the + // document itself. + if (provider->mContent == mDocument->GetContent()) { + return mDocument; + } + } + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLLabelIterator +//////////////////////////////////////////////////////////////////////////////// + +HTMLLabelIterator::HTMLLabelIterator(DocAccessible* aDocument, + const LocalAccessible* aAccessible, + LabelFilter aFilter) + : mRelIter(aDocument, aAccessible->GetContent(), nsGkAtoms::_for), + mAcc(aAccessible), + mLabelFilter(aFilter) {} + +bool HTMLLabelIterator::IsLabel(LocalAccessible* aLabel) { + dom::HTMLLabelElement* labelEl = + dom::HTMLLabelElement::FromNode(aLabel->GetContent()); + return labelEl && labelEl->GetControl() == mAcc->GetContent(); +} + +LocalAccessible* HTMLLabelIterator::Next() { + // Get either
such as Monorail. + if (aContext->IsTable() || + (aContext->LocalParent() && aContext->LocalParent()->IsTable())) { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + } + return nullptr; + }, + roles::ROW) + +MARKUPMAP( + ul, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +MARKUPMAP( + meter, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLMeterAccessible(aElement, aContext->Document()); + }, + roles::METER) diff --git a/accessible/base/IDSet.h b/accessible/base/IDSet.h new file mode 100644 index 0000000000..a149bf95a3 --- /dev/null +++ b/accessible/base/IDSet.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +/** + * A class to generate unique IDs in the range [ - 2^31, 0 ) + */ + +#ifndef MOZILLA_A11Y_IDSet_h_ +#define MOZILLA_A11Y_IDSet_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/SplayTree.h" + +namespace mozilla { +namespace a11y { + +/** + * On windows an accessible's id must be a negative 32 bit integer. It is + * important to support recycling arbitrary IDs because accessibles can be + * created and destroyed at any time in the life of a page. IDSet provides 2 + * operations: generate an ID in the range (0, mMaxId], and release an ID so + * it can be allocated again. Allocated ID are tracked by a sparse bitmap + * implemented with a splay tree. Nodes in the tree are keyed by the upper N + * bits of the ID, and the node contains a bitmap tracking the allocation of + * 2^(ceil(log2(mMaxId)) - N) IDs. + * + * Note that negation is handled by MsaaIdGenerator as it performs additional + * decoration on the ID generated by IDSet. + * @see mozilla::a11y::MsaaIdGenerator + */ +class IDSet { + public: + constexpr explicit IDSet(const uint32_t aMaxIdBits) + : mBitSet(), + mIdx(0), + mMaxId((1UL << aMaxIdBits) - 1UL), + mMaxIdx(mMaxId / bitsPerElt) {} + + /** + * Return a new unique id. + */ + uint32_t GetID() { + uint32_t idx = mIdx; + while (true) { + BitSetElt* elt = mBitSet.findOrInsert(BitSetElt(idx)); + if (elt->mBitvec[0] != UINT64_MAX) { + uint32_t i = CountTrailingZeroes64(~elt->mBitvec[0]); + + elt->mBitvec[0] |= (1ull << i); + mIdx = idx; + return (elt->mIdx * bitsPerElt + i); + } + + if (elt->mBitvec[1] != UINT64_MAX) { + uint32_t i = CountTrailingZeroes64(~elt->mBitvec[1]); + + elt->mBitvec[1] |= (1ull << i); + mIdx = idx; + return (elt->mIdx * bitsPerElt + bitsPerWord + i); + } + + idx++; + if (idx > mMaxIdx) { + idx = 0; + } + + if (idx == mIdx) { + MOZ_CRASH("used up all the available ids"); + } + } + } + + /** + * Free a no longer required id so it may be allocated again. + */ + void ReleaseID(uint32_t aID) { + MOZ_ASSERT(aID < mMaxId); + + uint32_t idx = aID / bitsPerElt; + mIdx = idx; + BitSetElt* elt = mBitSet.find(BitSetElt(idx)); + MOZ_ASSERT(elt); + + uint32_t vecIdx = (aID % bitsPerElt) / bitsPerWord; + elt->mBitvec[vecIdx] &= ~(1ull << (aID % bitsPerWord)); + if (elt->mBitvec[0] == 0 && elt->mBitvec[1] == 0) { + delete mBitSet.remove(*elt); + } + } + + private: + static const unsigned int wordsPerElt = 2; + static const unsigned int bitsPerWord = 64; + static const unsigned int bitsPerElt = wordsPerElt * bitsPerWord; + + struct BitSetElt : mozilla::SplayTreeNode { + explicit BitSetElt(uint32_t aIdx) : mIdx(aIdx) { + mBitvec[0] = mBitvec[1] = 0; + } + + uint64_t mBitvec[wordsPerElt]; + uint32_t mIdx; + + static int compare(const BitSetElt& a, const BitSetElt& b) { + if (a.mIdx == b.mIdx) { + return 0; + } + + if (a.mIdx < b.mIdx) { + return -1; + } + return 1; + } + }; + + SplayTree mBitSet; + uint32_t mIdx; + const uint32_t mMaxId; + const uint32_t mMaxIdx; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/Logging.cpp b/accessible/base/Logging.cpp new file mode 100644 index 0000000000..7b4551e77f --- /dev/null +++ b/accessible/base/Logging.cpp @@ -0,0 +1,993 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#include "Logging.h" + +#include "LocalAccessible-inl.h" +#include "AccEvent.h" +#include "DocAccessible.h" +#include "DocAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" +#include "OuterDocAccessible.h" + +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebProgress.h" +#include "prenv.h" +#include "nsIDocShellTreeItem.h" +#include "mozilla/Maybe.h" +#include "mozilla/PresShell.h" +#include "mozilla/StackWalk.h" +#include "mozilla/ToString.h" +#include "mozilla/dom/BorrowedAttrInfo.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/Selection.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +using mozilla::dom::BorrowedAttrInfo; + +MOZ_DEFINE_MALLOC_SIZE_OF(AccessibleLoggingMallocSizeOf) + +//////////////////////////////////////////////////////////////////////////////// +// Logging helpers + +static uint32_t sModules = 0; + +struct ModuleRep { + const char* mStr; + logging::EModules mModule; +}; + +static ModuleRep sModuleMap[] = {{"docload", logging::eDocLoad}, + {"doccreate", logging::eDocCreate}, + {"docdestroy", logging::eDocDestroy}, + {"doclifecycle", logging::eDocLifeCycle}, + + {"events", logging::eEvents}, + {"platforms", logging::ePlatforms}, + {"text", logging::eText}, + {"tree", logging::eTree}, + {"treeSize", logging::eTreeSize}, + + {"DOMEvents", logging::eDOMEvents}, + {"focus", logging::eFocus}, + {"selection", logging::eSelection}, + {"notifications", logging::eNotifications}, + + {"stack", logging::eStack}, + {"verbose", logging::eVerbose}, + {"cache", logging::eCache}}; + +static void EnableLogging(const char* aModulesStr) { + sModules = 0; + if (!aModulesStr) return; + + const char* token = aModulesStr; + while (*token != '\0') { + size_t tokenLen = strcspn(token, ","); + for (unsigned int idx = 0; idx < ArrayLength(sModuleMap); idx++) { + if (strncmp(token, sModuleMap[idx].mStr, tokenLen) == 0) { +#if !defined(MOZ_PROFILING) && (!defined(DEBUG) || defined(MOZ_OPTIMIZE)) + // Stack tracing on profiling enabled or debug not optimized builds. + if (strncmp(token, "stack", tokenLen) == 0) break; +#endif + sModules |= sModuleMap[idx].mModule; + printf("\n\nmodule enabled: %s\n", sModuleMap[idx].mStr); + break; + } + } + token += tokenLen; + + if (*token == ',') token++; // skip ',' char + } +} + +static void LogDocURI(dom::Document* aDocumentNode) { + nsIURI* uri = aDocumentNode->GetDocumentURI(); + if (uri) { + printf("uri: %s", uri->GetSpecOrDefault().get()); + } else { + printf("uri: null"); + } +} + +static void LogDocShellState(dom::Document* aDocumentNode) { + printf("docshell busy: "); + nsCOMPtr docShell = aDocumentNode->GetDocShell(); + if (!docShell) { + printf("null docshell"); + return; + } + + nsAutoCString docShellBusy; + nsIDocShell::BusyFlags busyFlags = nsIDocShell::BUSY_FLAGS_NONE; + docShell->GetBusyFlags(&busyFlags); + if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { + printf("'none'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_BUSY) { + printf("'busy'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_BEFORE_PAGE_LOAD) { + printf(", 'before page load'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_PAGE_LOADING) { + printf(", 'page loading'"); + } +} + +static void LogDocType(dom::Document* aDocumentNode) { + if (aDocumentNode->IsActive()) { + bool isContent = aDocumentNode->IsContentDocument(); + printf("%s document", (isContent ? "content" : "chrome")); + } else { + printf("document type: [failed]"); + } +} + +static void LogDocShellTree(dom::Document* aDocumentNode) { + if (aDocumentNode->IsActive()) { + nsCOMPtr treeItem(aDocumentNode->GetDocShell()); + if (!treeItem) { + printf("in-process docshell hierarchy, null docshell;"); + return; + } + nsCOMPtr parentTreeItem; + treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem)); + nsCOMPtr rootTreeItem; + treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem)); + printf( + "in-process docshell hierarchy, parent: %p, root: %p, " + "is top level: %s;", + static_cast(parentTreeItem), static_cast(rootTreeItem), + (nsCoreUtils::IsTopLevelContentDocInProcess(aDocumentNode) ? "yes" + : "no")); + } +} + +static void LogDocState(dom::Document* aDocumentNode) { + const char* docState = nullptr; + dom::Document::ReadyState docStateFlag = aDocumentNode->GetReadyStateEnum(); + switch (docStateFlag) { + case dom::Document::READYSTATE_UNINITIALIZED: + docState = "uninitialized"; + break; + case dom::Document::READYSTATE_LOADING: + docState = "loading"; + break; + case dom::Document::READYSTATE_INTERACTIVE: + docState = "interactive"; + break; + case dom::Document::READYSTATE_COMPLETE: + docState = "complete"; + break; + } + + printf("doc state: %s", docState); + printf(", %sinitial", aDocumentNode->IsInitialDocument() ? "" : "not "); + printf(", %sshowing", aDocumentNode->IsShowing() ? "" : "not "); + printf(", %svisible", aDocumentNode->IsVisible() ? "" : "not "); + printf( + ", %svisible considering ancestors", + nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocumentNode) + ? "" + : "not "); + printf(", %sactive", aDocumentNode->IsActive() ? "" : "not "); + printf(", %sresource", aDocumentNode->IsResourceDoc() ? "" : "not "); + + dom::Element* rootEl = aDocumentNode->GetBodyElement(); + if (!rootEl) { + rootEl = aDocumentNode->GetRootElement(); + } + printf(", has %srole content", rootEl ? "" : "no "); +} + +static void LogPresShell(dom::Document* aDocumentNode) { + PresShell* presShell = aDocumentNode->GetPresShell(); + printf("presshell: %p", static_cast(presShell)); + + nsIScrollableFrame* sf = nullptr; + if (presShell) { + printf(", is %s destroying", (presShell->IsDestroying() ? "" : "not")); + sf = presShell->GetRootScrollFrameAsScrollable(); + } + printf(", root scroll frame: %p", static_cast(sf)); +} + +static void LogDocLoadGroup(dom::Document* aDocumentNode) { + nsCOMPtr loadGroup = aDocumentNode->GetDocumentLoadGroup(); + printf("load group: %p", static_cast(loadGroup)); +} + +static void LogDocParent(dom::Document* aDocumentNode) { + dom::Document* parentDoc = aDocumentNode->GetInProcessParentDocument(); + printf("parent DOM document: %p", static_cast(parentDoc)); + if (parentDoc) { + printf(", parent acc document: %p", + static_cast(GetExistingDocAccessible(parentDoc))); + printf("\n parent "); + LogDocURI(parentDoc); + printf("\n"); + } +} + +static void LogDocInfo(dom::Document* aDocumentNode, DocAccessible* aDocument) { + printf(" DOM document: %p, acc document: %p\n ", + static_cast(aDocumentNode), static_cast(aDocument)); + + // log document info + if (aDocumentNode) { + LogDocURI(aDocumentNode); + printf("\n "); + LogDocShellState(aDocumentNode); + printf("; "); + LogDocType(aDocumentNode); + printf("\n "); + LogDocShellTree(aDocumentNode); + printf("\n "); + LogDocState(aDocumentNode); + printf("\n "); + LogPresShell(aDocumentNode); + printf("\n "); + LogDocLoadGroup(aDocumentNode); + printf(", "); + LogDocParent(aDocumentNode); + printf("\n"); + } +} + +static void LogShellLoadType(nsIDocShell* aDocShell) { + printf("load type: "); + + uint32_t loadType = 0; + aDocShell->GetLoadType(&loadType); + switch (loadType) { + case LOAD_NORMAL: + printf("normal; "); + break; + case LOAD_NORMAL_REPLACE: + printf("normal replace; "); + break; + case LOAD_HISTORY: + printf("history; "); + break; + case LOAD_NORMAL_BYPASS_CACHE: + printf("normal bypass cache; "); + break; + case LOAD_NORMAL_BYPASS_PROXY: + printf("normal bypass proxy; "); + break; + case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE: + printf("normal bypass proxy and cache; "); + break; + case LOAD_RELOAD_NORMAL: + printf("reload normal; "); + break; + case LOAD_RELOAD_BYPASS_CACHE: + printf("reload bypass cache; "); + break; + case LOAD_RELOAD_BYPASS_PROXY: + printf("reload bypass proxy; "); + break; + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + printf("reload bypass proxy and cache; "); + break; + case LOAD_LINK: + printf("link; "); + break; + case LOAD_REFRESH: + printf("refresh; "); + break; + case LOAD_REFRESH_REPLACE: + printf("refresh replace; "); + break; + case LOAD_RELOAD_CHARSET_CHANGE: + printf("reload charset change; "); + break; + case LOAD_BYPASS_HISTORY: + printf("bypass history; "); + break; + case LOAD_STOP_CONTENT: + printf("stop content; "); + break; + case LOAD_STOP_CONTENT_AND_REPLACE: + printf("stop content and replace; "); + break; + case LOAD_PUSHSTATE: + printf("load pushstate; "); + break; + case LOAD_REPLACE_BYPASS_CACHE: + printf("replace bypass cache; "); + break; + case LOAD_ERROR_PAGE: + printf("error page;"); + break; + default: + printf("unknown"); + } +} + +static void LogRequest(nsIRequest* aRequest) { + if (aRequest) { + nsAutoCString name; + aRequest->GetName(name); + printf(" request spec: %s\n", name.get()); + uint32_t loadFlags = 0; + aRequest->GetLoadFlags(&loadFlags); + printf(" request load flags: %x; ", loadFlags); + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) printf("document uri; "); + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) { + printf("retargeted document uri; "); + } + if (loadFlags & nsIChannel::LOAD_REPLACE) printf("replace; "); + if (loadFlags & nsIChannel::LOAD_INITIAL_DOCUMENT_URI) { + printf("initial document uri; "); + } + if (loadFlags & nsIChannel::LOAD_TARGETED) printf("targeted; "); + if (loadFlags & nsIChannel::LOAD_CALL_CONTENT_SNIFFERS) { + printf("call content sniffers; "); + } + if (loadFlags & nsIChannel::LOAD_BYPASS_URL_CLASSIFIER) { + printf("bypass classify uri; "); + } + } else { + printf(" no request"); + } +} + +static void LogDocAccState(DocAccessible* aDocument) { + printf("document acc state: "); + if (aDocument->HasLoadState(DocAccessible::eCompletelyLoaded)) { + printf("completely loaded;"); + } else if (aDocument->HasLoadState(DocAccessible::eReady)) { + printf("ready;"); + } else if (aDocument->HasLoadState(DocAccessible::eDOMLoaded)) { + printf("DOM loaded;"); + } else if (aDocument->HasLoadState(DocAccessible::eTreeConstructed)) { + printf("tree constructed;"); + } +} + +static void GetDocLoadEventType(AccEvent* aEvent, nsACString& aEventType) { + uint32_t type = aEvent->GetEventType(); + if (type == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED) { + aEventType.AssignLiteral("load stopped"); + } else if (type == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE) { + aEventType.AssignLiteral("load complete"); + } else if (type == nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD) { + aEventType.AssignLiteral("reload"); + } else if (type == nsIAccessibleEvent::EVENT_STATE_CHANGE) { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + if (event->GetState() == states::BUSY) { + aEventType.AssignLiteral("busy "); + if (event->IsStateEnabled()) { + aEventType.AppendLiteral("true"); + } else { + aEventType.AppendLiteral("false"); + } + } + } +} + +static void DescribeNode(nsINode* aNode, nsAString& aOutDescription) { + if (!aNode) { + aOutDescription.AppendLiteral("null"); + return; + } + + aOutDescription.AppendPrintf("0x%p, ", (void*)aNode); + aOutDescription.Append(aNode->NodeInfo()->QualifiedName()); + + if (!aNode->IsElement()) { + return; + } + + dom::Element* elm = aNode->AsElement(); + + nsAtom* idAtom = elm->GetID(); + if (idAtom) { + nsAutoCString id; + idAtom->ToUTF8String(id); + aOutDescription.AppendPrintf("@id=\"%s\" ", id.get()); + } else { + aOutDescription.Append(' '); + } + + uint32_t attrCount = elm->GetAttrCount(); + if (!attrCount || (idAtom && attrCount == 1)) { + return; + } + + aOutDescription.AppendLiteral("[ "); + + for (uint32_t index = 0; index < attrCount; index++) { + BorrowedAttrInfo info = elm->GetAttrInfoAt(index); + + // Skip redundant display of id attribute. + if (info.mName->Equals(nsGkAtoms::id)) { + continue; + } + + // name + nsAutoString name; + info.mName->GetQualifiedName(name); + aOutDescription.Append(name); + + aOutDescription.AppendLiteral("=\""); + + // value + nsAutoString value; + info.mValue->ToString(value); + for (uint32_t i = value.Length(); i > 0; --i) { + if (value[i - 1] == char16_t('"')) value.Insert(char16_t('\\'), i - 1); + } + aOutDescription.Append(value); + aOutDescription.AppendLiteral("\" "); + } + + aOutDescription.Append(']'); +} + +//////////////////////////////////////////////////////////////////////////////// +// namespace logging:: document life cycle logging methods + +static const char* sDocLoadTitle = "DOCLOAD"; +static const char* sDocCreateTitle = "DOCCREATE"; +static const char* sDocDestroyTitle = "DOCDESTROY"; +static const char* sDocEventTitle = "DOCEVENT"; +static const char* sFocusTitle = "FOCUS"; + +void logging::DocLoad(const char* aMsg, nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags) { + MsgBegin(sDocLoadTitle, "%s", aMsg); + + nsCOMPtr DOMWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow)); + nsPIDOMWindowOuter* window = nsPIDOMWindowOuter::From(DOMWindow); + if (!window) { + MsgEnd(); + return; + } + + nsCOMPtr documentNode = window->GetDoc(); + if (!documentNode) { + MsgEnd(); + return; + } + + DocAccessible* document = GetExistingDocAccessible(documentNode); + + LogDocInfo(documentNode, document); + + nsCOMPtr docShell = window->GetDocShell(); + printf("\n "); + LogShellLoadType(docShell); + printf("\n"); + LogRequest(aRequest); + printf("\n"); + printf(" state flags: %x", aStateFlags); + bool isDocLoading; + aWebProgress->GetIsLoadingDocument(&isDocLoading); + printf(", document is %sloading\n", (isDocLoading ? "" : "not ")); + + MsgEnd(); +} + +void logging::DocLoad(const char* aMsg, dom::Document* aDocumentNode) { + MsgBegin(sDocLoadTitle, "%s", aMsg); + + DocAccessible* document = GetExistingDocAccessible(aDocumentNode); + LogDocInfo(aDocumentNode, document); + + MsgEnd(); +} + +void logging::DocCompleteLoad(DocAccessible* aDocument, + bool aIsLoadEventTarget) { + MsgBegin(sDocLoadTitle, "document loaded *completely*"); + + printf(" DOM document: %p, acc document: %p\n", + static_cast(aDocument->DocumentNode()), + static_cast(aDocument)); + + printf(" "); + LogDocURI(aDocument->DocumentNode()); + printf("\n"); + + printf(" "); + LogDocAccState(aDocument); + printf("\n"); + + printf(" document is load event target: %s\n", + (aIsLoadEventTarget ? "true" : "false")); + + MsgEnd(); +} + +void logging::DocLoadEventFired(AccEvent* aEvent) { + nsAutoCString strEventType; + GetDocLoadEventType(aEvent, strEventType); + if (!strEventType.IsEmpty()) printf(" fire: %s\n", strEventType.get()); +} + +void logging::DocLoadEventHandled(AccEvent* aEvent) { + nsAutoCString strEventType; + GetDocLoadEventType(aEvent, strEventType); + if (strEventType.IsEmpty()) return; + + MsgBegin(sDocEventTitle, "handled '%s' event", strEventType.get()); + + DocAccessible* document = aEvent->GetAccessible()->AsDoc(); + if (document) LogDocInfo(document->DocumentNode(), document); + + MsgEnd(); +} + +void logging::DocCreate(const char* aMsg, dom::Document* aDocumentNode, + DocAccessible* aDocument) { + DocAccessible* document = + aDocument ? aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocCreateTitle, "%s", aMsg); + LogDocInfo(aDocumentNode, document); + MsgEnd(); +} + +void logging::DocDestroy(const char* aMsg, dom::Document* aDocumentNode, + DocAccessible* aDocument) { + DocAccessible* document = + aDocument ? aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocDestroyTitle, "%s", aMsg); + LogDocInfo(aDocumentNode, document); + MsgEnd(); +} + +void logging::OuterDocDestroy(OuterDocAccessible* aOuterDoc) { + MsgBegin(sDocDestroyTitle, "outerdoc shutdown"); + logging::Address("outerdoc", aOuterDoc); + MsgEnd(); +} + +void logging::FocusNotificationTarget(const char* aMsg, + const char* aTargetDescr, + LocalAccessible* aTarget) { + MsgBegin(sFocusTitle, "%s", aMsg); + AccessibleNNode(aTargetDescr, aTarget); + MsgEnd(); +} + +void logging::FocusNotificationTarget(const char* aMsg, + const char* aTargetDescr, + nsINode* aTargetNode) { + MsgBegin(sFocusTitle, "%s", aMsg); + Node(aTargetDescr, aTargetNode); + MsgEnd(); +} + +void logging::FocusNotificationTarget(const char* aMsg, + const char* aTargetDescr, + nsISupports* aTargetThing) { + MsgBegin(sFocusTitle, "%s", aMsg); + + if (aTargetThing) { + nsCOMPtr targetNode(do_QueryInterface(aTargetThing)); + if (targetNode) { + AccessibleNNode(aTargetDescr, targetNode); + } else { + printf(" %s: %p, window\n", aTargetDescr, + static_cast(aTargetThing)); + } + } + + MsgEnd(); +} + +void logging::ActiveItemChangeCausedBy(const char* aCause, + LocalAccessible* aTarget) { + SubMsgBegin(); + printf(" Caused by: %s\n", aCause); + AccessibleNNode("Item", aTarget); + SubMsgEnd(); +} + +void logging::ActiveWidget(LocalAccessible* aWidget) { + SubMsgBegin(); + + AccessibleNNode("Widget", aWidget); + printf(" Widget is active: %s, has operable items: %s\n", + (aWidget && aWidget->IsActiveWidget() ? "true" : "false"), + (aWidget && aWidget->AreItemsOperable() ? "true" : "false")); + + SubMsgEnd(); +} + +void logging::FocusDispatched(LocalAccessible* aTarget) { + SubMsgBegin(); + AccessibleNNode("A11y target", aTarget); + SubMsgEnd(); +} + +void logging::SelChange(dom::Selection* aSelection, DocAccessible* aDocument, + int16_t aReason) { + SelectionType type = aSelection->GetType(); + + const char* strType = 0; + if (type == SelectionType::eNormal) { + strType = "normal"; + } else if (type == SelectionType::eSpellCheck) { + strType = "spellcheck"; + } else { + strType = "unknown"; + } + + bool isIgnored = !aDocument || !aDocument->IsContentLoaded(); + printf( + "\nSelection changed, selection type: %s, notification %s, reason: %d\n", + strType, (isIgnored ? "ignored" : "pending"), aReason); + + Stack(); +} + +void logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, ...) { + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + va_list vl; + va_start(vl, aExtraFlags); + const char* descr = va_arg(vl, const char*); + if (descr) { + LocalAccessible* acc = va_arg(vl, LocalAccessible*); + MsgBegin("TREE", "%s; doc: %p", aMsg, acc ? acc->Document() : nullptr); + AccessibleInfo(descr, acc); + while ((descr = va_arg(vl, const char*))) { + AccessibleInfo(descr, va_arg(vl, LocalAccessible*)); + } + } else { + MsgBegin("TREE", "%s", aMsg); + } + va_end(vl); + MsgEnd(); + + if (aExtraFlags & eStack) { + Stack(); + } + } +} + +void logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, + const char* aMsg1, LocalAccessible* aAcc, + const char* aMsg2, nsINode* aNode) { + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + MsgBegin("TREE", "%s; doc: %p", aMsg, aAcc ? aAcc->Document() : nullptr); + AccessibleInfo(aMsg1, aAcc); + LocalAccessible* acc = + aAcc ? aAcc->Document()->GetAccessible(aNode) : nullptr; + if (acc) { + AccessibleInfo(aMsg2, acc); + } else { + Node(aMsg2, aNode); + } + MsgEnd(); + } +} + +void logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, + LocalAccessible* aParent) { + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + MsgBegin("TREE", "%s; doc: %p", aMsg, aParent->Document()); + AccessibleInfo("container", aParent); + for (uint32_t idx = 0; idx < aParent->ChildCount(); idx++) { + AccessibleInfo("child", aParent->LocalChildAt(idx)); + } + MsgEnd(); + } +} + +void logging::Tree(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot, GetTreePrefix aPrefixFunc, + void* aGetTreePrefixData) { + logging::MsgBegin(aTitle, "%s", aMsgText); + + nsAutoString level; + LocalAccessible* root = aRoot; + do { + const char* prefix = + aPrefixFunc ? aPrefixFunc(aGetTreePrefixData, root) : ""; + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::AccessibleInfo(prefix, root); + if (root->LocalFirstChild() && !root->LocalFirstChild()->IsDoc()) { + level.AppendLiteral(u" "); + root = root->LocalFirstChild(); + continue; + } + int32_t idxInParent = root != aRoot && root->mParent + ? root->mParent->mChildren.IndexOf(root) + : -1; + if (idxInParent != -1 && + idxInParent < + static_cast(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + continue; + } + while (root != aRoot && (root = root->LocalParent())) { + level.Cut(0, 2); + int32_t idxInParent = !root->IsDoc() && root->mParent + ? root->mParent->mChildren.IndexOf(root) + : -1; + if (idxInParent != -1 && + idxInParent < + static_cast(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + break; + } + } + } while (root && root != aRoot); + + logging::MsgEnd(); +} + +void logging::DOMTree(const char* aTitle, const char* aMsgText, + DocAccessible* aDocument) { + logging::MsgBegin(aTitle, "%s", aMsgText); + nsAutoString level; + nsINode* root = aDocument->DocumentNode(); + do { + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::Node("", root); + if (root->GetFirstChild()) { + level.AppendLiteral(u" "); + root = root->GetFirstChild(); + continue; + } + if (root->GetNextSibling()) { + root = root->GetNextSibling(); + continue; + } + while ((root = root->GetParentNode())) { + level.Cut(0, 2); + if (root->GetNextSibling()) { + root = root->GetNextSibling(); + break; + } + } + } while (root); + logging::MsgEnd(); +} + +void logging::TreeSize(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot) { + logging::MsgBegin(aTitle, "%s", aMsgText); + logging::AccessibleInfo("Logging tree size from: ", aRoot); + size_t b = 0; + size_t n = 0; + LocalAccessible* root = aRoot; + do { + // Process the current acc + b += AccessibleLoggingMallocSizeOf(root); + n++; + + // Get next acc + if (root->LocalFirstChild() && !root->LocalFirstChild()->IsDoc()) { + root = root->LocalFirstChild(); + continue; + } + int32_t idxInParent = root != aRoot && root->mParent + ? root->mParent->mChildren.IndexOf(root) + : -1; + if (idxInParent != -1 && + idxInParent < + static_cast(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + continue; + } + while (root != aRoot && (root = root->LocalParent())) { + int32_t idxInParent = !root->IsDoc() && root->mParent + ? root->mParent->mChildren.IndexOf(root) + : -1; + if (idxInParent != -1 && + idxInParent < + static_cast(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + break; + } + } + } while (root && root != aRoot); + + printf("\nTree contains %zu accessibles and is %zu bytes\n", n, b); + logging::MsgEnd(); +} + +void logging::MsgBegin(const char* aTitle, const char* aMsgText, ...) { + printf("\nA11Y %s: ", aTitle); + + va_list argptr; + va_start(argptr, aMsgText); + vprintf(aMsgText, argptr); + va_end(argptr); + + PRIntervalTime time = PR_IntervalNow(); + uint32_t mins = (PR_IntervalToSeconds(time) / 60) % 60; + uint32_t secs = PR_IntervalToSeconds(time) % 60; + uint32_t msecs = PR_IntervalToMilliseconds(time) % 1000; + printf("; %02u:%02u.%03u", mins, secs, msecs); + + printf("\n {\n"); +} + +void logging::MsgEnd() { printf(" }\n"); } + +void logging::SubMsgBegin() { printf(" {\n"); } + +void logging::SubMsgEnd() { printf(" }\n"); } + +void logging::MsgEntry(const char* aEntryText, ...) { + printf(" "); + + va_list argptr; + va_start(argptr, aEntryText); + vprintf(aEntryText, argptr); + va_end(argptr); + + printf("\n"); +} + +void logging::Text(const char* aText) { printf(" %s\n", aText); } + +void logging::Address(const char* aDescr, LocalAccessible* aAcc) { + if (!aAcc->IsDoc()) { + printf(" %s accessible: %p, node: %p\n", aDescr, + static_cast(aAcc), static_cast(aAcc->GetNode())); + } + + DocAccessible* doc = aAcc->Document(); + dom::Document* docNode = doc->DocumentNode(); + printf(" document: %p, node: %p\n", static_cast(doc), + static_cast(docNode)); + + printf(" "); + LogDocURI(docNode); + printf("\n"); +} + +void logging::Node(const char* aDescr, nsINode* aNode) { + Maybe idxInParent = aNode->ComputeIndexInParentNode(); + nsAutoString nodeDesc; + DescribeNode(aNode, nodeDesc); + printf(" %s: %s, idx in parent %s\n", aDescr, + NS_ConvertUTF16toUTF8(nodeDesc).get(), ToString(idxInParent).c_str()); +} + +void logging::Document(DocAccessible* aDocument) { + printf(" Document: %p, document node: %p\n", static_cast(aDocument), + static_cast(aDocument->DocumentNode())); + + printf(" Document "); + LogDocURI(aDocument->DocumentNode()); + printf("\n"); +} + +void logging::AccessibleInfo(const char* aDescr, LocalAccessible* aAccessible) { + printf(" %s: %p; ", aDescr, static_cast(aAccessible)); + if (!aAccessible) { + printf("\n"); + return; + } + if (aAccessible->IsDefunct()) { + printf("defunct\n"); + return; + } + if (!aAccessible->Document() || aAccessible->Document()->IsDefunct()) { + printf("document is shutting down, no info\n"); + return; + } + + nsAutoString role; + GetAccService()->GetStringRole(aAccessible->Role(), role); + printf("role: %s", NS_ConvertUTF16toUTF8(role).get()); + + nsAutoString name; + aAccessible->Name(name); + if (!name.IsEmpty()) { + printf(", name: '%s'", NS_ConvertUTF16toUTF8(name).get()); + } + + printf(", idx: %d", aAccessible->IndexInParent()); + + nsAutoString nodeDesc; + DescribeNode(aAccessible->GetNode(), nodeDesc); + printf(", node: %s\n", NS_ConvertUTF16toUTF8(nodeDesc).get()); +} + +void logging::AccessibleNNode(const char* aDescr, + LocalAccessible* aAccessible) { + printf(" %s: %p; ", aDescr, static_cast(aAccessible)); + if (!aAccessible) return; + + nsAutoString role; + GetAccService()->GetStringRole(aAccessible->Role(), role); + nsAutoString name; + aAccessible->Name(name); + + printf("role: %s, name: '%s';\n", NS_ConvertUTF16toUTF8(role).get(), + NS_ConvertUTF16toUTF8(name).get()); + + nsAutoCString nodeDescr(aDescr); + nodeDescr.AppendLiteral(" node"); + Node(nodeDescr.get(), aAccessible->GetNode()); + + Document(aAccessible->Document()); +} + +void logging::AccessibleNNode(const char* aDescr, nsINode* aNode) { + DocAccessible* document = + GetAccService()->GetDocAccessible(aNode->OwnerDoc()); + + if (document) { + LocalAccessible* accessible = document->GetAccessible(aNode); + if (accessible) { + AccessibleNNode(aDescr, accessible); + return; + } + } + + nsAutoCString nodeDescr("[not accessible] "); + nodeDescr.Append(aDescr); + Node(nodeDescr.get(), aNode); + + if (document) { + Document(document); + return; + } + + printf(" [contained by not accessible document]:\n"); + LogDocInfo(aNode->OwnerDoc(), document); + printf("\n"); +} + +void logging::DOMEvent(const char* aDescr, nsINode* aOrigTarget, + const nsAString& aEventType) { + logging::MsgBegin("DOMEvents", "event '%s' %s", + NS_ConvertUTF16toUTF8(aEventType).get(), aDescr); + logging::AccessibleNNode("Target", aOrigTarget); + logging::MsgEnd(); +} + +void logging::Stack() { + if (IsEnabled(eStack)) { + printf(" stack: \n"); + MozWalkTheStack(stdout); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// namespace logging:: initialization + +bool logging::IsEnabled(uint32_t aModules) { return sModules & aModules; } + +bool logging::IsEnabledAll(uint32_t aModules) { + return (sModules & aModules) == aModules; +} + +bool logging::IsEnabled(const nsAString& aModuleStr) { + for (unsigned int idx = 0; idx < ArrayLength(sModuleMap); idx++) { + if (aModuleStr.EqualsASCII(sModuleMap[idx].mStr)) { + return sModules & sModuleMap[idx].mModule; + } + } + + return false; +} + +void logging::Enable(const nsCString& aModules) { + EnableLogging(aModules.get()); +} + +void logging::CheckEnv() { EnableLogging(PR_GetEnv("A11YLOG")); } diff --git a/accessible/base/Logging.h b/accessible/base/Logging.h new file mode 100644 index 0000000000..2a6a93faa9 --- /dev/null +++ b/accessible/base/Logging.h @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_logs_h__ +#define mozilla_a11y_logs_h__ + +#include "nscore.h" +#include "nsStringFwd.h" +#include "mozilla/Attributes.h" + +class nsINode; +class nsIRequest; +class nsISupports; +class nsIWebProgress; + +namespace mozilla { + +namespace dom { +class Document; +class Selection; +} // namespace dom + +namespace a11y { + +class AccEvent; +class LocalAccessible; +class DocAccessible; +class OuterDocAccessible; + +namespace logging { + +enum EModules { + eDocLoad = 1 << 0, + eDocCreate = 1 << 1, + eDocDestroy = 1 << 2, + eDocLifeCycle = eDocLoad | eDocCreate | eDocDestroy, + + eEvents = 1 << 3, + ePlatforms = 1 << 4, + eText = 1 << 5, + eTree = 1 << 6, + eTreeSize = 1 << 7, + + eDOMEvents = 1 << 8, + eFocus = 1 << 9, + eSelection = 1 << 10, + eNotifications = eDOMEvents | eSelection | eFocus, + + // extras + eStack = 1 << 11, + eVerbose = 1 << 12, + eCache = 1 << 13, +}; + +/** + * Return true if any of the given modules is logged. + */ +bool IsEnabled(uint32_t aModules); + +/** + * Return true if all of the given modules are logged. + */ +bool IsEnabledAll(uint32_t aModules); + +/** + * Return true if the given module is logged. + */ +bool IsEnabled(const nsAString& aModules); + +/** + * Log the document loading progress. + */ +void DocLoad(const char* aMsg, nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags); +void DocLoad(const char* aMsg, dom::Document* aDocumentNode); +void DocCompleteLoad(DocAccessible* aDocument, bool aIsLoadEventTarget); + +/** + * Log that document load event was fired. + */ +void DocLoadEventFired(AccEvent* aEvent); + +/** + * Log that document laod event was handled. + */ +void DocLoadEventHandled(AccEvent* aEvent); + +/** + * Log the document was created. + */ +void DocCreate(const char* aMsg, dom::Document* aDocumentNode, + DocAccessible* aDocument = nullptr); + +/** + * Log the document was destroyed. + */ +void DocDestroy(const char* aMsg, dom::Document* aDocumentNode, + DocAccessible* aDocument = nullptr); + +/** + * Log the outer document was destroyed. + */ +void OuterDocDestroy(OuterDocAccessible* OuterDoc); + +/** + * Log the focus notification target. + */ +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + LocalAccessible* aTarget); +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsINode* aTargetNode); +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsISupports* aTargetThing); + +/** + * Log a cause of active item descendant change (submessage). + */ +void ActiveItemChangeCausedBy(const char* aMsg, LocalAccessible* aTarget); + +/** + * Log the active widget (submessage). + */ +void ActiveWidget(LocalAccessible* aWidget); + +/** + * Log the focus event was dispatched (submessage). + */ +void FocusDispatched(LocalAccessible* aTarget); + +/** + * Log the selection change. + */ +void SelChange(dom::Selection* aSelection, DocAccessible* aDocument, + int16_t aReason); + +/** + * Log the given accessible elements info. + */ +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, ...); +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, const char* aMsg1, + LocalAccessible* aAcc, const char* aMsg2, nsINode* aNode); +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, LocalAccessible* aParent); + +/** + * Log the accessible/DOM tree. + */ +typedef const char* (*GetTreePrefix)(void* aData, LocalAccessible*); +void Tree(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot, + GetTreePrefix aPrefixFunc = nullptr, + void* aGetTreePrefixData = nullptr); +void DOMTree(const char* aTitle, const char* aMsgText, DocAccessible* aDoc); + +/** + * Log the tree size in bytes. + */ +void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot); + +/** + * Log the message ('title: text' format) on new line. Print the start and end + * boundaries of the message body designated by '{' and '}' (2 spaces indent for + * body). + */ +void MsgBegin(const char* aTitle, const char* aMsgText, ...) + MOZ_FORMAT_PRINTF(2, 3); +void MsgEnd(); + +/** + * Print start and end boundaries of the message body designated by '{' and '}' + * (2 spaces indent for body). + */ +void SubMsgBegin(); +void SubMsgEnd(); + +/** + * Log the entry into message body (4 spaces indent). + */ +void MsgEntry(const char* aEntryText, ...) MOZ_FORMAT_PRINTF(1, 2); + +/** + * Log the text, two spaces offset is used. + */ +void Text(const char* aText); + +/** + * Log the accessible object address as message entry (4 spaces indent). + */ +void Address(const char* aDescr, LocalAccessible* aAcc); + +/** + * Log the DOM node info as message entry. + */ +void Node(const char* aDescr, nsINode* aNode); + +/** + * Log the document accessible info as message entry. + */ +void Document(DocAccessible* aDocument); + +/** + * Log the accessible and its DOM node as a message entry. + */ +void AccessibleInfo(const char* aDescr, LocalAccessible* aAccessible); +void AccessibleNNode(const char* aDescr, LocalAccessible* aAccessible); +void AccessibleNNode(const char* aDescr, nsINode* aNode); + +/** + * Log the DOM event. + */ +void DOMEvent(const char* aDescr, nsINode* aOrigTarget, + const nsAString& aEventType); + +/** + * Log the call stack, two spaces offset is used. + */ +void Stack(); + +/** + * Enable logging of the specified modules, all other modules aren't logged. + */ +void Enable(const nsCString& aModules); + +/** + * Enable logging of modules specified by A11YLOG environment variable, + * all other modules aren't logged. + */ +void CheckEnv(); + +} // namespace logging + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/MathMLMarkupMap.h b/accessible/base/MathMLMarkupMap.h new file mode 100644 index 0000000000..a03dccb358 --- /dev/null +++ b/accessible/base/MathMLMarkupMap.h @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +MARKUPMAP(math, New_HyperText, roles::MATHML_MATH) + +MARKUPMAP(mi_, New_HyperText, roles::MATHML_IDENTIFIER) + +MARKUPMAP(mn_, New_HyperText, roles::MATHML_NUMBER) + +MARKUPMAP(mo_, New_HyperText, roles::MATHML_OPERATOR, + AttrFromDOM(accent_, accent_), AttrFromDOM(fence_, fence_), + AttrFromDOM(separator_, separator_), AttrFromDOM(largeop_, largeop_)) + +MARKUPMAP(mtext_, New_HyperText, roles::MATHML_TEXT) + +MARKUPMAP(ms_, New_HyperText, roles::MATHML_STRING_LITERAL) + +MARKUPMAP(mglyph_, New_HyperText, roles::MATHML_GLYPH) + +MARKUPMAP(mrow_, New_HyperText, roles::MATHML_ROW) + +MARKUPMAP(mfrac_, New_HyperText, roles::MATHML_FRACTION, + AttrFromDOM(bevelled_, bevelled_), + AttrFromDOM(linethickness_, linethickness_)) + +MARKUPMAP(msqrt_, New_HyperText, roles::MATHML_SQUARE_ROOT) + +MARKUPMAP(mroot_, New_HyperText, roles::MATHML_ROOT) + +MARKUPMAP(mfenced_, New_HyperText, roles::MATHML_ROW) + +MARKUPMAP(menclose_, New_HyperText, roles::MATHML_ENCLOSED, + AttrFromDOM(notation_, notation_)) + +MARKUPMAP(mstyle_, New_HyperText, roles::MATHML_STYLE) + +MARKUPMAP(msub_, New_HyperText, roles::MATHML_SUB) + +MARKUPMAP(msup_, New_HyperText, roles::MATHML_SUP) + +MARKUPMAP(msubsup_, New_HyperText, roles::MATHML_SUB_SUP) + +MARKUPMAP(munder_, New_HyperText, roles::MATHML_UNDER, + AttrFromDOM(accentunder_, accentunder_), AttrFromDOM(align, align)) + +MARKUPMAP(mover_, New_HyperText, roles::MATHML_OVER, + AttrFromDOM(accent_, accent_), AttrFromDOM(align, align)) + +MARKUPMAP(munderover_, New_HyperText, roles::MATHML_UNDER_OVER, + AttrFromDOM(accent_, accent_), + AttrFromDOM(accentunder_, accentunder_), AttrFromDOM(align, align)) + +MARKUPMAP(mmultiscripts_, New_HyperText, roles::MATHML_MULTISCRIPTS) + +MARKUPMAP( + mtable_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE, AttrFromDOM(align, align), + AttrFromDOM(columnlines_, columnlines_), AttrFromDOM(rowlines_, rowlines_)) + +MARKUPMAP( + mlabeledtr_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_LABELED_ROW) + +MARKUPMAP( + mtr_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE_ROW) + +MARKUPMAP( + mtd_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableCellAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(maction_, New_HyperText, roles::MATHML_ACTION, + AttrFromDOM(actiontype_, actiontype_), + AttrFromDOM(selection_, selection_)) + +MARKUPMAP(merror_, New_HyperText, roles::MATHML_ERROR) + +MARKUPMAP(mstack_, New_HyperText, roles::MATHML_STACK, + AttrFromDOM(align, align), AttrFromDOM(position, position)) + +MARKUPMAP(mlongdiv_, New_HyperText, roles::MATHML_LONG_DIVISION, + AttrFromDOM(longdivstyle_, longdivstyle_)) + +MARKUPMAP(msgroup_, New_HyperText, roles::MATHML_STACK_GROUP, + AttrFromDOM(position, position), AttrFromDOM(shift_, shift_)) + +MARKUPMAP(msrow_, New_HyperText, roles::MATHML_STACK_ROW, + AttrFromDOM(position, position)) + +MARKUPMAP(mscarries_, New_HyperText, roles::MATHML_STACK_CARRIES, + AttrFromDOM(location_, location_), AttrFromDOM(position, position)) + +MARKUPMAP(mscarry_, New_HyperText, roles::MATHML_STACK_CARRY, + AttrFromDOM(crossout_, crossout_)) + +MARKUPMAP(msline_, New_HyperText, roles::MATHML_STACK_LINE, + AttrFromDOM(position, position)) diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp new file mode 100644 index 0000000000..c7a702831f --- /dev/null +++ b/accessible/base/NotificationController.cpp @@ -0,0 +1,1089 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NotificationController.h" + +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "nsEventShell.h" +#include "TextLeafAccessible.h" +#include "TextUpdater.h" + +#include "nsIContentInlines.h" + +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccessibilityService.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector +//////////////////////////////////////////////////////////////////////////////// + +NotificationController::NotificationController(DocAccessible* aDocument, + PresShell* aPresShell) + : EventQueue(aDocument), + mObservingState(eNotObservingRefresh), + mPresShell(aPresShell), + mEventGeneration(0) { + // Schedule initial accessible tree construction. + ScheduleProcessing(); +} + +NotificationController::~NotificationController() { + NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); + if (mDocument) { + Shutdown(); + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed"); +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: AddRef/Release and cycle collection + +NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) +NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) + if (tmp->mDocument) { + tmp->Shutdown(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) + for (const auto& entry : tmp->mContentInsertions) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key"); + cb.NoteXPCOMChild(entry.GetKey()); + nsTArray>* list = entry.GetData().get(); + for (uint32_t i = 0; i < list->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions value item"); + cb.NoteXPCOMChild(list->ElementAt(i)); + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: public + +void NotificationController::Shutdown() { + if (mObservingState != eNotObservingRefresh && + mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { + // Note, this was our last chance to unregister, since we're about to + // clear mPresShell further down in this function. + mObservingState = eNotObservingRefresh; + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed (and we just " + "passed our last change to unregister)"); + // Immediately null out mPresShell, to prevent us from being registered as a + // refresh observer again. + mPresShell = nullptr; + + // Shutdown handling child documents. + int32_t childDocCount = mHangingChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + if (!mHangingChildDocuments[idx]->IsDefunct()) { + mHangingChildDocuments[idx]->Shutdown(); + } + } + + mHangingChildDocuments.Clear(); + + mDocument = nullptr; + + mTextHash.Clear(); + mContentInsertions.Clear(); + mNotifications.Clear(); + mFocusEvent = nullptr; + mEvents.Clear(); + mRelocations.Clear(); +} + +void NotificationController::CoalesceHideEvent(AccHideEvent* aHideEvent) { + LocalAccessible* parent = aHideEvent->LocalParent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + if (parent->HideEventTarget()) { + DropMutationEvent(aHideEvent); + break; + } + + if (parent->ShowEventTarget()) { + AccShowEvent* showEvent = + downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ShowEvent)); + if (showEvent->EventGeneration() < aHideEvent->EventGeneration()) { + DropMutationEvent(aHideEvent); + break; + } + } + + parent = parent->LocalParent(); + } +} + +bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) { + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + // We have to allow there to be a hide and then a show event for a target + // because of targets getting moved. However we need to coalesce a show and + // then a hide for a target which means we need to check for that here. + if (aEvent->GetAccessible()->ShowEventTarget()) { + AccTreeMutationEvent* showEvent = + mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent); + DropMutationEvent(showEvent); + return false; + } + + // If this is an additional hide event, the accessible may be hidden, or + // moved again after a move. Preserve the original hide event since + // its properties are consistent with the tree that existed before + // the next batch of mutation events is processed. + if (aEvent->GetAccessible()->HideEventTarget()) { + return false; + } + } + + AccMutationEvent* mutEvent = downcast_accEvent(aEvent); + mEventGeneration++; + mutEvent->SetEventGeneration(mEventGeneration); + + if (!mFirstMutationEvent) { + mFirstMutationEvent = aEvent; + ScheduleProcessing(); + } + + if (mLastMutationEvent) { + NS_ASSERTION(!mLastMutationEvent->NextEvent(), + "why isn't the last event the end?"); + mLastMutationEvent->SetNextEvent(aEvent); + } + + aEvent->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent = aEvent; + mMutationMap.PutEvent(aEvent); + + // Because we could be hiding the target of a show event we need to get rid + // of any such events. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + CoalesceHideEvent(downcast_accEvent(aEvent)); + + // mLastMutationEvent will point to something other than aEvent if and only + // if aEvent was just coalesced away. In that case a parent accessible + // must already have the required reorder and text change events so we are + // done here. + if (mLastMutationEvent != aEvent) { + return false; + } + } + + // We need to fire a reorder event after all of the events targeted at shown + // or hidden children of a container. So either queue a new one, or move an + // existing one to the end of the queue if the container already has a + // reorder event. + LocalAccessible* container = aEvent->GetAccessible()->LocalParent(); + RefPtr reorder; + if (!container->ReorderEventTarget()) { + reorder = new AccReorderEvent(container); + container->SetReorderEventTarget(true); + mMutationMap.PutEvent(reorder); + + // Since this is the first child of container that is changing, the name + // and/or description of dependent Accessibles may be changing. + if (PushNameOrDescriptionChange(aEvent)) { + ScheduleProcessing(); + } + } else { + AccReorderEvent* event = downcast_accEvent( + mMutationMap.GetEvent(container, EventMap::ReorderEvent)); + reorder = event; + if (mFirstMutationEvent == event) { + mFirstMutationEvent = event->NextEvent(); + } else { + event->PrevEvent()->SetNextEvent(event->NextEvent()); + } + + event->NextEvent()->SetPrevEvent(event->PrevEvent()); + event->SetNextEvent(nullptr); + } + + reorder->SetEventGeneration(mEventGeneration); + reorder->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent->SetNextEvent(reorder); + mLastMutationEvent = reorder; + + // It is not possible to have a text change event for something other than a + // hyper text accessible. + if (!container->IsHyperText()) { + return true; + } + + MOZ_ASSERT(mutEvent); + + nsString text; + aEvent->GetAccessible()->AppendTextTo(text); + if (text.IsEmpty()) { + return true; + } + + LocalAccessible* target = aEvent->GetAccessible(); + int32_t offset = container->AsHyperText()->GetChildOffset(target); + AccTreeMutationEvent* prevEvent = aEvent->PrevEvent(); + while (prevEvent && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + prevEvent = prevEvent->PrevEvent(); + } + + if (prevEvent && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE && + mutEvent->IsHide()) { + AccHideEvent* prevHide = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevHide->mTextChangeEvent; + if (prevTextChange && prevHide->LocalParent() == mutEvent->LocalParent()) { + if (prevHide->mNextSibling == target) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (prevHide->mPrevSibling == target) { + nsString temp; + target->AppendTextTo(temp); + + uint32_t extraLen = temp.Length(); + temp += prevTextChange->mModifiedText; + ; + prevTextChange->mModifiedText = temp; + prevTextChange->mStart -= extraLen; + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } else if (prevEvent && mutEvent->IsShow() && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + AccShowEvent* prevShow = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevShow->mTextChangeEvent; + if (prevTextChange && prevShow->LocalParent() == target->LocalParent()) { + int32_t index = target->IndexInParent(); + int32_t prevIndex = prevShow->GetAccessible()->IndexInParent(); + if (prevIndex + 1 == index) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (index + 1 == prevIndex) { + nsString temp; + target->AppendTextTo(temp); + prevTextChange->mStart -= temp.Length(); + temp += prevTextChange->mModifiedText; + prevTextChange->mModifiedText = temp; + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } + + if (!mutEvent->mTextChangeEvent) { + mutEvent->mTextChangeEvent = new AccTextChangeEvent( + container, offset, text, mutEvent->IsShow(), + aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput); + } + + return true; +} + +void NotificationController::DropMutationEvent(AccTreeMutationEvent* aEvent) { + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + // We don't fully drop reorder events, we just change them to inner reorder + // events. + AccReorderEvent* reorderEvent = downcast_accEvent(aEvent); + + MOZ_ASSERT(reorderEvent); + reorderEvent->SetInner(); + return; + } else if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetShowEventTarget(false); + } else { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetHideEventTarget(false); + + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + MOZ_ASSERT(hideEvent); + + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible()); + } + } + + // Do the work to splice the event out of the list. + if (mFirstMutationEvent == aEvent) { + mFirstMutationEvent = aEvent->NextEvent(); + } else { + aEvent->PrevEvent()->SetNextEvent(aEvent->NextEvent()); + } + + if (mLastMutationEvent == aEvent) { + mLastMutationEvent = aEvent->PrevEvent(); + } else { + aEvent->NextEvent()->SetPrevEvent(aEvent->PrevEvent()); + } + + aEvent->SetPrevEvent(nullptr); + aEvent->SetNextEvent(nullptr); + mMutationMap.RemoveEvent(aEvent); +} + +void NotificationController::CoalesceMutationEvents() { + AccTreeMutationEvent* event = mFirstMutationEvent; + while (event) { + AccTreeMutationEvent* nextEvent = event->NextEvent(); + uint32_t eventType = event->GetEventType(); + if (event->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + LocalAccessible* acc = event->GetAccessible(); + while (acc) { + if (acc->IsDoc()) { + break; + } + + // if a parent of the reorder event's target is being hidden that + // hide event's target must have a parent that is also a reorder event + // target. That means we don't need this reorder event. + if (acc->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + LocalAccessible* parent = acc->LocalParent(); + if (parent && parent->ReorderEventTarget()) { + AccReorderEvent* reorder = downcast_accEvent( + mMutationMap.GetEvent(parent, EventMap::ReorderEvent)); + + // We want to make sure that a reorder event comes after any show or + // hide events targeted at the children of its target. We keep the + // invariant that event generation goes up as you are farther in the + // queue, so we want to use the spot of the event with the higher + // generation number, and keep that generation number. + if (reorder && + reorder->EventGeneration() < event->EventGeneration()) { + reorder->SetEventGeneration(event->EventGeneration()); + + // It may be true that reorder was before event, and we coalesced + // away all the show / hide events between them. In that case + // event is already immediately after reorder in the queue and we + // do not need to rearrange the list of events. + if (event != reorder->NextEvent()) { + // There really should be a show or hide event before the first + // reorder event. + if (reorder->PrevEvent()) { + reorder->PrevEvent()->SetNextEvent(reorder->NextEvent()); + } else { + mFirstMutationEvent = reorder->NextEvent(); + } + + reorder->NextEvent()->SetPrevEvent(reorder->PrevEvent()); + event->PrevEvent()->SetNextEvent(reorder); + reorder->SetPrevEvent(event->PrevEvent()); + event->SetPrevEvent(reorder); + reorder->SetNextEvent(event); + } + } + DropMutationEvent(event); + break; + } + + acc = parent; + } + } else if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + // if the parent of a show event is being either shown or hidden then + // we don't need to fire a show event for a subtree of that change. + if (parent->ShowEventTarget() || parent->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + parent = parent->LocalParent(); + } + } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { + MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE, + "mutation event list has an invalid event"); + + AccHideEvent* hideEvent = downcast_accEvent(event); + CoalesceHideEvent(hideEvent); + } + + event = nextEvent; + } +} + +void NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) { + // Schedule child document binding to the tree. + mHangingChildDocuments.AppendElement(aDocument); + ScheduleProcessing(); +} + +void NotificationController::ScheduleContentInsertion( + LocalAccessible* aContainer, nsTArray>& aInsertions) { + if (!aInsertions.IsEmpty()) { + mContentInsertions.GetOrInsertNew(aContainer)->AppendElements(aInsertions); + ScheduleProcessing(); + } +} + +void NotificationController::ScheduleProcessing() { + // If notification flush isn't planned yet, start notification flush + // asynchronously (after style and layout). + // Note: the mPresShell null-check might be unnecessary; it's just to prevent + // a null-deref here, if we somehow get called after we've been shut down. + if (mObservingState == eNotObservingRefresh && mPresShell) { + if (mPresShell->AddRefreshObserver(this, FlushType::Display, + "Accessibility notifications")) { + mObservingState = eRefreshObserving; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: protected + +bool NotificationController::IsUpdatePending() { + return mPresShell->IsLayoutFlushObserver() || + mObservingState == eRefreshProcessingForUpdate || WaitingForParent() || + mContentInsertions.Count() != 0 || mNotifications.Length() != 0 || + mTextHash.Count() != 0 || + !mDocument->HasLoadState(DocAccessible::eTreeConstructed); +} + +bool NotificationController::WaitingForParent() { + DocAccessible* parentdoc = mDocument->ParentDocument(); + if (!parentdoc) { + return false; + } + + NotificationController* parent = parentdoc->mNotificationController; + if (!parent || parent == this) { + // Do not wait for nothing or ourselves + return false; + } + + // Wait for parent's notifications processing + return parent->mContentInsertions.Count() != 0 || + parent->mNotifications.Length() != 0; +} + +void NotificationController::ProcessMutationEvents() { + // Firing an event can indirectly run script; e.g. an XPCOM event observer + // or querying a XUL interface. Further mutations might be queued as a result. + // It's important that the mutation queue and state bits from one tick don't + // interfere with the next tick. Otherwise, we can end up dropping events. + // Therefore: + // 1. Clear the state bits, which we only need for coalescence. + for (AccTreeMutationEvent* event = mFirstMutationEvent; event; + event = event->NextEvent()) { + LocalAccessible* acc = event->GetAccessible(); + acc->SetShowEventTarget(false); + acc->SetHideEventTarget(false); + acc->SetReorderEventTarget(false); + } + // 2. Keep the current queue locally, but clear the queue on the instance. + RefPtr firstEvent = mFirstMutationEvent; + mFirstMutationEvent = mLastMutationEvent = nullptr; + mMutationMap.Clear(); + mEventGeneration = 0; + + // Group the show events by the parent of their target. + nsTHashMap, nsTArray> + showEvents; + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) { + continue; + } + + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + showEvents.LookupOrInsert(parent).AppendElement(event); + } + + // We need to fire show events for the children of an accessible in the order + // of their indices at this point. So sort each set of events for the same + // container by the index of their target. We do this before firing any events + // because firing an event might indirectly run script which might alter the + // tree, breaking our sort. However, we don't actually fire the events yet. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + struct AccIdxComparator { + bool LessThan(const AccTreeMutationEvent* a, + const AccTreeMutationEvent* b) const { + int32_t aIdx = a->GetAccessible()->IndexInParent(); + int32_t bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx); + return aIdx < bIdx; + } + bool Equals(const AccTreeMutationEvent* a, + const AccTreeMutationEvent* b) const { + DebugOnly aIdx = a->GetAccessible()->IndexInParent(); + DebugOnly bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx); + return false; + } + }; + + nsTArray& events = iter.Data(); + events.Sort(AccIdxComparator()); + } + + // there is no reason to fire a hide event for a child of a show event + // target. That can happen if something is inserted into the tree and + // removed before the next refresh driver tick, but it should not be + // observable outside gecko so it should be safe to coalesce away any such + // events. This means that it should be fine to fire all of the hide events + // first, and then deal with any shown subtrees. + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_HIDE) { + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + + // Fire menupopup end event before a hide event if a menu goes away. + + // XXX: We don't look into children of hidden subtree to find hiding + // menupopup (as we did prior bug 570275) because we don't do that when + // menu is showing (and that's impossible until bug 606924 is fixed). + // Nevertheless we should do this at least because layout coalesces + // the changes before our processing and we may miss some menupopup + // events. Now we just want to be consistent in content insertion/removal + // handling. + if (event->mAccessible->ARIARole() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + event->mAccessible); + if (!mDocument) { + return; + } + } + + AccHideEvent* hideEvent = downcast_accEvent(event); + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(event->mAccessible); + } + } + + // Fire the show events we sorted earlier. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + nsTArray& events = iter.Data(); + for (AccTreeMutationEvent* event : events) { + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + } + } + + // Now we can fire the reorder events after all the show and hide events. + for (const uint32_t reorderType : {nsIAccessibleEvent::EVENT_INNER_REORDER, + nsIAccessibleEvent::EVENT_REORDER}) { + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != reorderType) { + continue; + } + + if (event->GetAccessible()->IsDefunct()) { + // An inner reorder target may have been hidden itself and no + // longer bound to the document. + MOZ_ASSERT(reorderType == nsIAccessibleEvent::EVENT_INNER_REORDER, + "An 'outer' reorder target should not be defunct"); + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + LocalAccessible* target = event->GetAccessible(); + target->Document()->MaybeNotifyOfValueChange(target); + if (!mDocument) { + return; + } + } + } + + // Our events are in a doubly linked list. Clear the pointers to reduce + // pressure on the cycle collector. Even though clearing the previous pointers + // removes cycles, this isn't enough. The cycle collector still gets bogged + // down when there are lots of mutation events if the next pointers aren't + // cleared. Even without the cycle collector, not clearing the next pointers + // potentially results in deep recursion because releasing each event releases + // its next event. + RefPtr event = firstEvent; + while (event) { + RefPtr next = event->NextEvent(); + event->SetNextEvent(nullptr); + event->SetPrevEvent(nullptr); + event = next; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: private + +void NotificationController::WillRefresh(mozilla::TimeStamp aTime) { + AUTO_PROFILER_MARKER_TEXT("NotificationController::WillRefresh", A11Y, {}, + ""_ns); + Telemetry::AutoTimer timer; + // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. + + AUTO_PROFILER_LABEL("NotificationController::WillRefresh", A11Y); + + // If mDocument is null, the document accessible that this notification + // controller was created for is now shut down. This means we've lost our + // ability to unregister ourselves, which is bad. (However, it also shouldn't + // be logically possible for us to get here with a null mDocument; the only + // thing that clears that pointer is our Shutdown() method, which first + // unregisters and fatally asserts if that fails). + MOZ_RELEASE_ASSERT( + mDocument, + "The document was shut down while refresh observer is attached!"); + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + // Wait until an update, we have started, or an interruptible reflow is + // finished. We also check the existance of our pres context and root pres + // context, since if we can't reach either of these the frame tree is being + // destroyed. + nsPresContext* pc = mPresShell->GetPresContext(); + if (mObservingState == eRefreshProcessing || + mObservingState == eRefreshProcessingForUpdate || + mPresShell->IsReflowInterrupted() || !pc || !pc->GetRootPresContext()) { + return; + } + + // Process parent's notifications before ours, to get proper ordering between + // e.g. tab event and content event. + if (WaitingForParent()) { + mDocument->ParentDocument()->mNotificationController->WillRefresh(aTime); + if (!mDocument || ipc::ProcessChild::ExpectingShutdown()) { + return; + } + } + + // Any generic notifications should be queued if we're processing content + // insertions or generic notifications. + mObservingState = eRefreshProcessingForUpdate; + + // Initial accessible tree construction. + if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { + // (1) If document is not bound to parent at this point, or + // (2) the PresShell is not initialized (and it isn't about:blank), + // then the document is not ready yet (process notifications later). + if (!mDocument->IsBoundToParent() || + (!mPresShell->DidInitialize() && + !mDocument->DocumentNode()->IsInitialDocument())) { + mObservingState = eRefreshObserving; + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "initial tree created"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + mDocument->DoInitialUpdate(); + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + NS_ASSERTION(mContentInsertions.Count() == 0, + "Pending content insertions while initial accessible tree " + "isn't created!"); + } + + mDocument->ProcessPendingUpdates(); + + // Process rendered text change notifications. + for (nsIContent* textNode : mTextHash) { + LocalAccessible* textAcc = mDocument->GetAccessible(textNode); + + // If the text node is not in tree or doesn't have a frame, or placed in + // another document, then this case should have been handled already by + // content removal notifications. + nsINode* containerNode = textNode->GetFlattenedTreeParentNode(); + if (!containerNode || textNode->OwnerDoc() != mDocument->DocumentNode()) { + MOZ_ASSERT(!textAcc, + "Text node was removed but accessible is kept alive!"); + continue; + } + + nsIFrame* textFrame = textNode->GetPrimaryFrame(); + if (!textFrame) { + MOZ_ASSERT(!textAcc, + "Text node isn't rendered but accessible is kept alive!"); + continue; + } + +#ifdef A11Y_LOG + nsIContent* containerElm = + containerNode->IsElement() ? containerNode->AsElement() : nullptr; +#endif + + nsIFrame::RenderedText text = textFrame->GetRenderedText( + 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + + // Remove text accessible if rendered text is empty. + if (textAcc) { + if (text.mString.IsEmpty()) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node lost its content; doc: %p", + mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } +#endif + + mDocument->ContentRemoved(textAcc); + continue; + } + + // Update text of the accessible and fire text change events. +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eText)) { + logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEntry( + "old text '%s'", + NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); + logging::MsgEntry("new text: '%s'", + NS_ConvertUTF16toUTF8(text.mString).get()); + logging::MsgEnd(); + } +#endif + + TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString); + continue; + } + + // Append an accessible if rendered text is not empty. + if (!text.mString.IsEmpty()) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node gains new content; doc: %p", + mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } +#endif + + MOZ_ASSERT(mDocument->AccessibleOrTrueContainer(containerNode), + "Text node having rendered text hasn't accessible document!"); + + LocalAccessible* container = + mDocument->AccessibleOrTrueContainer(containerNode, true); + if (container) { + nsTArray>* list = + mContentInsertions.GetOrInsertNew(container); + list->AppendElement(textNode); + } + } + } + mTextHash.Clear(); + + // Process content inserted notifications to update the tree. + // Processing an insertion can indirectly run script (e.g. querying a XUL + // interface), which might result in another insertion being queued. + // We don't want to lose any queued insertions if this happens. Therefore, we + // move the current insertions into a temporary data structure and process + // them from there. Any insertions queued during processing will get handled + // in subsequent refresh driver ticks. + const auto contentInsertions = std::move(mContentInsertions); + for (const auto& entry : contentInsertions) { + mDocument->ProcessContentInserted(entry.GetKey(), entry.GetData().get()); + if (!mDocument) { + return; + } + } + + // Bind hanging child documents unless we are using IPC and the + // document has no IPC actor. If we fail to bind the child doc then + // shut it down. + uint32_t hangingDocCnt = mHangingChildDocuments.Length(); + nsTArray> newChildDocs; + for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { + DocAccessible* childDoc = mHangingChildDocuments[idx]; + if (childDoc->IsDefunct()) { + continue; + } + + if (IPCAccessibilityActive() && !mDocument->IPCDoc()) { + childDoc->Shutdown(); + continue; + } + + nsIContent* ownerContent = childDoc->DocumentNode()->GetEmbedderElement(); + if (ownerContent) { + LocalAccessible* outerDocAcc = mDocument->GetAccessible(ownerContent); + if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { + if (mDocument->AppendChildDocument(childDoc)) { + newChildDocs.AppendElement(std::move(mHangingChildDocuments[idx])); + continue; + } + + outerDocAcc->RemoveChild(childDoc); + } + + // Failed to bind the child document, destroy it. + childDoc->Shutdown(); + } + } + + // Clear the hanging documents list, even if we didn't bind them. + mHangingChildDocuments.Clear(); + MOZ_ASSERT(mDocument, "Illicit document shutdown"); + if (!mDocument) { + return; + } + + // If the document is ready and all its subdocuments are completely loaded + // then process the document load. + if (mDocument->HasLoadState(DocAccessible::eReady) && + !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + hangingDocCnt == 0) { + uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; + for (; childDocIdx < childDocCnt; childDocIdx++) { + DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); + if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) { + break; + } + } + + if (childDocIdx == childDocCnt) { + mDocument->ProcessLoad(); + if (!mDocument) { + return; + } + } + } + + // Process invalidation list of the document after all accessible tree + // mutation is done. + mDocument->ProcessInvalidationList(); + + // Process relocation list. + for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) { + // owner should be in a document and have na associated DOM node (docs + // sometimes don't) + if (mRelocations[idx]->IsInDocument() && + mRelocations[idx]->HasOwnContent()) { + mDocument->DoARIAOwnsRelocation(mRelocations[idx]); + } + } + mRelocations.Clear(); + + // Process only currently queued generic notifications. + // These are used for processing aria-activedescendant, DOMMenuItemActive, + // etc. Therefore, they must be processed after relocations, since relocated + // subtrees might not have been created before relocation processing and the + // target might be inside a relocated subtree. + const nsTArray> notifications = + std::move(mNotifications); + + uint32_t notificationCount = notifications.Length(); + for (uint32_t idx = 0; idx < notificationCount; idx++) { + notifications[idx]->Process(); + if (!mDocument) { + return; + } + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + // If a generic notification occurs after this point then we may be allowed to + // process it synchronously. However we do not want to reenter if fireing + // events causes script to run. + mObservingState = eRefreshProcessing; + + mDocument->SendAccessiblesWillMove(); + + // Send any queued cache updates before we fire any mutation events so the + // cache is up to date when mutation events are fired. We do this after + // insertions (but not their events) so that cache updates dependent on the + // tree work correctly; e.g. line start calculation. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + CoalesceMutationEvents(); + ProcessMutationEvents(); + + // When firing mutation events, mObservingState is set to + // eRefreshProcessing. Any calls to ScheduleProcessing() that + // occur before mObservingState is reset will be dropped because we only + // schedule a tick if mObservingState == eNotObservingRefresh. + // This sometimes results in our viewport cache being out-of-date after + // processing mutation events. Call ProcessQueuedCacheUpdates again to + // ensure it is updated. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + if (mDocument) { + mDocument->ClearMutationData(); + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + ProcessEventQueue(); + + if (IPCAccessibilityActive()) { + size_t newDocCount = newChildDocs.Length(); + for (size_t i = 0; i < newDocCount; i++) { + DocAccessible* childDoc = newChildDocs[i]; + if (childDoc->IsDefunct()) { + continue; + } + + LocalAccessible* parent = childDoc->LocalParent(); + DocAccessibleChild* parentIPCDoc = mDocument->IPCDoc(); + MOZ_DIAGNOSTIC_ASSERT(parentIPCDoc); + uint64_t id = reinterpret_cast(parent->UniqueID()); + MOZ_DIAGNOSTIC_ASSERT(id); + DocAccessibleChild* ipcDoc = childDoc->IPCDoc(); + if (ipcDoc) { + parentIPCDoc->SendBindChildDoc(WrapNotNull(ipcDoc), id); + continue; + } + + ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager()); + childDoc->SetIPCDoc(ipcDoc); + + nsCOMPtr browserChild = + do_GetInterface(mDocument->DocumentNode()->GetDocShell()); + if (browserChild) { + static_cast(browserChild.get()) + ->SendPDocAccessibleConstructor( + ipcDoc, parentIPCDoc, id, + childDoc->DocumentNode()->GetBrowsingContext()); +#ifndef XP_WIN + ipcDoc->SendPDocAccessiblePlatformExtConstructor(); +#endif + } + } + } + + if (!mDocument) { + // A null mDocument means we've gotten a Shutdown() call (presumably via + // some script that we triggered above), and that means we're done here. + // Note: in this case, it's important that don't modify mObservingState; + // Shutdown() will have *unregistered* us as a refresh observer, and we + // don't want to mistakenly overwrite mObservingState and fool ourselves + // into thinking we've re-registered when we really haven't! + MOZ_ASSERT(mObservingState == eNotObservingRefresh, + "We've been shutdown, which means we should've been " + "unregistered as a refresh observer"); + return; + } + mObservingState = eRefreshObserving; + + // Stop further processing if there are no new notifications of any kind or + // events and document load is processed. + if (mContentInsertions.Count() == 0 && mNotifications.IsEmpty() && + !mFocusEvent && mEvents.IsEmpty() && mTextHash.Count() == 0 && + mHangingChildDocuments.IsEmpty() && + mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { + mObservingState = eNotObservingRefresh; + } +} + +void NotificationController::EventMap::PutEvent(AccTreeMutationEvent* aEvent) { + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + mTable.InsertOrUpdate(addr, RefPtr{aEvent}); +} + +AccTreeMutationEvent* NotificationController::EventMap::GetEvent( + LocalAccessible* aTarget, EventType aType) { + uint64_t addr = reinterpret_cast(aTarget); + MOZ_ASSERT((addr & 0x3) == 0, "target is not 4 byte aligned"); + + addr |= aType; + return mTable.GetWeak(addr); +} + +void NotificationController::EventMap::RemoveEvent( + AccTreeMutationEvent* aEvent) { + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + + MOZ_ASSERT(mTable.GetWeak(addr) == aEvent, "mTable has the wrong event"); + mTable.Remove(addr); +} + +NotificationController::EventMap::EventType +NotificationController::EventMap::GetEventType(AccTreeMutationEvent* aEvent) { + switch (aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_SHOW: + return ShowEvent; + case nsIAccessibleEvent::EVENT_HIDE: + return HideEvent; + case nsIAccessibleEvent::EVENT_REORDER: + case nsIAccessibleEvent::EVENT_INNER_REORDER: + return ReorderEvent; + default: + MOZ_ASSERT_UNREACHABLE("event has invalid type"); + return ShowEvent; + } +} diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h new file mode 100644 index 0000000000..bc1a692c23 --- /dev/null +++ b/accessible/base/NotificationController.h @@ -0,0 +1,393 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_NotificationController_h_ +#define mozilla_a11y_NotificationController_h_ + +#include "EventQueue.h" + +#include "nsClassHashtable.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIFrame.h" +#include "nsRefreshObservers.h" +#include "nsTHashSet.h" + +#include + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class DocAccessible; + +/** + * Notification interface. + */ +class Notification { + public: + NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::Notification) + + /** + * Process notification. + */ + virtual void Process() = 0; + + protected: + Notification() {} + + /** + * Protected destructor, to discourage deletion outside of Release(): + */ + virtual ~Notification() {} + + private: + Notification(const Notification&); + Notification& operator=(const Notification&); +}; + +/** + * Template class for generic notification. + * + * @note Instance is kept as a weak ref, the caller must guarantee it exists + * longer than the document accessible owning the notification controller + * that this notification is processed by. + */ +template +class TNotification : public Notification { + public: + typedef void (Class::*Callback)(Args*...); + + TNotification(Class* aInstance, Callback aCallback, Args*... aArgs) + : mInstance(aInstance), mCallback(aCallback), mArgs(aArgs...) {} + virtual ~TNotification() { mInstance = nullptr; } + + virtual void Process() override { + ProcessHelper(std::index_sequence_for{}); + } + + private: + TNotification(const TNotification&); + TNotification& operator=(const TNotification&); + + template + void ProcessHelper(std::index_sequence) { + (mInstance->*mCallback)(std::get(mArgs)...); + } + + Class* mInstance; + Callback mCallback; + std::tuple...> mArgs; +}; + +/** + * Used to process notifications from core for the document accessible. + */ +class NotificationController final : public EventQueue, + public nsARefreshObserver { + public: + NotificationController(DocAccessible* aDocument, PresShell* aPresShell); + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; + NS_IMETHOD_(MozExternalRefCountType) Release(void) override; + + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(NotificationController) + + /** + * Shutdown the notification controller. + */ + void Shutdown(); + + /** + * Add an accessible event into the queue to process it later. + */ + void QueueEvent(AccEvent* aEvent) { + if (PushEvent(aEvent)) { + ScheduleProcessing(); + } + } + + /** + * Queue a mutation event to emit if not coalesced away. Returns true if the + * event was queued and has not yet been coalesced. + */ + bool QueueMutationEvent(AccTreeMutationEvent* aEvent); + + /** + * Coalesce all queued mutation events. + */ + void CoalesceMutationEvents(); + + /** + * Schedule binding the child document to the tree of this document. + */ + void ScheduleChildDocBinding(DocAccessible* aDocument); + + /** + * Schedule the accessible tree update because of rendered text changes. + */ + inline void ScheduleTextUpdate(nsIContent* aTextNode) { + // Make sure we are not called with a node that is not in the DOM tree or + // not visible. + MOZ_ASSERT(aTextNode->GetParentNode(), "A text node is not in DOM"); + MOZ_ASSERT(aTextNode->GetPrimaryFrame(), + "A text node doesn't have a frame"); + MOZ_ASSERT(aTextNode->GetPrimaryFrame()->StyleVisibility()->IsVisible(), + "A text node is not visible"); + + mTextHash.Insert(aTextNode); + + ScheduleProcessing(); + } + + /** + * Pend accessible tree update for content insertion. + */ + void ScheduleContentInsertion(LocalAccessible* aContainer, + nsTArray>& aInsertions); + + /** + * Pend an accessible subtree relocation. + */ + void ScheduleRelocation(LocalAccessible* aOwner) { + if (!mRelocations.Contains(aOwner)) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mRelocations.AppendElement(aOwner); + ScheduleProcessing(); + } + } + + /** + * Start to observe refresh to make notifications and events processing after + * layout. + */ + void ScheduleProcessing(); + + /** + * Process the generic notification synchronously if there are no pending + * layout changes and no notifications are pending or being processed right + * now. Otherwise, queue it up to process asynchronously. + * + * @note The caller must guarantee that the given instance still exists when + * the notification is processed. + */ + template + inline void HandleNotification( + Class* aInstance, + typename TNotification::Callback aMethod, + Args*... aArgs) { + if (!IsUpdatePending()) { +#ifdef A11Y_LOG + if (mozilla::a11y::logging::IsEnabled( + mozilla::a11y::logging::eNotifications)) { + mozilla::a11y::logging::Text("sync notification processing"); + } +#endif + (aInstance->*aMethod)(aArgs...); + return; + } + + RefPtr notification = + new TNotification(aInstance, aMethod, aArgs...); + if (notification) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mNotifications.AppendElement(notification); + ScheduleProcessing(); + } + } + + /** + * Schedule the generic notification to process asynchronously. + * + * @note The caller must guarantee that the given instance still exists when + * the notification is processed. + */ + template + inline void ScheduleNotification( + Class* aInstance, typename TNotification::Callback aMethod) { + RefPtr notification = + new TNotification(aInstance, aMethod); + if (notification) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mNotifications.AppendElement(notification); + ScheduleProcessing(); + } + } + + template + inline void ScheduleNotification( + Class* aInstance, typename TNotification::Callback aMethod, + Arg* aArg) { + RefPtr notification = + new TNotification(aInstance, aMethod, aArg); + if (notification) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mNotifications.AppendElement(notification); + ScheduleProcessing(); + } + } + +#ifdef DEBUG + bool IsUpdating() const { + return mObservingState == eRefreshProcessingForUpdate; + } +#endif + + protected: + virtual ~NotificationController(); + + nsCycleCollectingAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + /** + * Return true if the accessible tree state update is pending. + */ + bool IsUpdatePending(); + + /** + * Return true if we should wait for processing from the parent before we can + * process our own queue. + */ + bool WaitingForParent(); + + private: + NotificationController(const NotificationController&); + NotificationController& operator=(const NotificationController&); + + // nsARefreshObserver + virtual void WillRefresh(mozilla::TimeStamp aTime) override; + + private: + /** + * Remove a specific hide event if it should not be propagated. + */ + void CoalesceHideEvent(AccHideEvent* aHideEvent); + + /** + * get rid of a mutation event that is no longer necessary. + */ + void DropMutationEvent(AccTreeMutationEvent* aEvent); + + /** + * Fire all necessary mutation events. + */ + void ProcessMutationEvents(); + + /** + * Indicates whether we're waiting on an event queue processing from our + * notification controller to flush events. + */ + enum eObservingState { + eNotObservingRefresh, + eRefreshObserving, + eRefreshProcessing, + eRefreshProcessingForUpdate + }; + eObservingState mObservingState; + + /** + * The presshell of the document accessible. + */ + PresShell* mPresShell; + + /** + * Child documents that needs to be bound to the tree. + */ + nsTArray> mHangingChildDocuments; + + /** + * Pending accessible tree update notifications for content insertions. + */ + nsClassHashtable, + nsTArray>> + mContentInsertions; + + template + class nsCOMPtrHashKey : public PLDHashEntryHdr { + public: + typedef T* KeyType; + typedef const T* KeyTypePointer; + + explicit nsCOMPtrHashKey(const T* aKey) : mKey(const_cast(aKey)) {} + nsCOMPtrHashKey(nsCOMPtrHashKey&& aOther) + : PLDHashEntryHdr(std::move(aOther)), mKey(std::move(aOther.mKey)) {} + ~nsCOMPtrHashKey() {} + + KeyType GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const { return aKey == mKey; } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return NS_PTR_TO_INT32(aKey) >> 2; + } + + enum { ALLOW_MEMMOVE = true }; + + protected: + nsCOMPtr mKey; + }; + + /** + * Pending accessible tree update notifications for rendered text changes. + */ + nsTHashSet> mTextHash; + + /** + * Other notifications like DOM events. Don't make this an AutoTArray; we + * use SwapElements() on it. + */ + nsTArray> mNotifications; + + /** + * Holds all scheduled relocations. + */ + nsTArray> mRelocations; + + /** + * A list of all mutation events we may want to emit. Ordered from the first + * event that should be emitted to the last one to emit. + */ + RefPtr mFirstMutationEvent; + RefPtr mLastMutationEvent; + + /** + * A class to map an accessible and event type to an event. + */ + class EventMap { + public: + enum EventType { + ShowEvent = 0x0, + HideEvent = 0x1, + ReorderEvent = 0x2, + }; + + void PutEvent(AccTreeMutationEvent* aEvent); + AccTreeMutationEvent* GetEvent(LocalAccessible* aTarget, EventType aType); + void RemoveEvent(AccTreeMutationEvent* aEvent); + void Clear() { mTable.Clear(); } + + private: + EventType GetEventType(AccTreeMutationEvent* aEvent); + + nsRefPtrHashtable mTable; + }; + + EventMap mMutationMap; + uint32_t mEventGeneration; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_NotificationController_h_ diff --git a/accessible/base/Pivot.cpp b/accessible/base/Pivot.cpp new file mode 100644 index 0000000000..0d5df5e27a --- /dev/null +++ b/accessible/base/Pivot.cpp @@ -0,0 +1,669 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Pivot.h" + +#include "AccIterator.h" +#include "LocalAccessible.h" +#include "RemoteAccessible.h" +#include "DocAccessible.h" +#include "nsAccUtils.h" + +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/HyperTextAccessibleBase.h" +#include "mozilla/dom/ChildIterator.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Pivot +//////////////////////////////////////////////////////////////////////////////// + +Pivot::Pivot(Accessible* aRoot) : mRoot(aRoot) { MOZ_COUNT_CTOR(Pivot); } + +Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot); } + +Accessible* Pivot::AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule, + uint16_t* aFilterResult) { + Accessible* matched = aAnchor; + *aFilterResult = aRule.Match(aAnchor); + + if (aAnchor && aAnchor != mRoot) { + for (Accessible* temp = aAnchor->Parent(); temp && temp != mRoot; + temp = temp->Parent()) { + uint16_t filtered = aRule.Match(temp); + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + *aFilterResult = filtered; + matched = temp; + } + } + } + + return matched; +} + +Accessible* Pivot::SearchBackward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent) { + // Initial position could be unset, in that case return null. + if (!aAnchor) { + return nullptr; + } + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + + Accessible* acc = AdjustStartPosition(aAnchor, aRule, &filtered); + + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return acc; + } + + while (acc && acc != mRoot) { + Accessible* parent = acc->Parent(); +#if defined(ANDROID) + MOZ_ASSERT( + acc->IsLocal() || (acc->IsRemote() && parent->IsRemote()), + "Pivot::SearchBackward climbed out of remote subtree in Android!"); +#endif + int32_t idxInParent = acc->IndexInParent(); + while (idxInParent > 0 && parent) { + acc = parent->ChildAt(--idxInParent); + if (!acc) { + continue; + } + + filtered = aRule.Match(acc); + + Accessible* lastChild = acc->LastChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + lastChild) { + parent = acc; + acc = lastChild; + idxInParent = acc->IndexInParent(); + filtered = aRule.Match(acc); + lastChild = acc->LastChild(); + } + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + acc = parent; + if (!acc) { + break; + } + + filtered = aRule.Match(acc); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + return nullptr; +} + +Accessible* Pivot::SearchForward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent) { + // Initial position could be not set, in that case begin search from root. + Accessible* acc = aAnchor ? aAnchor : mRoot; + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + acc = AdjustStartPosition(acc, aRule, &filtered); + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return acc; + } + + while (acc) { + Accessible* firstChild = acc->FirstChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + firstChild) { + acc = firstChild; + filtered = aRule.Match(acc); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + firstChild = acc->FirstChild(); + } + + Accessible* sibling = nullptr; + Accessible* temp = acc; + do { + if (temp == mRoot) { + break; + } + + sibling = temp->NextSibling(); + + if (sibling) { + break; + } + temp = temp->Parent(); +#if defined(ANDROID) + MOZ_ASSERT( + acc->IsLocal() || (acc->IsRemote() && temp->IsRemote()), + "Pivot::SearchForward climbed out of remote subtree in Android!"); +#endif + + } while (temp); + + if (!sibling) { + break; + } + + acc = sibling; + filtered = aRule.Match(acc); + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + return nullptr; +} + +Accessible* Pivot::SearchForText(Accessible* aAnchor, bool aBackward) { + Accessible* accessible = aAnchor; + while (true) { + Accessible* child = nullptr; + + while ((child = (aBackward ? accessible->LastChild() + : accessible->FirstChild()))) { + accessible = child; + if (child->IsHyperText()) { + return child; + } + } + + Accessible* sibling = nullptr; + Accessible* temp = accessible; + do { + if (temp == mRoot) { + break; + } + + // Unlike traditional pre-order traversal we revisit the parent + // nodes when we go up the tree. This is because our starting point + // may be a subtree or a leaf. If it's parent matches, it should + // take precedent over a sibling. + if (temp != aAnchor && temp->IsHyperText()) { + return temp; + } + + if (sibling) { + break; + } + + sibling = aBackward ? temp->PrevSibling() : temp->NextSibling(); + } while ((temp = temp->Parent())); + + if (!sibling) { + break; + } + + accessible = sibling; + if (accessible->IsHyperText()) { + return accessible; + } + } + + return nullptr; +} + +Accessible* Pivot::Next(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchForward(aAnchor, aRule, aIncludeStart); +} + +Accessible* Pivot::Prev(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchBackward(aAnchor, aRule, aIncludeStart); +} + +Accessible* Pivot::First(PivotRule& aRule) { + return SearchForward(mRoot, aRule, true); +} + +Accessible* Pivot::Last(PivotRule& aRule) { + Accessible* lastAcc = mRoot; + + // First go to the last accessible in pre-order + while (lastAcc && lastAcc->HasChildren()) { + lastAcc = lastAcc->LastChild(); + } + + // Search backwards from last accessible and find the last occurrence in the + // doc + return SearchBackward(lastAcc, aRule, true); +} + +Accessible* Pivot::NextText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType) { + int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset; + Accessible* tempPosition = aAnchor; + + // if we're starting on a text leaf, translate the offsets to the + // HyperTextAccessible parent and start from there. + if (aAnchor->IsTextLeaf() && aAnchor->Parent() && + aAnchor->Parent()->IsHyperText()) { + tempPosition = aAnchor->Parent(); + HyperTextAccessibleBase* text = tempPosition->AsHyperTextBase(); + int32_t childOffset = text->GetChildOffset(aAnchor); + if (tempEnd == -1) { + tempStart = 0; + tempEnd = 0; + } + tempStart += childOffset; + tempEnd += childOffset; + } + + while (true) { + MOZ_ASSERT(tempPosition); + Accessible* curPosition = tempPosition; + HyperTextAccessibleBase* text = nullptr; + // Find the nearest text node using a preorder traversal starting from + // the current node. + if (!(text = tempPosition->AsHyperTextBase())) { + tempPosition = SearchForText(tempPosition, false); + if (!tempPosition) { + return nullptr; + } + + if (tempPosition != curPosition) { + tempStart = tempEnd = -1; + } + text = tempPosition->AsHyperTextBase(); + } + + // If the search led to the parent of the node we started on (e.g. when + // starting on a text leaf), start the text movement from the end of that + // node, otherwise we just default to 0. + if (tempEnd == -1) { + tempEnd = tempPosition == curPosition->Parent() + ? text->GetChildOffset(curPosition) + : 0; + } + + // If there's no more text on the current node, try to find the next text + // node; if there isn't one, bail out. + if (tempEnd == static_cast(text->CharacterCount())) { + if (tempPosition == mRoot) { + return nullptr; + } + + // If we're currently sitting on a link, try move to either the next + // sibling or the parent, whichever is closer to the current end + // offset. Otherwise, do a forward search for the next node to land on + // (we don't do this in the first case because we don't want to go to the + // subtree). + Accessible* sibling = tempPosition->NextSibling(); + if (tempPosition->IsLink()) { + if (sibling && sibling->IsLink()) { + tempStart = tempEnd = -1; + tempPosition = sibling; + } else { + tempStart = tempPosition->StartOffset(); + tempEnd = tempPosition->EndOffset(); + tempPosition = tempPosition->Parent(); + } + } else { + tempPosition = SearchForText(tempPosition, false); + if (!tempPosition) { + return nullptr; + } + + tempStart = tempEnd = -1; + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundaryType) { + case nsIAccessiblePivot::CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case nsIAccessiblePivot::WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + case nsIAccessiblePivot::LINE_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_LINE_START; + endBoundary = nsIAccessibleText::BOUNDARY_LINE_END; + break; + default: + return nullptr; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0, currentEnd = tempEnd; + text->TextAtOffset(tempEnd, endBoundary, &newStart, &tempEnd, unusedText); + text->TextBeforeOffset(tempEnd, startBoundary, &newStart, &newEnd, + unusedText); + int32_t potentialStart = newEnd == tempEnd ? newStart : newEnd; + tempStart = potentialStart > tempStart ? potentialStart : currentEnd; + + // The offset range we've obtained might have embedded characters in it, + // limit the range to the start of the first occurrence of an embedded + // character. + Accessible* childAtOffset = nullptr; + for (int32_t i = tempStart; i < tempEnd; i++) { + childAtOffset = text->GetChildAtOffset(i); + if (childAtOffset && childAtOffset->IsHyperText()) { + tempEnd = i; + break; + } + } + // If there's an embedded character at the very start of the range, we + // instead want to traverse into it. So restart the movement with + // the child as the starting point. + if (childAtOffset && childAtOffset->IsHyperText() && + tempStart == static_cast(childAtOffset->StartOffset())) { + tempPosition = childAtOffset; + tempStart = tempEnd = -1; + continue; + } + + *aStartOffset = tempStart; + *aEndOffset = tempEnd; + + MOZ_ASSERT(tempPosition); + return tempPosition; + } +} + +Accessible* Pivot::PrevText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType) { + int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset; + Accessible* tempPosition = aAnchor; + + // if we're starting on a text leaf, translate the offsets to the + // HyperTextAccessible parent and start from there. + if (aAnchor->IsTextLeaf() && aAnchor->Parent() && + aAnchor->Parent()->IsHyperText()) { + tempPosition = aAnchor->Parent(); + HyperTextAccessibleBase* text = tempPosition->AsHyperTextBase(); + int32_t childOffset = text->GetChildOffset(aAnchor); + if (tempStart == -1) { + tempStart = nsAccUtils::TextLength(aAnchor); + tempEnd = tempStart; + } + tempStart += childOffset; + tempEnd += childOffset; + } + + while (true) { + MOZ_ASSERT(tempPosition); + + Accessible* curPosition = tempPosition; + HyperTextAccessibleBase* text; + // Find the nearest text node using a reverse preorder traversal starting + // from the current node. + if (!(text = tempPosition->AsHyperTextBase())) { + tempPosition = SearchForText(tempPosition, true); + if (!tempPosition) { + return nullptr; + } + + if (tempPosition != curPosition) { + tempStart = tempEnd = -1; + } + text = tempPosition->AsHyperTextBase(); + } + + // If the search led to the parent of the node we started on (e.g. when + // starting on a text leaf), start the text movement from the end offset + // of that node. Otherwise we just default to the last offset in the parent. + if (tempStart == -1) { + if (tempPosition != curPosition && + tempPosition == curPosition->Parent()) { + tempStart = text->GetChildOffset(curPosition) + + nsAccUtils::TextLength(curPosition); + } else { + tempStart = text->CharacterCount(); + } + } + + // If there's no more text on the current node, try to find the previous + // text node; if there isn't one, bail out. + if (tempStart == 0) { + if (tempPosition == mRoot) { + return nullptr; + } + + // If we're currently sitting on a link, try move to either the previous + // sibling or the parent, whichever is closer to the current end + // offset. Otherwise, do a forward search for the next node to land on + // (we don't do this in the first case because we don't want to go to the + // subtree). + Accessible* sibling = tempPosition->PrevSibling(); + if (tempPosition->IsLink()) { + if (sibling && sibling->IsLink()) { + HyperTextAccessibleBase* siblingText = sibling->AsHyperTextBase(); + tempStart = tempEnd = + siblingText ? siblingText->CharacterCount() : -1; + tempPosition = sibling; + } else { + tempStart = tempPosition->StartOffset(); + tempEnd = tempPosition->EndOffset(); + tempPosition = tempPosition->Parent(); + } + } else { + tempPosition = SearchForText(tempPosition, true); + if (!tempPosition) { + return nullptr; + } + + HyperTextAccessibleBase* tempText = tempPosition->AsHyperTextBase(); + tempStart = tempEnd = tempText->CharacterCount(); + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundaryType) { + case nsIAccessiblePivot::CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case nsIAccessiblePivot::WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + case nsIAccessiblePivot::LINE_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_LINE_START; + endBoundary = nsIAccessibleText::BOUNDARY_LINE_END; + break; + default: + return nullptr; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0, currentStart = tempStart, + potentialEnd = 0; + text->TextBeforeOffset(tempStart, startBoundary, &newStart, &newEnd, + unusedText); + if (newStart < tempStart) { + tempStart = newEnd >= currentStart ? newStart : newEnd; + } else { + // XXX: In certain odd cases newStart is equal to tempStart + text->TextBeforeOffset(tempStart - 1, startBoundary, &newStart, + &tempStart, unusedText); + } + text->TextAtOffset(tempStart, endBoundary, &newStart, &potentialEnd, + unusedText); + tempEnd = potentialEnd < tempEnd ? potentialEnd : currentStart; + + // The offset range we've obtained might have embedded characters in it, + // limit the range to the start of the last occurrence of an embedded + // character. + Accessible* childAtOffset = nullptr; + for (int32_t i = tempEnd - 1; i >= tempStart; i--) { + childAtOffset = text->GetChildAtOffset(i); + if (childAtOffset && childAtOffset->IsHyperText()) { + tempStart = childAtOffset->EndOffset(); + break; + } + } + // If there's an embedded character at the very end of the range, we + // instead want to traverse into it. So restart the movement with + // the child as the starting point. + if (childAtOffset && childAtOffset->IsHyperText() && + tempEnd == static_cast(childAtOffset->EndOffset())) { + tempPosition = childAtOffset; + tempStart = tempEnd = static_cast( + childAtOffset->AsHyperTextBase()->CharacterCount()); + continue; + } + + *aStartOffset = tempStart; + *aEndOffset = tempEnd; + + MOZ_ASSERT(tempPosition); + return tempPosition; + } +} + +Accessible* Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) { + Accessible* match = nullptr; + Accessible* child = + mRoot ? mRoot->ChildAtPoint(aX, aY, + Accessible::EWhichChildAtPoint::DeepestChild) + : nullptr; + while (child && (mRoot != child)) { + uint16_t filtered = aRule.Match(child); + + // Ignore any matching nodes that were below this one + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + match = nullptr; + } + + // Match if no node below this is a match + if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && !match) { + LayoutDeviceIntRect childRect = child->IsLocal() + ? child->AsLocal()->Bounds() + : child->AsRemote()->Bounds(); + // Double-check child's bounds since the deepest child may have been out + // of bounds. This assures we don't return a false positive. + if (childRect.Contains(aX, aY)) { + match = child; + } + } + + child = child->Parent(); + } + + return match; +} + +// Role Rule + +PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole) + : mRole(aRole), mDirectDescendantsFrom(nullptr) {} + +PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole, + Accessible* aDirectDescendantsFrom) + : mRole(aRole), mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +uint16_t PivotRoleRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) { + // If we've specified mDirectDescendantsFrom, we should ignore + // non-direct descendants of from the specified AoP. Because + // pivot performs a preorder traversal, the first aAcc + // object(s) that don't equal mDirectDescendantsFrom will be + // mDirectDescendantsFrom's children. We'll process them, but ignore + // their subtrees thereby processing direct descendants of + // mDirectDescendantsFrom only. + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (aAcc && aAcc->Role() == mRole) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// State Rule + +PivotStateRule::PivotStateRule(uint64_t aState) : mState(aState) {} + +uint16_t PivotStateRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (aAcc && (aAcc->State() & mState)) { + result = nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + return result; +} + +// LocalAccInSameDocRule + +uint16_t LocalAccInSameDocRule::Match(Accessible* aAcc) { + LocalAccessible* acc = aAcc ? aAcc->AsLocal() : nullptr; + if (!acc) { + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + if (acc->IsOuterDoc()) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + return nsIAccessibleTraversalRule::FILTER_MATCH; +} + +// Radio Button Name Rule + +PivotRadioNameRule::PivotRadioNameRule(const nsString& aName) : mName(aName) {} + +uint16_t PivotRadioNameRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + RemoteAccessible* remote = aAcc->AsRemote(); + if (!remote) { + // We need the cache to be able to fetch the name attribute below. + return result; + } + + if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (remote->IsHTMLRadioButton()) { + nsString currName = remote->GetCachedHTMLNameAttribute(); + if (!currName.IsEmpty() && mName.Equals(currName)) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// MustPruneSameDocRule + +uint16_t MustPruneSameDocRule::Match(Accessible* aAcc) { + if (!aAcc) { + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + return nsIAccessibleTraversalRule::FILTER_MATCH; +} diff --git a/accessible/base/Pivot.h b/accessible/base/Pivot.h new file mode 100644 index 0000000000..c00f1613a2 --- /dev/null +++ b/accessible/base/Pivot.h @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_Pivot_h_ +#define mozilla_a11y_Pivot_h_ + +#include +#include "Role.h" +#include "mozilla/dom/ChildIterator.h" + +namespace mozilla { +namespace a11y { + +class DocAccessible; +class Accessible; + +class PivotRule { + public: + // A filtering function that returns a bitmask from + // nsIAccessibleTraversalRule: FILTER_IGNORE (0x0): Don't match this + // accessible. FILTER_MATCH (0x1): Match this accessible FILTER_IGNORE_SUBTREE + // (0x2): Ignore accessible's subtree. + virtual uint16_t Match(Accessible* aAcc) = 0; +}; + +// The Pivot class is used for searching for accessible nodes in a given subtree +// with a given criteria. Since it only holds a weak reference to the root, +// this class is meant to be used primarily on the stack. +class Pivot final { + public: + explicit Pivot(Accessible* aRoot); + Pivot() = delete; + Pivot(const Pivot&) = delete; + Pivot& operator=(const Pivot&) = delete; + + ~Pivot(); + + // Return the next accessible after aAnchor in pre-order that matches the + // given rule. If aIncludeStart, return aAnchor if it matches the rule. + Accessible* Next(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart = false); + + // Return the previous accessible before aAnchor in pre-order that matches the + // given rule. If aIncludeStart, return aAnchor if it matches the rule. + Accessible* Prev(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart = false); + + // Return the first accessible within the root that matches the pivot rule. + Accessible* First(PivotRule& aRule); + + // Return the last accessible within the root that matches the pivot rule. + Accessible* Last(PivotRule& aRule); + + // Return the next range of text according to the boundary type. + Accessible* NextText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType); + + // Return the previous range of text according to the boundary type. + Accessible* PrevText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType); + + // Return the accessible at the given screen coordinate if it matches the + // pivot rule. + Accessible* AtPoint(int32_t aX, int32_t aY, PivotRule& aRule); + + private: + Accessible* AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule, + uint16_t* aFilterResult); + + // Search in preorder for the first accessible to match the rule. + Accessible* SearchForward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + // Reverse search in preorder for the first accessible to match the rule. + Accessible* SearchBackward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + // Search in preorder for the first text accessible. + Accessible* SearchForText(Accessible* aAnchor, bool aBackward); + + Accessible* mRoot; +}; + +/** + * This rule matches accessibles on a given role, filtering out non-direct + * descendants if necessary. + */ +class PivotRoleRule : public PivotRule { + public: + explicit PivotRoleRule(role aRole); + explicit PivotRoleRule(role aRole, Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + role mRole; + Accessible* mDirectDescendantsFrom; +}; + +/** + * This rule matches accessibles with a given state. + */ +class PivotStateRule : public PivotRule { + public: + explicit PivotStateRule(uint64_t aState); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + uint64_t mState; +}; + +/** + * This rule matches any local LocalAccessible (i.e. not RemoteAccessible) in + * the same document as the anchor. That is, it includes any descendant + * OuterDocAccessible, but not its descendants. + */ +class LocalAccInSameDocRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override; +}; + +/** + * This rule matches remote radio button accessibles with the given name + * attribute. It assumes the cache is enabled. + */ +class PivotRadioNameRule : public PivotRule { + public: + explicit PivotRadioNameRule(const nsString& aName); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + const nsString& mName; +}; + +/** + * This rule doesn't search iframes. Subtrees that should be + * pruned by way of nsAccUtils::MustPrune are also not searched. + */ + +class MustPruneSameDocRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_Pivot_h_ diff --git a/accessible/base/Platform.h b/accessible/base/Platform.h new file mode 100644 index 0000000000..359587843a --- /dev/null +++ b/accessible/base/Platform.h @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_Platform_h +#define mozilla_a11y_Platform_h + +#include +#include "nsStringFwd.h" + +#if defined(ANDROID) +# include "nsTArray.h" +# include "nsRect.h" +#endif + +#ifdef MOZ_WIDGET_COCOA +# include "mozilla/a11y/Role.h" +# include "nsTArray.h" +#endif + +#if defined(XP_WIN) +# include "Units.h" +#endif + +namespace mozilla { +namespace a11y { + +class RemoteAccessible; + +enum EPlatformDisabledState { + ePlatformIsForceEnabled = -1, + ePlatformIsEnabled = 0, + ePlatformIsDisabled = 1 +}; + +/** + * Return the platform disabled state. + */ +EPlatformDisabledState PlatformDisabledState(); + +#ifdef MOZ_ACCESSIBILITY_ATK +/** + * Perform initialization that should be done as soon as possible, in order + * to minimize startup time. + * XXX: this function and the next defined in ApplicationAccessibleWrap.cpp + */ +void PreInit(); +#endif + +#if defined(MOZ_ACCESSIBILITY_ATK) || defined(XP_MACOSX) +/** + * Is platform accessibility enabled. + * Only used on linux with atk and MacOS for now. + */ +bool ShouldA11yBeEnabled(); +#endif + +#if defined(XP_WIN) +/* + * Name of platform service that instantiated accessibility + */ +void SetInstantiator(const uint32_t aInstantiatorPid); +bool GetInstantiator(nsIFile** aOutInstantiator); +#endif + +/** + * Called to initialize platform specific accessibility support. + * Note this is called after internal accessibility support is initialized. + */ +void PlatformInit(); + +/** + * Shutdown platform accessibility. + * Note this is called before internal accessibility support is shutdown. + */ +void PlatformShutdown(); + +/** + * called when a new RemoteAccessible is created, so the platform may setup a + * wrapper for it, or take other action. + */ +void ProxyCreated(RemoteAccessible* aProxy); + +/** + * Called just before a RemoteAccessible is destroyed so its wrapper can be + * disposed of and other action taken. + */ +void ProxyDestroyed(RemoteAccessible*); + +/** + * Callied when an event is fired on a proxied accessible. + */ +void ProxyEvent(RemoteAccessible* aTarget, uint32_t aEventType); +void ProxyStateChangeEvent(RemoteAccessible* aTarget, uint64_t aState, + bool aEnabled); + +#if defined(XP_WIN) +void ProxyFocusEvent(RemoteAccessible* aTarget, + const LayoutDeviceIntRect& aCaretRect); +void ProxyCaretMoveEvent(RemoteAccessible* aTarget, + const LayoutDeviceIntRect& aCaretRect, + int32_t aGranularity); +#else +void ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, int32_t aGranularity); +#endif +void ProxyTextChangeEvent(RemoteAccessible* aTarget, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser); +void ProxyShowHideEvent(RemoteAccessible* aTarget, RemoteAccessible* aParent, + bool aInsert, bool aFromUser); +void ProxySelectionEvent(RemoteAccessible* aTarget, RemoteAccessible* aWidget, + uint32_t aType); + +#if defined(ANDROID) +void ProxyVirtualCursorChangeEvent(RemoteAccessible* aTarget, + RemoteAccessible* aOldPosition, + int32_t aOldStartOffset, + int32_t aOldEndOffset, + RemoteAccessible* aNewPosition, + int32_t aNewStartOffset, + int32_t aNewEndOffset, int16_t aReason, + int16_t aBoundaryType, bool aFromUser); + +void ProxyScrollingEvent(RemoteAccessible* aTarget, uint32_t aEventType, + uint32_t aScrollX, uint32_t aScrollY, + uint32_t aMaxScrollX, uint32_t aMaxScrollY); + +void ProxyAnnouncementEvent(RemoteAccessible* aTarget, + const nsAString& aAnnouncement, uint16_t aPriority); + +bool LocalizeString(const nsAString& aToken, nsAString& aLocalized); +#endif + +#ifdef MOZ_WIDGET_COCOA +class TextRangeData; +void ProxyTextSelectionChangeEvent(RemoteAccessible* aTarget, + const nsTArray& aSelection); + +void ProxyRoleChangedEvent(RemoteAccessible* aTarget, const a11y::role& aRole, + uint8_t aRoleMapEntryIndex); +#endif + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_Platform_h diff --git a/accessible/base/Relation.h b/accessible/base/Relation.h new file mode 100644 index 0000000000..24fceeab02 --- /dev/null +++ b/accessible/base/Relation.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_relation_h_ +#define mozilla_a11y_relation_h_ + +#include "AccIterator.h" + +#include + +namespace mozilla { +namespace a11y { + +/** + * A collection of relation targets of a certain type. Targets are computed + * lazily while enumerating. + */ +class Relation { + public: + Relation() : mFirstIter(nullptr), mLastIter(nullptr) {} + + explicit Relation(AccIterable* aIter) : mFirstIter(aIter), mLastIter(aIter) {} + + explicit Relation(Accessible* aAcc) + : mFirstIter(nullptr), mLastIter(nullptr) { + AppendTarget(aAcc); + } + + Relation(DocAccessible* aDocument, nsIContent* aContent) + : mFirstIter(nullptr), mLastIter(nullptr) { + AppendTarget(aDocument, aContent); + } + + Relation(Relation&& aOther) + : mFirstIter(std::move(aOther.mFirstIter)), mLastIter(aOther.mLastIter) { + aOther.mLastIter = nullptr; + } + + Relation& operator=(Relation&& aRH) { + mFirstIter = std::move(aRH.mFirstIter); + mLastIter = aRH.mLastIter; + aRH.mLastIter = nullptr; + return *this; + } + + inline void AppendIter(AccIterable* aIter) { + if (mLastIter) { + mLastIter->mNextIter.reset(aIter); + } else { + mFirstIter.reset(aIter); + } + + mLastIter = aIter; + } + + /** + * Append the given accessible to the set of related accessibles. + */ + inline void AppendTarget(Accessible* aAcc) { + if (aAcc) AppendIter(new SingleAccIterator(aAcc)); + } + + /** + * Append the one accessible for this content node to the set of related + * accessibles. + */ + void AppendTarget(DocAccessible* aDocument, nsIContent* aContent) { + if (aContent) AppendTarget(aDocument->GetAccessible(aContent)); + } + + /** + * compute and return the next related accessible. + */ + inline Accessible* Next() { + Accessible* target = nullptr; + + while (mFirstIter && !(target = mFirstIter->Next())) { + mFirstIter = std::move(mFirstIter->mNextIter); + } + + if (!mFirstIter) mLastIter = nullptr; + + return target; + } + + inline LocalAccessible* LocalNext() { + Accessible* next = Next(); + return next ? next->AsLocal() : nullptr; + } + + private: + Relation& operator=(const Relation&) = delete; + Relation(const Relation&) = delete; + + std::unique_ptr mFirstIter; + AccIterable* mLastIter; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/RelationType.h b/accessible/base/RelationType.h new file mode 100644 index 0000000000..b0b1dbf853 --- /dev/null +++ b/accessible/base/RelationType.h @@ -0,0 +1,168 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_relationtype_h_ +#define mozilla_a11y_relationtype_h_ + +namespace mozilla { +namespace a11y { + +enum class RelationType { + + /** + * This object is labelled by a target object. + */ + LABELLED_BY = 0x00, + + /** + * This object is label for a target object. + */ + LABEL_FOR = 0x01, + + /** + * This object is described by the target object. + */ + DESCRIBED_BY = 0x02, + + /** + * This object is describes the target object. + */ + DESCRIPTION_FOR = 0x3, + + /** + * This object is a child of a target object. + */ + NODE_CHILD_OF = 0x4, + + /** + * This object is a parent of a target object. A dual relation to + * NODE_CHILD_OF. + */ + NODE_PARENT_OF = 0x5, + + /** + * Some attribute of this object is affected by a target object. + */ + CONTROLLED_BY = 0x06, + + /** + * This object is interactive and controls some attribute of a target object. + */ + CONTROLLER_FOR = 0x07, + + /** + * Content flows from this object to a target object, i.e. has content that + * flows logically to another object in a sequential way, e.g. text flow. + */ + FLOWS_TO = 0x08, + + /** + * Content flows to this object from a target object, i.e. has content that + * flows logically from another object in a sequential way, e.g. text flow. + */ + FLOWS_FROM = 0x09, + + /** + * This object is a member of a group of one or more objects. When there is + * more than one object in the group each member may have one and the same + * target, e.g. a grouping object. It is also possible that each member has + * multiple additional targets, e.g. one for every other member in the group. + */ + MEMBER_OF = 0x0a, + + /** + * This object is a sub window of a target object. + */ + SUBWINDOW_OF = 0x0b, + + /** + * This object embeds a target object. This relation can be used on the + * OBJID_CLIENT accessible for a top level window to show where the content + * areas are. + */ + EMBEDS = 0x0c, + + /** + * This object is embedded by a target object. + */ + EMBEDDED_BY = 0x0d, + + /** + * This object is a transient component related to the target object. When + * this object is activated the target object doesn't lose focus. + */ + POPUP_FOR = 0x0e, + + /** + * This object is a parent window of the target object. + */ + PARENT_WINDOW_OF = 0x0f, + + /** + * Part of a form/dialog with a related default button. It is used for + * MSAA/XPCOM, it isn't for IA2 or ATK. + */ + DEFAULT_BUTTON = 0x10, + + /** + * The target object is the containing document object. + */ + CONTAINING_DOCUMENT = 0x11, + + /** + * The target object is the topmost containing document object in the tab + * pane. + */ + CONTAINING_TAB_PANE = 0x12, + + /** + * The target object is the containing window object. + */ + CONTAINING_WINDOW = 0x13, + + /** + * The target object is the containing application object. + */ + CONTAINING_APPLICATION = 0x14, + + /** + * The target object provides the detailed, extended description for this + * object. It provides more detailed information than would normally be + * provided using the DESCRIBED_BY relation. A common use for this relation is + * in digital publishing where an extended description needs to be conveyed in + * a book that requires structural markup or the embedding of other technology + * to provide illustrative content. + */ + DETAILS = 0x15, + + /** + * This object provides the detailed, extended description for the target + * object. See DETAILS relation. + */ + DETAILS_FOR = 0x16, + + /** + * The target object is the error message for this object. + */ + ERRORMSG = 0x17, + + /** + * This object is the error message for the target object. + */ + ERRORMSG_FOR = 0x18, + + /** + * The target object is the anchor referenced by this link. + */ + LINKS_TO = 0x19, + + LAST = LINKS_TO +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/RelationTypeMap.h b/accessible/base/RelationTypeMap.h new file mode 100644 index 0000000000..e819682368 --- /dev/null +++ b/accessible/base/RelationTypeMap.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +/** + * Usage: declare the macro RELATIONTYPE()with the following arguments: + * RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) + */ + +RELATIONTYPE(LABELLED_BY, "labelled by", ATK_RELATION_LABELLED_BY, + NAVRELATION_LABELLED_BY, IA2_RELATION_LABELLED_BY) + +RELATIONTYPE(LABEL_FOR, "label for", ATK_RELATION_LABEL_FOR, + NAVRELATION_LABEL_FOR, IA2_RELATION_LABEL_FOR) + +RELATIONTYPE(DESCRIBED_BY, "described by", ATK_RELATION_DESCRIBED_BY, + NAVRELATION_DESCRIBED_BY, IA2_RELATION_DESCRIBED_BY) + +RELATIONTYPE(DESCRIPTION_FOR, "description for", ATK_RELATION_DESCRIPTION_FOR, + NAVRELATION_DESCRIPTION_FOR, IA2_RELATION_DESCRIPTION_FOR) + +RELATIONTYPE(NODE_CHILD_OF, "node child of", ATK_RELATION_NODE_CHILD_OF, + NAVRELATION_NODE_CHILD_OF, IA2_RELATION_NODE_CHILD_OF) + +RELATIONTYPE(NODE_PARENT_OF, "node parent of", ATK_RELATION_NODE_PARENT_OF, + NAVRELATION_NODE_PARENT_OF, IA2_RELATION_NODE_PARENT_OF) + +RELATIONTYPE(CONTROLLED_BY, "controlled by", ATK_RELATION_CONTROLLED_BY, + NAVRELATION_CONTROLLED_BY, IA2_RELATION_CONTROLLED_BY) + +RELATIONTYPE(CONTROLLER_FOR, "controller for", ATK_RELATION_CONTROLLER_FOR, + NAVRELATION_CONTROLLER_FOR, IA2_RELATION_CONTROLLER_FOR) + +RELATIONTYPE(FLOWS_TO, "flows to", ATK_RELATION_FLOWS_TO, NAVRELATION_FLOWS_TO, + IA2_RELATION_FLOWS_TO) + +RELATIONTYPE(FLOWS_FROM, "flows from", ATK_RELATION_FLOWS_FROM, + NAVRELATION_FLOWS_FROM, IA2_RELATION_FLOWS_FROM) + +RELATIONTYPE(MEMBER_OF, "member of", ATK_RELATION_MEMBER_OF, + NAVRELATION_MEMBER_OF, IA2_RELATION_MEMBER_OF) + +RELATIONTYPE(SUBWINDOW_OF, "subwindow of", ATK_RELATION_SUBWINDOW_OF, + NAVRELATION_SUBWINDOW_OF, IA2_RELATION_SUBWINDOW_OF) + +RELATIONTYPE(EMBEDS, "embeds", ATK_RELATION_EMBEDS, NAVRELATION_EMBEDS, + IA2_RELATION_EMBEDS) + +RELATIONTYPE(EMBEDDED_BY, "embedded by", ATK_RELATION_EMBEDDED_BY, + NAVRELATION_EMBEDDED_BY, IA2_RELATION_EMBEDDED_BY) + +RELATIONTYPE(POPUP_FOR, "popup for", ATK_RELATION_POPUP_FOR, + NAVRELATION_POPUP_FOR, IA2_RELATION_POPUP_FOR) + +RELATIONTYPE(PARENT_WINDOW_OF, "parent window of", + ATK_RELATION_PARENT_WINDOW_OF, NAVRELATION_PARENT_WINDOW_OF, + IA2_RELATION_PARENT_WINDOW_OF) + +RELATIONTYPE(DEFAULT_BUTTON, "default button", ATK_RELATION_NULL, + NAVRELATION_DEFAULT_BUTTON, IA2_RELATION_NULL) + +RELATIONTYPE(CONTAINING_DOCUMENT, "containing document", ATK_RELATION_NULL, + NAVRELATION_CONTAINING_DOCUMENT, IA2_RELATION_CONTAINING_DOCUMENT) + +RELATIONTYPE(CONTAINING_TAB_PANE, "containing tab pane", ATK_RELATION_NULL, + NAVRELATION_CONTAINING_TAB_PANE, IA2_RELATION_CONTAINING_TAB_PANE) + +RELATIONTYPE(CONTAINING_WINDOW, "containing window", ATK_RELATION_NULL, + NAVRELATION_CONTAINING_WINDOW, IA2_RELATION_CONTAINING_WINDOW) + +RELATIONTYPE(CONTAINING_APPLICATION, "containing application", + ATK_RELATION_NULL, NAVRELATION_CONTAINING_APPLICATION, + IA2_RELATION_CONTAINING_APPLICATION) + +RELATIONTYPE(DETAILS, "details", ATK_RELATION_DETAILS, NAVRELATION_DETAILS, + IA2_RELATION_DETAILS) + +RELATIONTYPE(DETAILS_FOR, "details for", ATK_RELATION_DETAILS_FOR, + NAVRELATION_DETAILS_FOR, IA2_RELATION_DETAILS_FOR) + +RELATIONTYPE(ERRORMSG, "error", ATK_RELATION_ERROR_MESSAGE, NAVRELATION_ERROR, + IA2_RELATION_ERROR) + +RELATIONTYPE(ERRORMSG_FOR, "error for", ATK_RELATION_ERROR_FOR, + NAVRELATION_ERROR_FOR, IA2_RELATION_ERROR_FOR) + +RELATIONTYPE(LINKS_TO, "links to", ATK_RELATION_NULL, NAVRELATION_LINKS_TO, + IA2_RELATION_NULL) diff --git a/accessible/base/Role.h b/accessible/base/Role.h new file mode 100644 index 0000000000..5c148c4e3b --- /dev/null +++ b/accessible/base/Role.h @@ -0,0 +1,1105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef _role_h_ +#define _role_h_ + +/** + * @note Make sure to update the localized role names when changing the list. + * @note When adding a new role, be sure to also add it to base/RoleMap.h and + * update nsIAccessibleRole. + */ + +namespace mozilla { +namespace a11y { +namespace roles { + +enum Role { + /** + * Used when the accessible has no strongly-defined role. + */ + NOTHING = 0, + + /** + * Represents a title or caption bar for a window. It is used by MSAA only, + * supported automatically by MS Windows. + */ + TITLEBAR = 1, + + /** + * Represents the menu bar (positioned beneath the title bar of a window) + * from which menus are selected by the user. The role is used by + * xul:menubar or role="menubar". + */ + MENUBAR = 2, + + /** + * Represents a vertical or horizontal scroll bar, which is part of the client + * area or used in a control. + */ + SCROLLBAR = 3, + + /** + * Represents a special mouse pointer, which allows a user to manipulate user + * interface elements such as windows. For example, a user clicks and drags + * a sizing grip in the lower-right corner of a window to resize it. + */ + GRIP = 4, + + /** + * Represents a system sound, which is associated with various system events. + */ + SOUND = 5, + + /** + * Represents the system mouse pointer. + */ + CURSOR = 6, + + /** + * Represents the system caret. The role is supported for caret. + */ + CARET = 7, + + /** + * Represents an alert or a condition that a user should be notified about. + * Assistive Technologies typically respond to the role by reading the entire + * onscreen contents of containers advertising this role. Should be used for + * warning dialogs, etc. The role is used by xul:browsermessage, + * role="alert". + */ + ALERT = 8, + + /** + * Represents the window frame, which contains child objects such as + * a title bar, client, and other objects contained in a window. The role + * is supported automatically by MS Windows. + */ + WINDOW = 9, + + /** + * A sub-document ( or + + `, + 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( + `
    +
  • Clickable list item
  • +
  • Clickable list item
  • +
  • Clickable list item
  • +
+ + + +
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: ` + // + //
    + //
  • Listitem
  • + //
`, + // 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.` + ); +} diff --git a/accessible/tests/crashtests/1072792.xhtml b/accessible/tests/crashtests/1072792.xhtml new file mode 100644 index 0000000000..f99c64c5d3 --- /dev/null +++ b/accessible/tests/crashtests/1072792.xhtml @@ -0,0 +1,5 @@ + + +
            + + diff --git a/accessible/tests/crashtests/1380199.html b/accessible/tests/crashtests/1380199.html new file mode 100644 index 0000000000..c690597173 --- /dev/null +++ b/accessible/tests/crashtests/1380199.html @@ -0,0 +1,15 @@ + +> + +
            +
            + + + diff --git a/accessible/tests/mochitest/elm/test_MathMLSpec.html b/accessible/tests/mochitest/elm/test_MathMLSpec.html new file mode 100644 index 0000000000..a55c77668a --- /dev/null +++ b/accessible/tests/mochitest/elm/test_MathMLSpec.html @@ -0,0 +1,616 @@ + + + + HTML a11y spec tests + + + + + + + + + + + + + + + + + + Mozilla Bug 658272 +
            +

            + +
            +  
            + + + + + + a + 2 + + + + + 2 + + + = + + c + 2 + + + + Arbitrary text + + InterpretedStringLiteral + + + + + x + 2 + + + x + 5 + + + + + x + y + + + + [ + + X + , + Y + + closing-fence + + + + a + + + b + + + + x + + + + y + + + + + + b + 1 + 2 + + + + x + + + y + + + z + + + + + + x + ^ + + + + 0 + + + + R + i + + + j + k + + l + + + + + + + (2.1) + + + + E + = + + m + + + c + 2 + + + + + + + + ( + + + 1 + 0 + 0 + + + 0 + 1 + 0 + + + 0 + 0 + 1 + + + ) + + + + + 6 + 8 + + + + 3 + + 2 + + + 4 + + 2 + + + + 3 + 4 + + + + + + Division by zero: + + 1 + 0 + + + + + + + + + x + 2 + + + + y + + + + + + + + x + 2 + + y + + + + + x^{2} + y + + + + + + + + 1 + + + 1 + + + 523 + + - + + 15 + + + 508 + + + + 5 + 1 + 5 + + + + + 123 + ×321 + + + + 123 + 246 + 369 + + + + + + + diff --git a/accessible/tests/mochitest/elm/test_figure.html b/accessible/tests/mochitest/elm/test_figure.html new file mode 100644 index 0000000000..82ac961e36 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_figure.html @@ -0,0 +1,60 @@ + + + + HTML5 figure/figcaption tests + + + + + + + + + + + + + + + + Mozilla Bug 658272 +
            +

            + +
            +  
            + +
            +
            figure caption
            +
            + + + diff --git a/accessible/tests/mochitest/elm/test_listbox.xhtml b/accessible/tests/mochitest/elm/test_listbox.xhtml new file mode 100644 index 0000000000..2315959e3a --- /dev/null +++ b/accessible/tests/mochitest/elm/test_listbox.xhtml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + Mozilla Bug 418371 + +

            + +
            +    
            + + + + +
            + +
            + diff --git a/accessible/tests/mochitest/elm/test_nsApplicationAcc.html b/accessible/tests/mochitest/elm/test_nsApplicationAcc.html new file mode 100644 index 0000000000..2e7aabf882 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_nsApplicationAcc.html @@ -0,0 +1,67 @@ + + + + application accessible name + + + + + + + + + + + Mozilla Bug 454211 + + +

            + +
            +  
            + + + diff --git a/accessible/tests/mochitest/elm/test_shadowroot.html b/accessible/tests/mochitest/elm/test_shadowroot.html new file mode 100644 index 0000000000..bc221090b4 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_shadowroot.html @@ -0,0 +1,35 @@ + + + + ShadowRoot tests + + + + + + + + + Mozilla Bug 1026125 +
            +

            + +
            +  
            + + + + + diff --git a/accessible/tests/mochitest/elm/test_shadowroot_subframe.html b/accessible/tests/mochitest/elm/test_shadowroot_subframe.html new file mode 100644 index 0000000000..20e2baf681 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_shadowroot_subframe.html @@ -0,0 +1,68 @@ + + + + ShadowRoot tests + + + + + + + + +
            +
            + + + diff --git a/accessible/tests/mochitest/events.js b/accessible/tests/mochitest/events.js new file mode 100644 index 0000000000..a6c216e01d --- /dev/null +++ b/accessible/tests/mochitest/events.js @@ -0,0 +1,2660 @@ +/* import-globals-from common.js */ +/* import-globals-from states.js */ +/* import-globals-from text.js */ + +// XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests. +/* eslint-disable no-redeclare */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; +const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; +const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; +const EVENT_DOCUMENT_LOAD_COMPLETE = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; +const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; +const EVENT_DOCUMENT_LOAD_STOPPED = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; +const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; +const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; +const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; +const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; +const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; +const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; +const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; +const EVENT_OBJECT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; +const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; +const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; +const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; +const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; +const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; +const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; +const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; +const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; +const EVENT_TEXT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; +const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; +const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; +const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; +const EVENT_TEXT_SELECTION_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; +const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; +const EVENT_VIRTUALCURSOR_CHANGED = + nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; + +const kNotFromUserInput = 0; +const kFromUserInput = 1; + +// ////////////////////////////////////////////////////////////////////////////// +// General + +/** + * Set up this variable to dump events into DOM. + */ +var gA11yEventDumpID = ""; + +/** + * Set up this variable to dump event processing into console. + */ +var gA11yEventDumpToConsole = false; + +/** + * Set up this variable to dump event processing into error console. + */ +var gA11yEventDumpToAppConsole = false; + +/** + * Semicolon separated set of logging features. + */ +var gA11yEventDumpFeature = ""; + +/** + * Function to detect HTML elements when given a node. + */ +function isHTMLElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == "http://www.w3.org/1999/xhtml" + ); +} + +function isXULElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ); +} + +/** + * Executes the function when requested event is handled. + * + * @param aEventType [in] event type + * @param aTarget [in] event target + * @param aFunc [in] function to call when event is handled + * @param aContext [in, optional] object in which context the function is + * called + * @param aArg1 [in, optional] argument passed into the function + * @param aArg2 [in, optional] argument passed into the function + */ +function waitForEvent( + aEventType, + aTargetOrFunc, + aFunc, + aContext, + aArg1, + aArg2 +) { + var handler = { + handleEvent: function handleEvent(aEvent) { + var target = aTargetOrFunc; + if (typeof aTargetOrFunc == "function") { + target = aTargetOrFunc.call(); + } + + if (target) { + if (target instanceof nsIAccessible && target != aEvent.accessible) { + return; + } + + if (Node.isInstance(target) && target != aEvent.DOMNode) { + return; + } + } + + unregisterA11yEventListener(aEventType, this); + + window.setTimeout(function () { + aFunc.call(aContext, aArg1, aArg2); + }, 0); + }, + }; + + registerA11yEventListener(aEventType, handler); +} + +/** + * Generate mouse move over image map what creates image map accessible (async). + * See waitForImageMap() function. + */ +function waveOverImageMap(aImageMapID) { + var imageMapNode = getNode(aImageMapID); + synthesizeMouse( + imageMapNode, + 10, + 10, + { type: "mousemove" }, + imageMapNode.ownerGlobal + ); +} + +/** + * Call the given function when the tree of the given image map is built. + */ +function waitForImageMap(aImageMapID, aTestFunc) { + waveOverImageMap(aImageMapID); + + var imageMapAcc = getAccessible(aImageMapID); + if (imageMapAcc.firstChild) { + aTestFunc(); + return; + } + + waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); +} + +/** + * Register accessibility event listener. + * + * @param aEventType the accessible event type (see nsIAccessibleEvent for + * available constants). + * @param aEventHandler event listener object, when accessible event of the + * given type is handled then 'handleEvent' method of + * this object is invoked with nsIAccessibleEvent object + * as the first argument. + */ +function registerA11yEventListener(aEventType, aEventHandler) { + listenA11yEvents(true); + addA11yEventListener(aEventType, aEventHandler); +} + +/** + * Unregister accessibility event listener. Must be called for every registered + * event listener (see registerA11yEventListener() function) when the listener + * is not needed. + */ +function unregisterA11yEventListener(aEventType, aEventHandler) { + removeA11yEventListener(aEventType, aEventHandler); + listenA11yEvents(false); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue + +/** + * Return value of invoke method of invoker object. Indicates invoker was unable + * to prepare action. + */ +const INVOKER_ACTION_FAILED = 1; + +/** + * Return value of eventQueue.onFinish. Indicates eventQueue should not finish + * tests. + */ +const DO_NOT_FINISH_TEST = 1; + +/** + * Creates event queue for the given event type. The queue consists of invoker + * objects, each of them generates the event of the event type. When queue is + * started then every invoker object is asked to generate event after timeout. + * When event is caught then current invoker object is asked to check whether + * event was handled correctly. + * + * Invoker interface is: + * + * var invoker = { + * // Generates accessible event or event sequence. If returns + * // INVOKER_ACTION_FAILED constant then stop tests. + * invoke: function(){}, + * + * // [optional] Invoker's check of handled event for correctness. + * check: function(aEvent){}, + * + * // [optional] Invoker's check before the next invoker is proceeded. + * finalCheck: function(aEvent){}, + * + * // [optional] Is called when event of any registered type is handled. + * debugCheck: function(aEvent){}, + * + * // [ignored if 'eventSeq' is defined] DOM node event is generated for + * // (used in the case when invoker expects single event). + * DOMNode getter: function() {}, + * + * // [optional] if true then event sequences are ignored (no failure if + * // sequences are empty). Use you need to invoke an action, do some check + * // after timeout and proceed a next invoker. + * noEventsOnAction getter: function() {}, + * + * // Array of checker objects defining expected events on invoker's action. + * // + * // Checker object interface: + * // + * // var checker = { + * // * DOM or a11y event type. * + * // type getter: function() {}, + * // + * // * DOM node or accessible. * + * // target getter: function() {}, + * // + * // * DOM event phase (false - bubbling). * + * // phase getter: function() {}, + * // + * // * Callback, called to match handled event. * + * // match : function(aEvent) {}, + * // + * // * Callback, called when event is handled + * // check: function(aEvent) {}, + * // + * // * Checker ID * + * // getID: function() {}, + * // + * // * Event that don't have predefined order relative other events. * + * // async getter: function() {}, + * // + * // * Event that is not expected. * + * // unexpected getter: function() {}, + * // + * // * No other event of the same type is not allowed. * + * // unique getter: function() {} + * // }; + * eventSeq getter() {}, + * + * // Array of checker objects defining unexpected events on invoker's + * // action. + * unexpectedEventSeq getter() {}, + * + * // The ID of invoker. + * getID: function(){} // returns invoker ID + * }; + * + * // Used to add a possible scenario of expected/unexpected events on + * // invoker's action. + * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) + * + * + * @param aEventType [in, optional] the default event type (isn't used if + * invoker defines eventSeq property). + */ +function eventQueue(aEventType) { + // public + + /** + * Add invoker object into queue. + */ + this.push = function eventQueue_push(aEventInvoker) { + this.mInvokers.push(aEventInvoker); + }; + + /** + * Start the queue processing. + */ + this.invoke = function eventQueue_invoke() { + listenA11yEvents(true); + + // XXX: Intermittent test_events_caretmove.html fails withouth timeout, + // see bug 474952. + this.processNextInvokerInTimeout(true); + }; + + /** + * This function is called when all events in the queue were handled. + * Override it if you need to be notified of this. + */ + this.onFinish = function eventQueue_finish() {}; + + // private + + /** + * Process next invoker. + */ + // eslint-disable-next-line complexity + this.processNextInvoker = function eventQueue_processNextInvoker() { + // Some scenario was matched, we wait on next invoker processing. + if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerNotScheduled, + "scenario was matched, wait for next invoker activation" + ); + return; + } + + this.setInvokerStatus( + kInvokerNotScheduled, + "the next invoker is processed now" + ); + + // Finish processing of the current invoker if any. + var testFailed = false; + + var invoker = this.getInvoker(); + if (invoker) { + if ("finalCheck" in invoker) { + invoker.finalCheck(); + } + + if (this.mScenarios && this.mScenarios.length) { + var matchIdx = -1; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (!this.areExpectedEventsLeft(eventSeq)) { + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + if ( + (checker.unexpected && checker.wasCaught) || + (!checker.unexpected && checker.wasCaught != 1) + ) { + break; + } + } + + // Ok, we have matched scenario. Report it was completed ok. In + // case of empty scenario guess it was matched but if later we + // find out that non empty scenario was matched then it will be + // a final match. + if (idx == eventSeq.length) { + if ( + matchIdx != -1 && + !!eventSeq.length && + this.mScenarios[matchIdx].length + ) { + ok( + false, + "We have a matched scenario at index " + + matchIdx + + " already." + ); + } + + if (matchIdx == -1 || eventSeq.length) { + matchIdx = scnIdx; + } + + // Report everything is ok. + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Test with ID = '" + this.getEventID(checker) + "' succeed. "; + + if (checker.unexpected) { + ok(true, msg + `There's no unexpected '${typeStr}' event.`); + } else if (checker.todo) { + todo(false, `Todo event '${typeStr}' was caught`); + } else { + ok(true, `${msg} Event '${typeStr}' was handled.`); + } + } + } + } + } + + // We don't have completely matched scenario. Report each failure/success + // for every scenario. + if (matchIdx == -1) { + testFailed = true; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Scenario #" + + scnIdx + + " of test with ID = '" + + this.getEventID(checker) + + "' failed. "; + + if (checker.wasCaught > 1) { + ok(false, msg + "Dupe " + typeStr + " event."); + } + + if (checker.unexpected) { + if (checker.wasCaught) { + ok(false, msg + "There's unexpected " + typeStr + " event."); + } + } else if (!checker.wasCaught) { + var rf = checker.todo ? todo : ok; + rf(false, `${msg} '${typeStr} event is missed.`); + } + } + } + } + } + } + + this.clearEventHandler(); + + // Check if need to stop the test. + if (testFailed || this.mIndex == this.mInvokers.length - 1) { + listenA11yEvents(false); + + var res = this.onFinish(); + if (res != DO_NOT_FINISH_TEST) { + SimpleTest.executeSoon(SimpleTest.finish); + } + + return; + } + + // Start processing of next invoker. + invoker = this.getNextInvoker(); + + // Set up event listeners. Process a next invoker if no events were added. + if (!this.setEventHandler(invoker)) { + this.processNextInvoker(); + return; + } + + if (gLogger.isEnabled()) { + gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); + gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); + } + + var infoText = "Invoke the '" + invoker.getID() + "' test { "; + var scnCount = this.mScenarios ? this.mScenarios.length : 0; + for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { + infoText += "scenario #" + scnIdx + ": "; + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + infoText += eventSeq[idx].unexpected + ? "un" + : "" + + "expected '" + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + "' event; "; + } + } + infoText += " }"; + info(infoText); + + if (invoker.invoke() == INVOKER_ACTION_FAILED) { + // Invoker failed to prepare action, fail and finish tests. + this.processNextInvoker(); + return; + } + + if (this.hasUnexpectedEventsScenario()) { + this.processNextInvokerInTimeout(true); + } + }; + + this.processNextInvokerInTimeout = + function eventQueue_processNextInvokerInTimeout(aUncondProcess) { + this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); + + // No need to wait extra timeout when a) we know we don't need to do that + // and b) there's no any single unexpected event. + if (!aUncondProcess && this.areAllEventsExpected()) { + // We need delay to avoid events coalesce from different invokers. + var queue = this; + SimpleTest.executeSoon(function () { + queue.processNextInvoker(); + }); + return; + } + + // Check in timeout invoker didn't fire registered events. + window.setTimeout( + function (aQueue) { + aQueue.processNextInvoker(); + }, + 300, + this + ); + }; + + /** + * Handle events for the current invoker. + */ + // eslint-disable-next-line complexity + this.handleEvent = function eventQueue_handleEvent(aEvent) { + var invoker = this.getInvoker(); + if (!invoker) { + // skip events before test was started + return; + } + + if (!this.mScenarios) { + // Bad invoker object, error will be reported before processing of next + // invoker in the queue. + this.processNextInvoker(); + return; + } + + if ("debugCheck" in invoker) { + invoker.debugCheck(aEvent); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + // Search through handled expected events to report error if one of them + // is handled for a second time. + if ( + !checker.unexpected && + checker.wasCaught > 0 && + eventQueue.isSameEvent(checker, aEvent) + ) { + checker.wasCaught++; + continue; + } + + // Search through unexpected events, any match results in error report + // after this invoker processing (in case of matched scenario only). + if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { + checker.wasCaught++; + continue; + } + + // Report an error if we handled not expected event of unique type + // (i.e. event types are matched, targets differs). + if ( + !checker.unexpected && + checker.unique && + eventQueue.compareEventTypes(checker, aEvent) + ) { + var isExpected = false; + for (var jdx = 0; jdx < eventSeq.length; jdx++) { + isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); + if (isExpected) { + break; + } + } + + if (!isExpected) { + ok( + false, + "Unique type " + + eventQueue.getEventTypeAsString(checker) + + " event was handled." + ); + } + } + } + } + + var hasMatchedCheckers = false; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + // Check if handled event matches expected sync event. + var nextChecker = this.getNextExpectedEvent(eventSeq); + if (nextChecker) { + if (eventQueue.compareEvents(nextChecker, aEvent)) { + this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); + hasMatchedCheckers = true; + continue; + } + } + + // Check if handled event matches any expected async events. + var haveUnmatchedAsync = false; + for (idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) { + break; + } + + if (!eventSeq[idx].wasCaught) { + haveUnmatchedAsync = true; + } + + if (!eventSeq[idx].unexpected && eventSeq[idx].async) { + if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { + this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); + hasMatchedCheckers = true; + break; + } + } + } + } + + if (hasMatchedCheckers) { + var invoker = this.getInvoker(); + if ("check" in invoker) { + invoker.check(aEvent); + } + } + + for (idx = 0; idx < eventSeq.length; idx++) { + if (!eventSeq[idx].wasCaught) { + if (eventSeq[idx] instanceof orderChecker) { + eventSeq[idx].wasCaught++; + } else { + break; + } + } + } + + // If we don't have more events to wait then schedule next invoker. + if (this.hasMatchedScenario()) { + if (this.mNextInvokerStatus == kInvokerNotScheduled) { + this.processNextInvokerInTimeout(); + } else if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerPending, + "Full match. Void the cancelation of next invoker processing" + ); + } + return; + } + + // If we have scheduled a next invoker then cancel in case of match. + if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) { + this.setInvokerStatus( + kInvokerCanceled, + "Cancel the scheduled invoker in case of match" + ); + } + }; + + // Helpers + this.processMatchedChecker = function eventQueue_function( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx + ) { + aMatchedChecker.wasCaught++; + + if ("check" in aMatchedChecker) { + aMatchedChecker.check(aEvent); + } + + eventQueue.logEvent( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + this.areExpectedEventsLeft(), + this.mNextInvokerStatus + ); + }; + + this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent( + aEventSeq + ) { + if (!("idx" in aEventSeq)) { + aEventSeq.idx = 0; + } + + while ( + aEventSeq.idx < aEventSeq.length && + (aEventSeq[aEventSeq.idx].unexpected || + aEventSeq[aEventSeq.idx].todo || + aEventSeq[aEventSeq.idx].async || + aEventSeq[aEventSeq.idx] instanceof orderChecker || + aEventSeq[aEventSeq.idx].wasCaught > 0) + ) { + aEventSeq.idx++; + } + + return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; + }; + + this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft( + aScenario + ) { + function scenarioHasUnhandledExpectedEvent(aEventSeq) { + // Check if we have unhandled async (can be anywhere in the sequance) or + // sync expcected events yet. + for (var idx = 0; idx < aEventSeq.length; idx++) { + if ( + !aEventSeq[idx].unexpected && + !aEventSeq[idx].todo && + !aEventSeq[idx].wasCaught && + !(aEventSeq[idx] instanceof orderChecker) + ) { + return true; + } + } + + return false; + } + + if (aScenario) { + return scenarioHasUnhandledExpectedEvent(aScenario); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (scenarioHasUnhandledExpectedEvent(eventSeq)) { + return true; + } + } + return false; + }; + + this.areAllEventsExpected = function eventQueue_areAllEventsExpected() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx].unexpected || eventSeq[idx].todo) { + return false; + } + } + } + + return true; + }; + + this.isUnexpectedEventScenario = + function eventQueue_isUnexpectedEventsScenario(aScenario) { + for (var idx = 0; idx < aScenario.length; idx++) { + if (!aScenario[idx].unexpected && !aScenario[idx].todo) { + break; + } + } + + return idx == aScenario.length; + }; + + this.hasUnexpectedEventsScenario = + function eventQueue_hasUnexpectedEventsScenario() { + if (this.getInvoker().noEventsOnAction) { + return true; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) { + return true; + } + } + + return false; + }; + + this.hasMatchedScenario = function eventQueue_hasMatchedScenario() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var scn = this.mScenarios[scnIdx]; + if ( + !this.isUnexpectedEventScenario(scn) && + !this.areExpectedEventsLeft(scn) + ) { + return true; + } + } + return false; + }; + + this.getInvoker = function eventQueue_getInvoker() { + return this.mInvokers[this.mIndex]; + }; + + this.getNextInvoker = function eventQueue_getNextInvoker() { + return this.mInvokers[++this.mIndex]; + }; + + this.setEventHandler = function eventQueue_setEventHandler(aInvoker) { + if (!("scenarios" in aInvoker) || !aInvoker.scenarios.length) { + var eventSeq = aInvoker.eventSeq; + var unexpectedEventSeq = aInvoker.unexpectedEventSeq; + if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) { + eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)]; + } + + if (eventSeq || unexpectedEventSeq) { + defineScenario(aInvoker, eventSeq, unexpectedEventSeq); + } + } + + if (aInvoker.noEventsOnAction) { + return true; + } + + this.mScenarios = aInvoker.scenarios; + if (!this.mScenarios || !this.mScenarios.length) { + ok(false, "Broken invoker '" + aInvoker.getID() + "'"); + return false; + } + + // Register event listeners. + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + if (gLogger.isEnabled()) { + var msg = + "scenario #" + + scnIdx + + ", registered events number: " + + eventSeq.length; + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + // Do not warn about empty event sequances when more than one scenario + // was registered. + if (this.mScenarios.length == 1 && !eventSeq.length) { + ok( + false, + "Broken scenario #" + + scnIdx + + " of invoker '" + + aInvoker.getID() + + "'. No registered events" + ); + return false; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + eventSeq[idx].wasCaught = 0; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + if (gLogger.isEnabled()) { + var msg = "registered"; + if (eventSeq[idx].unexpected) { + msg += " unexpected"; + } + if (eventSeq[idx].async) { + msg += " async"; + } + + msg += + ": event type: " + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + ", target: " + + eventQueue.getEventTargetDescr(eventSeq[idx], true); + + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + if (!target) { + ok(false, "no target for DOM event!"); + return false; + } + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.addEventListener(eventType, this, phase); + } else { + // A11y event + addA11yEventListener(eventType, this); + } + } + } + + return true; + }; + + this.clearEventHandler = function eventQueue_clearEventHandler() { + if (!this.mScenarios) { + return; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.removeEventListener(eventType, this, phase); + } else { + // A11y event + removeA11yEventListener(eventType, this); + } + } + } + this.mScenarios = null; + }; + + this.getEventID = function eventQueue_getEventID(aChecker) { + if ("getID" in aChecker) { + return aChecker.getID(); + } + + var invoker = this.getInvoker(); + return invoker.getID(); + }; + + this.setInvokerStatus = function eventQueue_setInvokerStatus( + aStatus, + aLogMsg + ) { + this.mNextInvokerStatus = aStatus; + + // Uncomment it to debug invoker processing logic. + // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); + }; + + this.mDefEventType = aEventType; + + this.mInvokers = []; + this.mIndex = -1; + this.mScenarios = null; + + this.mNextInvokerStatus = kInvokerNotScheduled; +} + +// ////////////////////////////////////////////////////////////////////////////// +// eventQueue static members and constants + +const kInvokerNotScheduled = 0; +const kInvokerPending = 1; +const kInvokerCanceled = 2; + +eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString( + aEventOrChecker +) { + if (Event.isInstance(aEventOrChecker)) { + return aEventOrChecker.type; + } + + if (aEventOrChecker instanceof nsIAccessibleEvent) { + return eventTypeToString(aEventOrChecker.eventType); + } + + return typeof aEventOrChecker.type == "string" + ? aEventOrChecker.type + : eventTypeToString(aEventOrChecker.type); +}; + +eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr( + aEventOrChecker, + aDontForceTarget +) { + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.originalTarget); + } + + // XXXbz this block doesn't seem to be reachable... + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.accessible); + } + + var descr = aEventOrChecker.targetDescr; + if (descr) { + return descr; + } + + if (aDontForceTarget) { + return "no target description"; + } + + var target = "target" in aEventOrChecker ? aEventOrChecker.target : null; + return prettyName(target); +}; + +eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) { + return "phase" in aChecker ? aChecker.phase : true; +}; + +eventQueue.getEventTarget = function eventQueue_getEventTarget(aChecker) { + if ("eventTarget" in aChecker) { + switch (aChecker.eventTarget) { + case "element": + return aChecker.target; + case "document": + default: + return aChecker.target.ownerDocument; + } + } + return aChecker.target.ownerDocument; +}; + +eventQueue.compareEventTypes = function eventQueue_compareEventTypes( + aChecker, + aEvent +) { + var eventType = Event.isInstance(aEvent) ? aEvent.type : aEvent.eventType; + return aChecker.type == eventType; +}; + +eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) { + if (!eventQueue.compareEventTypes(aChecker, aEvent)) { + return false; + } + + // If checker provides "match" function then allow the checker to decide + // whether event is matched. + if ("match" in aChecker) { + return aChecker.match(aEvent); + } + + var target1 = aChecker.target; + if (target1 instanceof nsIAccessible) { + var target2 = Event.isInstance(aEvent) + ? getAccessible(aEvent.target) + : aEvent.accessible; + + return target1 == target2; + } + + // If original target isn't suitable then extend interface to support target + // (original target is used in test_elm_media.html). + var target2 = Event.isInstance(aEvent) + ? aEvent.originalTarget + : aEvent.DOMNode; + return target1 == target2; +}; + +eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) { + // We don't have stored info about handled event other than its type and + // target, thus we should filter text change and state change events since + // they may occur on the same element because of complex changes. + return ( + this.compareEvents(aChecker, aEvent) && + !(aEvent instanceof nsIAccessibleTextChangeEvent) && + !(aEvent instanceof nsIAccessibleStateChangeEvent) + ); +}; + +eventQueue.invokerStatusToMsg = function eventQueue_invokerStatusToMsg( + aInvokerStatus, + aMsg +) { + var msg = "invoker status: "; + switch (aInvokerStatus) { + case kInvokerNotScheduled: + msg += "not scheduled"; + break; + case kInvokerPending: + msg += "pending"; + break; + case kInvokerCanceled: + msg += "canceled"; + break; + } + + if (aMsg) { + msg += " (" + aMsg + ")"; + } + + return msg; +}; + +eventQueue.logEvent = function eventQueue_logEvent( + aOrigEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + aAreExpectedEventsLeft, + aInvokerStatus +) { + // Dump DOM event information. Skip a11y event since it is dumped by + // gA11yEventObserver. + if (Event.isInstance(aOrigEvent)) { + var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); + info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); + gLogger.logToDOM(info); + } + + var infoMsg = + "unhandled expected events: " + + aAreExpectedEventsLeft + + ", " + + eventQueue.invokerStatusToMsg(aInvokerStatus); + + var currType = eventQueue.getEventTypeAsString(aMatchedChecker); + var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); + var consoleMsg = + "*****\nScenario " + + aScenarioIdx + + ", event " + + aEventIdx + + " matched: " + + currType + + "\n" + + infoMsg + + "\n*****"; + gLogger.logToConsole(consoleMsg); + + var emphText = "matched "; + var msg = + "EQ event, type: " + + currType + + ", target: " + + currTargetDescr + + ", " + + infoMsg; + gLogger.logToDOM(msg, true, emphText); +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Action sequence + +/** + * Deal with action sequence. Used when you need to execute couple of actions + * each after other one. + */ +function sequence() { + /** + * Append new sequence item. + * + * @param aProcessor [in] object implementing interface + * { + * // execute item action + * process: function() {}, + * // callback, is called when item was processed + * onProcessed: function() {} + * }; + * @param aEventType [in] event type of expected event on item action + * @param aTarget [in] event target of expected event on item action + * @param aItemID [in] identifier of item + */ + this.append = function sequence_append( + aProcessor, + aEventType, + aTarget, + aItemID + ) { + var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); + this.items.push(item); + }; + + /** + * Process next sequence item. + */ + this.processNext = function sequence_processNext() { + this.idx++; + if (this.idx >= this.items.length) { + ok(false, "End of sequence: nothing to process!"); + SimpleTest.finish(); + return; + } + + this.items[this.idx].startProcess(); + }; + + this.items = []; + this.idx = -1; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Defines a scenario of expected/unexpected events. Each invoker can have + * one or more scenarios of events. Only one scenario must be completed. + */ +function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) { + if (!("scenarios" in aInvoker)) { + aInvoker.scenarios = []; + } + + // Create unified event sequence concatenating expected and unexpected + // events. + if (!aEventSeq) { + aEventSeq = []; + } + + for (var idx = 0; idx < aEventSeq.length; idx++) { + aEventSeq[idx].unexpected |= false; + aEventSeq[idx].async |= false; + } + + if (aUnexpectedEventSeq) { + for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { + aUnexpectedEventSeq[idx].unexpected = true; + aUnexpectedEventSeq[idx].async = false; + } + + aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); + } + + aInvoker.scenarios.push(aEventSeq); +} + +/** + * Invokers defined below take a checker object (or array of checker objects). + * An invoker listens for default event type registered in event queue object + * until its checker is provided. + * + * Note, checker object or array of checker objects is optional. + */ + +/** + * Click invoker. + */ +function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthClick_invoke() { + var targetNode = this.DOMNode; + if (targetNode.nodeType == targetNode.DOCUMENT_NODE) { + targetNode = this.DOMNode.body + ? this.DOMNode.body + : this.DOMNode.documentElement; + } + + // Scroll the node into view, otherwise synth click may fail. + if (isHTMLElement(targetNode)) { + targetNode.scrollIntoView(true); + } else if (isXULElement(targetNode)) { + var targetAcc = getAccessible(targetNode); + targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); + } + + var x = 1, + y = 1; + if (aArgs && "where" in aArgs) { + if (aArgs.where == "right") { + if (isHTMLElement(targetNode)) { + x = targetNode.offsetWidth - 1; + } else if (isXULElement(targetNode)) { + x = targetNode.getBoundingClientRect().width - 1; + } + } else if (aArgs.where == "center") { + if (isHTMLElement(targetNode)) { + x = targetNode.offsetWidth / 2; + y = targetNode.offsetHeight / 2; + } else if (isXULElement(targetNode)) { + x = targetNode.getBoundingClientRect().width / 2; + y = targetNode.getBoundingClientRect().height / 2; + } + } + } + synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); + }; + + this.finalCheck = function synthClick_finalCheck() { + // Scroll top window back. + window.top.scrollTo(0, 0); + }; + + this.getID = function synthClick_getID() { + return prettyName(aNodeOrID) + " click"; + }; +} + +/** + * Scrolls the node into view. + */ +function scrollIntoView(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function scrollIntoView_invoke() { + var targetNode = this.DOMNode; + if (isHTMLElement(targetNode)) { + targetNode.scrollIntoView(true); + } else if (isXULElement(targetNode)) { + var targetAcc = getAccessible(targetNode); + targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); + } + }; + + this.getID = function scrollIntoView_getID() { + return prettyName(aNodeOrID) + " scrollIntoView"; + }; +} + +/** + * Mouse move invoker. + */ +function synthMouseMove(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function synthMouseMove_invoke() { + synthesizeMouse(this.DOMNode, 5, 5, { type: "mousemove" }); + synthesizeMouse(this.DOMNode, 6, 6, { type: "mousemove" }); + }; + + this.getID = function synthMouseMove_getID() { + return prettyName(aID) + " mouse move"; + }; +} + +/** + * General key press invoker. + */ +function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthKey_invoke() { + synthesizeKey(this.mKey, this.mArgs, this.mWindow); + }; + + this.getID = function synthKey_getID() { + var key = this.mKey; + switch (this.mKey) { + case "VK_TAB": + key = "tab"; + break; + case "VK_DOWN": + key = "down"; + break; + case "VK_UP": + key = "up"; + break; + case "VK_LEFT": + key = "left"; + break; + case "VK_RIGHT": + key = "right"; + break; + case "VK_HOME": + key = "home"; + break; + case "VK_END": + key = "end"; + break; + case "VK_ESCAPE": + key = "escape"; + break; + case "VK_RETURN": + key = "enter"; + break; + } + if (aArgs) { + if (aArgs.shiftKey) { + key += " shift"; + } + if (aArgs.ctrlKey) { + key += " ctrl"; + } + if (aArgs.altKey) { + key += " alt"; + } + } + return prettyName(aNodeOrID) + " '" + key + " ' key"; + }; + + this.mKey = aKey; + this.mArgs = aArgs ? aArgs : {}; + this.mWindow = aArgs ? aArgs.window : null; +} + +/** + * Tab key invoker. + */ +function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: false, window: aWindow }, + aCheckerOrEventSeq + ); +} + +/** + * Shift tab key invoker. + */ +function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: true }, + aCheckerOrEventSeq + ); +} + +/** + * Escape key invoker. + */ +function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_ESCAPE", + null, + aCheckerOrEventSeq + ); +} + +/** + * Down arrow key invoker. + */ +function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_DOWN", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Up arrow key invoker. + */ +function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq); +} + +/** + * Left arrow key invoker. + */ +function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_LEFT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Right arrow key invoker. + */ +function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_RIGHT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Home key invoker. + */ +function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); +} + +/** + * End key invoker. + */ +function synthEndKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); +} + +/** + * Enter key invoker + */ +function synthEnterKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); +} + +/** + * Synth alt + down arrow to open combobox. + */ +function synthOpenComboboxKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); + + this.getID = function synthOpenComboboxKey_getID() { + return "open combobox (alt + down arrow) " + prettyName(aID); + }; +} + +/** + * Focus invoker. + */ +function synthFocus(aNodeOrID, aCheckerOrEventSeq) { + var checkerOfEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(aNodeOrID); + this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); + + this.invoke = function synthFocus_invoke() { + if (this.DOMNode.editor) { + this.DOMNode.selectionStart = this.DOMNode.selectionEnd = + this.DOMNode.value.length; + } + this.DOMNode.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " focus"; + }; +} + +/** + * Focus invoker. Focus the HTML body of content document of iframe. + */ +function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) { + var frameDoc = getNode(aNodeOrID).contentDocument; + var checkerOrEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(frameDoc); + this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); + + this.invoke = function synthFocus_invoke() { + this.DOMNode.body.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " frame document focus"; + }; +} + +/** + * Change the current item when the widget doesn't have a focus. + */ +function changeCurrentItem(aID, aItemID) { + this.eventSeq = [new nofocusChecker()]; + + this.invoke = function changeCurrentItem_invoke() { + var controlNode = getNode(aID); + var itemNode = getNode(aItemID); + + // HTML + if (controlNode.localName == "input") { + if (controlNode.checked) { + this.reportError(); + } + + controlNode.checked = true; + return; + } + + if (controlNode.localName == "select") { + if (controlNode.selectedIndex == itemNode.index) { + this.reportError(); + } + + controlNode.selectedIndex = itemNode.index; + return; + } + + // XUL + if (controlNode.localName == "tree") { + if (controlNode.currentIndex == aItemID) { + this.reportError(); + } + + controlNode.currentIndex = aItemID; + return; + } + + if (controlNode.localName == "menulist") { + if (controlNode.selectedItem == itemNode) { + this.reportError(); + } + + controlNode.selectedItem = itemNode; + return; + } + + if (controlNode.currentItem == itemNode) { + ok( + false, + "Error in test: proposed current item is already current" + + prettyName(aID) + ); + } + + controlNode.currentItem = itemNode; + }; + + this.getID = function changeCurrentItem_getID() { + return "current item change for " + prettyName(aID); + }; + + this.reportError = function changeCurrentItem_reportError() { + ok( + false, + "Error in test: proposed current item '" + + aItemID + + "' is already current" + ); + }; +} + +/** + * Toggle top menu invoker. + */ +function toggleTopMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq); + + this.getID = function toggleTopMenu_getID() { + return "toggle top menu on " + prettyName(aID); + }; +} + +/** + * Context menu invoker. + */ +function synthContextMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, { + button: 0, + type: "contextmenu", + }); + + this.getID = function synthContextMenu_getID() { + return "context menu on " + prettyName(aID); + }; +} + +/** + * Open combobox, autocomplete and etc popup, check expandable states. + */ +function openCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID), + ]; + + this.invoke = function openCombobox_invoke() { + getNode(aComboboxID).focus(); + synthesizeKey("VK_DOWN", { altKey: true }); + }; + + this.getID = function openCombobox_getID() { + return "open combobox " + prettyName(aComboboxID); + }; +} + +/** + * Close combobox, autocomplete and etc popup, check expandable states. + */ +function closeCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID), + ]; + + this.invoke = function closeCombobox_invoke() { + synthesizeKey("KEY_Escape"); + }; + + this.getID = function closeCombobox_getID() { + return "close combobox " + prettyName(aComboboxID); + }; +} + +/** + * Select all invoker. + */ +function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthSelectAll_invoke() { + if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement") { + this.DOMNode.select(); + } else { + window.getSelection().selectAllChildren(this.DOMNode); + } + }; + + this.getID = function synthSelectAll_getID() { + return aNodeOrID + " selectall"; + }; +} + +/** + * Move the caret to the end of line. + */ +function moveToLineEnd(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_RIGHT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthEndKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to the end of previous line if any. + */ +function moveToPrevLineEnd(aID, aCaretOffset) { + this.__proto__ = new synthAction( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + + this.invoke = function moveToPrevLineEnd_invoke() { + synthesizeKey("KEY_ArrowUp"); + + if (MAC) { + synthesizeKey("Key_ArrowRight", { metaKey: true }); + } else { + synthesizeKey("KEY_End"); + } + }; + + this.getID = function moveToPrevLineEnd_getID() { + return "move to previous line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the line. + */ +function moveToLineStart(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_LEFT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthHomeKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line start in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the text. + */ +function moveToTextStart(aID) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_UP", + { metaKey: true }, + new caretMoveChecker(0, true, aID) + ); + } else { + this.__proto__ = new synthKey( + aID, + "VK_HOME", + { ctrlKey: true }, + new caretMoveChecker(0, true, aID) + ); + } + + this.getID = function moveToTextStart_getID() { + return "move to text start in " + prettyName(aID); + }; +} + +/** + * Move the caret in text accessible. + */ +function moveCaretToDOMPoint( + aID, + aDOMPointNodeID, + aDOMPointOffset, + aExpectedOffset, + aFocusTargetID, + aCheckFunc +) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.DOMPointNode = getNode(aDOMPointNodeID); + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + this.focusNode = this.focus ? this.focus.DOMNode : null; + + this.invoke = function moveCaretToDOMPoint_invoke() { + if (this.focusNode) { + this.focusNode.focus(); + } + + var selection = this.DOMPointNode.ownerGlobal.getSelection(); + var selRange = selection.getRangeAt(0); + selRange.setStart(this.DOMPointNode, aDOMPointOffset); + selRange.collapse(true); + + selection.removeRange(selRange); + selection.addRange(selRange); + }; + + this.getID = function moveCaretToDOMPoint_getID() { + return ( + "Set caret on " + + prettyName(aID) + + " at point: " + + prettyName(aDOMPointNodeID) + + " node with offset " + + aDOMPointOffset + ); + }; + + this.finalCheck = function moveCaretToDOMPoint_finalCheck() { + if (aCheckFunc) { + aCheckFunc.call(); + } + }; + + this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +/** + * Set caret offset in text accessible. + */ +function setCaretOffset(aID, aOffset, aFocusTargetID) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.offset = aOffset == -1 ? this.target.characterCount : aOffset; + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + + this.invoke = function setCaretOffset_invoke() { + this.target.caretOffset = this.offset; + }; + + this.getID = function setCaretOffset_getID() { + return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; + }; + + this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue checkers + +/** + * Common invoker checker (see eventSeq of eventQueue). + */ +function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) { + this.type = aEventType; + this.async = aIsAsync; + + this.__defineGetter__("target", invokerChecker_targetGetter); + this.__defineSetter__("target", invokerChecker_targetSetter); + + // implementation details + function invokerChecker_targetGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.call(null, this.mTargetFuncArg); + } + if (typeof this.mTarget == "string") { + return getNode(this.mTarget); + } + + return this.mTarget; + } + + function invokerChecker_targetSetter(aValue) { + this.mTarget = aValue; + return this.mTarget; + } + + this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); + + function invokerChecker_targetDescrGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.name + ", arg: " + this.mTargetFuncArg; + } + + return prettyName(this.mTarget); + } + + this.mTarget = aTargetOrFunc; + this.mTargetFuncArg = aTargetFuncArg; +} + +/** + * event checker that forces preceeding async events to happen before this + * checker. + */ +function orderChecker() { + // XXX it doesn't actually work to inherit from invokerChecker, but maybe we + // should fix that? + // this.__proto__ = new invokerChecker(null, null, null, false); +} + +/** + * Generic invoker checker for todo events. + */ +function todo_invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + this.todo = true; +} + +/** + * Generic invoker checker for unexpected events. + */ +function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + + this.unexpected = true; +} + +/** + * Common invoker checker for async events. + */ +function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +function focusChecker(aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_FOCUS, + aTargetOrFunc, + aTargetFuncArg, + false + ); + + this.unique = true; // focus event must be unique for invoker action + + this.check = function focusChecker_check(aEvent) { + testStates(aEvent.accessible, STATE_FOCUSED); + }; +} + +function nofocusChecker(aID) { + this.__proto__ = new focusChecker(aID); + this.unexpected = true; +} + +/** + * Text inserted/removed events checker. + * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput + */ +function textChangeChecker( + aID, + aStart, + aEnd, + aTextOrFunc, + aIsInserted, + aFromUser, + aAsync +) { + this.target = getNode(aID); + this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; + this.startOffset = aStart; + this.endOffset = aEnd; + this.textOrFunc = aTextOrFunc; + this.async = aAsync; + + this.match = function stextChangeChecker_match(aEvent) { + if ( + !(aEvent instanceof nsIAccessibleTextChangeEvent) || + aEvent.accessible !== getAccessible(this.target) + ) { + return false; + } + + let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + let modifiedText = + typeof this.textOrFunc === "function" + ? this.textOrFunc() + : this.textOrFunc; + return modifiedText === tcEvent.modifiedText; + }; + + this.check = function textChangeChecker_check(aEvent) { + aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + + var modifiedText = + typeof this.textOrFunc == "function" + ? this.textOrFunc() + : this.textOrFunc; + var modifiedTextLen = + this.endOffset == -1 ? modifiedText.length : aEnd - aStart; + + is( + aEvent.start, + this.startOffset, + "Wrong start offset for " + prettyName(aID) + ); + is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); + var changeInfo = aIsInserted ? "inserted" : "removed"; + is( + aEvent.isInserted, + aIsInserted, + "Text was " + changeInfo + " for " + prettyName(aID) + ); + is( + aEvent.modifiedText, + modifiedText, + "Wrong " + changeInfo + " text for " + prettyName(aID) + ); + if (typeof aFromUser != "undefined") { + is( + aEvent.isFromUserInput, + aFromUser, + "wrong value of isFromUserInput() for " + prettyName(aID) + ); + } + }; +} + +/** + * Caret move events checker. + */ +function caretMoveChecker( + aCaretOffset, + aIsSelectionCollapsed, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync +) { + this.__proto__ = new invokerChecker( + EVENT_TEXT_CARET_MOVED, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function caretMoveChecker_check(aEvent) { + let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent); + is( + evt.caretOffset, + aCaretOffset, + "Wrong caret offset for " + prettyName(aEvent.accessible) + ); + is( + evt.isSelectionCollapsed, + aIsSelectionCollapsed, + "wrong collapsed value for " + prettyName(aEvent.accessible) + ); + }; +} + +function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new caretMoveChecker( + aCaretOffset, + true, // Caret is collapsed + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Text selection change checker. + */ +function textSelectionChecker( + aID, + aStartOffset, + aEndOffset, + aRangeStartContainer, + aRangeStartOffset, + aRangeEndContainer, + aRangeEndOffset +) { + this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); + + this.check = function textSelectionChecker_check(aEvent) { + if (aStartOffset == aEndOffset) { + ok(true, "Collapsed selection triggered text selection change event."); + } else { + testTextGetSelection(aID, aStartOffset, aEndOffset, 0); + + // Test selection test range + let selectionRanges = aEvent.QueryInterface( + nsIAccessibleTextSelectionChangeEvent + ).selectionRanges; + let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange); + is( + range.startContainer, + getAccessible(aRangeStartContainer), + "correct range start container" + ); + is(range.startOffset, aRangeStartOffset, "correct range start offset"); + is(range.endOffset, aRangeEndOffset, "correct range end offset"); + is( + range.endContainer, + getAccessible(aRangeEndContainer), + "correct range end container" + ); + } + }; +} + +/** + * Object attribute changed checker + */ +function objAttrChangedChecker(aID, aAttr) { + this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID); + + this.check = function objAttrChangedChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + } catch (e) { + ok(false, "Object attribute changed event was expected"); + } + + if (!event) { + return; + } + + is( + event.changedAttribute, + aAttr, + "Wrong attribute name of the object attribute changed event." + ); + }; + + this.match = function objAttrChangedChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) { + var scEvent = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.changedAttribute == aAttr + ); + } + return false; + }; +} + +/** + * State change checker. + */ +function stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync, + aSkipCurrentStateCheck +) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function stateChangeChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is( + event.isExtraState, + aIsExtraState, + "Wrong extra state bit of the statechange event." + ); + isState( + event.state, + aState, + aIsExtraState, + "Wrong state of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + if (aSkipCurrentStateCheck) { + todo(false, "State checking was skipped!"); + return; + } + + var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; + var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; + var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState; + var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0; + testStates( + event.accessible, + state, + extraState, + unxpdState, + unxpdExtraState + ); + }; + + this.match = function stateChangeChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleStateChangeEvent) { + var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.state == aState + ); + } + return false; + }; +} + +function asyncStateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg +) { + this.__proto__ = new stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Expanded state change checker. + */ +function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg + ); + + this.check = function expandedStateChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); + is( + event.isExtraState, + false, + "Wrong extra state bit of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED); + }; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event sequances (array of predefined checkers) + +/** + * Event seq for single selection change. + */ +function selChangeSeq(aUnselectedID, aSelectedID) { + if (!aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ]; + } + + // Return two possible scenarios: depending on widget type when selection is + // moved the the order of items that get selected and unselected may vary. + return [ + [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + ]; +} + +/** + * Event seq for item removed form the selection. + */ +function selRemoveSeq(aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID), + ]; +} + +/** + * Event seq for item added to the selection. + */ +function selAddSeq(aSelectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION_ADD, aSelectedID), + ]; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details. +// ////////////////////////////////////////////////////////////////////////////// + +// ////////////////////////////////////////////////////////////////////////////// +// General + +var gA11yEventListeners = {}; +var gA11yEventApplicantsCount = 0; + +var gA11yEventObserver = { + // eslint-disable-next-line complexity + observe: function observe(aSubject, aTopic, aData) { + if (aTopic != "accessible-event") { + return; + } + + var event; + try { + event = aSubject.QueryInterface(nsIAccessibleEvent); + } catch (ex) { + // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. + // Remove the leftover observer, otherwise it "leaks" to all the following tests. + Services.obs.removeObserver(this, "accessible-event"); + // Forward the exception, with added explanation. + throw new Error( + "[accessible/events.js, gA11yEventObserver.observe] This is expected " + + `if a previous test has been aborted... Initial exception was: [ ${ex} ]` + ); + } + var listenersArray = gA11yEventListeners[event.eventType]; + + var eventFromDumpArea = false; + if (gLogger.isEnabled()) { + // debug stuff + eventFromDumpArea = true; + + var target = event.DOMNode; + var dumpElm = gA11yEventDumpID + ? document.getElementById(gA11yEventDumpID) + : null; + + if (dumpElm) { + var parent = target; + while (parent && parent != dumpElm) { + parent = parent.parentNode; + } + } + + if (!dumpElm || parent != dumpElm) { + var type = eventTypeToString(event.eventType); + var info = "Event type: " + type; + + if (event instanceof nsIAccessibleStateChangeEvent) { + var stateStr = statesToString( + event.isExtraState ? 0 : event.state, + event.isExtraState ? event.state : 0 + ); + info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; + } else if (event instanceof nsIAccessibleTextChangeEvent) { + info += + ", start: " + + event.start + + ", length: " + + event.length + + ", " + + (event.isInserted ? "inserted" : "removed") + + " text: " + + event.modifiedText; + } + + info += ". Target: " + prettyName(event.accessible); + + if (listenersArray) { + info += ". Listeners count: " + listenersArray.length; + } + + if (gLogger.hasFeature("parentchain:" + type)) { + info += "\nParent chain:\n"; + var acc = event.accessible; + while (acc) { + info += " " + prettyName(acc) + "\n"; + acc = acc.parent; + } + } + + eventFromDumpArea = false; + gLogger.log(info); + } + } + + // Do not notify listeners if event is result of event log changes. + if (!listenersArray || eventFromDumpArea) { + return; + } + + for (var index = 0; index < listenersArray.length; index++) { + listenersArray[index].handleEvent(event); + } + }, +}; + +function listenA11yEvents(aStartToListen) { + if (aStartToListen) { + // Add observer when adding the first applicant only. + if (!gA11yEventApplicantsCount++) { + Services.obs.addObserver(gA11yEventObserver, "accessible-event"); + } + } else { + // Remove observer when there are no more applicants only. + // '< 0' case should not happen, but just in case: removeObserver() will throw. + // eslint-disable-next-line no-lonely-if + if (--gA11yEventApplicantsCount <= 0) { + Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); + } + } +} + +function addA11yEventListener(aEventType, aEventHandler) { + if (!(aEventType in gA11yEventListeners)) { + gA11yEventListeners[aEventType] = []; + } + + var listenersArray = gA11yEventListeners[aEventType]; + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + listenersArray.push(aEventHandler); + } +} + +function removeA11yEventListener(aEventType, aEventHandler) { + var listenersArray = gA11yEventListeners[aEventType]; + if (!listenersArray) { + return false; + } + + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + return false; + } + + listenersArray.splice(index, 1); + + if (!listenersArray.length) { + gA11yEventListeners[aEventType] = null; + delete gA11yEventListeners[aEventType]; + } + + return true; +} + +/** + * Used to dump debug information. + */ +var gLogger = { + /** + * Return true if dump is enabled. + */ + isEnabled: function debugOutput_isEnabled() { + return ( + gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole + ); + }, + + /** + * Dump information into DOM and console if applicable. + */ + log: function logger_log(aMsg) { + this.logToConsole(aMsg); + this.logToAppConsole(aMsg); + this.logToDOM(aMsg); + }, + + /** + * Log message to DOM. + * + * @param aMsg [in] the primary message + * @param aHasIndent [in, optional] if specified the message has an indent + * @param aPreEmphText [in, optional] the text is colored and appended prior + * primary message + */ + logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) { + if (gA11yEventDumpID == "") { + return; + } + + var dumpElm = document.getElementById(gA11yEventDumpID); + if (!dumpElm) { + ok( + false, + "No dump element '" + gA11yEventDumpID + "' within the document!" + ); + return; + } + + var containerTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "div" + : "description"; + + var container = document.createElement(containerTagName); + if (aHasIndent) { + container.setAttribute("style", "padding-left: 10px;"); + } + + if (aPreEmphText) { + var inlineTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "span" + : "description"; + var emphElm = document.createElement(inlineTagName); + emphElm.setAttribute("style", "color: blue;"); + emphElm.textContent = aPreEmphText; + + container.appendChild(emphElm); + } + + var textNode = document.createTextNode(aMsg); + container.appendChild(textNode); + + dumpElm.appendChild(container); + }, + + /** + * Log message to console. + */ + logToConsole: function logger_logToConsole(aMsg) { + if (gA11yEventDumpToConsole) { + dump("\n" + aMsg + "\n"); + } + }, + + /** + * Log message to error console. + */ + logToAppConsole: function logger_logToAppConsole(aMsg) { + if (gA11yEventDumpToAppConsole) { + Services.console.logStringMessage("events: " + aMsg); + } + }, + + /** + * Return true if logging feature is enabled. + */ + hasFeature: function logger_hasFeature(aFeature) { + var startIdx = gA11yEventDumpFeature.indexOf(aFeature); + if (startIdx == -1) { + return false; + } + + var endIdx = startIdx + aFeature.length; + return ( + endIdx == gA11yEventDumpFeature.length || + gA11yEventDumpFeature[endIdx] == ";" + ); + }, +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Sequence + +/** + * Base class of sequence item. + */ +function sequenceItem(aProcessor, aEventType, aTarget, aItemID) { + // private + + this.startProcess = function sequenceItem_startProcess() { + this.queue.invoke(); + }; + + this.queue = new eventQueue(); + this.queue.onFinish = function () { + aProcessor.onProcessed(); + return DO_NOT_FINISH_TEST; + }; + + var invoker = { + invoke: function invoker_invoke() { + return aProcessor.process(); + }, + getID: function invoker_getID() { + return aItemID; + }, + eventSeq: [new invokerChecker(aEventType, aTarget)], + }; + + this.queue.push(invoker); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Invoker base class for prepare an action. + */ +function synthAction(aNodeOrID, aEventsObj) { + this.DOMNode = getNode(aNodeOrID); + + if (aEventsObj) { + var scenarios = null; + if (aEventsObj instanceof Array) { + if (aEventsObj[0] instanceof Array) { + scenarios = aEventsObj; + } + // scenarios + else { + scenarios = [aEventsObj]; + } // event sequance + } else { + scenarios = [[aEventsObj]]; // a single checker object + } + + for (var i = 0; i < scenarios.length; i++) { + defineScenario(this, scenarios[i]); + } + } + + this.getID = function synthAction_getID() { + return prettyName(aNodeOrID) + " action"; + }; +} diff --git a/accessible/tests/mochitest/events/a11y.ini b/accessible/tests/mochitest/events/a11y.ini new file mode 100644 index 0000000000..40ef776175 --- /dev/null +++ b/accessible/tests/mochitest/events/a11y.ini @@ -0,0 +1,68 @@ +[DEFAULT] +support-files = + focus.html + scroll.html + slow_image.sjs + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + !/image/test/mochitest/animated-gif-finalframe.gif + !/image/test/mochitest/animated-gif.gif + +[test_announcement.html] +[test_aria_alert.html] +[test_aria_menu.html] +[test_aria_objattr.html] +[test_aria_owns.html] +[test_aria_statechange.html] +[test_attrs.html] +[test_attrchange.html] +[test_bug1322593.html] +[test_bug1322593-2.html] +[test_caretmove.html] +[test_coalescence.html] +[test_contextmenu.html] +[test_descrchange.html] +[test_dragndrop.html] +[test_flush.html] +[test_focusable_statechange.html] +[test_focus_aria_activedescendant.html] +[test_focus_autocomplete.html] +[test_focus_autocomplete.xhtml] +# Disabled on Linux and Windows due to frequent failures - bug 695019, bug 890795 +skip-if = os == 'win' || os == 'linux' +[test_focus_canvas.html] +[test_focus_contextmenu.xhtml] +[test_focus_controls.html] +[test_focus_doc.html] +[test_focus_general.html] +[test_focus_general.xhtml] +[test_focus_listcontrols.xhtml] +[test_focus_menu.xhtml] +[test_focus_name.html] +[test_focus_removal.html] +[test_focus_selects.html] +[test_focus_tabbox.xhtml] +skip-if = true +[test_focus_tree.xhtml] +[test_fromUserInput.html] +[test_label.xhtml] +[test_menu.xhtml] +[test_mutation.html] +[test_namechange.xhtml] +[test_namechange.html] +[test_scroll.xhtml] +[test_scroll_caret.xhtml] +[test_selection.html] +skip-if = os == 'mac' +[test_selection.xhtml] +skip-if = os == 'mac' +[test_selection_aria.html] +[test_statechange.html] +[test_statechange.xhtml] +[test_text.html] +[test_text_alg.html] +[test_textattrchange.html] +[test_textselchange.html] +[test_tree.xhtml] +[test_valuechange.html] +skip-if = os == 'mac' diff --git a/accessible/tests/mochitest/events/docload/a11y.ini b/accessible/tests/mochitest/events/docload/a11y.ini new file mode 100644 index 0000000000..6e014d511c --- /dev/null +++ b/accessible/tests/mochitest/events/docload/a11y.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + docload_wnd.html + !/accessible/tests/mochitest/*.js + +[test_docload_aria.html] +[test_docload_busy.html] +[test_docload_embedded.html] +[test_docload_iframe.html] +[test_docload_root.html] +skip-if = os == 'mac' # bug 1456997 +[test_docload_shutdown.html] +skip-if = os == 'mac' # bug 1456997 diff --git a/accessible/tests/mochitest/events/docload/docload_wnd.html b/accessible/tests/mochitest/events/docload/docload_wnd.html new file mode 100644 index 0000000000..93df1e86d4 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/docload_wnd.html @@ -0,0 +1,37 @@ + + + Accessible events testing for document + + + + + + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_aria.html b/accessible/tests/mochitest/events/docload/test_docload_aria.html new file mode 100644 index 0000000000..c5fc099918 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_aria.html @@ -0,0 +1,75 @@ + + + + Accessible events testing for ARIA document + + + + + + + + + + + + + + + + + Mozilla Bug 759833 + + +

            + +
            +  
            + + + + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_busy.html b/accessible/tests/mochitest/events/docload/test_docload_busy.html new file mode 100644 index 0000000000..37caf306bb --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_busy.html @@ -0,0 +1,83 @@ + + + + Accessible events testing for document + + + + + + + + + + + + + + + + + Mozilla Bug 658185 + + +

            + +
            +  
            + +
            + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_embedded.html b/accessible/tests/mochitest/events/docload/test_docload_embedded.html new file mode 100644 index 0000000000..18873dc904 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_embedded.html @@ -0,0 +1,85 @@ + + + + Accessible events testing for document + + + + + + + + + + + + + + + + Mozilla Bug 420845 + + + Mozilla Bug 754165 + + +

            + +
            +  
            + +
            + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_iframe.html b/accessible/tests/mochitest/events/docload/test_docload_iframe.html new file mode 100644 index 0000000000..d410ebb7e2 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_iframe.html @@ -0,0 +1,99 @@ + + + + Accessible events testing for document + + + + + + + + + + + + + + + + + Mozilla Bug 566103 + + +

            + +
            +  
            + +
            + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_root.html b/accessible/tests/mochitest/events/docload/test_docload_root.html new file mode 100644 index 0000000000..91ce3a10ee --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_root.html @@ -0,0 +1,125 @@ + + + + Accessible events testing for document + + + + + + + + + + + + + + + + Mozilla Bug 506206 + + +

            + +
            +  
            + + diff --git a/accessible/tests/mochitest/events/docload/test_docload_shutdown.html b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html new file mode 100644 index 0000000000..a111d9e43b --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html @@ -0,0 +1,142 @@ + + + + Accessible events testing for document + + + + + + + + + + + + + + + + Mozilla Bug 571459 + + +

            + +
            +  
            + + diff --git a/accessible/tests/mochitest/events/focus.html b/accessible/tests/mochitest/events/focus.html new file mode 100644 index 0000000000..ab055df82c --- /dev/null +++ b/accessible/tests/mochitest/events/focus.html @@ -0,0 +1,10 @@ + + + + editable document + + + + editable document + + diff --git a/accessible/tests/mochitest/events/scroll.html b/accessible/tests/mochitest/events/scroll.html new file mode 100644 index 0000000000..562e0a3825 --- /dev/null +++ b/accessible/tests/mochitest/events/scroll.html @@ -0,0 +1,181 @@ + + + + nsIAccessible actions testing for anchors + + + +

            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            +

            + link1 + +

            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            +

            + +

            heading 1

            +

            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            + text text text text text text text text text text text text text text
            +

            + + diff --git a/accessible/tests/mochitest/events/slow_image.sjs b/accessible/tests/mochitest/events/slow_image.sjs new file mode 100644 index 0000000000..f322568be6 --- /dev/null +++ b/accessible/tests/mochitest/events/slow_image.sjs @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + let x = { + data, + QueryInterface: ChromeUtils.generateQI([]), + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function (x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +function handleRequest(request, response) { + if (request.queryString == "complete") { + // Unblock the previous request. + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/json", false); + response.write("true"); // the payload doesn't matter. + + let blockedResponse = getGlobalState("a11y-image"); + if (blockedResponse) { + blockedResponse.setStatusLine(request.httpVersion, 200, "OK"); + blockedResponse.setHeader("Cache-Control", "no-cache", false); + blockedResponse.setHeader("Content-Type", "image/png", false); + blockedResponse.write(IMG_BYTES); + blockedResponse.finish(); + + setGlobalState(undefined, "a11y-image"); + } + } else { + // Getting the image + response.processAsync(); + // Store the response in the global state + setGlobalState(response, "a11y-image"); + } +} diff --git a/accessible/tests/mochitest/events/test_announcement.html b/accessible/tests/mochitest/events/test_announcement.html new file mode 100644 index 0000000000..eb303e4aa9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_announcement.html @@ -0,0 +1,61 @@ + + + + Announcement event and method testing + + + + + + + + + + + + + + + + + Mozilla Bug 1525980 + + +

            + +
            +  
            + + + diff --git a/accessible/tests/mochitest/events/test_aria_alert.html b/accessible/tests/mochitest/events/test_aria_alert.html new file mode 100644 index 0000000000..48f4197b50 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_alert.html @@ -0,0 +1,84 @@ + + + + ARIA alert event testing + + + + + + + + + + + + + + + + + Mozilla Bug 591199 + + +

            + +
            +  
            + + + diff --git a/accessible/tests/mochitest/events/test_aria_menu.html b/accessible/tests/mochitest/events/test_aria_menu.html new file mode 100644 index 0000000000..b240090cb9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_menu.html @@ -0,0 +1,267 @@ + + + + ARIA menu events testing + + + + + + + + + + + + + + + + + + Bug 606207 + + + Bug 614829 + + + Bug 615189 + + + Bug 673958 + + + Bug 933322 + + + Bug 934460 + + + Bug 970005 + + +

            + +
            +  
            + + + +
            outsidemenu
            + + + + + + + + + + + + + +
            Obla
            + +
            + + + diff --git a/accessible/tests/mochitest/events/test_aria_objattr.html b/accessible/tests/mochitest/events/test_aria_objattr.html new file mode 100644 index 0000000000..709089ca02 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_objattr.html @@ -0,0 +1,68 @@ + + + + Accessible ARIA object attribute changes + + + + + + + + + + + + + +

            + +
            +  
            + +
            aria-sort
            + +
            Fat free cheese
            + + diff --git a/accessible/tests/mochitest/events/test_aria_owns.html b/accessible/tests/mochitest/events/test_aria_owns.html new file mode 100644 index 0000000000..3c638ad838 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_owns.html @@ -0,0 +1,122 @@ + + + + Aria-owns targets shouldn't be on invalidation list so shouldn't have + show/hide events + + + + + + + + + + + + + + + + + Mozilla Bug 1296420 +
            + +
            +
            + +
            +
            + + + diff --git a/accessible/tests/mochitest/events/test_aria_statechange.html b/accessible/tests/mochitest/events/test_aria_statechange.html new file mode 100644 index 0000000000..7796d88ec4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_statechange.html @@ -0,0 +1,231 @@ + + + + ARIA state change event testing + + + + + + + + + + + + + + + + + Mozilla Bug 551684 +
            + + Mozilla Bug 648133 +
            + + Mozilla Bug 467143 + + + Mozilla Bug 989958 + + + Mozilla Bug 1136563 + + + Mozilla Bug 1355921 + + +

            + +
            +  
            +
            + + + + + + +
            A document
            + + +
            + + + + +
            + + +
            1
            +
            2
            +
            3
            + + diff --git a/accessible/tests/mochitest/events/test_attrchange.html b/accessible/tests/mochitest/events/test_attrchange.html new file mode 100644 index 0000000000..edd9195ddd --- /dev/null +++ b/accessible/tests/mochitest/events/test_attrchange.html @@ -0,0 +1,107 @@ + + + + Accessible attr change event testing + + + + + + + + + + + + + + + + + + + + + + +
            cell1cell2
            cell3cell4
            + + diff --git a/accessible/tests/mochitest/events/test_attrs.html b/accessible/tests/mochitest/events/test_attrs.html new file mode 100644 index 0000000000..c09bd9cf1e --- /dev/null +++ b/accessible/tests/mochitest/events/test_attrs.html @@ -0,0 +1,85 @@ + + + + Event object attributes tests + + + + + + + + + + + + + + + + + Mozilla Bug 540285 + +

            + +
            +  
            + + + + +
            + + diff --git a/accessible/tests/mochitest/events/test_bug1322593-2.html b/accessible/tests/mochitest/events/test_bug1322593-2.html new file mode 100644 index 0000000000..05bd31ffa6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593-2.html @@ -0,0 +1,77 @@ + + + + Accessible mutation events testing + + + + + + + + + + + + + + + + Mozilla Bug 1322593 + + +

            + +
            +  
            + +
            + hello + your + world +
            + + diff --git a/accessible/tests/mochitest/events/test_bug1322593.html b/accessible/tests/mochitest/events/test_bug1322593.html new file mode 100644 index 0000000000..968e808106 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593.html @@ -0,0 +1,74 @@ + + + + Accessible mutation events testing + + + + + + + + + + + + + + + + Mozilla Bug 1322593 + + +

            + +
            +  
            + +
            hello
            +
            world
            + + diff --git a/accessible/tests/mochitest/events/test_caretmove.html b/accessible/tests/mochitest/events/test_caretmove.html new file mode 100644 index 0000000000..d1091ac7f1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_caretmove.html @@ -0,0 +1,142 @@ + + + + Accessible caret move events testing + + + + + + + + + + + + + + + + Bug 454377 + + + Bug 567571 + + + Bug 824901 + +

            + +
            +  
            + + + + +

            text
            text

            +

            text

            text

            + +

            textohoho

            +

            textohoho

            +

            +

            + + + + diff --git a/accessible/tests/mochitest/events/test_coalescence.html b/accessible/tests/mochitest/events/test_coalescence.html new file mode 100644 index 0000000000..0f8ad52a8b --- /dev/null +++ b/accessible/tests/mochitest/events/test_coalescence.html @@ -0,0 +1,817 @@ + + + + Accessible mutation events coalescence testing + + + + + + + + + + + + + + + + + Mozilla Bug 513213 +
            + + Mozilla Bug 570275 + + +

            + +
            +  
            + +
            + + + + + + + + + + + + + +
            + +
            +
            +
            +
            +
            +
            + +
            +
            +
            opt
            +
            +
            + +
            +
            +
            opt1
            +
            opt2
            +
            +
            + +
            +
            btn
            +
            +
            opt
            +
            +
            + +
            +
            + +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            + + diff --git a/accessible/tests/mochitest/events/test_contextmenu.html b/accessible/tests/mochitest/events/test_contextmenu.html new file mode 100644 index 0000000000..e729f071e7 --- /dev/null +++ b/accessible/tests/mochitest/events/test_contextmenu.html @@ -0,0 +1,131 @@ + + + + Context menu tests + + + + + + + + + + + + + + + + + + Mozilla Bug 580535 +
            + +

            + +
            +  
            + + + +
            + + diff --git a/accessible/tests/mochitest/events/test_descrchange.html b/accessible/tests/mochitest/events/test_descrchange.html new file mode 100644 index 0000000000..1eaecd6b59 --- /dev/null +++ b/accessible/tests/mochitest/events/test_descrchange.html @@ -0,0 +1,142 @@ + + + + Accessible description change event testing + + + + + + + + + + + + + + + + + + Bug 991969 + + +

            + +
            +  
            + + + + +
            +
            d1
            + +
            +

            d1

            + +
            + + diff --git a/accessible/tests/mochitest/events/test_dragndrop.html b/accessible/tests/mochitest/events/test_dragndrop.html new file mode 100644 index 0000000000..2613a310a2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_dragndrop.html @@ -0,0 +1,106 @@ + + + + Accessible drag and drop event testing + + + + + + + + + + + + + + + + + Mozilla Bug 510441 + + +

            + +
            +  
            +
            + + +
            button
            + + +
            button
            + + diff --git a/accessible/tests/mochitest/events/test_flush.html b/accessible/tests/mochitest/events/test_flush.html new file mode 100644 index 0000000000..7d7b60b81e --- /dev/null +++ b/accessible/tests/mochitest/events/test_flush.html @@ -0,0 +1,74 @@ + + + + Flush delayed events testing + + + + + + + + + + + + + + + + Mozilla Bug 477551 + + +

            + +
            +  
            + + + + + diff --git a/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html new file mode 100644 index 0000000000..661284619a --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html @@ -0,0 +1,327 @@ + + + + + aria-activedescendant focus tests + + + + + + + + + + + + + + + Mozilla Bug 429547 + + + Mozilla Bug 761102 + +

            + +
            +  
            + +
            +
            item1
            +
            item2
            +
            roaming
            +
            roaming2
            +
            +
            item3
            + +
            + +
              +
            • option1
            • +
            • option2
            • +
            +
            + + + +
            +
            option
            +
            + +
            + + +
            +
            + + + diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.html b/accessible/tests/mochitest/events/test_focus_autocomplete.html new file mode 100644 index 0000000000..c179398cc0 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_autocomplete.html @@ -0,0 +1,83 @@ + + + + Form Autocomplete Tests + + + + + + + + + + + + + + + + + + diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml new file mode 100644 index 0000000000..69cdac14c5 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + Mozilla Bug 383759 + + + Mozilla Bug 673958 + + + Mozilla Bug 559766 + +

            + +
            +      
            + + + + + + + + + + + +
            + + diff --git a/accessible/tests/mochitest/events/test_focus_general.html b/accessible/tests/mochitest/events/test_focus_general.html new file mode 100644 index 0000000000..6919ed8860 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.html @@ -0,0 +1,176 @@ + + + + Accessible focus testing + + + + + + + + + + + + + + + + + Mozilla Bug 352220 + + + Mozilla Bug 550338 + + + Mozilla Bug 673958 + + + Mozilla Bug 961696 + +

            + +
            +  
            + +
            editable area
            + + + link + + + + + + + + +
            + + diff --git a/accessible/tests/mochitest/events/test_focus_general.xhtml b/accessible/tests/mochitest/events/test_focus_general.xhtml new file mode 100644 index 0000000000..c446359b32 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.xhtml @@ -0,0 +1,124 @@ + + + + + + + + + + + + Mozilla Bug 552368 + +

            + +
            +      
            + + + + + + + + diff --git a/accessible/tests/mochitest/events/test_namechange.html b/accessible/tests/mochitest/events/test_namechange.html new file mode 100644 index 0000000000..840e2dfb4f --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.html @@ -0,0 +1,185 @@ + + + + Accessible name change event testing + + + + + + + + + + + + + + + + + + Bug 991969 + + +

            + +
            +  
            + + initial + + + + +
            +
            l1
            + +
            +

            l1

            + +
            • hello
            + + + +
            + + diff --git a/accessible/tests/mochitest/events/test_namechange.xhtml b/accessible/tests/mochitest/events/test_namechange.xhtml new file mode 100644 index 0000000000..a6dd8cb218 --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + Mozilla Bug 986054 + + +

            + +
            +      
            + + + + + + + +
            +
            diff --git a/accessible/tests/mochitest/events/test_scroll.xhtml b/accessible/tests/mochitest/events/test_scroll.xhtml new file mode 100644 index 0000000000..d3cc2a7bda --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll.xhtml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + Mozilla Bug 691734 + + +

            + +
            +      
            + + + +
            +
            diff --git a/accessible/tests/mochitest/events/test_scroll_caret.xhtml b/accessible/tests/mochitest/events/test_scroll_caret.xhtml new file mode 100644 index 0000000000..f0f0fccfb2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll_caret.xhtml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + Mozilla Bug 1056459 + + +

            + +
            +      
            + + + +
            +
            diff --git a/accessible/tests/mochitest/events/test_selection.html b/accessible/tests/mochitest/events/test_selection.html new file mode 100644 index 0000000000..a749dd9c4c --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.html @@ -0,0 +1,109 @@ + + + + Accessible selection event testing + + + + + + + + + + + + + + + + + + Bug 414302 + + + Bug 810268 + + +

            + +
            +  
            + + + + + +

            Pizza

            + + + diff --git a/accessible/tests/mochitest/events/test_selection.xhtml b/accessible/tests/mochitest/events/test_selection.xhtml new file mode 100644 index 0000000000..9c34ddf286 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.xhtml @@ -0,0 +1,254 @@ + + + + + + + + + + + + Mozilla Bug 414302 + +

            + +
            +      
            + + + + + + + + + + + + + + + + + + + + + +
            hello
            + + + +
              +
            • one
            • +
            • two
            • +
            • three
            • +
            • four
            • +
            • five
            • +
            + + + +
            + +
            + + + diff --git a/accessible/tests/mochitest/events/test_statechange.xhtml b/accessible/tests/mochitest/events/test_statechange.xhtml new file mode 100644 index 0000000000..4d63c664f1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_statechange.xhtml @@ -0,0 +1,117 @@ + + + + + + + + + + +

            + +
            +      
            + + + + + + + + + + +
            + + + text1 + text2 + +
            + + + + + + + nomore text + +
            + + + + nomore + hidden + text2 + hidden2 + + + +
            + + + + +
            + + +
            text
            more text
            + +
            + + + + +
            textTEXTtext
            + +
            + + + textimage + +
            + + + + + + + + + +
            + + + + + + +
            + + + text
            text
            + +
            + + + + + + +

            Choose country from.

            + + + +

            Country

            + + + + + + +    + + + + + + + +
            + + +
            + + +
            + + + +
            + +
            + + + + + + + + + + + + + + + 14 + + + + + +
            +
            + i am visible + i am hidden +
            +
            + + + + + + + + +
            menuitem 2
            + + + +
            +

            This is a paragraph inside the article.

            +
            + +

            heading

            +
            aria_heading
            + + + 15 + + +
            + +
            + + + + + + + + + + + + + + + + + + + + + +
            + + + + + + + + + + + + + + + + + +
            + + + + + + + + + + + + + +

            Image: + x^2 + y^2 + z^2 + +

            + +

            Text: + x2 + + y2 + z2 + + +

            subtree
            + + +
            ab
            + + + + +
            ab
            + + +
            ab
            + + +
            ab
            + + +
            label
            + + +
            + + + + +
            label
            +
            + +
            root +
            +
            sub
            +
            +
            + + +
            label +
            + This content should not be included in the grouping's label. +
            +
            + + +
            abc
            + +
            a

            b

            c
            + +
            a

            b
            + + diff --git a/accessible/tests/mochitest/name/test_general.xhtml b/accessible/tests/mochitest/name/test_general.xhtml new file mode 100644 index 0000000000..a1c6f1461e --- /dev/null +++ b/accessible/tests/mochitest/name/test_general.xhtml @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + Mozilla Bug 444279 + + + Mozilla Bug 441991 + +

            + +
            +    
            + + + + + + + + + + + i am visible + i am hidden + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            diff --git a/accessible/tests/mochitest/name/test_link.html b/accessible/tests/mochitest/name/test_link.html new file mode 100644 index 0000000000..6a289dd44f --- /dev/null +++ b/accessible/tests/mochitest/name/test_link.html @@ -0,0 +1,87 @@ + + + + nsIAccessible::name calculation for HTML links (html:a) + + + + + + + + + + + + + + + + Mozilla Bug 459782 + +

            + +
            +  
            + + + 1 +
            + + + text + + 1 +
            + + + 1 +
            + + + img title + + + 1 +
            + + + + + + diff --git a/accessible/tests/mochitest/name/test_list.html b/accessible/tests/mochitest/name/test_list.html new file mode 100644 index 0000000000..95f0c06d2a --- /dev/null +++ b/accessible/tests/mochitest/name/test_list.html @@ -0,0 +1,103 @@ + + + + nsIAccessible::name calculation for HTML li + + + + + + + + + + + + + + + + Mozilla Bug 634200 + +

            + +
            +  
            + +
              +
            1. list end
            2. +
            + + + diff --git a/accessible/tests/mochitest/name/test_markup.html b/accessible/tests/mochitest/name/test_markup.html new file mode 100644 index 0000000000..735027f44f --- /dev/null +++ b/accessible/tests/mochitest/name/test_markup.html @@ -0,0 +1,58 @@ + + + + nsIAccessible::name calculation for elements + + + + + + + + + + + + + + + + + + + + Bug 459635 + + + Bug 666212 + + + Bug 786163 + + +

            + +
            +  
            + +
            + + diff --git a/accessible/tests/mochitest/name/test_svg.html b/accessible/tests/mochitest/name/test_svg.html new file mode 100644 index 0000000000..535fcdbf20 --- /dev/null +++ b/accessible/tests/mochitest/name/test_svg.html @@ -0,0 +1,53 @@ + + + + Accessible name and description for SVG elements + + + + + + + + + + + + + + + Mozilla Bug 459357 + +

            + +
            +  
            + + + A name + A description + + + + A tooltip + + + diff --git a/accessible/tests/mochitest/name/test_tree.xhtml b/accessible/tests/mochitest/name/test_tree.xhtml new file mode 100644 index 0000000000..3564481d00 --- /dev/null +++ b/accessible/tests/mochitest/name/test_tree.xhtml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + Mozilla Bug 546812 + + + Mozilla Bug 664376 + +

            + +
            +    
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            +
            diff --git a/accessible/tests/mochitest/pivot.js b/accessible/tests/mochitest/pivot.js new file mode 100644 index 0000000000..9e43134add --- /dev/null +++ b/accessible/tests/mochitest/pivot.js @@ -0,0 +1,664 @@ +/* import-globals-from common.js */ +/* import-globals-from events.js */ +/* import-globals-from role.js */ +/* import-globals-from states.js */ +/* import-globals-from text.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; +const PREFILTER_TRANSPARENT = nsIAccessibleTraversalRule.PREFILTER_TRANSPARENT; +const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; +const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; +const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; +const NO_BOUNDARY = nsIAccessiblePivot.NO_BOUNDARY; +const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY; +const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY; +const LINE_BOUNDARY = nsIAccessiblePivot.LINE_BOUNDARY; + +const NS_ERROR_NOT_IN_TREE = 0x80780026; +const NS_ERROR_INVALID_ARG = 0x80070057; + +// ////////////////////////////////////////////////////////////////////////////// +// Traversal rules + +/** + * Rule object to traverse all focusable nodes and text nodes. + */ +var HeadersTraversalRule = { + getMatchRoles() { + return [ROLE_HEADING]; + }, + + preFilter: PREFILTER_INVISIBLE, + + match(aAccessible) { + return FILTER_MATCH; + }, + + QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), +}; + +/** + * Traversal rule for all focusable nodes or leafs. + */ +var ObjectTraversalRule = { + getMatchRoles() { + return []; + }, + + preFilter: PREFILTER_INVISIBLE | PREFILTER_TRANSPARENT, + + match(aAccessible) { + var rv = FILTER_IGNORE; + var role = aAccessible.role; + if ( + hasState(aAccessible, STATE_FOCUSABLE) && + role != ROLE_DOCUMENT && + role != ROLE_INTERNAL_FRAME + ) { + rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH; + } else if ( + aAccessible.childCount == 0 && + role != ROLE_LISTITEM_MARKER && + aAccessible.name.trim() + ) { + rv = FILTER_MATCH; + } + + return rv; + }, + + QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Virtual state invokers and checkers + +/** + * A checker for virtual cursor changed events. + */ +function VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + aTextOffsets, + aPivotMoveMethod, + aIsFromUserInput, + aBoundaryType = NO_BOUNDARY +) { + this.__proto__ = new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc); + + this.match = function VCChangedChecker_match(aEvent) { + var event = null; + try { + event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); + } catch (e) { + return false; + } + + var expectedReason = + VCChangedChecker.methodReasonMap[aPivotMoveMethod] || + nsIAccessiblePivot.REASON_NONE; + + return ( + event.reason == expectedReason && event.boundaryType == aBoundaryType + ); + }; + + this.check = function VCChangedChecker_check(aEvent) { + SimpleTest.info("VCChangedChecker_check"); + + var event = null; + try { + event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); + } catch (e) { + SimpleTest.ok(false, "Does not support correct interface: " + e); + } + + var position = aDocAcc.virtualCursor.position; + var idMatches = position && position.DOMNode.id == aIdOrNameOrAcc; + var nameMatches = position && position.name == aIdOrNameOrAcc; + var accMatches = position == aIdOrNameOrAcc; + + SimpleTest.ok( + idMatches || nameMatches || accMatches, + "id or name matches - expecting " + + prettyName(aIdOrNameOrAcc) + + ", got '" + + prettyName(position) + ); + + SimpleTest.is( + aEvent.isFromUserInput, + aIsFromUserInput, + "Expected user input is " + aIsFromUserInput + "\n" + ); + + SimpleTest.is( + event.newAccessible, + position, + "new position in event is incorrect" + ); + + if (aTextOffsets) { + SimpleTest.is( + aDocAcc.virtualCursor.startOffset, + aTextOffsets[0], + "wrong start offset" + ); + SimpleTest.is( + aDocAcc.virtualCursor.endOffset, + aTextOffsets[1], + "wrong end offset" + ); + SimpleTest.is( + event.newStartOffset, + aTextOffsets[0], + "wrong start offset in event" + ); + SimpleTest.is( + event.newEndOffset, + aTextOffsets[1], + "wrong end offset in event" + ); + } + + var prevPosAndOffset = VCChangedChecker.getPreviousPosAndOffset( + aDocAcc.virtualCursor + ); + + if (prevPosAndOffset) { + SimpleTest.is( + event.oldAccessible, + prevPosAndOffset.position, + "previous position does not match" + ); + SimpleTest.is( + event.oldStartOffset, + prevPosAndOffset.startOffset, + "previous start offset does not match" + ); + SimpleTest.is( + event.oldEndOffset, + prevPosAndOffset.endOffset, + "previous end offset does not match" + ); + } + }; +} + +VCChangedChecker.prevPosAndOffset = {}; + +VCChangedChecker.storePreviousPosAndOffset = function storePreviousPosAndOffset( + aPivot +) { + VCChangedChecker.prevPosAndOffset[aPivot] = { + position: aPivot.position, + startOffset: aPivot.startOffset, + endOffset: aPivot.endOffset, + }; +}; + +VCChangedChecker.getPreviousPosAndOffset = function getPreviousPosAndOffset( + aPivot +) { + return VCChangedChecker.prevPosAndOffset[aPivot]; +}; + +VCChangedChecker.methodReasonMap = { + moveNext: nsIAccessiblePivot.REASON_NEXT, + movePrevious: nsIAccessiblePivot.REASON_PREV, + moveFirst: nsIAccessiblePivot.REASON_FIRST, + moveLast: nsIAccessiblePivot.REASON_LAST, + setTextRange: nsIAccessiblePivot.REASON_NONE, + moveNextByText: nsIAccessiblePivot.REASON_NEXT, + movePreviousByText: nsIAccessiblePivot.REASON_PREV, + moveToPoint: nsIAccessiblePivot.REASON_POINT, +}; + +/** + * Set a text range in the pivot and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aTextAccessible [in] accessible to set to virtual cursor's position + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. + */ +function setVCRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) { + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + SimpleTest.info(prettyName(aTextAccessible) + " " + aTextOffsets); + aDocAcc.virtualCursor.setTextRange( + aTextAccessible, + aTextOffsets[0], + aTextOffsets[1] + ); + }; + + this.getID = function setVCRangeInvoker_getID() { + return ( + "Set offset in " + + prettyName(aTextAccessible) + + " to (" + + aTextOffsets[0] + + ", " + + aTextOffsets[1] + + ")" + ); + }; + + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aTextAccessible, + aTextOffsets, + "setTextRange", + true + ), + ]; +} + +/** + * Move the pivot and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aRule [in] traversal rule object + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + * @param aIsFromUserInput [in] set user input flag when invoking method, and + * expect it in the event. + */ +function setVCPosInvoker( + aDocAcc, + aPivotMoveMethod, + aRule, + aIdOrNameOrAcc, + aIsFromUserInput +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + if (aPivotMoveMethod && aRule) { + var moved = false; + switch (aPivotMoveMethod) { + case "moveFirst": + case "moveLast": + moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aRule, + aIsFromUserInput === undefined ? true : aIsFromUserInput + ); + break; + case "moveNext": + case "movePrevious": + moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aRule, + aDocAcc.virtualCursor.position, + false, + aIsFromUserInput === undefined ? true : aIsFromUserInput + ); + break; + } + SimpleTest.is( + !!moved, + !!expectMove, + "moved pivot with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc + ); + } else { + aDocAcc.virtualCursor.position = getAccessible(aIdOrNameOrAcc); + } + }; + + this.getID = function setVCPosInvoker_getID() { + return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod; + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + null, + aPivotMoveMethod, + aIsFromUserInput === undefined ? !!aPivotMoveMethod : aIsFromUserInput + ), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Move the pivot by text and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aBoundary [in] boundary constant + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + * @param aIsFromUserInput [in] set user input flag when invoking method, and + * expect it in the event. + */ +function setVCTextInvoker( + aDocAcc, + aPivotMoveMethod, + aBoundary, + aTextOffsets, + aIdOrNameOrAcc, + aIsFromUserInput +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + SimpleTest.info(aDocAcc.virtualCursor.position); + var moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aBoundary, + aIsFromUserInput === undefined + ); + SimpleTest.is( + !!moved, + !!expectMove, + "moved pivot by text with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc + ); + }; + + this.getID = function setVCPosInvoker_getID() { + return ( + "Do " + + (expectMove ? "" : "no-op ") + + aPivotMoveMethod + + " in " + + prettyName(aIdOrNameOrAcc) + + ", " + + boundaryToString(aBoundary) + + ", [" + + aTextOffsets + + "]" + ); + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + aTextOffsets, + aPivotMoveMethod, + aIsFromUserInput === undefined ? true : aIsFromUserInput, + aBoundary + ), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Move the pivot to the position under the point. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aX [in] screen x coordinate + * @param aY [in] screen y coordinate + * @param aIgnoreNoMatch [in] don't unset position if no object was found at + * point. + * @param aRule [in] traversal rule object + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + */ +function moveVCCoordInvoker( + aDocAcc, + aX, + aY, + aIgnoreNoMatch, + aRule, + aIdOrNameOrAcc +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + var moved = aDocAcc.virtualCursor.moveToPoint( + aRule, + aX, + aY, + aIgnoreNoMatch + ); + SimpleTest.ok( + (expectMove && moved) || (!expectMove && !moved), + "moved pivot" + ); + }; + + this.getID = function setVCPosInvoker_getID() { + return ( + "Do " + (expectMove ? "" : "no-op ") + "moveToPoint " + aIdOrNameOrAcc + ); + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, "moveToPoint", true), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Change the pivot modalRoot + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aModalRootAcc [in] accessible of the modal root, or null + * @param aExpectedResult [in] error result expected. 0 if expecting success + */ +function setModalRootInvoker(aDocAcc, aModalRootAcc, aExpectedResult) { + this.invoke = function setModalRootInvoker_invoke() { + var errorResult = 0; + try { + aDocAcc.virtualCursor.modalRoot = aModalRootAcc; + } catch (x) { + SimpleTest.ok( + x.result, + "Unexpected exception when changing modal root: " + x + ); + errorResult = x.result; + } + + SimpleTest.is( + errorResult, + aExpectedResult, + "Did not get expected result when changing modalRoot" + ); + }; + + this.getID = function setModalRootInvoker_getID() { + return "Set modalRoot to " + prettyName(aModalRootAcc); + }; + + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; +} + +/** + * Add invokers to a queue to test a rule and an expected sequence of element ids + * or accessible names for that rule in the given document. + * + * @param aQueue [in] event queue in which to push invoker sequence. + * @param aDocAcc [in] the managing document of the virtual cursor we are + * testing + * @param aRule [in] the traversal rule to use in the invokers + * @param aModalRoot [in] a modal root to use in this traversal sequence + * @param aSequence [in] a sequence of accessible names or element ids to expect + * with the given rule in the given document + */ +function queueTraversalSequence(aQueue, aDocAcc, aRule, aModalRoot, aSequence) { + aDocAcc.virtualCursor.position = null; + + // Add modal root (if any) + aQueue.push(new setModalRootInvoker(aDocAcc, aModalRoot, 0)); + + aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); + + for (let i = 1; i < aSequence.length; i++) { + let invoker = new setVCPosInvoker(aDocAcc, "moveNext", aRule, aSequence[i]); + aQueue.push(invoker); + } + + // No further more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); + + for (let i = aSequence.length - 2; i >= 0; i--) { + let invoker = new setVCPosInvoker( + aDocAcc, + "movePrevious", + aRule, + aSequence[i] + ); + aQueue.push(invoker); + } + + // No previous more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); + + aQueue.push( + new setVCPosInvoker( + aDocAcc, + "moveLast", + aRule, + aSequence[aSequence.length - 1] + ) + ); + + // No further more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); + + // set isFromUserInput to false, just to test.. + aQueue.push( + new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0], false) + ); + + // No previous more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); + + // Remove modal root (if any). + aQueue.push(new setModalRootInvoker(aDocAcc, null, 0)); +} + +/** + * A checker for removing an accessible while the virtual cursor is on it. + */ +function removeVCPositionChecker(aDocAcc, aHiddenParentAcc) { + this.__proto__ = new invokerChecker(EVENT_REORDER, aHiddenParentAcc); + + this.check = function removeVCPositionChecker_check(aEvent) { + var errorResult = 0; + try { + aDocAcc.virtualCursor.moveNext(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, + NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position." + ); + }; +} + +/** + * Put the virtual cursor's position on an object, and then remove it. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPosNode [in] DOM node to hide after virtual cursor's position is + * set to it. + */ +function removeVCPositionInvoker(aDocAcc, aPosNode) { + this.accessible = getAccessible(aPosNode); + this.invoke = function removeVCPositionInvoker_invoke() { + aDocAcc.virtualCursor.position = this.accessible; + aPosNode.remove(); + }; + + this.getID = function removeVCPositionInvoker_getID() { + return "Bring virtual cursor to accessible, and remove its DOM node."; + }; + + this.eventSeq = [ + new removeVCPositionChecker(aDocAcc, this.accessible.parent), + ]; +} + +/** + * A checker for removing the pivot root and then calling moveFirst, and + * checking that an exception is thrown. + */ +function removeVCRootChecker(aPivot) { + this.__proto__ = new invokerChecker(EVENT_REORDER, aPivot.root.parent); + + this.check = function removeVCRootChecker_check(aEvent) { + var errorResult = 0; + try { + aPivot.moveLast(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, + NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position." + ); + }; +} + +/** + * Create a pivot, remove its root, and perform an operation where the root is + * needed. + * + * @param aRootNode [in] DOM node of which accessible will be the root of the + * pivot. Should have more than one child. + */ +function removeVCRootInvoker(aRootNode) { + this.pivot = gAccService.createAccessiblePivot(getAccessible(aRootNode)); + this.invoke = function removeVCRootInvoker_invoke() { + this.pivot.position = this.pivot.root.firstChild; + aRootNode.remove(); + }; + + this.getID = function removeVCRootInvoker_getID() { + return "Remove root of pivot from tree."; + }; + + this.eventSeq = [new removeVCRootChecker(this.pivot)]; +} + +/** + * A debug utility for writing proper sequences for queueTraversalSequence. + */ +function dumpTraversalSequence(aPivot, aRule) { + var sequence = []; + if (aPivot.moveFirst(aRule)) { + do { + sequence.push("'" + prettyName(aPivot.position) + "'"); + } while (aPivot.moveNext(aRule)); + } + SimpleTest.info("\n[" + sequence.join(", ") + "]\n"); +} diff --git a/accessible/tests/mochitest/pivot/a11y.ini b/accessible/tests/mochitest/pivot/a11y.ini new file mode 100644 index 0000000000..8add460947 --- /dev/null +++ b/accessible/tests/mochitest/pivot/a11y.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + doc_virtualcursor.html + doc_virtualcursor_text.html + !/accessible/tests/mochitest/*.js + +[test_virtualcursor.html] +[test_virtualcursor_text.html] diff --git a/accessible/tests/mochitest/pivot/doc_virtualcursor.html b/accessible/tests/mochitest/pivot/doc_virtualcursor.html new file mode 100644 index 0000000000..a456f2dfcd --- /dev/null +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor.html @@ -0,0 +1,38 @@ + + + + Pivot test document + + + +

            Main Title

            + +

            + Lorem ipsum dolor sit amet. Integer vitae urna + leo, id semper nulla. +

            +

            Second Section Title

            +

            + Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.

            + +

            + This is completely transparent +

            + +
            Hide me
            + +
              +
            • Hello
            • +
            • World
            • +
            + + diff --git a/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html new file mode 100644 index 0000000000..a0565058d9 --- /dev/null +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html @@ -0,0 +1,37 @@ + + + + Pivot test document + + + +
            This is the very beginning.
            +

            + This is the test of text. +

            +
            A multiword link is here. We will traverse
            +
            into, out, and between the subtrees.
            +

            Singularity.

            + + + + + + + + + +
            Magicalunicorns
            and wizardsreally exist.
            +
            Endless fun!
            +

            Objectsadjacentto eachother should be separate.

            +

            Hello real world

            + + Hello + +

            + ab
            + c +

            +
            End!
            + + diff --git a/accessible/tests/mochitest/pivot/test_virtualcursor.html b/accessible/tests/mochitest/pivot/test_virtualcursor.html new file mode 100644 index 0000000000..9f3225fcf6 --- /dev/null +++ b/accessible/tests/mochitest/pivot/test_virtualcursor.html @@ -0,0 +1,119 @@ + + + + Tests pivot functionality in virtual cursors + + + + + + + + + + + + + + + + + + + Mozilla Bug 698823 +

            + +
            +  
            + + diff --git a/accessible/tests/mochitest/pivot/test_virtualcursor_text.html b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html new file mode 100644 index 0000000000..23d46f3fe6 --- /dev/null +++ b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html @@ -0,0 +1,271 @@ + + + + Tests pivot functionality in virtual cursors + + + + + + + + + + + + + + + + + + + + Mozilla Bug 886076 +

            + +
            +  
            + + diff --git a/accessible/tests/mochitest/promisified-events.js b/accessible/tests/mochitest/promisified-events.js new file mode 100644 index 0000000000..71b2cf013f --- /dev/null +++ b/accessible/tests/mochitest/promisified-events.js @@ -0,0 +1,328 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded by head.js, so has the same globals, hence we import the +// globals from there. +/* import-globals-from common.js */ + +/* exported EVENT_ANNOUNCEMENT, EVENT_REORDER, EVENT_SCROLLING, + EVENT_SCROLLING_END, EVENT_SHOW, EVENT_TEXT_INSERTED, + EVENT_TEXT_REMOVED, EVENT_DOCUMENT_LOAD_COMPLETE, EVENT_HIDE, + EVENT_TEXT_ATTRIBUTE_CHANGED, EVENT_TEXT_CARET_MOVED, EVENT_SELECTION, + EVENT_DESCRIPTION_CHANGE, EVENT_NAME_CHANGE, EVENT_STATE_CHANGE, + EVENT_VALUE_CHANGE, EVENT_TEXT_VALUE_CHANGE, EVENT_FOCUS, + EVENT_DOCUMENT_RELOAD, EVENT_VIRTUALCURSOR_CHANGED, EVENT_ALERT, + EVENT_OBJECT_ATTRIBUTE_CHANGED, UnexpectedEvents, waitForEvent, + waitForEvents, waitForOrderedEvents, waitForStateChange, + stateChangeEventArgs */ + +const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; +const EVENT_DOCUMENT_LOAD_COMPLETE = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; +const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; +const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; +const EVENT_SCROLLING = nsIAccessibleEvent.EVENT_SCROLLING; +const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; +const EVENT_SCROLLING_END = nsIAccessibleEvent.EVENT_SCROLLING_END; +const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; +const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; +const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; +const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; +const EVENT_TEXT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; +const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; +const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; +const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; +const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; +const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; +const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; +const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; +const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; +const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; +const EVENT_VIRTUALCURSOR_CHANGED = + nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; +const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; +const EVENT_TEXT_SELECTION_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED; +const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED; +const EVENT_OBJECT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; +const EVENT_INNER_REORDER = nsIAccessibleEvent.EVENT_INNER_REORDER; + +const EventsLogger = { + enabled: false, + + log(msg) { + if (this.enabled) { + info(msg); + } + }, +}; + +/** + * Describe an event in string format. + * @param {nsIAccessibleEvent} event event to strigify + */ +function eventToString(event) { + let type = eventTypeToString(event.eventType); + let info = `Event type: ${type}`; + + if (event instanceof nsIAccessibleStateChangeEvent) { + let stateStr = statesToString( + event.isExtraState ? 0 : event.state, + event.isExtraState ? event.state : 0 + ); + info += `, state: ${stateStr}, is enabled: ${event.isEnabled}`; + } else if (event instanceof nsIAccessibleTextChangeEvent) { + let tcType = event.isInserted ? "inserted" : "removed"; + info += `, start: ${event.start}, length: ${event.length}, ${tcType} text: ${event.modifiedText}`; + } + + info += `. Target: ${prettyName(event.accessible)}`; + return info; +} + +function matchEvent(event, matchCriteria) { + if (!matchCriteria) { + return true; + } + + let acc = event.accessible; + switch (typeof matchCriteria) { + case "string": + let id = getAccessibleDOMNodeID(acc); + if (id === matchCriteria) { + EventsLogger.log(`Event matches DOMNode id: ${id}`); + return true; + } + break; + case "function": + if (matchCriteria(event)) { + EventsLogger.log( + `Lambda function matches event: ${eventToString(event)}` + ); + return true; + } + break; + default: + if (matchCriteria instanceof nsIAccessible) { + if (acc === matchCriteria) { + EventsLogger.log(`Event matches accessible: ${prettyName(acc)}`); + return true; + } + } else if (event.DOMNode == matchCriteria) { + EventsLogger.log( + `Event matches DOM node: ${prettyName(event.DOMNode)}` + ); + return true; + } + } + + return false; +} + +/** + * A helper function that returns a promise that resolves when an accessible + * event of the given type with the given target (defined by its id or + * accessible) is observed. + * @param {Number} eventType expected accessible event + * type + * @param {String|nsIAccessible|Function} matchCriteria expected content + * element id + * for the event + * @param {String} message Message to prepend to logging. + * @return {Promise} promise that resolves to an + * event + */ +function waitForEvent(eventType, matchCriteria, message) { + return new Promise(resolve => { + let eventObserver = { + observe(subject, topic, data) { + if (topic !== "accessible-event") { + return; + } + + let event = subject.QueryInterface(nsIAccessibleEvent); + if (EventsLogger.enabled) { + // Avoid calling eventToString if the EventsLogger isn't enabled in order + // to avoid an intermittent crash (bug 1307645). + EventsLogger.log(eventToString(event)); + } + + // If event type does not match expected type, skip the event. + if (event.eventType !== eventType) { + return; + } + + if (matchEvent(event, matchCriteria)) { + EventsLogger.log( + `Correct event type: ${eventTypeToString(eventType)}` + ); + Services.obs.removeObserver(this, "accessible-event"); + ok( + true, + `${message ? message + ": " : ""}Recieved ${eventTypeToString( + eventType + )} event` + ); + resolve(event); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-event"); + }); +} + +class UnexpectedEvents { + constructor(unexpected) { + if (unexpected.length) { + this.unexpected = unexpected; + Services.obs.addObserver(this, "accessible-event"); + } + } + + observe(subject, topic, data) { + if (topic !== "accessible-event") { + return; + } + + let event = subject.QueryInterface(nsIAccessibleEvent); + + let unexpectedEvent = this.unexpected.find( + ([etype, criteria]) => + etype === event.eventType && matchEvent(event, criteria) + ); + + if (unexpectedEvent) { + ok(false, `Got unexpected event: ${eventToString(event)}`); + } + } + + stop() { + if (this.unexpected) { + Services.obs.removeObserver(this, "accessible-event"); + } + } +} + +/** + * A helper function that waits for a sequence of accessible events in + * specified order. + * @param {Array} events a list of events to wait (same format as + * waitForEvent arguments) + * @param {String} message Message to prepend to logging. + * @param {Boolean} ordered Events need to be recieved in given order. + * @param {Object} invokerOrWindow a local window or a special content invoker + * it takes a list of arguments and a task + * function. + */ +async function waitForEvents( + events, + message, + ordered = false, + invokerOrWindow = null +) { + let expected = events.expected || events; + // Next expected event index. + let currentIdx = 0; + + let unexpectedListener = events.unexpected + ? new UnexpectedEvents(events.unexpected) + : null; + + let results = await Promise.all( + expected.map((evt, idx) => { + const [eventType, matchCriteria] = evt; + return waitForEvent(eventType, matchCriteria, message).then(result => { + return [result, idx == currentIdx++]; + }); + }) + ); + + if (unexpectedListener) { + let flushQueue = async win => { + // Flush all notifications or queued a11y events. + win.windowUtils.advanceTimeAndRefresh(100); + + // Flush all DOM async events. + await new Promise(r => win.setTimeout(r, 0)); + + // Flush all notifications or queued a11y events resulting from async DOM events. + win.windowUtils.advanceTimeAndRefresh(100); + + // Flush all notifications or a11y events that may have been queued in the last tick. + win.windowUtils.advanceTimeAndRefresh(100); + + // Return refresh to normal. + win.windowUtils.restoreNormalRefresh(); + }; + + if (invokerOrWindow instanceof Function) { + await invokerOrWindow([flushQueue.toString()], async _flushQueue => { + // eslint-disable-next-line no-eval, no-undef + await eval(_flushQueue)(content); + }); + } else { + await flushQueue(invokerOrWindow ? invokerOrWindow : window); + } + + unexpectedListener.stop(); + } + + if (ordered) { + ok( + results.every(([, isOrdered]) => isOrdered), + `${message ? message + ": " : ""}Correct event order` + ); + } + + return results.map(([event]) => event); +} + +function waitForOrderedEvents(events, message) { + return waitForEvents(events, message, true); +} + +function stateChangeEventArgs(id, state, isEnabled, isExtra = false) { + return [ + EVENT_STATE_CHANGE, + e => { + e.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + e.state == state && + e.isExtraState == isExtra && + isEnabled == e.isEnabled && + (typeof id == "string" + ? id == getAccessibleDOMNodeID(e.accessible) + : getAccessible(id) == e.accessible) + ); + }, + ]; +} + +function waitForStateChange(id, state, isEnabled, isExtra = false) { + return waitForEvent(...stateChangeEventArgs(id, state, isEnabled, isExtra)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Utility functions ported from events.js. + +/** + * This function selects all text in the passed-in element if it has an editor, + * before setting focus to it. This simulates behavio with the keyboard when + * tabbing to the element. This does explicitly what synthFocus did implicitly. + * This should be called only if you really want this behavior. + * @param {string} id The element ID to focus + */ +function selectAllTextAndFocus(id) { + const elem = getNode(id); + if (elem.editor) { + elem.selectionStart = elem.selectionEnd = elem.value.length; + } + + elem.focus(); +} diff --git a/accessible/tests/mochitest/relations.js b/accessible/tests/mochitest/relations.js new file mode 100644 index 0000000000..aa956649ff --- /dev/null +++ b/accessible/tests/mochitest/relations.js @@ -0,0 +1,204 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +var RELATION_CONTROLLED_BY = nsIAccessibleRelation.RELATION_CONTROLLED_BY; +var RELATION_CONTROLLER_FOR = nsIAccessibleRelation.RELATION_CONTROLLER_FOR; +var RELATION_DEFAULT_BUTTON = nsIAccessibleRelation.RELATION_DEFAULT_BUTTON; +var RELATION_DESCRIBED_BY = nsIAccessibleRelation.RELATION_DESCRIBED_BY; +var RELATION_DESCRIPTION_FOR = nsIAccessibleRelation.RELATION_DESCRIPTION_FOR; +var RELATION_EMBEDDED_BY = nsIAccessibleRelation.RELATION_EMBEDDED_BY; +var RELATION_EMBEDS = nsIAccessibleRelation.RELATION_EMBEDS; +var RELATION_FLOWS_FROM = nsIAccessibleRelation.RELATION_FLOWS_FROM; +var RELATION_FLOWS_TO = nsIAccessibleRelation.RELATION_FLOWS_TO; +var RELATION_LABEL_FOR = nsIAccessibleRelation.RELATION_LABEL_FOR; +var RELATION_LABELLED_BY = nsIAccessibleRelation.RELATION_LABELLED_BY; +var RELATION_MEMBER_OF = nsIAccessibleRelation.RELATION_MEMBER_OF; +var RELATION_NODE_CHILD_OF = nsIAccessibleRelation.RELATION_NODE_CHILD_OF; +var RELATION_NODE_PARENT_OF = nsIAccessibleRelation.RELATION_NODE_PARENT_OF; +var RELATION_PARENT_WINDOW_OF = nsIAccessibleRelation.RELATION_PARENT_WINDOW_OF; +var RELATION_POPUP_FOR = nsIAccessibleRelation.RELATION_POPUP_FOR; +var RELATION_SUBWINDOW_OF = nsIAccessibleRelation.RELATION_SUBWINDOW_OF; +var RELATION_CONTAINING_DOCUMENT = + nsIAccessibleRelation.RELATION_CONTAINING_DOCUMENT; +var RELATION_CONTAINING_TAB_PANE = + nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE; +var RELATION_CONTAINING_APPLICATION = + nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION; +const RELATION_DETAILS = nsIAccessibleRelation.RELATION_DETAILS; +const RELATION_DETAILS_FOR = nsIAccessibleRelation.RELATION_DETAILS_FOR; +const RELATION_ERRORMSG = nsIAccessibleRelation.RELATION_ERRORMSG; +const RELATION_ERRORMSG_FOR = nsIAccessibleRelation.RELATION_ERRORMSG_FOR; +const RELATION_LINKS_TO = nsIAccessibleRelation.RELATION_LINKS_TO; + +// ////////////////////////////////////////////////////////////////////////////// +// General + +/** + * Test the accessible relation. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID + * attribute or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + * @param aRelatedIdentifiers [in] identifier or array of identifiers of + * expected related accessibles + */ +function testRelation(aIdentifier, aRelType, aRelatedIdentifiers) { + var relation = getRelationByType(aIdentifier, aRelType); + + var relDescr = getRelationErrorMsg(aIdentifier, aRelType); + var relDescrStart = getRelationErrorMsg(aIdentifier, aRelType, true); + + if (!relation || !relation.targetsCount) { + if (!aRelatedIdentifiers) { + ok(true, "No" + relDescr); + return; + } + + var msg = + relDescrStart + + "has no expected targets: '" + + prettyName(aRelatedIdentifiers) + + "'"; + + ok(false, msg); + return; + } else if (!aRelatedIdentifiers) { + ok(false, "There are unexpected targets of " + relDescr); + return; + } + + var relatedIds = + aRelatedIdentifiers instanceof Array + ? aRelatedIdentifiers + : [aRelatedIdentifiers]; + + var targets = []; + for (let idx = 0; idx < relatedIds.length; idx++) { + targets.push(getAccessible(relatedIds[idx])); + } + + if (targets.length != relatedIds.length) { + return; + } + + var actualTargets = relation.getTargets(); + + // Check if all given related accessibles are targets of obtained relation. + for (let idx = 0; idx < targets.length; idx++) { + var isFound = false; + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + if (targets[idx] == relatedAcc) { + isFound = true; + break; + } + } + + ok(isFound, prettyName(relatedIds[idx]) + " is not a target of" + relDescr); + } + + // Check if all obtained targets are given related accessibles. + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + let idx; + // eslint-disable-next-line no-empty + for (idx = 0; idx < targets.length && relatedAcc != targets[idx]; idx++) {} + + if (idx == targets.length) { + ok( + false, + "There is unexpected target" + prettyName(relatedAcc) + "of" + relDescr + ); + } + } +} + +/** + * Test that the given accessible relations don't exist. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID + * attribute or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + * @param aUnrelatedIdentifiers [in] identifier or array of identifiers of + * accessibles that shouldn't exist for this + * relation. + */ +function testAbsentRelation(aIdentifier, aRelType, aUnrelatedIdentifiers) { + var relation = getRelationByType(aIdentifier, aRelType); + + var relDescr = getRelationErrorMsg(aIdentifier, aRelType); + + if (!aUnrelatedIdentifiers) { + ok(false, "No identifiers given for unrelated accessibles."); + return; + } + + if (!relation || !relation.targetsCount) { + ok(true, "No relations exist."); + return; + } + + var relatedIds = + aUnrelatedIdentifiers instanceof Array + ? aUnrelatedIdentifiers + : [aUnrelatedIdentifiers]; + + var targets = []; + for (let idx = 0; idx < relatedIds.length; idx++) { + targets.push(getAccessible(relatedIds[idx])); + } + + if (targets.length != relatedIds.length) { + return; + } + + var actualTargets = relation.getTargets(); + + // Any found targets that match given accessibles should be called out. + for (let idx = 0; idx < targets.length; idx++) { + var notFound = true; + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + if (targets[idx] == relatedAcc) { + notFound = false; + break; + } + } + + ok(notFound, prettyName(relatedIds[idx]) + " is a target of " + relDescr); + } +} + +/** + * Return related accessible for the given relation type. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID attribute + * or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + */ +function getRelationByType(aIdentifier, aRelType) { + var acc = getAccessible(aIdentifier); + if (!acc) { + return null; + } + + var relation = null; + try { + relation = acc.getRelationByType(aRelType); + } catch (e) { + ok(false, "Can't get" + getRelationErrorMsg(aIdentifier, aRelType)); + } + + return relation; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details + +function getRelationErrorMsg(aIdentifier, aRelType, aIsStartSentence) { + var relStr = relationTypeToString(aRelType); + var msg = aIsStartSentence ? "Relation of '" : " relation of '"; + msg += relStr + "' type for '" + prettyName(aIdentifier) + "'"; + msg += aIsStartSentence ? " " : "."; + + return msg; +} diff --git a/accessible/tests/mochitest/relations/a11y.ini b/accessible/tests/mochitest/relations/a11y.ini new file mode 100644 index 0000000000..89ffaffdce --- /dev/null +++ b/accessible/tests/mochitest/relations/a11y.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_embeds.xhtml] +skip-if = os == 'linux' && !debug # bug 1411145 +[test_general.html] +[test_general.xhtml] +[test_groupInfoUpdate.html] +[test_tabbrowser.xhtml] +[test_tree.xhtml] +[test_ui_modalprompt.html] +[test_shadowdom.html] +[test_update.html] diff --git a/accessible/tests/mochitest/relations/test_embeds.xhtml b/accessible/tests/mochitest/relations/test_embeds.xhtml new file mode 100644 index 0000000000..d49ce1f73c --- /dev/null +++ b/accessible/tests/mochitest/relations/test_embeds.xhtml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + Mozilla Bug 707654 + +

            + +
            +    
            + +
            +
            diff --git a/accessible/tests/mochitest/relations/test_general.html b/accessible/tests/mochitest/relations/test_general.html new file mode 100644 index 0000000000..d16b7c1492 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_general.html @@ -0,0 +1,456 @@ + + + + nsIAccessible::getAccessibleRelated() tests + + + + + + + + + + + + + + + + Bug 475298 + + + Bug 527461 + + + Bug 558036 + + + Bug 682790 + + + Bug 687393 + + + Bug 864224 + +

            + +
            +  
            + + + + + + + + + + + + + + + + + + + + + + label + + + label1 + label2 + + + description + + + description1 + description2 + + +
            Yellow
            +
            Orange
            +
            +
            Blue
            +
            Green
            +
            Light green
            +
            Green2
            +
            +
            Super light green
            +
            +
            + +
            +
            +
            cell 1,1
            +
            cell 1,2
            +
            +
            +
            cell 2,1
            +
            cell 2,2
            +
            +
            +
            cell 3,1
            +
            cell 3,2
            +
            +
            + +
              +
            • Item 1 +
                +
              • Item 1A
              • +
              • Item 1B
              • +
              +
            • +
            + +
            +
            +
            1
            +
            +
            +
            +
            1.1
            +
            1.2
            +
            +
            +
            + +
            +
            + cell1cell2 +
            +
            + cell3cell4 +
            +
            + cell5cell6 +
            +
            + +
            +
            Item 1 +
            +
            Item 1A
            +
            Item 1B
            +
            +
            +
            + +
            +
            1 +
            +
            +
            1.1
            +
            1.2
            +
            +
            +
            +
            + +
            +
            1 +
            +
            +
            1.1
            +
            1.2
            +
            +
            +
            +
            + + + +
            + +
            +
            tabpanel
            + +
            1
            +
            a
            + + +
            live region
            + + flow to + flow from + + flow to + flow from + flow from + +
            + + + + + +
            + + + + + + +
            tabple caption
            cell1cell2
            + +
            + legend + +
            + +
            +
            +
            + + diff --git a/accessible/tests/mochitest/relations/test_general.xhtml b/accessible/tests/mochitest/relations/test_general.xhtml new file mode 100644 index 0000000000..bc3b328fd9 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_general.xhtml @@ -0,0 +1,237 @@ + + + + + + + + + + + + Mozilla Bug 475298 +
            + + Mozilla Bug 67389 +
            + +

            + +
            +      
            + + + + + + + + + + label + + + label + label + + + + + + description + + + label + label + + + description + + + description + description + + + Yellow + Orange + + Blue + Green + Light green + + + Dark green + + + + + + + + + + + + + + + + diff --git a/accessible/tests/mochitest/role/test_aria.xhtml b/accessible/tests/mochitest/role/test_aria.xhtml new file mode 100644 index 0000000000..9aea4ec222 --- /dev/null +++ b/accessible/tests/mochitest/role/test_aria.xhtml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + Mozilla Bug 494345 + + + Mozilla Bug 1033283 + +

            + +
            +    
            + + + +